Logic + UI upgrade

This commit is contained in:
EdiFarcas
2025-07-07 11:08:29 +03:00
parent 024e5ca656
commit 1e1926539a
14 changed files with 305 additions and 245 deletions
+66 -20
View File
@@ -1,36 +1,82 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). # Car Fuel Tracking App
A modern web application to track your car's fuel fill-ups, mileage, and fuel statistics. Built with Next.js, React, Prisma, and PostgreSQL, it provides a beautiful dashboard for managing your vehicles and analyzing fuel efficiency over time.
## Features
- 🚗 **Multi-car support**: Track multiple vehicles, each with its own stats and history.
-**Fill-up logging**: Record every fuel fill-up with mileage, liters, cost, and currency.
- 🛣️ **Mileage tracking**: Log odometer readings to monitor your driving habits.
- 📊 **Statistics dashboard**: View total distance, fuel used, cost, average consumption, and more.
- 🔒 **Authentication**: Secure login and registration with hashed passwords.
- 🌗 **Responsive & modern UI**: Clean, mobile-friendly design with dark mode support.
## Tech Stack
- [Next.js](https://nextjs.org/) (App Router)
- [React](https://react.dev/)
- [Prisma ORM](https://www.prisma.io/)
- [PostgreSQL](https://www.postgresql.org/)
- [NextAuth.js](https://next-auth.js.org/) (Credentials provider)
- [Tailwind CSS](https://tailwindcss.com/)
- TypeScript
## Getting Started ## Getting Started
First, run the development server: ### Prerequisites
- Node.js 18+
- PostgreSQL database
### 1. Clone the repository
```bash ```bash
npm run dev git clone <your-repo-url>
# or cd Car-Fuel-Tracking-App
yarn dev
# or
pnpm dev
# or
bun dev
``` ```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. ### 2. Install dependencies
```bash
npm install
```
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. ### 3. Configure environment variables
Create a `.env` file in the root with the following:
```
DATABASE_URL=postgresql://<user>:<password>@<host>:<port>/<db>
NEXTAUTH_SECRET=your_secret_key
```
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. ### 4. Set up the database
Run Prisma migrations to set up the schema:
```bash
npx prisma migrate deploy
# or for development
npx prisma migrate dev
```
## Learn More ### 5. Start the development server
```bash
npm run dev
```
To learn more about Next.js, take a look at the following resources: Open [http://localhost:3000](http://localhost:3000) in your browser.
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. ## Project Structure
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! - `src/app/` — Next.js app directory (routes, pages, API)
- `src/components/` — Reusable UI components
- `prisma/schema.prisma` — Database schema
- `src/lib/` — Auth and Prisma helpers
- `public/` — Static assets
## Deploy on Vercel ## Scripts
- `npm run dev` — Start development server
- `npm run build` — Build for production
- `npm start` — Start production server
- `npm run lint` — Lint code
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. ## Database Schema
See [`prisma/schema.prisma`](prisma/schema.prisma) for models: `User`, `Car`, `FillUp`, `MileageEntry`, enums for `FuelType` and `Currency`.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. ---
> Built with passion using Next.js, Prisma, and Tailwind CSS.
+7 -7
View File
@@ -27,16 +27,16 @@ export default function LoginPage() {
}; };
return ( return (
<main className="max-w-md mx-auto p-6"> <main className="max-w-md mx-auto p-8 bg-[var(--muted)] rounded-xl shadow space-y-6">
<h1 className="text-2xl font-bold mb-4">Login</h1> <h1 className="text-2xl font-bold text-[var(--primary)]">Login</h1>
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-5">
<input <input
type="email" type="email"
placeholder="Email" placeholder="Email"
value={email} value={email}
required required
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
className="w-full border px-3 py-2 rounded" className="w-full border border-[var(--border)] px-4 py-3 rounded-lg bg-white focus:outline-none focus:ring-2 focus:ring-[var(--primary)]"
/> />
<input <input
type="password" type="password"
@@ -44,12 +44,12 @@ export default function LoginPage() {
value={password} value={password}
required required
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
className="w-full border px-3 py-2 rounded" className="w-full border border-[var(--border)] px-4 py-3 rounded-lg bg-white focus:outline-none focus:ring-2 focus:ring-[var(--primary)]"
/> />
<button type="submit" className="w-full bg-blue-600 text-white py-2 rounded"> <button type="submit" className="w-full bg-[var(--primary)] text-white py-3 rounded-lg font-semibold shadow hover:bg-blue-700 transition">
Sign In Sign In
</button> </button>
{error && <p className="text-red-500">{error}</p>} {error && <p className="text-red-500 text-center">{error}</p>}
</form> </form>
</main> </main>
); );
+7 -9
View File
@@ -11,13 +11,11 @@ export default function RegisterPage() {
const handleRegister = async (e: React.FormEvent) => { const handleRegister = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
const res = await fetch('/api/auth/register', { const res = await fetch('/api/auth/register', {
method: 'POST', method: 'POST',
body: JSON.stringify({ email, password }), body: JSON.stringify({ email, password }),
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
}); });
if (res.ok) { if (res.ok) {
router.push('/auth/login'); router.push('/auth/login');
} else { } else {
@@ -27,16 +25,16 @@ export default function RegisterPage() {
}; };
return ( return (
<main className="max-w-md mx-auto p-6"> <main className="max-w-md mx-auto p-8 bg-[var(--muted)] rounded-xl shadow space-y-6">
<h1 className="text-2xl font-bold mb-4">Register</h1> <h1 className="text-2xl font-bold text-[var(--secondary)]">Register</h1>
<form onSubmit={handleRegister} className="space-y-4"> <form onSubmit={handleRegister} className="space-y-5">
<input <input
type="email" type="email"
placeholder="Email" placeholder="Email"
value={email} value={email}
required required
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
className="w-full border px-3 py-2 rounded" className="w-full border border-[var(--border)] px-4 py-3 rounded-lg bg-white focus:outline-none focus:ring-2 focus:ring-[var(--secondary)]"
/> />
<input <input
type="password" type="password"
@@ -44,12 +42,12 @@ export default function RegisterPage() {
value={password} value={password}
required required
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
className="w-full border px-3 py-2 rounded" className="w-full border border-[var(--border)] px-4 py-3 rounded-lg bg-white focus:outline-none focus:ring-2 focus:ring-[var(--secondary)]"
/> />
<button type="submit" className="w-full bg-green-600 text-white py-2 rounded"> <button type="submit" className="w-full bg-[var(--secondary)] text-white py-3 rounded-lg font-semibold shadow hover:bg-green-700 transition">
Register Register
</button> </button>
{error && <p className="text-red-500">{error}</p>} {error && <p className="text-red-500 text-center">{error}</p>}
</form> </form>
</main> </main>
); );
+64 -45
View File
@@ -57,56 +57,75 @@ export default function FillUpsPage() {
}; };
return ( return (
<main className="max-w-xl mx-auto p-6 space-y-6"> <main className="max-w-2xl mx-auto p-8 space-y-8 bg-[var(--muted)] rounded-xl shadow">
<h1 className="text-2xl font-bold">Fuel Fill-Ups</h1> <h1 className="text-2xl font-bold text-[var(--primary)]">Fuel Fill-Ups</h1>
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="grid grid-cols-1 sm:grid-cols-2 gap-4 bg-white p-6 rounded-lg border border-[var(--border)]">
<input <div className="flex flex-col gap-1">
name="mileage" <label htmlFor="mileage" className="font-medium text-[var(--primary)]">Odometer</label>
type="number" <input
placeholder="Odometer" id="mileage"
value={form.mileage} name="mileage"
onChange={handleChange} type="number"
required placeholder="e.g. 102300"
className="w-full border px-3 py-2 rounded" value={form.mileage}
/> onChange={handleChange}
<input required
name="liters" 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)]"
type="number" />
step="0.01" <span className="text-xs text-gray-500">Enter the odometer reading at fill-up</span>
placeholder="Liters" </div>
value={form.liters} <div className="flex flex-col gap-1">
onChange={handleChange} <label htmlFor="liters" className="font-medium text-[var(--primary)]">Liters</label>
required <input
className="w-full border px-3 py-2 rounded" id="liters"
/> name="liters"
<input type="number"
name="cost" step="0.01"
type="number" placeholder="e.g. 45.5"
step="0.01" value={form.liters}
placeholder="Total Cost" onChange={handleChange}
value={form.cost} required
onChange={handleChange} 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)]"
required />
className="w-full border px-3 py-2 rounded" <span className="text-xs text-gray-500">How many liters did you fill?</span>
/> </div>
<select <div className="flex flex-col gap-1">
name="currency" <label htmlFor="cost" className="font-medium text-[var(--primary)]">Total Cost</label>
value={form.currency} <input
onChange={handleChange} id="cost"
className="w-full border px-3 py-2 rounded" name="cost"
> type="number"
{currencies.map((c) => ( step="0.01"
<option key={c} value={c}>{c}</option> placeholder="e.g. 300.00"
))} value={form.cost}
</select> onChange={handleChange}
<button type="submit" className="w-full bg-green-600 text-white py-2 rounded"> 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">Total price paid for this fill-up</span>
</div>
<div className="flex flex-col gap-1">
<label htmlFor="currency" className="font-medium text-[var(--primary)]">Currency</label>
<select
id="currency"
name="currency"
value={form.currency}
onChange={handleChange}
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)]"
>
{currencies.map((c) => (
<option key={c} value={c}>{c}</option>
))}
</select>
</div>
<button type="submit" className="col-span-full bg-[var(--secondary)] text-white py-2 rounded-lg font-semibold shadow hover:bg-green-700 transition mt-2">
Add Fill-Up Add Fill-Up
</button> </button>
{error && <p className="text-red-500">{error}</p>} {error && <p className="col-span-full text-red-500 text-center mt-2">{error}</p>}
</form> </form>
<ul className="space-y-2"> <ul className="space-y-3">
{fillups.map((fill) => ( {fillups.map((fill) => (
<FillUpCard key={fill.id} fill={fill} /> // ✅ <FillUpCard key={fill.id} fill={fill} /> // ✅
))} ))}
+26 -16
View File
@@ -44,30 +44,40 @@ export default function MileagePage() {
}; };
return ( return (
<main className="max-w-xl mx-auto p-6 space-y-6"> <main className="max-w-xl mx-auto p-6 space-y-6 bg-[var(--muted)] rounded-xl shadow">
<h1 className="text-2xl font-bold">Mileage Log</h1> <h1 className="text-2xl font-bold text-[var(--primary)] flex items-center gap-2">
<span role="img" aria-label="mileage">🛣</span> Mileage Log
</h1>
<form onSubmit={handleSubmit} className="flex items-center gap-4"> <form onSubmit={handleSubmit} className="flex flex-col sm:flex-row items-center gap-4 bg-white p-6 rounded-lg border border-[var(--border)] shadow">
<input <div className="flex-1 flex flex-col gap-1 w-full">
type="number" <label htmlFor="mileage" className="font-medium text-[var(--primary)]">Odometer</label>
value={mileage} <input
onChange={(e) => setMileage(e.target.value)} id="mileage"
placeholder="Odometer (e.g. 102300)" type="number"
className="flex-1 border px-3 py-2 rounded" value={mileage}
required onChange={(e) => setMileage(e.target.value)}
/> placeholder="e.g. 102300"
<button type="submit" className="bg-blue-600 text-white px-4 py-2 rounded"> className="w-full 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)] placeholder:text-gray-500 dark:placeholder:text-gray-400"
required
/>
<span className="text-xs text-[var(--primary)] font-semibold">Enter the current odometer reading</span>
</div>
<button type="submit" className="w-full sm:w-auto bg-[var(--secondary)] text-white py-2 px-6 rounded-lg font-semibold shadow hover:bg-green-700 transition mt-2 sm:mt-6">
Add Add
</button> </button>
</form> </form>
{error && <p className="text-red-500">{error}</p>} {error && <p className="text-red-500 text-center">{error}</p>}
<ul className="space-y-2"> <ul className="space-y-2">
{entries.map((entry) => ( {entries.map((entry) => (
<li key={entry.id} className="border rounded p-3"> <li key={entry.id} className="border rounded p-3 bg-white shadow-sm flex items-center gap-4">
<p className="font-medium">{entry.mileage} km</p> <span className="text-[var(--primary)] text-xl"></span>
<p className="text-sm text-gray-500">{new Date(entry.date).toLocaleString()}</p> <div>
<p className="font-medium text-gray-800">{entry.mileage} km</p>
<p className="text-sm text-gray-600">{new Date(entry.date).toLocaleString()}</p>
</div>
</li> </li>
))} ))}
</ul> </ul>
+9 -12
View File
@@ -22,33 +22,30 @@ export default async function CarDetailPage({ params }: CarDetailPageProps) {
}); });
if (!car) { if (!car) {
return <div className="p-6">Car not found or unauthorized access.</div>; return <div className="p-8 text-center text-red-500 text-lg font-semibold">Car not found or unauthorized access.</div>;
} }
return ( return (
<main className="max-w-3xl mx-auto p-6 space-y-4"> <main className="max-w-3xl mx-auto p-8 space-y-6 bg-[var(--muted)] rounded-xl shadow">
<h1 className="text-3xl font-bold">{car.name}</h1> <h1 className="text-3xl font-bold text-[var(--primary)]">{car.name}</h1>
<p className="text-gray-700">{car.make} {car.model} ({car.year})</p> <p className="text-lg text-gray-700">{car.make} {car.model} ({car.year})</p>
<p className="text-gray-600">Fuel Type: {car.fuelType}</p> <p className="text-gray-600">Fuel Type: <span className="font-semibold text-[var(--secondary)]">{car.fuelType}</span></p>
<div className="flex flex-wrap gap-4 mt-8">
<div className="flex gap-4 mt-6">
<Link <Link
href={`/dashboard/cars/${car.id}/mileage`} href={`/dashboard/cars/${car.id}/mileage`}
className="bg-blue-600 text-white px-4 py-2 rounded" className="bg-[var(--primary)] text-white px-5 py-2 rounded-lg font-semibold shadow hover:bg-blue-700 transition"
> >
Add Mileage Add Mileage
</Link> </Link>
<Link <Link
href={`/dashboard/cars/${car.id}/fillups`} href={`/dashboard/cars/${car.id}/fillups`}
className="bg-green-600 text-white px-4 py-2 rounded" className="bg-[var(--secondary)] text-white px-5 py-2 rounded-lg font-semibold shadow hover:bg-green-700 transition"
> >
Add Fill-Up Add Fill-Up
</Link> </Link>
<Link <Link
href={`/dashboard/cars/${car.id}/stats`} href={`/dashboard/cars/${car.id}/stats`}
className="bg-purple-600 text-white px-4 py-2 rounded" className="bg-[var(--accent)] text-white px-5 py-2 rounded-lg font-semibold shadow hover:bg-purple-800 transition"
> >
📊 View Stats 📊 View Stats
</Link> </Link>
+18 -13
View File
@@ -16,7 +16,7 @@ export default async function StatsPage({ params }: StatsProps) {
const car = await prisma.car.findFirst({ const car = await prisma.car.findFirst({
where: { where: {
id: params.carId, id: params.carId,
user: { email: session.user?.email! }, user: { email: session.user?.email || '' },
}, },
include: { include: {
fillUps: { fillUps: {
@@ -30,9 +30,13 @@ export default async function StatsPage({ params }: StatsProps) {
if (fillUps.length < 2) { if (fillUps.length < 2) {
return ( return (
<main className="max-w-xl mx-auto p-6"> <main className="max-w-xl mx-auto p-8 flex flex-col items-center justify-center bg-[var(--muted)] rounded-xl shadow space-y-4">
<h1 className="text-2xl font-bold mb-2">Not Enough Data</h1> <div className="flex flex-col items-center gap-2">
<p className="text-gray-600">Add at least 2 fill-ups to see statistics.</p> <span className="text-5xl text-[var(--primary)]"></span>
<h1 className="text-2xl font-bold text-[var(--primary)]">Not Enough Data</h1>
<p className="text-gray-700 text-center">Add at least 2 fill-ups to see your fuel statistics and insights.</p>
<a href={`/dashboard/cars/${params.carId}/fillups`} className="mt-4 inline-block bg-[var(--secondary)] text-white px-6 py-2 rounded-lg font-semibold shadow hover:bg-green-700 transition"> Add Fill-Up</a>
</div>
</main> </main>
); );
} }
@@ -49,15 +53,16 @@ export default async function StatsPage({ params }: StatsProps) {
const costPerKm = totalCost / totalDistance; const costPerKm = totalCost / totalDistance;
return ( return (
<main className="max-w-xl mx-auto p-6 space-y-4"> <main className="max-w-2xl mx-auto p-8 space-y-8 bg-[var(--muted)] rounded-xl shadow">
<h1 className="text-2xl font-bold">Fuel Stats</h1> <h1 className="text-3xl font-bold text-[var(--primary)] flex items-center gap-2">
<span role="img" aria-label="stats">📊</span> Fuel Stats
<div className="space-y-2"> </h1>
<StatCard label="Total Distance" value={`${totalDistance.toFixed(0)} km`} /> <div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
<StatCard label="Total Fuel Used" value={`${totalLiters.toFixed(2)} L`} /> <StatCard label="Total Distance" value={`${totalDistance.toFixed(0)} km`} icon="🛣️" color="primary" />
<StatCard label="Total Fuel Cost" value={`${totalCost.toFixed(2)} ${fillUps[0].currency}`} /> <StatCard label="Total Fuel Used" value={`${totalLiters.toFixed(2)} L`} icon="⛽" color="secondary" />
<StatCard label="Average Consumption" value={`${avgConsumption.toFixed(2)} L / 100km`} /> <StatCard label="Total Fuel Cost" value={`${totalCost.toFixed(2)} ${fillUps[0].currency}`} icon="💸" color="accent" />
<StatCard label="Average Cost / km" value={`${costPerKm.toFixed(2)} ${fillUps[0].currency}`} /> <StatCard label="Average Consumption" value={`${avgConsumption.toFixed(2)} L / 100km`} icon="📏" color="primary" />
<StatCard label="Average Cost / km" value={`${costPerKm.toFixed(2)} ${fillUps[0].currency}`} icon="💰" color="secondary" />
</div> </div>
</main> </main>
); );
+10 -12
View File
@@ -22,13 +22,11 @@ export default function AddCarPage() {
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
const res = await fetch('/api/cars', { const res = await fetch('/api/cars', {
method: 'POST', method: 'POST',
body: JSON.stringify(form), body: JSON.stringify(form),
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
}); });
if (res.ok) { if (res.ok) {
router.push('/dashboard'); router.push('/dashboard');
} else { } else {
@@ -38,16 +36,16 @@ export default function AddCarPage() {
}; };
return ( return (
<main className="max-w-md mx-auto p-6 space-y-4"> <main className="max-w-lg mx-auto p-8 bg-[var(--muted)] rounded-xl shadow space-y-6">
<h1 className="text-2xl font-bold">Add New Car</h1> <h1 className="text-2xl font-bold text-[var(--primary)]">Add New Car</h1>
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-5">
<input <input
name="name" name="name"
value={form.name} value={form.name}
onChange={handleChange} onChange={handleChange}
required required
placeholder="Car Name" placeholder="Car Name"
className="w-full border px-3 py-2 rounded" className="w-full border border-[var(--border)] px-4 py-3 rounded-lg bg-white focus:outline-none focus:ring-2 focus:ring-[var(--primary)]"
/> />
<input <input
name="make" name="make"
@@ -55,7 +53,7 @@ export default function AddCarPage() {
onChange={handleChange} onChange={handleChange}
required required
placeholder="Make (e.g. BMW)" placeholder="Make (e.g. BMW)"
className="w-full border px-3 py-2 rounded" className="w-full border border-[var(--border)] px-4 py-3 rounded-lg bg-white focus:outline-none focus:ring-2 focus:ring-[var(--primary)]"
/> />
<input <input
name="model" name="model"
@@ -63,7 +61,7 @@ export default function AddCarPage() {
onChange={handleChange} onChange={handleChange}
required required
placeholder="Model (e.g. 320i)" placeholder="Model (e.g. 320i)"
className="w-full border px-3 py-2 rounded" className="w-full border border-[var(--border)] px-4 py-3 rounded-lg bg-white focus:outline-none focus:ring-2 focus:ring-[var(--primary)]"
/> />
<input <input
name="year" name="year"
@@ -72,22 +70,22 @@ export default function AddCarPage() {
onChange={handleChange} onChange={handleChange}
required required
placeholder="Year" placeholder="Year"
className="w-full border px-3 py-2 rounded" className="w-full border border-[var(--border)] px-4 py-3 rounded-lg bg-white focus:outline-none focus:ring-2 focus:ring-[var(--primary)]"
/> />
<select <select
name="fuelType" name="fuelType"
value={form.fuelType} value={form.fuelType}
onChange={handleChange} onChange={handleChange}
className="w-full border px-3 py-2 rounded" className="w-full border border-[var(--border)] px-4 py-3 rounded-lg bg-white focus:outline-none focus:ring-2 focus:ring-[var(--primary)]"
> >
{fuelTypes.map((type) => ( {fuelTypes.map((type) => (
<option key={type} value={type}>{type}</option> <option key={type} value={type}>{type}</option>
))} ))}
</select> </select>
<button type="submit" className="w-full bg-blue-600 text-white py-2 rounded"> <button type="submit" className="w-full bg-[var(--primary)] text-white py-3 rounded-lg font-semibold shadow hover:bg-blue-700 transition">
Save Car Save Car
</button> </button>
{error && <p className="text-red-500">{error}</p>} {error && <p className="text-red-500 text-center">{error}</p>}
</form> </form>
</main> </main>
); );
+10 -8
View File
@@ -1,6 +1,6 @@
import Link from 'next/link';
import { getSession } from '@/lib/auth'; import { getSession } from '@/lib/auth';
import { prisma } from '@/lib/prisma'; import { prisma } from '@/lib/prisma';
import Link from 'next/link';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
import CarCard from '@/components/CarCard'; import CarCard from '@/components/CarCard';
@@ -15,22 +15,24 @@ export default async function DashboardPage() {
}); });
return ( return (
<main className="max-w-4xl mx-auto p-6 space-y-6"> <main className="flex flex-col gap-8 max-w-5xl mx-auto p-6">
<div className="flex justify-between items-center"> <div className="flex flex-col sm:flex-row justify-between items-center gap-4">
<h1 className="text-2xl font-bold">Your Cars</h1> <h1 className="text-3xl font-bold text-[var(--primary)]">Your Cars</h1>
<Link href="/dashboard/cars/new" className="bg-blue-600 text-white px-4 py-2 rounded"> <Link href="/dashboard/cars/new" className="bg-[var(--primary)] text-white px-5 py-2 rounded-lg font-semibold shadow hover:bg-blue-700 transition">
+ Add Car + Add Car
</Link> </Link>
</div> </div>
{user?.cars.length ? ( {user?.cars.length ? (
<ul className="grid gap-4"> <ul className="grid gap-6 sm:grid-cols-2 md:grid-cols-3">
{user.cars.map((car) => ( {user.cars.map((car) => (
<CarCard key={car.id} car={car} /> <CarCard key={car.id} car={car} />
))} ))}
</ul> </ul>
) : ( ) : (
<p>No cars added yet.</p> <div className="text-center text-gray-500 py-12">
<p className="mb-4">No cars added yet.</p>
<Link href="/dashboard/cars/new" className="text-[var(--primary)] underline hover:text-blue-700">Add your first car</Link>
</div>
)} )}
</main> </main>
); );
+17
View File
@@ -3,6 +3,11 @@
:root { :root {
--background: #ffffff; --background: #ffffff;
--foreground: #171717; --foreground: #171717;
--primary: #2563eb; /* blue-600 */
--secondary: #22c55e; /* green-500 */
--accent: #a21caf; /* purple-700 */
--muted: #f3f4f6; /* gray-100 */
--border: #e5e7eb; /* gray-200 */
} }
@theme inline { @theme inline {
@@ -16,6 +21,8 @@
:root { :root {
--background: #0a0a0a; --background: #0a0a0a;
--foreground: #ededed; --foreground: #ededed;
--muted: #171717;
--border: #232323;
} }
} }
@@ -23,4 +30,14 @@ body {
background: var(--background); background: var(--background);
color: var(--foreground); color: var(--foreground);
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;
transition: background 0.2s, color 0.2s;
}
::-webkit-scrollbar {
width: 8px;
background: var(--muted);
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 4px;
} }
+17 -4
View File
@@ -1,6 +1,7 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google"; import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css"; import "./globals.css";
import Link from "next/link";
const geistSans = Geist({ const geistSans = Geist({
variable: "--font-geist-sans", variable: "--font-geist-sans",
@@ -13,8 +14,8 @@ const geistMono = Geist_Mono({
}); });
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Create Next App", title: "Car Fuel Tracker",
description: "Generated by create next app", description: "Track your car's fuel and mileage easily.",
}; };
export default function RootLayout({ export default function RootLayout({
@@ -25,9 +26,21 @@ export default function RootLayout({
return ( return (
<html lang="en"> <html lang="en">
<body <body
className={`${geistSans.variable} ${geistMono.variable} antialiased`} className={`${geistSans.variable} ${geistMono.variable} antialiased bg-[var(--background)] text-[var(--foreground)]`}
> >
{children} <header className="w-full px-6 py-4 flex items-center justify-between border-b border-[var(--border)] bg-[var(--muted)] shadow-sm">
<Link href="/dashboard" className="text-2xl font-bold tracking-tight text-[var(--primary)]">
🚗 Car Fuel Tracker
</Link>
<nav className="flex gap-4">
<Link href="/dashboard" className="hover:text-[var(--primary)] transition">Dashboard</Link>
<Link href="/dashboard/cars/new" className="hover:text-[var(--secondary)] transition">Add Car</Link>
</nav>
</header>
<div className="min-h-screen flex flex-col">
<div className="h-4" /> {/* Spacer between navbar and content */}
{children}
</div>
</body> </body>
</html> </html>
); );
+35 -89
View File
@@ -1,102 +1,48 @@
import Image from "next/image"; import Image from "next/image";
import Link from "next/link";
export default function Home() { export default function Home() {
return ( return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]"> <div className="flex flex-col min-h-screen bg-[var(--background)] text-[var(--foreground)]">
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start"> <section className="flex-1 flex flex-col items-center justify-center gap-10 p-8 sm:p-20">
<Image <div className="flex flex-col items-center gap-4">
className="dark:invert" <Image
src="/next.svg" className="dark:invert"
alt="Next.js logo" src="/next.svg"
width={180} alt="Next.js logo"
height={38} width={120}
priority height={30}
/> priority
<ol className="list-inside list-decimal text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]"> />
<li className="mb-2 tracking-[-.01em]"> <h1 className="text-4xl font-bold tracking-tight text-[var(--primary)] mb-2">
Get started by editing{" "} Welcome to Car Fuel Tracker
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold"> </h1>
src/app/page.tsx <p className="text-lg text-gray-500 max-w-xl text-center">
</code> Track your car's fuel fill-ups, mileage, and stats with a beautiful,
. simple dashboard.
</li> </p>
<li className="tracking-[-.01em]"> </div>
Save and see your changes instantly. <div className="flex gap-4 flex-wrap justify-center">
</li> <Link
</ol> href="/dashboard"
className="rounded-lg bg-[var(--primary)] text-white px-6 py-3 font-semibold shadow hover:bg-blue-700 transition"
<div className="flex gap-4 items-center flex-col sm:flex-row"> >
Go to Dashboard
</Link>
<a <a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto" className="rounded-lg border border-[var(--border)] bg-[var(--muted)] text-[var(--primary)] px-6 py-3 font-semibold hover:bg-[var(--primary)] hover:text-white transition"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" href="https://nextjs.org/docs"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<Image Next.js Docs
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a> </a>
</div> </div>
</main> </section>
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center"> <footer className="w-full py-4 flex items-center justify-center border-t border-[var(--border)] bg-[var(--muted)] text-sm text-gray-500">
<a <span>
className="flex items-center gap-2 hover:underline hover:underline-offset-4" &copy; {new Date().getFullYear()} Car Fuel Tracker. Built with Next.js.
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" </span>
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org
</a>
</footer> </footer>
</div> </div>
); );
+6 -6
View File
@@ -1,13 +1,13 @@
import Link from 'next/link';
import { Car } from '@prisma/client'; import { Car } from '@prisma/client';
import Link from 'next/link';
export default function CarCard({ car }: { car: Car }) { export default function CarCard({ car }: { car: Car }) {
return ( return (
<Link href={`/dashboard/cars/${car.id}`}> <Link href={`/dashboard/cars/${car.id}`} className="block group">
<div className="border rounded-xl p-4 shadow-sm hover:shadow-md transition bg-gray-50"> <div className="border border-[var(--border)] rounded-xl p-6 shadow-sm bg-white group-hover:shadow-lg transition flex flex-col gap-2 h-full">
<h2 className="text-xl font-bold text-black">{car.name}</h2> <h2 className="text-xl font-bold text-[var(--primary)] group-hover:underline">{car.name}</h2>
<p className="text-gray-600">{car.make} {car.model} ({car.year})</p> <p className="text-gray-700">{car.make} {car.model} <span className="text-gray-400">({car.year})</span></p>
<p className="text-sm text-gray-500 mt-1">Fuel Type: {car.fuelType}</p> <p className="text-sm text-[var(--secondary)] mt-1 font-semibold">Fuel Type: {car.fuelType}</p>
</div> </div>
</Link> </Link>
); );
+13 -4
View File
@@ -1,8 +1,17 @@
export default function StatCard({ label, value }: { label: string; value: string }) { export default function StatCard({ label, value, icon, color }: { label: string; value: string; icon?: string; color?: 'primary' | 'secondary' | 'accent' }) {
const valueClass =
label === 'Total Fuel Cost'
? 'text-gray-900'
: color
? `text-[var(--${color})]`
: 'text-[var(--foreground)]';
return ( return (
<div className="border rounded-xl p-4 shadow-sm bg-white"> <div className="border rounded-xl p-4 shadow-sm bg-white flex items-center gap-4">
<p className="text-sm text-gray-500">{label}</p> {icon && <span className={`text-2xl ${color ? `text-[var(--${color})]` : ''}`}>{icon}</span>}
<p className="text-2xl font-semibold">{value}</p> <div>
<p className="text-sm text-gray-500">{label}</p>
<p className={`text-2xl font-semibold ${valueClass}`}>{value}</p>
</div>
</div> </div>
); );
} }