Upgrades until 15.07.2025

This commit is contained in:
EdiFarcas
2025-07-15 13:25:00 +03:00
parent d80eedfaa4
commit 1b4ebd9246
22 changed files with 439 additions and 234 deletions
+43
View File
@@ -12,6 +12,7 @@
"@next-auth/prisma-adapter": "^1.0.7",
"@prisma/client": "^6.11.0",
"bcryptjs": "^3.0.2",
"framer-motion": "^12.23.3",
"next": "15.3.4",
"next-auth": "^4.24.11",
"react": "^19.0.0",
@@ -3927,6 +3928,33 @@
"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": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -5245,6 +5273,21 @@
"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": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+1
View File
@@ -13,6 +13,7 @@
"@next-auth/prisma-adapter": "^1.0.7",
"@prisma/client": "^6.11.0",
"bcryptjs": "^3.0.2",
"framer-motion": "^12.23.3",
"next": "15.3.4",
"next-auth": "^4.24.11",
"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

+2 -1
View File
@@ -26,7 +26,7 @@ export async function POST(req: Request) {
const session = await getSession();
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) {
return NextResponse.json({ message: 'Missing fields' }, { status: 400 });
}
@@ -48,6 +48,7 @@ export async function POST(req: Request) {
currency,
carId,
fuelType,
date: date ? new Date(date) : undefined,
},
});
@@ -19,6 +19,21 @@ const currencies = ['EUR', 'USD', 'RON', 'GBP'];
export default function FillUpsPage() {
const { carId } = useParams();
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 [form, setForm] = useState({
mileage: '',
@@ -26,6 +41,7 @@ export default function FillUpsPage() {
cost: '',
currency: 'EUR',
fuelType: '',
date: '',
});
const [error, setError] = useState('');
@@ -58,7 +74,7 @@ export default function FillUpsPage() {
if (res.ok) {
const newFill = await res.json();
setFillups((prev) => [newFill, ...prev]);
setForm({ mileage: '', liters: '', cost: '', currency: 'EUR', fuelType: carFuelTypes[0] || '' });
setForm({ mileage: '', liters: '', cost: '', currency: 'EUR', fuelType: carFuelTypes[0] || '', date: '' });
setError('');
} else {
const data = await res.json();
@@ -77,10 +93,23 @@ export default function FillUpsPage() {
}
return (
<main className="max-w-2xl mx-auto p-8 space-y-8 bg-[var(--muted)] rounded-xl shadow">
<h1 className="text-2xl font-bold text-[var(--primary)]">Fuel Fill-Ups</h1>
<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)] 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)]">
<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">
<label htmlFor="mileage" className="font-medium text-[var(--primary)]">Odometer</label>
<input
@@ -186,11 +215,69 @@ export default function FillUpsPage() {
{error && <p className="col-span-full text-red-500 text-center mt-2">{error}</p>}
</form>
<ul className="space-y-3">
{fillups.map((fill) => (
<FillUpCard key={fill.id} fill={{ ...fill, car: { fuelType: fill.fuelType } }} />
))}
</ul>
{/* Toggle and Filters for Fill-Ups */}
{!showFillUps && (
<div className="flex justify-center mb-4">
<button
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>
);
}
+73 -59
View File
@@ -1,3 +1,4 @@
import { getSession } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
import { redirect } from 'next/navigation';
@@ -12,14 +13,13 @@ interface CarDetailPageProps {
}
export default async function CarDetailPage({ params }: CarDetailPageProps) {
const resolvedParams = await params;
const session = await getSession();
if (!session) redirect('/auth/login');
const userEmail = session.user?.email || '';
const car = await prisma.car.findFirst({
where: {
id: resolvedParams.carId,
id: params.carId,
user: { email: userEmail },
},
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" /> },
};
// Normalize fuelTypes for compatibility
const fuelTypes = Array.isArray(car.fuelTypes) ? car.fuelTypes : car.fuelTypes ? [car.fuelTypes] : [];
return (
<main className="max-w-3xl mx-auto p-0 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)]">
<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-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 */}
<div className="flex flex-col items-center gap-3 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>
<h1 className="text-4xl font-extrabold text-[var(--primary)] leading-tight tracking-tight flex items-center gap-3">
{car.name}
<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>
{Array.isArray(car.fuelTypes) && car.fuelTypes.length > 1 ? (
<span className="ml-2 flex items-center">
<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>
<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>
</span>
) : Array.isArray(car.fuelTypes) && car.fuelTypes.length === 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>
) : null}
</h1>
<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">
<div className="flex flex-row flex-wrap items-center justify-center gap-2 w-full">
<span className="text-3xl sm:text-4xl md:text-6xl text-[var(--primary)] drop-shadow-lg"><FaCarSide /></span>
<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>{car.name}</span>
<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>
</h1>
</div>
{/* Fuel Types Row */}
{fuelTypes.length > 0 && (
<div className="flex flex-row flex-wrap items-center justify-center gap-2 w-full mt-1">
{fuelTypes.length > 1 ? (
<>
<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>
{/* Car Details */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 px-8">
<div className="flex items-center gap-3">
<FaIndustry className="text-2xl text-gray-400" />
<span className="font-semibold text-[var(--foreground)]">Make:</span>
<span className="ml-1 text-gray-400 font-mono text-lg">{car.make}</span>
<div className="flex flex-col gap-4 sm:gap-6 md:gap-8 px-1 sm:px-2 md:px-8 w-full">
{/* Make & Model Row */}
<div className="flex flex-col md:flex-row items-center justify-center gap-2 md:gap-8 w-full">
<div className="flex items-center gap-2 md:gap-3">
<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 className="flex items-center gap-3">
<FaCarSide className="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" />
<div className="flex items-center gap-2 md:gap-3 justify-center w-full">
<FaCalendarAlt className="text-xl md:text-2xl text-gray-400" />
<span className="font-semibold text-[var(--foreground)]">Year:</span>
<span className="ml-1 text-gray-400 font-mono 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
}
<span className="text-gray-400 font-mono text-base md:text-lg">{car.year}</span>
</div>
</div>
{/* Actions */}
<div className="flex flex-wrap gap-4 justify-center px-8 pb-8">
<Link
href={`/dashboard/cars/${car.id}/mileage`}
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"
>
Add Mileage
</Link>
<Link
href={`/dashboard/cars/${car.id}/fillups`}
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"
>
Add Fill-Up
</Link>
<Link
href={`/dashboard/cars/${car.id}/stats`}
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"
>
📊 View Stats
</Link>
<DeleteCarButton carId={car.id} />
<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">
<div className="w-full max-w-xs md:w-auto md:max-w-none flex justify-center">
<Link
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>
</div>
<div className="w-full max-w-xs md:w-auto md:max-w-none flex justify-center">
<Link
href={`/dashboard/cars/${car.id}/fillups`}
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"
>
Add Fill-Up
</Link>
</div>
<div className="w-full max-w-xs md:w-auto md:max-w-none flex justify-center">
<Link
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>
</main>
);
}
+31 -16
View File
@@ -17,14 +17,22 @@ export default function AddCarPage() {
const [error, setError] = useState('');
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value, type, checked } = e.target;
const { name, value } = e.target;
if (name === 'fuelTypes') {
// Only checkboxes will trigger this block
const checked = (e.target as HTMLInputElement).checked;
setForm((prev) => {
let updated = prev.fuelTypes;
if (checked) {
updated = [...prev.fuelTypes, value];
// Only add if less than 2 selected
if (prev.fuelTypes.length < 2) {
updated = [...prev.fuelTypes, value];
}
} 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 };
});
@@ -116,19 +124,26 @@ export default function AddCarPage() {
<div>
<label className="block mb-1 font-semibold text-[var(--foreground)]">Fuel Types</label>
<div className="flex flex-wrap gap-4">
{fuelTypes.map((type) => (
<label key={type} className="flex items-center gap-2">
<input
type="checkbox"
name="fuelTypes"
value={type}
checked={form.fuelTypes.includes(type)}
onChange={handleChange}
className="accent-[var(--primary)]"
/>
{type}
</label>
))}
{fuelTypes.map((type) => {
const checked = form.fuelTypes.includes(type);
const disabled =
(!checked && form.fuelTypes.length >= 2) ||
(checked && form.fuelTypes.length === 1); // can't uncheck last
return (
<label key={type} className="flex items-center gap-2">
<input
type="checkbox"
name="fuelTypes"
value={type}
checked={checked}
onChange={handleChange}
className="accent-[var(--primary)]"
disabled={disabled}
/>
{type}
</label>
);
})}
</div>
</div>
<button
+6 -6
View File
@@ -9,12 +9,12 @@
--accent: #a21caf; /* purple-700 */
--muted: #f3f4f6; /* gray-100 */
--border: #e5e7eb; /* gray-200 */
--statcard-primary-bg: #e0e7ff; /* indigo-100 */
--statcard-primary-bg: #e5e7eb; /* gray-200 */
--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-accent-bg: #f3e8ff; /* purple-100 */
--statcard-accent-border: #a21caf; /* purple-700 */
--statcard-accent-bg: #e0f2fe; /* sky-100 */
--statcard-accent-border: #0ea5e9; /* sky-500 */
--statcard-default-bg: var(--background);
--statcard-default-border: var(--border);
}
@@ -29,8 +29,8 @@
--statcard-primary-border: #2563eb; /* blue-600 */
--statcard-secondary-bg: #052e16; /* green-950 */
--statcard-secondary-border: #22c55e; /* green-500 */
--statcard-accent-bg: #3b0764; /* purple-950 */
--statcard-accent-border: #a21caf; /* purple-700 */
--statcard-accent-bg: #0f172a; /* slate-900 */
--statcard-accent-border: #0ea5e9; /* sky-500 */
--statcard-default-bg: var(--background);
--statcard-default-border: var(--border);
}
+2
View File
@@ -3,6 +3,7 @@ import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import ClientNavbar from "../components/ClientNavbar";
import Providers from "./providers";
import Footer from "../components/Footer";
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -52,6 +53,7 @@ export default async function RootLayout({
<div className="min-h-screen flex flex-col">
<div className="h-4" /> {/* Spacer between navbar and content */}
{children}
<Footer />
</div>
</Providers>
</body>
+3 -14
View File
@@ -28,9 +28,9 @@ function UseCaseBlocks() {
<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">
{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>
<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>
</div>
))}
@@ -66,7 +66,7 @@ function FeatureGrid() {
{features.map((f, i) => (
<div
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)}
onMouseEnter={() => setOpen(i)}
onMouseLeave={() => setOpen(null)}
@@ -170,17 +170,6 @@ export default function Home() {
{/* ...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>&copy; {new Date().getFullYear()} Car Fuel Tracker.</span>
</footer>
</div>
);
}
+17 -13
View File
@@ -23,19 +23,23 @@ export default function CarGrid({ cars }: { cars: any[] }) {
return (
<div>
<div className="flex flex-wrap gap-4 mb-4 items-center">
<label htmlFor="fuel-filter" className="font-medium text-sm">Fuel:</label>
<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)]">
{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 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)]">
{sortOptions.map((opt) => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
<div className="flex flex-wrap gap-4 mb-4 items-center sm:flex-nowrap">
<div className="flex flex-col sm:flex-row gap-2 w-full sm:w-auto">
<label htmlFor="fuel-filter" className="font-medium text-sm">Fuel:</label>
<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)]">
{fuelTypes.map((type) => (
<option key={type} value={type}>{type.charAt(0) + type.slice(1).toLowerCase()}</option>
))}
</select>
</div>
<div className="flex flex-col sm:flex-row gap-2 w-full sm:w-auto">
<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)]">
{sortOptions.map((opt) => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
</div>
</div>
{filtered.length ? (
<ul className="grid gap-4 sm:grid-cols-2 md:grid-cols-3" role="list" aria-label="Your cars">
+3 -36
View File
@@ -1,10 +1,9 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useEffect, useState } from "react";
import { useState } from "react";
import { useSession } from "next-auth/react";
import ThemeToggle from "./ThemeToggle";
import Image from "./Image";
export default function ClientNavbar() {
const { data: session } = useSession();
@@ -12,19 +11,6 @@ export default function ClientNavbar() {
const pathname = usePathname();
const userInitial = session?.user?.name?.[0]?.toUpperCase() || session?.user?.email?.[0]?.toUpperCase() || "U";
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 }) {
const isActive = pathname === href;
@@ -43,27 +29,7 @@ export default function ClientNavbar() {
return (
<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">
<span className="relative w-10 h-10 block">
{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 className="text-3xl">🚗</span>
<span>FuelTrack</span>
</Link>
{/* 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">
<NavLink href="/dashboard">Dashboard</NavLink>
{isLoggedIn && <NavLink href="/dashboard/cars/new">Add Car</NavLink>}
<ThemeToggle />
{isLoggedIn ? (
<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>
+1
View File
@@ -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="font-semibold text-[var(--foreground)]">
{fill.mileage} km - {fill.liters} {isElectric ? 'kWh' : 'L'}
<span className="ml-2 text-xs text-[var(--primary)] font-bold">{fill.fuelType}</span>
</div>
<div className="text-sm text-[var(--foreground)]/60">
{fill.cost} {fill.currency} {new Date(fill.date).toLocaleString()}
+16
View File
@@ -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>&copy; {new Date().getFullYear()} FuelTrack</span>
</footer>
);
}
-3
View File
@@ -1,3 +0,0 @@
import Image from "next/image";
export default Image;
+3 -3
View File
@@ -24,17 +24,17 @@ export default function LiveStatsPreview() {
<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>
<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="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>
</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="font-semibold text-lg">Avg. Efficiency</span>
<span className="text-2xl font-bold text-[var(--primary)]">{loading ? "..." : `${stats?.avgEfficiency.toFixed(1)} L/100km`}</span>
</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="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>
+21
View File
@@ -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>
);
}
+24 -17
View File
@@ -37,23 +37,30 @@ export default function RecentActivity() {
);
return (
<ul className="space-y-3" role="list" aria-label="Recent activity">
{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 className="mb-8">
<ul
className="space-y-3 overflow-y-auto pr-2"
style={{ maxHeight: `${3 * 5.5}rem`, minHeight: activity.length > 0 ? '5.5rem' : undefined }}
role="list"
aria-label="Recent activity"
>
{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 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>
</li>
))}
</ul>
</li>
))}
</ul>
</div>
);
}
+10 -13
View File
@@ -1,4 +1,3 @@
import { useState } from "react";
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 }) {
@@ -25,20 +24,18 @@ export default function StatCard({ label, value, icon, color, info }: { label: s
return (
<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}` }}
>
<div className="flex flex-row items-center justify-center w-full gap-4">
{icon && <span className={`text-2xl ${color ? `text-[var(--${color})]` : ''}`}>{icon}</span>}
<div className="flex flex-col items-center w-full">
<p className="text-sm text-[var(--foreground)]/60 flex items-center gap-1 text-center w-full justify-center">
{label}
{info && (
<InfoTooltip label={`Info: ${label}`} description={info} />
)}
</p>
<p className={`text-2xl font-semibold ${valueClass} text-center w-full`}>{value}</p>
</div>
{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">
<span className="text-sm text-[var(--foreground)]/60 flex items-center gap-1 text-left overflow-hidden text-ellipsis whitespace-nowrap max-w-[40%]">
{label}
{info && (
<InfoTooltip label={`Info: ${label}`} description={info} />
)}
</span>
<span className={`text-2xl font-semibold ${valueClass} text-left break-words flex-1 min-w-0`}>{value}</span>
</div>
</div>
);
+86 -43
View File
@@ -26,9 +26,8 @@ export default function StatsPageClient({ fillUps, carId, error, loading = false
const [units, setUnits] = useState<Units>('metric');
// Hybrid: fuel type selection
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(() => {
if (selectedFuelType === 'ALL') return fillUps;
return fillUps.filter(f => f.fuelType === selectedFuelType);
}, [fillUps, selectedFuelType]);
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 cars 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
const last30 = filtered;
const prev30 = fillUps.filter(f => {
@@ -128,11 +148,11 @@ export default function StatsPageClient({ fillUps, carId, error, loading = false
const first = filtered[0];
const last = filtered[filtered.length - 1];
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 totalCost = filtered.slice(1).reduce((sum, f) => sum + f.cost, 0);
const totalLiters = filtered.slice(0, -1).reduce((sum, f) => sum + (units === 'imperial' ? litersToGallons(f.liters) : f.liters), 0);
const totalCost = filtered.slice(0).reduce((sum, f) => sum + f.cost, 0);
const avgConsumption = (() => {
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') {
return lPer100kmToMpg((lit / dist) * 100);
} else {
@@ -186,61 +206,57 @@ export default function StatsPageClient({ fillUps, carId, error, loading = false
const isElectric = fuelType === 'ELECTRIC';
// If electric, always use metric units
const displayUnits = isElectric ? 'metric' : units;
return (
<main className="flex flex-col items-center justify-center py-8 px-2">
<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">
<main className="flex flex-col items-center justify-center py-8 px-2 w-full">
{/* 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">
<span role="img" aria-label="stats">📊</span> Fuel Stats
</h1>
<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 && (
<select
value={selectedFuelType}
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 => (
<option key={ft} value={ft}>{ft}</option>
))}
</select>
<div className="flex flex-col sm:flex-row gap-2 w-full sm:w-auto">
<select
value={selectedFuelType}
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"
>
{allFuelTypes.map(ft => (
<option key={ft} value={ft}>{ft}</option>
))}
</select>
</div>
)}
{!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 && (
<span className="text-xs font-semibold text-[var(--foreground)]/70 mb-1 sm:mb-0 sm:mr-2">Units: kWh/100km</span>
)}
</div>
<div className="flex 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 flex-wrap items-center gap-2 mb-2 w-full justify-center">
<div className="flex flex-col sm:flex-row gap-2 w-full sm:w-auto items-center">
<StatsTimeRangeFilter value={range} onChange={setRange} />
</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} />
</div>
</div>
<div className="w-full overflow-x-auto flex justify-center">
<FuelStatsChart fillUps={chartFillUps} units={displayUnits} />
</div>
{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>
)}
<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">
</div>
{/* 2x2 Responsive CSS grid for stats, chart, achievements (achievements only under chart) */}
<div
className="w-full max-w-7xl mx-auto grid grid-cols-1 grid-rows-[auto_auto] gap-4 items-start mb-0
lg:grid-cols-2 lg:grid-rows-1 lg:gap-6"
>
{/* Stats Cards: top left (1x1) */}
<div
className="col-span-1 row-span-1 lg:col-start-1 lg:row-start-1 flex justify-center"
>
<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">
<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 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>
<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>
{/* 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>
);
}
+1 -1
View File
@@ -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 }) {
return (
<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">
<button
type="button"