mirror of
https://github.com/EdiFarcas/Car-Fuel-Tracking-App.git
synced 2026-06-22 05:00:53 +03:00
Upgrades until 15.07.2025
This commit is contained in:
Generated
+43
@@ -12,6 +12,7 @@
|
|||||||
"@next-auth/prisma-adapter": "^1.0.7",
|
"@next-auth/prisma-adapter": "^1.0.7",
|
||||||
"@prisma/client": "^6.11.0",
|
"@prisma/client": "^6.11.0",
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
|
"framer-motion": "^12.23.3",
|
||||||
"next": "15.3.4",
|
"next": "15.3.4",
|
||||||
"next-auth": "^4.24.11",
|
"next-auth": "^4.24.11",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
@@ -3927,6 +3928,33 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/framer-motion": {
|
||||||
|
"version": "12.23.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.3.tgz",
|
||||||
|
"integrity": "sha512-llmLkf44zuIZOPSrE4bl4J0UTg6bav+rlKEfMRKgvDMXqBrUtMg6cspoQeRVK3nqRLxTmAJhfGXk39UDdZD7Kw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"motion-dom": "^12.23.2",
|
||||||
|
"motion-utils": "^12.23.2",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@emotion/is-prop-valid": "*",
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@emotion/is-prop-valid": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/function-bind": {
|
"node_modules/function-bind": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
@@ -5245,6 +5273,21 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/motion-dom": {
|
||||||
|
"version": "12.23.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.2.tgz",
|
||||||
|
"integrity": "sha512-73j6xDHX/NvVh5L5oS1ouAVnshsvmApOq4F3VZo5MkYSD/YVsVLal4Qp9wvVgJM9uU2bLZyc0Sn8B9c/MMKk4g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"motion-utils": "^12.23.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/motion-utils": {
|
||||||
|
"version": "12.23.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.2.tgz",
|
||||||
|
"integrity": "sha512-cIEXlBlXAOUyiAtR0S+QPQUM9L3Diz23Bo+zM420NvSd/oPQJwg6U+rT+WRTpp0rizMsBGQOsAwhWIfglUcZfA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"@next-auth/prisma-adapter": "^1.0.7",
|
"@next-auth/prisma-adapter": "^1.0.7",
|
||||||
"@prisma/client": "^6.11.0",
|
"@prisma/client": "^6.11.0",
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
|
"framer-motion": "^12.23.3",
|
||||||
"next": "15.3.4",
|
"next": "15.3.4",
|
||||||
"next-auth": "^4.24.11",
|
"next-auth": "^4.24.11",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.0 MiB |
@@ -26,7 +26,7 @@ export async function POST(req: Request) {
|
|||||||
const session = await getSession();
|
const session = await getSession();
|
||||||
if (!session) return NextResponse.json({ message: 'Unauthorized' }, { status: 401 });
|
if (!session) return NextResponse.json({ message: 'Unauthorized' }, { status: 401 });
|
||||||
|
|
||||||
const { carId, mileage, liters, cost, currency, fuelType } = await req.json();
|
const { carId, mileage, liters, cost, currency, fuelType, date } = await req.json();
|
||||||
if (!carId || !mileage || !liters || !cost || !currency || !fuelType) {
|
if (!carId || !mileage || !liters || !cost || !currency || !fuelType) {
|
||||||
return NextResponse.json({ message: 'Missing fields' }, { status: 400 });
|
return NextResponse.json({ message: 'Missing fields' }, { status: 400 });
|
||||||
}
|
}
|
||||||
@@ -48,6 +48,7 @@ export async function POST(req: Request) {
|
|||||||
currency,
|
currency,
|
||||||
carId,
|
carId,
|
||||||
fuelType,
|
fuelType,
|
||||||
|
date: date ? new Date(date) : undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,21 @@ const currencies = ['EUR', 'USD', 'RON', 'GBP'];
|
|||||||
export default function FillUpsPage() {
|
export default function FillUpsPage() {
|
||||||
const { carId } = useParams();
|
const { carId } = useParams();
|
||||||
const [fillups, setFillups] = useState<FillUp[]>([]);
|
const [fillups, setFillups] = useState<FillUp[]>([]);
|
||||||
|
// Toggle for showing/hiding fill-ups
|
||||||
|
const [showFillUps, setShowFillUps] = useState(false);
|
||||||
|
// Filter and sort state
|
||||||
|
const [filterFuelType, setFilterFuelType] = useState('');
|
||||||
|
const [sortOrder, setSortOrder] = useState('date-desc');
|
||||||
|
// Filter and sort logic for fill-ups
|
||||||
|
const filteredFillUps = fillups
|
||||||
|
.filter(f => !filterFuelType || f.fuelType === filterFuelType)
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (sortOrder === 'date-desc') return new Date(b.date).getTime() - new Date(a.date).getTime();
|
||||||
|
if (sortOrder === 'date-asc') return new Date(a.date).getTime() - new Date(b.date).getTime();
|
||||||
|
if (sortOrder === 'mileage-desc') return b.mileage - a.mileage;
|
||||||
|
if (sortOrder === 'mileage-asc') return a.mileage - b.mileage;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
const [carFuelTypes, setCarFuelTypes] = useState<string[]>([]);
|
const [carFuelTypes, setCarFuelTypes] = useState<string[]>([]);
|
||||||
const [form, setForm] = useState({
|
const [form, setForm] = useState({
|
||||||
mileage: '',
|
mileage: '',
|
||||||
@@ -26,6 +41,7 @@ export default function FillUpsPage() {
|
|||||||
cost: '',
|
cost: '',
|
||||||
currency: 'EUR',
|
currency: 'EUR',
|
||||||
fuelType: '',
|
fuelType: '',
|
||||||
|
date: '',
|
||||||
});
|
});
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
@@ -58,7 +74,7 @@ export default function FillUpsPage() {
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const newFill = await res.json();
|
const newFill = await res.json();
|
||||||
setFillups((prev) => [newFill, ...prev]);
|
setFillups((prev) => [newFill, ...prev]);
|
||||||
setForm({ mileage: '', liters: '', cost: '', currency: 'EUR', fuelType: carFuelTypes[0] || '' });
|
setForm({ mileage: '', liters: '', cost: '', currency: 'EUR', fuelType: carFuelTypes[0] || '', date: '' });
|
||||||
setError('');
|
setError('');
|
||||||
} else {
|
} else {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@@ -77,10 +93,23 @@ export default function FillUpsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="max-w-2xl mx-auto p-8 space-y-8 bg-[var(--muted)] rounded-xl shadow">
|
<main className="max-w-2xl mx-auto p-8 space-y-8 bg-[var(--muted)] rounded-xl shadow mb-8">
|
||||||
<h1 className="text-2xl font-bold text-[var(--primary)]">Fuel Fill-Ups</h1>
|
<h1 className="text-2xl font-bold text-[var(--primary)] text-center">Fuel Fill-Ups</h1>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="grid grid-cols-1 sm:grid-cols-2 gap-4 bg-white p-6 rounded-lg border border-[var(--border)]">
|
<form onSubmit={handleSubmit} className="grid grid-cols-1 sm:grid-cols-2 gap-4 bg-white p-6 rounded-lg border border-[var(--border)]">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label htmlFor="date" className="font-medium text-[var(--primary)]">Date & Time</label>
|
||||||
|
<input
|
||||||
|
id="date"
|
||||||
|
name="date"
|
||||||
|
type="datetime-local"
|
||||||
|
value={form.date}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
className="border border-[var(--border)] px-4 py-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--primary)] bg-[var(--muted)] text-[var(--foreground)]"
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-gray-500">Select the date and hour of the fill-up</span>
|
||||||
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label htmlFor="mileage" className="font-medium text-[var(--primary)]">Odometer</label>
|
<label htmlFor="mileage" className="font-medium text-[var(--primary)]">Odometer</label>
|
||||||
<input
|
<input
|
||||||
@@ -186,11 +215,69 @@ export default function FillUpsPage() {
|
|||||||
{error && <p className="col-span-full text-red-500 text-center mt-2">{error}</p>}
|
{error && <p className="col-span-full text-red-500 text-center mt-2">{error}</p>}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<ul className="space-y-3">
|
{/* Toggle and Filters for Fill-Ups */}
|
||||||
{fillups.map((fill) => (
|
{!showFillUps && (
|
||||||
<FillUpCard key={fill.id} fill={{ ...fill, car: { fuelType: fill.fuelType } }} />
|
<div className="flex justify-center mb-4">
|
||||||
))}
|
<button
|
||||||
</ul>
|
type="button"
|
||||||
|
className={`px-4 py-2 rounded-lg font-semibold shadow border border-[var(--border)] transition-all bg-[var(--muted)] text-[var(--primary)]`}
|
||||||
|
onClick={() => setShowFillUps(true)}
|
||||||
|
>
|
||||||
|
Show Fill-Ups
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{showFillUps && (
|
||||||
|
<div className="flex flex-col items-center gap-4 mb-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="px-4 py-2 rounded-lg font-semibold shadow border border-[var(--border)] transition-all w-full max-w-xs bg-[var(--primary)] text-white mb-2"
|
||||||
|
onClick={() => setShowFillUps(false)}
|
||||||
|
>
|
||||||
|
Hide Fill-Ups
|
||||||
|
</button>
|
||||||
|
<div className="flex flex-wrap gap-2 items-center justify-center w-full sm:flex-nowrap">
|
||||||
|
<div className="flex flex-col sm:flex-row gap-2 w-full sm:w-auto">
|
||||||
|
<label className="text-sm font-medium text-[var(--primary)]">Filter by Fuel Type:</label>
|
||||||
|
<select
|
||||||
|
value={filterFuelType}
|
||||||
|
onChange={e => setFilterFuelType(e.target.value)}
|
||||||
|
className="border border-[var(--border)] px-2 py-1 rounded-lg bg-[var(--background)] text-[var(--foreground)] font-semibold"
|
||||||
|
>
|
||||||
|
<option value="">All</option>
|
||||||
|
{carFuelTypes.map((type) => (
|
||||||
|
<option key={type} value={type}>{type}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col sm:flex-row gap-2 w-full sm:w-auto">
|
||||||
|
<label className="text-sm font-medium text-[var(--primary)] sm:ml-2">Sort by:</label>
|
||||||
|
<select
|
||||||
|
value={sortOrder}
|
||||||
|
onChange={e => setSortOrder(e.target.value)}
|
||||||
|
className="border border-[var(--border)] px-2 py-1 rounded-lg bg-[var(--background)] text-[var(--foreground)] font-semibold"
|
||||||
|
>
|
||||||
|
<option value="date-desc">Newest</option>
|
||||||
|
<option value="date-asc">Oldest</option>
|
||||||
|
<option value="mileage-desc">Highest Mileage</option>
|
||||||
|
<option value="mileage-asc">Lowest Mileage</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{showFillUps && (
|
||||||
|
<div className="mb-8">
|
||||||
|
<ul
|
||||||
|
className="space-y-3 overflow-y-auto pr-2"
|
||||||
|
style={{ maxHeight: `${3 * 6.5}rem`, minHeight: filteredFillUps.length > 0 ? '6.5rem' : undefined }}
|
||||||
|
>
|
||||||
|
{filteredFillUps.map((fill) => (
|
||||||
|
<FillUpCard key={fill.id} fill={{ ...fill, car: { fuelType: fill.fuelType } }} />
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
import { getSession } from '@/lib/auth';
|
import { getSession } from '@/lib/auth';
|
||||||
import { prisma } from '@/lib/prisma';
|
import { prisma } from '@/lib/prisma';
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
@@ -12,14 +13,13 @@ interface CarDetailPageProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default async function CarDetailPage({ params }: CarDetailPageProps) {
|
export default async function CarDetailPage({ params }: CarDetailPageProps) {
|
||||||
const resolvedParams = await params;
|
|
||||||
const session = await getSession();
|
const session = await getSession();
|
||||||
if (!session) redirect('/auth/login');
|
if (!session) redirect('/auth/login');
|
||||||
|
|
||||||
const userEmail = session.user?.email || '';
|
const userEmail = session.user?.email || '';
|
||||||
const car = await prisma.car.findFirst({
|
const car = await prisma.car.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: resolvedParams.carId,
|
id: params.carId,
|
||||||
user: { email: userEmail },
|
user: { email: userEmail },
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
@@ -44,74 +44,88 @@ export default async function CarDetailPage({ params }: CarDetailPageProps) {
|
|||||||
ELECTRIC: { color: 'bg-green-100 text-green-700', icon: <FaBolt className="inline mr-1 text-green-500" /> },
|
ELECTRIC: { color: 'bg-green-100 text-green-700', icon: <FaBolt className="inline mr-1 text-green-500" /> },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Normalize fuelTypes for compatibility
|
||||||
|
const fuelTypes = Array.isArray(car.fuelTypes) ? car.fuelTypes : car.fuelTypes ? [car.fuelTypes] : [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="max-w-3xl mx-auto p-0 md:p-8 mt-8">
|
<main className="max-w-full md:max-w-3xl mx-auto p-2 sm:p-4 md:p-8 mt-8">
|
||||||
<div className="rounded-3xl shadow-2xl bg-[var(--muted)] p-0 md:p-10 flex flex-col gap-10 border border-[var(--border)]">
|
<div className="rounded-3xl shadow-2xl bg-[var(--muted)] p-2 sm:p-4 md:p-10 flex flex-col gap-4 sm:gap-6 md:gap-10 border border-[var(--border)] w-[96vw] max-w-[98vw] sm:w-[90vw] sm:max-w-[95vw] md:w-auto md:max-w-none">
|
||||||
{/* Car Icon & Header */}
|
{/* Car Icon & Header */}
|
||||||
<div className="flex flex-col items-center gap-3 p-8 rounded-t-3xl bg-[var(--muted)] border-b border-[var(--border)] shadow-sm">
|
<div className="flex flex-col items-center gap-2 sm:gap-3 p-2 sm:p-4 md:p-8 rounded-t-3xl bg-[var(--muted)] border-b border-[var(--border)] shadow-sm">
|
||||||
<span className="text-6xl text-[var(--primary)] drop-shadow-lg mb-2"><FaCarSide /></span>
|
<div className="flex flex-row flex-wrap items-center justify-center gap-2 w-full">
|
||||||
<h1 className="text-4xl font-extrabold text-[var(--primary)] leading-tight tracking-tight flex items-center gap-3">
|
<span className="text-3xl sm:text-4xl md:text-6xl text-[var(--primary)] drop-shadow-lg"><FaCarSide /></span>
|
||||||
{car.name}
|
<h1 className="text-xl sm:text-2xl md:text-4xl font-extrabold text-[var(--primary)] leading-tight tracking-tight flex flex-row flex-wrap items-center gap-1 sm:gap-2 md:gap-3">
|
||||||
<span className="inline-block px-3 py-1 rounded-full text-base font-bold bg-gray-200 text-gray-700 ml-2">{car.year}</span>
|
<span>{car.name}</span>
|
||||||
{Array.isArray(car.fuelTypes) && car.fuelTypes.length > 1 ? (
|
<span className="inline-block px-2 md:px-3 py-1 rounded-full text-sm md:text-base font-bold bg-gray-200 text-gray-700 ml-2">{car.year}</span>
|
||||||
<span className="ml-2 flex items-center">
|
</h1>
|
||||||
<span className={`px-2 py-1 rounded-l-full text-xs font-bold ${fuelMeta[car.fuelTypes[0]]?.color || 'bg-gray-100 text-gray-700'}`} style={{ borderTopRightRadius: 0, borderBottomRightRadius: 0, borderRight: '1px solid #ccc' }}>{fuelMeta[car.fuelTypes[0]]?.icon}{car.fuelTypes[0]}</span>
|
</div>
|
||||||
<span className={`px-2 py-1 rounded-r-full text-xs font-bold ${fuelMeta[car.fuelTypes[1]]?.color || 'bg-gray-100 text-gray-700'}`} style={{ borderTopLeftRadius: 0, borderBottomLeftRadius: 0 }}>{fuelMeta[car.fuelTypes[1]]?.icon}{car.fuelTypes[1]}</span>
|
{/* Fuel Types Row */}
|
||||||
</span>
|
{fuelTypes.length > 0 && (
|
||||||
) : Array.isArray(car.fuelTypes) && car.fuelTypes.length === 1 ? (
|
<div className="flex flex-row flex-wrap items-center justify-center gap-2 w-full mt-1">
|
||||||
<span className={`ml-2 px-2 py-1 rounded-full text-xs font-bold ${fuelMeta[car.fuelTypes[0]]?.color || 'bg-gray-100 text-gray-700'}`}>{fuelMeta[car.fuelTypes[0]]?.icon}{car.fuelTypes[0]}</span>
|
{fuelTypes.length > 1 ? (
|
||||||
) : null}
|
<>
|
||||||
</h1>
|
<span className={`px-2 py-1 rounded-l-full text-xs font-bold ${fuelMeta[fuelTypes[0]]?.color || 'bg-gray-100 text-gray-700'}`} style={{ borderTopRightRadius: 0, borderBottomRightRadius: 0, borderRight: '1px solid #ccc' }}>{fuelMeta[fuelTypes[0]]?.icon}{fuelTypes[0]}</span>
|
||||||
|
<span className={`px-2 py-1 rounded-r-full text-xs font-bold ${fuelMeta[fuelTypes[1]]?.color || 'bg-gray-100 text-gray-700'}`} style={{ borderTopLeftRadius: 0, borderBottomLeftRadius: 0 }}>{fuelMeta[fuelTypes[1]]?.icon}{fuelTypes[1]}</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span className={`px-2 py-1 rounded-full text-xs font-bold ${fuelMeta[fuelTypes[0]]?.color || 'bg-gray-100 text-gray-700'}`}>{fuelMeta[fuelTypes[0]]?.icon}{fuelTypes[0]}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/* Car Details */}
|
{/* Car Details */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 px-8">
|
<div className="flex flex-col gap-4 sm:gap-6 md:gap-8 px-1 sm:px-2 md:px-8 w-full">
|
||||||
<div className="flex items-center gap-3">
|
{/* Make & Model Row */}
|
||||||
<FaIndustry className="text-2xl text-gray-400" />
|
<div className="flex flex-col md:flex-row items-center justify-center gap-2 md:gap-8 w-full">
|
||||||
<span className="font-semibold text-[var(--foreground)]">Make:</span>
|
<div className="flex items-center gap-2 md:gap-3">
|
||||||
<span className="ml-1 text-gray-400 font-mono text-lg">{car.make}</span>
|
<FaIndustry className="text-xl md:text-2xl text-gray-400" />
|
||||||
|
<span className="font-semibold text-[var(--foreground)]">Make:</span>
|
||||||
|
<span className="ml-1 text-gray-400 font-mono text-base md:text-lg">{car.make}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 md:gap-3 md:ml-8">
|
||||||
|
<FaCarSide className="text-xl md:text-2xl text-gray-400" />
|
||||||
|
<span className="font-semibold text-[var(--foreground)]">Model:</span>
|
||||||
|
<span className="ml-1 text-gray-400 font-mono text-base md:text-lg">{car.model}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-2 md:gap-3 justify-center w-full">
|
||||||
<FaCarSide className="text-2xl text-gray-400" />
|
<FaCalendarAlt className="text-xl md:text-2xl text-gray-400" />
|
||||||
<span className="font-semibold text-[var(--foreground)]">Model:</span>
|
|
||||||
<span className="ml-1 text-gray-400 font-mono text-lg">{car.model}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<FaCalendarAlt className="text-2xl text-gray-400" />
|
|
||||||
<span className="font-semibold text-[var(--foreground)]">Year:</span>
|
<span className="font-semibold text-[var(--foreground)]">Year:</span>
|
||||||
<span className="ml-1 text-gray-400 font-mono text-lg">{car.year}</span>
|
<span className="text-gray-400 font-mono text-base md:text-lg">{car.year}</span>
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
{Array.isArray(car.fuelTypes) && car.fuelTypes.length > 0
|
|
||||||
? car.fuelTypes.map((ft: string) => (
|
|
||||||
<span key={ft} className={`ml-1 px-2 py-1 rounded-full text-xs font-bold flex items-center gap-1 ${fuelMeta[ft]?.color || 'bg-gray-100 text-gray-700'}`}>{fuelMeta[ft]?.icon}{ft}</span>
|
|
||||||
))
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex flex-wrap gap-4 justify-center px-8 pb-8">
|
<div className="flex flex-col md:flex-row flex-wrap gap-2 md:gap-4 justify-center items-center px-1 sm:px-2 md:px-8 pb-2 sm:pb-4 md:pb-8 w-full">
|
||||||
<Link
|
<div className="w-full max-w-xs md:w-auto md:max-w-none flex justify-center">
|
||||||
href={`/dashboard/cars/${car.id}/mileage`}
|
<Link
|
||||||
className="flex items-center gap-2 bg-[var(--primary)] text-white px-6 py-3 rounded-lg font-bold shadow-lg hover:bg-blue-700 hover:scale-[1.03] transition-all"
|
href={`/dashboard/cars/${car.id}/mileage`}
|
||||||
>
|
className="flex items-center justify-center gap-2 bg-blue-500 dark:bg-blue-600 text-white px-4 md:px-6 py-2 md:py-3 rounded-lg font-bold shadow-lg hover:bg-blue-700 dark:hover:bg-blue-800 hover:scale-[1.03] transition-all w-full"
|
||||||
➕ Add Mileage
|
>
|
||||||
</Link>
|
➕ Add Mileage
|
||||||
<Link
|
</Link>
|
||||||
href={`/dashboard/cars/${car.id}/fillups`}
|
</div>
|
||||||
className="flex items-center gap-2 bg-[var(--secondary)] text-white px-6 py-3 rounded-lg font-bold shadow-lg hover:bg-green-700 hover:scale-[1.03] transition-all"
|
<div className="w-full max-w-xs md:w-auto md:max-w-none flex justify-center">
|
||||||
>
|
<Link
|
||||||
➕ Add Fill-Up
|
href={`/dashboard/cars/${car.id}/fillups`}
|
||||||
</Link>
|
className="flex items-center justify-center gap-2 bg-green-500 dark:bg-green-600 text-white px-4 md:px-6 py-2 md:py-3 rounded-lg font-bold shadow-lg hover:bg-green-700 dark:hover:bg-green-800 hover:scale-[1.03] transition-all w-full"
|
||||||
<Link
|
>
|
||||||
href={`/dashboard/cars/${car.id}/stats`}
|
➕ Add Fill-Up
|
||||||
className="flex items-center gap-2 bg-[var(--accent)] text-white px-6 py-3 rounded-lg font-bold shadow-lg hover:bg-purple-800 hover:scale-[1.03] transition-all"
|
</Link>
|
||||||
>
|
</div>
|
||||||
📊 View Stats
|
<div className="w-full max-w-xs md:w-auto md:max-w-none flex justify-center">
|
||||||
</Link>
|
<Link
|
||||||
<DeleteCarButton carId={car.id} />
|
href={`/dashboard/cars/${car.id}/stats`}
|
||||||
|
className="flex items-center justify-center gap-2 bg-indigo-500 dark:bg-indigo-600 text-white px-4 md:px-6 py-2 md:py-3 rounded-lg font-bold shadow-lg hover:bg-indigo-700 dark:hover:bg-indigo-800 hover:scale-[1.03] transition-all w-full"
|
||||||
|
>
|
||||||
|
📊 View Stats
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="w-full max-w-xs md:w-auto md:max-w-none flex justify-center">
|
||||||
|
<DeleteCarButton carId={car.id} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,14 +17,22 @@ export default function AddCarPage() {
|
|||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||||
const { name, value, type, checked } = e.target;
|
const { name, value } = e.target;
|
||||||
if (name === 'fuelTypes') {
|
if (name === 'fuelTypes') {
|
||||||
|
// Only checkboxes will trigger this block
|
||||||
|
const checked = (e.target as HTMLInputElement).checked;
|
||||||
setForm((prev) => {
|
setForm((prev) => {
|
||||||
let updated = prev.fuelTypes;
|
let updated = prev.fuelTypes;
|
||||||
if (checked) {
|
if (checked) {
|
||||||
updated = [...prev.fuelTypes, value];
|
// Only add if less than 2 selected
|
||||||
|
if (prev.fuelTypes.length < 2) {
|
||||||
|
updated = [...prev.fuelTypes, value];
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
updated = prev.fuelTypes.filter((t: string) => t !== value);
|
// Only remove if more than 1 selected
|
||||||
|
if (prev.fuelTypes.length > 1) {
|
||||||
|
updated = prev.fuelTypes.filter((t: string) => t !== value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return { ...prev, fuelTypes: updated };
|
return { ...prev, fuelTypes: updated };
|
||||||
});
|
});
|
||||||
@@ -116,19 +124,26 @@ export default function AddCarPage() {
|
|||||||
<div>
|
<div>
|
||||||
<label className="block mb-1 font-semibold text-[var(--foreground)]">Fuel Types</label>
|
<label className="block mb-1 font-semibold text-[var(--foreground)]">Fuel Types</label>
|
||||||
<div className="flex flex-wrap gap-4">
|
<div className="flex flex-wrap gap-4">
|
||||||
{fuelTypes.map((type) => (
|
{fuelTypes.map((type) => {
|
||||||
<label key={type} className="flex items-center gap-2">
|
const checked = form.fuelTypes.includes(type);
|
||||||
<input
|
const disabled =
|
||||||
type="checkbox"
|
(!checked && form.fuelTypes.length >= 2) ||
|
||||||
name="fuelTypes"
|
(checked && form.fuelTypes.length === 1); // can't uncheck last
|
||||||
value={type}
|
return (
|
||||||
checked={form.fuelTypes.includes(type)}
|
<label key={type} className="flex items-center gap-2">
|
||||||
onChange={handleChange}
|
<input
|
||||||
className="accent-[var(--primary)]"
|
type="checkbox"
|
||||||
/>
|
name="fuelTypes"
|
||||||
{type}
|
value={type}
|
||||||
</label>
|
checked={checked}
|
||||||
))}
|
onChange={handleChange}
|
||||||
|
className="accent-[var(--primary)]"
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
{type}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
|||||||
+7
-7
@@ -9,12 +9,12 @@
|
|||||||
--accent: #a21caf; /* purple-700 */
|
--accent: #a21caf; /* purple-700 */
|
||||||
--muted: #f3f4f6; /* gray-100 */
|
--muted: #f3f4f6; /* gray-100 */
|
||||||
--border: #e5e7eb; /* gray-200 */
|
--border: #e5e7eb; /* gray-200 */
|
||||||
--statcard-primary-bg: #e0e7ff; /* indigo-100 */
|
--statcard-primary-bg: #e5e7eb; /* gray-200 */
|
||||||
--statcard-primary-border: #2563eb; /* blue-600 */
|
--statcard-primary-border: #2563eb; /* blue-600 */
|
||||||
--statcard-secondary-bg: #dcfce7; /* green-100 */
|
--statcard-secondary-bg: #f1f5f9; /* gray-100/blue-50 */
|
||||||
--statcard-secondary-border: #22c55e; /* green-500 */
|
--statcard-secondary-border: #22c55e; /* green-500 */
|
||||||
--statcard-accent-bg: #f3e8ff; /* purple-100 */
|
--statcard-accent-bg: #e0f2fe; /* sky-100 */
|
||||||
--statcard-accent-border: #a21caf; /* purple-700 */
|
--statcard-accent-border: #0ea5e9; /* sky-500 */
|
||||||
--statcard-default-bg: var(--background);
|
--statcard-default-bg: var(--background);
|
||||||
--statcard-default-border: var(--border);
|
--statcard-default-border: var(--border);
|
||||||
}
|
}
|
||||||
@@ -29,8 +29,8 @@
|
|||||||
--statcard-primary-border: #2563eb; /* blue-600 */
|
--statcard-primary-border: #2563eb; /* blue-600 */
|
||||||
--statcard-secondary-bg: #052e16; /* green-950 */
|
--statcard-secondary-bg: #052e16; /* green-950 */
|
||||||
--statcard-secondary-border: #22c55e; /* green-500 */
|
--statcard-secondary-border: #22c55e; /* green-500 */
|
||||||
--statcard-accent-bg: #3b0764; /* purple-950 */
|
--statcard-accent-bg: #0f172a; /* slate-900 */
|
||||||
--statcard-accent-border: #a21caf; /* purple-700 */
|
--statcard-accent-border: #0ea5e9; /* sky-500 */
|
||||||
--statcard-default-bg: var(--background);
|
--statcard-default-bg: var(--background);
|
||||||
--statcard-default-border: var(--border);
|
--statcard-default-border: var(--border);
|
||||||
}
|
}
|
||||||
@@ -53,4 +53,4 @@ body {
|
|||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: var(--border);
|
background: var(--border);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ import { Geist, Geist_Mono } from "next/font/google";
|
|||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import ClientNavbar from "../components/ClientNavbar";
|
import ClientNavbar from "../components/ClientNavbar";
|
||||||
import Providers from "./providers";
|
import Providers from "./providers";
|
||||||
|
import Footer from "../components/Footer";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
@@ -52,6 +53,7 @@ export default async function RootLayout({
|
|||||||
<div className="min-h-screen flex flex-col">
|
<div className="min-h-screen flex flex-col">
|
||||||
<div className="h-4" /> {/* Spacer between navbar and content */}
|
<div className="h-4" /> {/* Spacer between navbar and content */}
|
||||||
{children}
|
{children}
|
||||||
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
</Providers>
|
</Providers>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+3
-14
@@ -28,9 +28,9 @@ function UseCaseBlocks() {
|
|||||||
<h2 className="text-3xl font-bold text-center text-[var(--primary)] mb-8">Who Is This For?</h2>
|
<h2 className="text-3xl font-bold text-center text-[var(--primary)] mb-8">Who Is This For?</h2>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-8">
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-8">
|
||||||
{blocks.map((b) => (
|
{blocks.map((b) => (
|
||||||
<div key={b.title} className="group bg-white/40 dark:bg-[var(--muted)]/60 rounded-xl shadow p-6 flex flex-col items-center gap-3 border border-[var(--border)] transition-all hover:scale-105 hover:bg-white/60 dark:hover:bg-[var(--muted)]/80 animate-fade-in backdrop-blur">
|
<div key={b.title} className="group bg-[var(--muted)] rounded-xl shadow p-6 flex flex-col items-center gap-3 border border-[var(--border)] transition-all hover:scale-105 hover:bg-[var(--primary)]/10 animate-fade-in">
|
||||||
<span className="text-4xl group-hover:scale-125 transition-transform">{b.icon}</span>
|
<span className="text-4xl group-hover:scale-125 transition-transform">{b.icon}</span>
|
||||||
<h3 className="font-semibold text-lg text-[var(--primary)] group-hover:text-[var(--secondary)]">{b.title}</h3>
|
<h3 className="font-semibold text-lg text-[var(--primary)] group-hover:text-[var(--secondary)] text-center w-full">{b.title}</h3>
|
||||||
<p className="text-center text-[var(--foreground)]/80 group-hover:text-[var(--foreground)]">{b.desc}</p>
|
<p className="text-center text-[var(--foreground)]/80 group-hover:text-[var(--foreground)]">{b.desc}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -66,7 +66,7 @@ function FeatureGrid() {
|
|||||||
{features.map((f, i) => (
|
{features.map((f, i) => (
|
||||||
<div
|
<div
|
||||||
key={f.title}
|
key={f.title}
|
||||||
className={`flex flex-col items-center text-center gap-2 bg-white/40 dark:bg-[var(--muted)]/60 rounded-xl shadow p-6 border border-[var(--border)] transition-all duration-300 cursor-pointer ${open === i ? "scale-105 bg-white/60 dark:bg-[var(--muted)]/80" : ""} backdrop-blur`}
|
className={`flex flex-col items-center text-center gap-2 bg-[var(--muted)] rounded-xl shadow p-6 border border-[var(--border)] transition-all duration-300 cursor-pointer ${open === i ? "scale-105 bg-[var(--primary)]/10" : ""}`}
|
||||||
onClick={() => setOpen(open === i ? null : i)}
|
onClick={() => setOpen(open === i ? null : i)}
|
||||||
onMouseEnter={() => setOpen(i)}
|
onMouseEnter={() => setOpen(i)}
|
||||||
onMouseLeave={() => setOpen(null)}
|
onMouseLeave={() => setOpen(null)}
|
||||||
@@ -170,17 +170,6 @@ export default function Home() {
|
|||||||
|
|
||||||
{/* ...existing reviews, etc... */}
|
{/* ...existing reviews, etc... */}
|
||||||
|
|
||||||
{/* Footer */}
|
|
||||||
<footer className="w-full py-6 flex flex-col items-center justify-center border-t border-[var(--border)] bg-[var(--muted)] text-sm text-gray-500 gap-2 mt-auto">
|
|
||||||
<nav className="flex gap-6 mb-1">
|
|
||||||
<Link href="/about" className="hover:text-[var(--primary)] transition">About</Link>
|
|
||||||
<Link href="/contact" className="hover:text-[var(--primary)] transition">Contact</Link>
|
|
||||||
<Link href="/bugs" className="hover:text-[var(--primary)] transition">Bug Report</Link>
|
|
||||||
<Link href="/improvment_ideas" className="hover:text-[var(--primary)] transition">Improvment Ideas</Link>
|
|
||||||
<Link href="/privacy" className="hover:text-[var(--primary)] transition">Privacy Policy</Link>
|
|
||||||
</nav>
|
|
||||||
<span>© {new Date().getFullYear()} Car Fuel Tracker.</span>
|
|
||||||
</footer>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-13
@@ -23,19 +23,23 @@ export default function CarGrid({ cars }: { cars: any[] }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex flex-wrap gap-4 mb-4 items-center">
|
<div className="flex flex-wrap gap-4 mb-4 items-center sm:flex-nowrap">
|
||||||
<label htmlFor="fuel-filter" className="font-medium text-sm">Fuel:</label>
|
<div className="flex flex-col sm:flex-row gap-2 w-full sm:w-auto">
|
||||||
<select id="fuel-filter" value={fuel} onChange={e => setFuel(e.target.value)} className="border rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-[var(--primary)]">
|
<label htmlFor="fuel-filter" className="font-medium text-sm">Fuel:</label>
|
||||||
{fuelTypes.map((type) => (
|
<select id="fuel-filter" value={fuel} onChange={e => setFuel(e.target.value)} className="border rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-[var(--primary)]">
|
||||||
<option key={type} value={type}>{type.charAt(0) + type.slice(1).toLowerCase()}</option>
|
{fuelTypes.map((type) => (
|
||||||
))}
|
<option key={type} value={type}>{type.charAt(0) + type.slice(1).toLowerCase()}</option>
|
||||||
</select>
|
))}
|
||||||
<label htmlFor="sort-cars" className="font-medium text-sm ml-4">Sort by:</label>
|
</select>
|
||||||
<select id="sort-cars" value={sort} onChange={e => setSort(e.target.value)} className="border rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-[var(--primary)]">
|
</div>
|
||||||
{sortOptions.map((opt) => (
|
<div className="flex flex-col sm:flex-row gap-2 w-full sm:w-auto">
|
||||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
<label htmlFor="sort-cars" className="font-medium text-sm sm:ml-4">Sort by:</label>
|
||||||
))}
|
<select id="sort-cars" value={sort} onChange={e => setSort(e.target.value)} className="border rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-[var(--primary)]">
|
||||||
</select>
|
{sortOptions.map((opt) => (
|
||||||
|
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{filtered.length ? (
|
{filtered.length ? (
|
||||||
<ul className="grid gap-4 sm:grid-cols-2 md:grid-cols-3" role="list" aria-label="Your cars">
|
<ul className="grid gap-4 sm:grid-cols-2 md:grid-cols-3" role="list" aria-label="Your cars">
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useState } from "react";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import ThemeToggle from "./ThemeToggle";
|
import ThemeToggle from "./ThemeToggle";
|
||||||
import Image from "./Image";
|
|
||||||
|
|
||||||
export default function ClientNavbar() {
|
export default function ClientNavbar() {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
@@ -12,19 +11,6 @@ export default function ClientNavbar() {
|
|||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const userInitial = session?.user?.name?.[0]?.toUpperCase() || session?.user?.email?.[0]?.toUpperCase() || "U";
|
const userInitial = session?.user?.name?.[0]?.toUpperCase() || session?.user?.email?.[0]?.toUpperCase() || "U";
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [theme, setTheme] = useState<string | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Listen for theme changes
|
|
||||||
const updateTheme = () => {
|
|
||||||
if (document.documentElement.classList.contains("dark")) setTheme("dark");
|
|
||||||
else setTheme("light");
|
|
||||||
};
|
|
||||||
updateTheme();
|
|
||||||
const observer = new MutationObserver(updateTheme);
|
|
||||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] });
|
|
||||||
return () => observer.disconnect();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
function NavLink({ href, children }: { href: string; children: React.ReactNode }) {
|
function NavLink({ href, children }: { href: string; children: React.ReactNode }) {
|
||||||
const isActive = pathname === href;
|
const isActive = pathname === href;
|
||||||
@@ -43,27 +29,7 @@ export default function ClientNavbar() {
|
|||||||
return (
|
return (
|
||||||
<nav className="flex w-full items-center justify-between">
|
<nav className="flex w-full items-center justify-between">
|
||||||
<Link href="/" className="flex items-center gap-2 text-2xl font-bold tracking-tight text-[var(--primary)] hover:scale-105 transition">
|
<Link href="/" className="flex items-center gap-2 text-2xl font-bold tracking-tight text-[var(--primary)] hover:scale-105 transition">
|
||||||
<span className="relative w-10 h-10 block">
|
<span className="text-3xl">🚗</span>
|
||||||
{theme === "dark" ? (
|
|
||||||
<Image
|
|
||||||
src="/Light_Logo.png"
|
|
||||||
alt="FuelTrack logo light"
|
|
||||||
className="w-10 h-10 object-contain transition-colors"
|
|
||||||
width={40}
|
|
||||||
height={40}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Image
|
|
||||||
src="/Dark_Logo.png"
|
|
||||||
alt="FuelTrack logo dark"
|
|
||||||
className="w-10 h-10 object-contain transition-colors"
|
|
||||||
width={40}
|
|
||||||
height={40}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<span>FuelTrack</span>
|
<span>FuelTrack</span>
|
||||||
</Link>
|
</Link>
|
||||||
{/* Desktop nav */}
|
{/* Desktop nav */}
|
||||||
@@ -103,6 +69,7 @@ export default function ClientNavbar() {
|
|||||||
<div className="absolute right-0 mt-2 w-48 bg-[var(--muted)] rounded-xl shadow-lg border border-[var(--border)] z-50 flex flex-col p-2 gap-2 animate-fade-in">
|
<div className="absolute right-0 mt-2 w-48 bg-[var(--muted)] rounded-xl shadow-lg border border-[var(--border)] z-50 flex flex-col p-2 gap-2 animate-fade-in">
|
||||||
<NavLink href="/dashboard">Dashboard</NavLink>
|
<NavLink href="/dashboard">Dashboard</NavLink>
|
||||||
{isLoggedIn && <NavLink href="/dashboard/cars/new">Add Car</NavLink>}
|
{isLoggedIn && <NavLink href="/dashboard/cars/new">Add Car</NavLink>}
|
||||||
|
<ThemeToggle />
|
||||||
{isLoggedIn ? (
|
{isLoggedIn ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="w-8 h-8 rounded-full bg-[var(--primary)] text-white flex items-center justify-center font-bold text-base border-2 border-[var(--border)] shadow">{userInitial}</span>
|
<span className="w-8 h-8 rounded-full bg-[var(--primary)] text-white flex items-center justify-center font-bold text-base border-2 border-[var(--border)] shadow">{userInitial}</span>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export default function FillUpCard({ fill }: { fill: any }) {
|
|||||||
<div className="border border-[var(--border)] rounded p-3 bg-[var(--background)] shadow-sm">
|
<div className="border border-[var(--border)] rounded p-3 bg-[var(--background)] shadow-sm">
|
||||||
<div className="font-semibold text-[var(--foreground)]">
|
<div className="font-semibold text-[var(--foreground)]">
|
||||||
{fill.mileage} km - {fill.liters} {isElectric ? 'kWh' : 'L'}
|
{fill.mileage} km - {fill.liters} {isElectric ? 'kWh' : 'L'}
|
||||||
|
<span className="ml-2 text-xs text-[var(--primary)] font-bold">{fill.fuelType}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-[var(--foreground)]/60">
|
<div className="text-sm text-[var(--foreground)]/60">
|
||||||
{fill.cost} {fill.currency} — {new Date(fill.date).toLocaleString()}
|
{fill.cost} {fill.currency} — {new Date(fill.date).toLocaleString()}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export default function Footer() {
|
||||||
|
return (
|
||||||
|
<footer className="w-full py-6 flex flex-col items-center justify-center border-t border-[var(--border)] bg-[var(--muted)] text-sm text-gray-500 gap-2 mt-auto">
|
||||||
|
<nav className="flex flex-wrap justify-center gap-4 sm:gap-6 mb-1 w-full px-2">
|
||||||
|
<Link href="/about" className="hover:text-[var(--primary)] transition">About</Link>
|
||||||
|
<Link href="/contact" className="hover:text-[var(--primary)] transition">Contact</Link>
|
||||||
|
<Link href="/bugs" className="hover:text-[var(--primary)] transition">Bug Report</Link>
|
||||||
|
<Link href="/improvment_ideas" className="hover:text-[var(--primary)] transition">Improvment Ideas</Link>
|
||||||
|
<Link href="/privacy" className="hover:text-[var(--primary)] transition">Privacy Policy</Link>
|
||||||
|
</nav>
|
||||||
|
<span>© {new Date().getFullYear()} FuelTrack</span>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import Image from "next/image";
|
|
||||||
|
|
||||||
export default Image;
|
|
||||||
|
|||||||
@@ -24,17 +24,17 @@ export default function LiveStatsPreview() {
|
|||||||
<section className="w-full max-w-4xl mx-auto py-8 px-4">
|
<section className="w-full max-w-4xl mx-auto py-8 px-4">
|
||||||
<h2 className="text-2xl font-bold text-center text-[var(--primary)] mb-6">Live Community Stats</h2>
|
<h2 className="text-2xl font-bold text-center text-[var(--primary)] mb-6">Live Community Stats</h2>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6">
|
||||||
<div className="bg-white/40 dark:bg-[var(--muted)]/60 rounded-xl shadow p-6 flex flex-col items-center gap-2 border border-[var(--border)] animate-fade-in backdrop-blur">
|
<div className="bg-[var(--muted)] rounded-xl shadow p-6 flex flex-col items-center gap-2 border border-[var(--border)] animate-fade-in">
|
||||||
<span className="text-3xl">⛽</span>
|
<span className="text-3xl">⛽</span>
|
||||||
<span className="font-semibold text-lg">Avg. Fuel Price</span>
|
<span className="font-semibold text-lg">Avg. Fuel Price</span>
|
||||||
<span className="text-2xl font-bold text-[var(--primary)]">{loading ? "..." : `${stats?.avgFuelPrice.toFixed(2)} RON/L`}</span>
|
<span className="text-2xl font-bold text-[var(--primary)]">{loading ? "..." : `${stats?.avgFuelPrice.toFixed(2)} RON/L`}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white/40 dark:bg-[var(--muted)]/60 rounded-xl shadow p-6 flex flex-col items-center gap-2 border border-[var(--border)] animate-fade-in backdrop-blur">
|
<div className="bg-[var(--muted)] rounded-xl shadow p-6 flex flex-col items-center gap-2 border border-[var(--border)] animate-fade-in">
|
||||||
<span className="text-3xl">📉</span>
|
<span className="text-3xl">📉</span>
|
||||||
<span className="font-semibold text-lg">Avg. Efficiency</span>
|
<span className="font-semibold text-lg">Avg. Efficiency</span>
|
||||||
<span className="text-2xl font-bold text-[var(--primary)]">{loading ? "..." : `${stats?.avgEfficiency.toFixed(1)} L/100km`}</span>
|
<span className="text-2xl font-bold text-[var(--primary)]">{loading ? "..." : `${stats?.avgEfficiency.toFixed(1)} L/100km`}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white/40 dark:bg-[var(--muted)]/60 rounded-xl shadow p-6 flex flex-col items-center gap-2 border border-[var(--border)] animate-fade-in backdrop-blur">
|
<div className="bg-[var(--muted)] rounded-xl shadow p-6 flex flex-col items-center gap-2 border border-[var(--border)] animate-fade-in">
|
||||||
<span className="text-3xl">🌱</span>
|
<span className="text-3xl">🌱</span>
|
||||||
<span className="font-semibold text-lg text-center w-full">CO₂ Saved by Hybrids/EVs</span>
|
<span className="font-semibold text-lg text-center w-full">CO₂ Saved by Hybrids/EVs</span>
|
||||||
<span className="text-2xl font-bold text-green-700">{loading ? "..." : `${stats?.totalCO2Saved.toLocaleString()} kg`}</span>
|
<span className="text-2xl font-bold text-green-700">{loading ? "..." : `${stats?.totalCO2Saved.toLocaleString()} kg`}</span>
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
"use client";
|
||||||
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
|
export default function PageTransition({ children }: { children: React.ReactNode }) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
return (
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.div
|
||||||
|
key={pathname}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.35, ease: "easeInOut" }}
|
||||||
|
style={{ minHeight: "100vh" }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -37,23 +37,30 @@ export default function RecentActivity() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className="space-y-3" role="list" aria-label="Recent activity">
|
<div className="mb-8">
|
||||||
{activity.map((item) => (
|
<ul
|
||||||
<li key={item.type + item.id} className="flex items-center gap-3 bg-[var(--muted)] rounded-lg p-4 border border-[var(--border)] shadow-sm focus-within:ring-2 focus-within:ring-[var(--primary)]" tabIndex={0} aria-label={`${item.type === 'fillup' ? 'Fill-Up' : 'Mileage'} for ${item.car.name}`}>
|
className="space-y-3 overflow-y-auto pr-2"
|
||||||
<span className={`text-2xl ${item.type === 'fillup' ? 'text-[var(--secondary)]' : 'text-[var(--primary)]'}`} aria-hidden="true">{item.type === 'fillup' ? '⛽' : '🛣️'}</span>
|
style={{ maxHeight: `${3 * 5.5}rem`, minHeight: activity.length > 0 ? '5.5rem' : undefined }}
|
||||||
<div className="flex-1">
|
role="list"
|
||||||
<div className="font-semibold text-[var(--foreground)]">
|
aria-label="Recent activity"
|
||||||
{item.type === 'fillup' ? 'Fill-Up' : 'Mileage'} for <span className="text-[var(--primary)]">{item.car.name}</span>
|
>
|
||||||
|
{activity.map((item) => (
|
||||||
|
<li key={item.type + item.id} className="flex items-center gap-3 bg-[var(--muted)] rounded-lg p-4 border border-[var(--border)] shadow-sm focus-within:ring-2 focus-within:ring-[var(--primary)]" tabIndex={0} aria-label={`${item.type === 'fillup' ? 'Fill-Up' : 'Mileage'} for ${item.car.name}`}>
|
||||||
|
<span className={`text-2xl ${item.type === 'fillup' ? 'text-[var(--secondary)]' : 'text-[var(--primary)]'}`} aria-hidden="true">{item.type === 'fillup' ? '⛽' : '🛣️'}</span>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="font-semibold text-[var(--foreground)]">
|
||||||
|
{item.type === 'fillup' ? 'Fill-Up' : 'Mileage'} for <span className="text-[var(--primary)]">{item.car.name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-[var(--foreground)]/60">
|
||||||
|
{item.type === 'fillup'
|
||||||
|
? `${item.liters} ${item.fuelType === 'ELECTRIC' ? 'kWh' : item.fuelType === 'LPG' ? 'L (LPG)' : item.fuelType === 'GASOLINE' ? 'L (Gasoline)' : item.fuelType === 'DIESEL' ? 'L (Diesel)' : 'L'}, ${item.mileage} km${item.cost ? `, ${item.cost} ${item.currency}` : ''}`
|
||||||
|
: `${item.mileage} km`}
|
||||||
|
{' '}on {new Date(item.date).toLocaleDateString()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-[var(--foreground)]/60">
|
</li>
|
||||||
{item.type === 'fillup'
|
))}
|
||||||
? `${item.liters} ${item.fuelType === 'ELECTRIC' ? 'kWh' : item.fuelType === 'LPG' ? 'L (LPG)' : item.fuelType === 'GASOLINE' ? 'L (Gasoline)' : item.fuelType === 'DIESEL' ? 'L (Diesel)' : 'L'}, ${item.mileage} km${item.cost ? `, ${item.cost} ${item.currency}` : ''}`
|
</ul>
|
||||||
: `${item.mileage} km`}
|
</div>
|
||||||
{' '}on {new Date(item.date).toLocaleDateString()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-13
@@ -1,4 +1,3 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import InfoTooltip from "./InfoTooltip";
|
import InfoTooltip from "./InfoTooltip";
|
||||||
|
|
||||||
export default function StatCard({ label, value, icon, color, info }: { label: string; value: string; icon?: string; color?: 'primary' | 'secondary' | 'accent'; info?: string }) {
|
export default function StatCard({ label, value, icon, color, info }: { label: string; value: string; icon?: string; color?: 'primary' | 'secondary' | 'accent'; info?: string }) {
|
||||||
@@ -25,20 +24,18 @@ export default function StatCard({ label, value, icon, color, info }: { label: s
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="rounded-xl p-4 shadow-sm flex flex-col justify-center w-full"
|
className="rounded-xl p-3 shadow-sm flex flex-row items-center gap-4 w-full h-full min-w-0 min-h-0"
|
||||||
style={{ background: cardBg, border: `1.5px solid ${cardBorder}` }}
|
style={{ background: cardBg, border: `1.5px solid ${cardBorder}` }}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center justify-center w-full gap-4">
|
{icon && <span className={`text-2xl ${color ? `text-[var(--${color})]` : ''}`}>{icon}</span>}
|
||||||
{icon && <span className={`text-2xl ${color ? `text-[var(--${color})]` : ''}`}>{icon}</span>}
|
<div className="flex flex-row items-center w-full min-w-0 gap-2">
|
||||||
<div className="flex flex-col items-center w-full">
|
<span className="text-sm text-[var(--foreground)]/60 flex items-center gap-1 text-left overflow-hidden text-ellipsis whitespace-nowrap max-w-[40%]">
|
||||||
<p className="text-sm text-[var(--foreground)]/60 flex items-center gap-1 text-center w-full justify-center">
|
{label}
|
||||||
{label}
|
{info && (
|
||||||
{info && (
|
<InfoTooltip label={`Info: ${label}`} description={info} />
|
||||||
<InfoTooltip label={`Info: ${label}`} description={info} />
|
)}
|
||||||
)}
|
</span>
|
||||||
</p>
|
<span className={`text-2xl font-semibold ${valueClass} text-left break-words flex-1 min-w-0`}>{value}</span>
|
||||||
<p className={`text-2xl font-semibold ${valueClass} text-center w-full`}>{value}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -26,9 +26,8 @@ export default function StatsPageClient({ fillUps, carId, error, loading = false
|
|||||||
const [units, setUnits] = useState<Units>('metric');
|
const [units, setUnits] = useState<Units>('metric');
|
||||||
// Hybrid: fuel type selection
|
// Hybrid: fuel type selection
|
||||||
const allFuelTypes = Array.from(new Set(fillUps.map(f => f.fuelType).filter(Boolean)));
|
const allFuelTypes = Array.from(new Set(fillUps.map(f => f.fuelType).filter(Boolean)));
|
||||||
const [selectedFuelType, setSelectedFuelType] = useState<string>('ALL');
|
const [selectedFuelType, setSelectedFuelType] = useState<string>(allFuelTypes[0] || '');
|
||||||
const filteredByFuel = useMemo(() => {
|
const filteredByFuel = useMemo(() => {
|
||||||
if (selectedFuelType === 'ALL') return fillUps;
|
|
||||||
return fillUps.filter(f => f.fuelType === selectedFuelType);
|
return fillUps.filter(f => f.fuelType === selectedFuelType);
|
||||||
}, [fillUps, selectedFuelType]);
|
}, [fillUps, selectedFuelType]);
|
||||||
const now = useMemo(() => new Date(), []);
|
const now = useMemo(() => new Date(), []);
|
||||||
@@ -70,6 +69,27 @@ export default function StatsPageClient({ fillUps, carId, error, loading = false
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Comparative insights logic
|
||||||
|
// (removed duplicate block)
|
||||||
|
|
||||||
|
// Stats calculations
|
||||||
|
if (filtered.length < 2) {
|
||||||
|
return (
|
||||||
|
<main className="flex flex-col items-center justify-center py-16 px-2 min-h-[60vh]">
|
||||||
|
<div className="w-full max-w-md mx-auto bg-[var(--muted)] rounded-xl shadow p-8 flex flex-col items-center">
|
||||||
|
<span className="text-7xl mb-2 opacity-80" role="img" aria-label="No stats">📉</span>
|
||||||
|
<h1 className="text-2xl font-bold text-[var(--primary)] text-center">No Stats Yet</h1>
|
||||||
|
<p className="text-[var(--foreground)]/80 text-center max-w-xs mb-4">Add at least 2 fill-ups to unlock your personalized fuel stats, insights, and achievements. Start tracking your car’s journey today!</p>
|
||||||
|
<a href={`/dashboard/cars/${carId}/fillups`} className="mt-4 inline-block bg-[var(--primary)] text-white px-6 py-2 rounded-lg font-semibold shadow hover:bg-[var(--secondary)] transition focus:outline-none focus:ring-2 focus:ring-[var(--primary)]">➕ Add Your First Fill-Up</a>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- MAIN RETURN BLOCK ---
|
||||||
|
|
||||||
|
// MAIN RETURN BLOCK (only one return statement)
|
||||||
|
|
||||||
// Comparative insights logic
|
// Comparative insights logic
|
||||||
const last30 = filtered;
|
const last30 = filtered;
|
||||||
const prev30 = fillUps.filter(f => {
|
const prev30 = fillUps.filter(f => {
|
||||||
@@ -128,11 +148,11 @@ export default function StatsPageClient({ fillUps, carId, error, loading = false
|
|||||||
const first = filtered[0];
|
const first = filtered[0];
|
||||||
const last = filtered[filtered.length - 1];
|
const last = filtered[filtered.length - 1];
|
||||||
const totalDistance = units === 'imperial' ? kmToMiles(last.mileage - first.mileage) : last.mileage - first.mileage;
|
const totalDistance = units === 'imperial' ? kmToMiles(last.mileage - first.mileage) : last.mileage - first.mileage;
|
||||||
const totalLiters = filtered.slice(1).reduce((sum, f) => sum + (units === 'imperial' ? litersToGallons(f.liters) : f.liters), 0);
|
const totalLiters = filtered.slice(0, -1).reduce((sum, f) => sum + (units === 'imperial' ? litersToGallons(f.liters) : f.liters), 0);
|
||||||
const totalCost = filtered.slice(1).reduce((sum, f) => sum + f.cost, 0);
|
const totalCost = filtered.slice(0).reduce((sum, f) => sum + f.cost, 0);
|
||||||
const avgConsumption = (() => {
|
const avgConsumption = (() => {
|
||||||
const dist = last.mileage - first.mileage;
|
const dist = last.mileage - first.mileage;
|
||||||
const lit = filtered.slice(1).reduce((sum, f) => sum + f.liters, 0);
|
const lit = filtered.slice(0, -1).reduce((sum, f) => sum + f.liters, 0);
|
||||||
if (units === 'imperial') {
|
if (units === 'imperial') {
|
||||||
return lPer100kmToMpg((lit / dist) * 100);
|
return lPer100kmToMpg((lit / dist) * 100);
|
||||||
} else {
|
} else {
|
||||||
@@ -186,61 +206,57 @@ export default function StatsPageClient({ fillUps, carId, error, loading = false
|
|||||||
const isElectric = fuelType === 'ELECTRIC';
|
const isElectric = fuelType === 'ELECTRIC';
|
||||||
// If electric, always use metric units
|
// If electric, always use metric units
|
||||||
const displayUnits = isElectric ? 'metric' : units;
|
const displayUnits = isElectric ? 'metric' : units;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col items-center justify-center py-8 px-2">
|
<main className="flex flex-col items-center justify-center py-8 px-2 w-full">
|
||||||
<div className="w-full max-w-2xl mx-auto p-4 sm:p-8 space-y-8 bg-[var(--muted)] rounded-xl shadow m-2 sm:m-4 mb-8 px-2 sm:px-8 lg:px-16">
|
{/* Centered header and controls */}
|
||||||
|
<div className="w-full max-w-2xl mx-auto p-4 sm:p-8 bg-[var(--muted)] rounded-xl shadow m-2 sm:m-4 mb-4 px-2 sm:px-8 lg:px-16 flex flex-col items-center">
|
||||||
<h1 className="text-3xl sm:text-4xl font-extrabold text-[var(--primary)] flex items-center gap-3 m-2 sm:m-4 mt-0 justify-center">
|
<h1 className="text-3xl sm:text-4xl font-extrabold text-[var(--primary)] flex items-center gap-3 m-2 sm:m-4 mt-0 justify-center">
|
||||||
<span role="img" aria-label="stats">📊</span> Fuel Stats
|
<span role="img" aria-label="stats">📊</span> Fuel Stats
|
||||||
</h1>
|
</h1>
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center gap-2 mb-2 w-full justify-center">
|
<div className="flex flex-col sm:flex-row sm:items-center gap-2 mb-2 w-full justify-center">
|
||||||
|
<span className="text-xs font-semibold text-[var(--foreground)]/70 mb-1 sm:mb-0 sm:mr-2">Fuel:</span>
|
||||||
{allFuelTypes.length > 1 && (
|
{allFuelTypes.length > 1 && (
|
||||||
<select
|
<div className="flex flex-col sm:flex-row gap-2 w-full sm:w-auto">
|
||||||
value={selectedFuelType}
|
<select
|
||||||
onChange={e => setSelectedFuelType(e.target.value)}
|
value={selectedFuelType}
|
||||||
className="border border-[var(--border)] px-2 py-1 rounded-lg bg-[var(--background)] text-[var(--foreground)] font-semibold"
|
onChange={e => setSelectedFuelType(e.target.value)}
|
||||||
>
|
className="border border-[var(--border)] px-2 py-1 rounded-lg bg-[var(--background)] text-[var(--foreground)] font-semibold"
|
||||||
<option value="ALL">All Fuels</option>
|
>
|
||||||
{allFuelTypes.map(ft => (
|
{allFuelTypes.map(ft => (
|
||||||
<option key={ft} value={ft}>{ft}</option>
|
<option key={ft} value={ft}>{ft}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isElectric && (
|
{!isElectric && (
|
||||||
<UnitsToggle units={units} setUnits={setUnits} disabled={isElectric} />
|
<div className="flex flex-col sm:flex-row gap-2 w-full sm:w-auto items-center justify-center">
|
||||||
|
<UnitsToggle units={units} setUnits={setUnits} disabled={isElectric} />
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{isElectric && (
|
{isElectric && (
|
||||||
<span className="text-xs font-semibold text-[var(--foreground)]/70 mb-1 sm:mb-0 sm:mr-2">Units: kWh/100km</span>
|
<span className="text-xs font-semibold text-[var(--foreground)]/70 mb-1 sm:mb-0 sm:mr-2">Units: kWh/100km</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap items-center gap-2 mb-2 w-full justify-center">
|
<div className="flex flex-col sm:flex-row flex-wrap items-center gap-2 mb-2 w-full justify-center">
|
||||||
<div className="flex items-center gap-2 w-auto">
|
<div className="flex flex-col sm:flex-row gap-2 w-full sm:w-auto items-center">
|
||||||
<StatsTimeRangeFilter value={range} onChange={setRange} />
|
<StatsTimeRangeFilter value={range} onChange={setRange} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-end h-full">
|
<div className="flex flex-col sm:flex-row gap-2 w-full sm:w-auto items-center">
|
||||||
<StatsExportButton fillUps={filtered} units={displayUnits} isElectric={isElectric} />
|
<StatsExportButton fillUps={filtered} units={displayUnits} isElectric={isElectric} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full overflow-x-auto flex justify-center">
|
</div>
|
||||||
<FuelStatsChart fillUps={chartFillUps} units={displayUnits} />
|
{/* 2x2 Responsive CSS grid for stats, chart, achievements (achievements only under chart) */}
|
||||||
</div>
|
<div
|
||||||
{comparison && (
|
className="w-full max-w-7xl mx-auto grid grid-cols-1 grid-rows-[auto_auto] gap-4 items-start mb-0
|
||||||
<div className={`flex items-center gap-2 p-4 rounded-lg mb-2 font-medium ${comparison.improved ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'} w-full justify-center mx-auto`}>
|
lg:grid-cols-2 lg:grid-rows-1 lg:gap-6"
|
||||||
{comparison.improved ? '⬇️' : '⬆️'}
|
>
|
||||||
<span>
|
{/* Stats Cards: top left (1x1) */}
|
||||||
Avg. consumption: <b>{units === 'imperial' ? `${lPer100kmToMpg(avgLast30 ?? 0).toFixed(2)} MPG` : `${avgLast30?.toFixed(2)} L/100km`}</b> ({comparison.improved ? '-' : '+'}{comparison.percent}%)
|
<div
|
||||||
<span className="ml-2 text-xs text-[var(--foreground)]/60">(Prev: {units === 'imperial' ? `${lPer100kmToMpg(avgPrev30 ?? 0).toFixed(2)} MPG` : `${avgPrev30?.toFixed(2)} L/100km`})</span>
|
className="col-span-1 row-span-1 lg:col-start-1 lg:row-start-1 flex justify-center"
|
||||||
</span>
|
>
|
||||||
</div>
|
<div className="bg-[var(--background)] border border-[var(--border)] rounded-3xl shadow-2xl p-4 sm:p-6 w-full max-w-full animate-fadein flex justify-center items-start mb-0">
|
||||||
)}
|
<div className="flex flex-row flex-wrap gap-4 sm:gap-6 w-full justify-center items-stretch">
|
||||||
{insight && (
|
|
||||||
<div className="mb-4 text-[var(--foreground)]/80 text-sm flex items-center gap-2 w-full justify-center mx-auto">
|
|
||||||
<span role="img" aria-label="tip">💡</span> {insight}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="w-full flex justify-center">
|
|
||||||
<div className="bg-[var(--background)] border border-[var(--border)] rounded-3xl shadow-2xl p-4 sm:p-10 mb-6 w-full max-w-3xl m-0 sm:m-4 mb-8 animate-fadein flex justify-center items-center">
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-8 w-full">
|
|
||||||
<StatCard label={`Total Distance`} value={`${totalDistance.toFixed(0)} ${displayUnits === 'imperial' ? 'mi' : 'km'}`} icon="🛣️" color="primary" info={displayUnits === 'imperial' ? 'Total miles driven in selected period.' : 'Total kilometers driven in selected period.'} />
|
<StatCard label={`Total Distance`} value={`${totalDistance.toFixed(0)} ${displayUnits === 'imperial' ? 'mi' : 'km'}`} icon="🛣️" color="primary" info={displayUnits === 'imperial' ? 'Total miles driven in selected period.' : 'Total kilometers driven in selected period.'} />
|
||||||
<StatCard label={`Total Fuel Used`} value={`${totalLiters.toFixed(2)} ${isElectric ? 'kWh' : (displayUnits === 'imperial' ? 'gal' : 'L')}`} icon="⛽" color="secondary" info={isElectric ? 'Total kilowatt-hours used in selected period.' : (displayUnits === 'imperial' ? 'Total gallons used in selected period.' : 'Total liters used in selected period.')} />
|
<StatCard label={`Total Fuel Used`} value={`${totalLiters.toFixed(2)} ${isElectric ? 'kWh' : (displayUnits === 'imperial' ? 'gal' : 'L')}`} icon="⛽" color="secondary" info={isElectric ? 'Total kilowatt-hours used in selected period.' : (displayUnits === 'imperial' ? 'Total gallons used in selected period.' : 'Total liters used in selected period.')} />
|
||||||
<StatCard label="Total Fuel Cost" value={`${totalCost.toFixed(2)} ${filtered[0].currency}`} icon="💸" color="accent" info="Sum of all fill-up costs in selected period." />
|
<StatCard label="Total Fuel Cost" value={`${totalCost.toFixed(2)} ${filtered[0].currency}`} icon="💸" color="accent" info="Sum of all fill-up costs in selected period." />
|
||||||
@@ -250,8 +266,35 @@ export default function StatsPageClient({ fillUps, carId, error, loading = false
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Achievements achievements={achievements} />
|
|
||||||
|
{/* Chart: top right (1x1) */}
|
||||||
|
<div
|
||||||
|
className="col-span-1 row-span-1 lg:col-start-2 lg:row-start-1 flex flex-col items-center"
|
||||||
|
>
|
||||||
|
<div className="w-full overflow-x-auto flex justify-center">
|
||||||
|
<FuelStatsChart fillUps={chartFillUps} units={displayUnits} />
|
||||||
|
</div>
|
||||||
|
{/* Achievements: only under chart on desktop */}
|
||||||
|
<div className="w-full mt-4 flex justify-center mb-0">
|
||||||
|
<Achievements achievements={achievements} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Comparative insights and tips */}
|
||||||
|
{comparison && (
|
||||||
|
<div className={`flex items-center gap-2 p-4 rounded-lg mb-2 font-medium ${comparison.improved ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'} w-full justify-center mx-auto`}>
|
||||||
|
{comparison.improved ? '⬇️' : '⬆️'}
|
||||||
|
<span>
|
||||||
|
Avg. consumption: <b>{units === 'imperial' ? `${lPer100kmToMpg(avgLast30 ?? 0).toFixed(2)} MPG` : `${avgLast30?.toFixed(2)} L/100km`}</b> ({comparison.improved ? '-' : '+'}{comparison.percent}%)
|
||||||
|
<span className="ml-2 text-xs text-[var(--foreground)]/60">(Prev: {units === 'imperial' ? `${lPer100kmToMpg(avgPrev30 ?? 0).toFixed(2)} MPG` : `${avgPrev30?.toFixed(2)} L/100km`})</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{insight && (
|
||||||
|
<div className="mb-4 text-[var(--foreground)]/80 text-sm flex items-center gap-2 w-full justify-center mx-auto">
|
||||||
|
<span role="img" aria-label="tip">💡</span> {insight}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export type Units = "metric" | "imperial";
|
|||||||
export default function UnitsToggle({ units, setUnits, disabled = false }: { units: Units; setUnits: (u: Units) => void; disabled?: boolean }) {
|
export default function UnitsToggle({ units, setUnits, disabled = false }: { units: Units; setUnits: (u: Units) => void; disabled?: boolean }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-1 sm:gap-2 w-full max-w-xs">
|
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-1 sm:gap-2 w-full max-w-xs">
|
||||||
<span className="text-xs font-semibold text-[var(--foreground)]/70 mb-1 sm:mb-0 sm:mr-2">Fuel:</span>
|
<span className="text-xs font-semibold text-[var(--foreground)]/70 mb-1 sm:mb-0 sm:mr-2">Metrics:</span>
|
||||||
<div className="flex flex-col sm:flex-row gap-2 w-full items-center">
|
<div className="flex flex-col sm:flex-row gap-2 w-full items-center">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
Reference in New Issue
Block a user