diff --git a/package-lock.json b/package-lock.json index e3686f4..f2284fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 0eacb88..5a69cb9 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/Larger_Background_Dark.png b/public/Larger_Background_Dark.png new file mode 100644 index 0000000..3cc7fe2 Binary files /dev/null and b/public/Larger_Background_Dark.png differ diff --git a/public/Larger_Background_Light.png b/public/Larger_Background_Light.png new file mode 100644 index 0000000..3f040c9 Binary files /dev/null and b/public/Larger_Background_Light.png differ diff --git a/src/app/api/fillups/route.ts b/src/app/api/fillups/route.ts index 7d74bac..5cea56c 100644 --- a/src/app/api/fillups/route.ts +++ b/src/app/api/fillups/route.ts @@ -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, }, }); diff --git a/src/app/dashboard/cars/[carId]/fillups/page.tsx b/src/app/dashboard/cars/[carId]/fillups/page.tsx index e756f97..f502534 100644 --- a/src/app/dashboard/cars/[carId]/fillups/page.tsx +++ b/src/app/dashboard/cars/[carId]/fillups/page.tsx @@ -19,6 +19,21 @@ const currencies = ['EUR', 'USD', 'RON', 'GBP']; export default function FillUpsPage() { const { carId } = useParams(); const [fillups, setFillups] = useState([]); + // 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([]); 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 ( -
-

Fuel Fill-Ups

+
+

Fuel Fill-Ups

+
+ + + Select the date and hour of the fill-up +
{error}

} -
    - {fillups.map((fill) => ( - - ))} -
+ {/* Toggle and Filters for Fill-Ups */} + {!showFillUps && ( +
+ +
+ )} + {showFillUps && ( +
+ +
+
+ + +
+
+ + +
+
+
+ )} + {showFillUps && ( +
+
    0 ? '6.5rem' : undefined }} + > + {filteredFillUps.map((fill) => ( + + ))} +
+
+ )}
); } diff --git a/src/app/dashboard/cars/[carId]/page.tsx b/src/app/dashboard/cars/[carId]/page.tsx index 0d1e9a9..e58818b 100644 --- a/src/app/dashboard/cars/[carId]/page.tsx +++ b/src/app/dashboard/cars/[carId]/page.tsx @@ -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: }, }; + // Normalize fuelTypes for compatibility + const fuelTypes = Array.isArray(car.fuelTypes) ? car.fuelTypes : car.fuelTypes ? [car.fuelTypes] : []; + return ( -
-
+
+
{/* Car Icon & Header */} -
- -

- {car.name} - {car.year} - {Array.isArray(car.fuelTypes) && car.fuelTypes.length > 1 ? ( - - {fuelMeta[car.fuelTypes[0]]?.icon}{car.fuelTypes[0]} - {fuelMeta[car.fuelTypes[1]]?.icon}{car.fuelTypes[1]} - - ) : Array.isArray(car.fuelTypes) && car.fuelTypes.length === 1 ? ( - {fuelMeta[car.fuelTypes[0]]?.icon}{car.fuelTypes[0]} - ) : null} -

+
+
+ +

+ {car.name} + {car.year} +

+
+ {/* Fuel Types Row */} + {fuelTypes.length > 0 && ( +
+ {fuelTypes.length > 1 ? ( + <> + {fuelMeta[fuelTypes[0]]?.icon}{fuelTypes[0]} + {fuelMeta[fuelTypes[1]]?.icon}{fuelTypes[1]} + + ) : ( + {fuelMeta[fuelTypes[0]]?.icon}{fuelTypes[0]} + )} +
+ )}
{/* Car Details */} -
-
- - Make: - {car.make} +
+ {/* Make & Model Row */} +
+
+ + Make: + {car.make} +
+
+ + Model: + {car.model} +
-
- - Model: - {car.model} -
-
- +
+ Year: - {car.year} -
-
- {Array.isArray(car.fuelTypes) && car.fuelTypes.length > 0 - ? car.fuelTypes.map((ft: string) => ( - {fuelMeta[ft]?.icon}{ft} - )) - : null - } + {car.year}
{/* Actions */} -
- - βž• Add Mileage - - - βž• Add Fill-Up - - - πŸ“Š View Stats - - +
+
+ + βž• Add Mileage + +
+
+ + βž• Add Fill-Up + +
+
+ + πŸ“Š View Stats + +
+
+ +
); } + diff --git a/src/app/dashboard/cars/new/page.tsx b/src/app/dashboard/cars/new/page.tsx index cabf0e4..5d9af8d 100644 --- a/src/app/dashboard/cars/new/page.tsx +++ b/src/app/dashboard/cars/new/page.tsx @@ -17,14 +17,22 @@ export default function AddCarPage() { const [error, setError] = useState(''); const handleChange = (e: React.ChangeEvent) => { - 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() {
- {fuelTypes.map((type) => ( - - ))} + {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 ( + + ); + })}