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
First, run the development server:
### Prerequisites
- Node.js 18+
- PostgreSQL database
### 1. Clone the repository
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
git clone <your-repo-url>
cd Car-Fuel-Tracking-App
```
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.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
## Project Structure
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 (
<main className="max-w-md mx-auto p-6">
<h1 className="text-2xl font-bold mb-4">Login</h1>
<form onSubmit={handleSubmit} className="space-y-4">
<main className="max-w-md mx-auto p-8 bg-[var(--muted)] rounded-xl shadow space-y-6">
<h1 className="text-2xl font-bold text-[var(--primary)]">Login</h1>
<form onSubmit={handleSubmit} className="space-y-5">
<input
type="email"
placeholder="Email"
value={email}
required
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
type="password"
@@ -44,12 +44,12 @@ export default function LoginPage() {
value={password}
required
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
</button>
{error && <p className="text-red-500">{error}</p>}
{error && <p className="text-red-500 text-center">{error}</p>}
</form>
</main>
);
+7 -9
View File
@@ -11,13 +11,11 @@ export default function RegisterPage() {
const handleRegister = async (e: React.FormEvent) => {
e.preventDefault();
const res = await fetch('/api/auth/register', {
method: 'POST',
body: JSON.stringify({ email, password }),
headers: { 'Content-Type': 'application/json' },
});
if (res.ok) {
router.push('/auth/login');
} else {
@@ -27,16 +25,16 @@ export default function RegisterPage() {
};
return (
<main className="max-w-md mx-auto p-6">
<h1 className="text-2xl font-bold mb-4">Register</h1>
<form onSubmit={handleRegister} className="space-y-4">
<main className="max-w-md mx-auto p-8 bg-[var(--muted)] rounded-xl shadow space-y-6">
<h1 className="text-2xl font-bold text-[var(--secondary)]">Register</h1>
<form onSubmit={handleRegister} className="space-y-5">
<input
type="email"
placeholder="Email"
value={email}
required
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
type="password"
@@ -44,12 +42,12 @@ export default function RegisterPage() {
value={password}
required
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
</button>
{error && <p className="text-red-500">{error}</p>}
{error && <p className="text-red-500 text-center">{error}</p>}
</form>
</main>
);
+64 -45
View File
@@ -57,56 +57,75 @@ export default function FillUpsPage() {
};
return (
<main className="max-w-xl mx-auto p-6 space-y-6">
<h1 className="text-2xl font-bold">Fuel Fill-Ups</h1>
<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>
<form onSubmit={handleSubmit} className="space-y-4">
<input
name="mileage"
type="number"
placeholder="Odometer"
value={form.mileage}
onChange={handleChange}
required
className="w-full border px-3 py-2 rounded"
/>
<input
name="liters"
type="number"
step="0.01"
placeholder="Liters"
value={form.liters}
onChange={handleChange}
required
className="w-full border px-3 py-2 rounded"
/>
<input
name="cost"
type="number"
step="0.01"
placeholder="Total Cost"
value={form.cost}
onChange={handleChange}
required
className="w-full border px-3 py-2 rounded"
/>
<select
name="currency"
value={form.currency}
onChange={handleChange}
className="w-full border px-3 py-2 rounded"
>
{currencies.map((c) => (
<option key={c} value={c}>{c}</option>
))}
</select>
<button type="submit" className="w-full bg-green-600 text-white py-2 rounded">
<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="mileage" className="font-medium text-[var(--primary)]">Odometer</label>
<input
id="mileage"
name="mileage"
type="number"
placeholder="e.g. 102300"
value={form.mileage}
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">Enter the odometer reading at fill-up</span>
</div>
<div className="flex flex-col gap-1">
<label htmlFor="liters" className="font-medium text-[var(--primary)]">Liters</label>
<input
id="liters"
name="liters"
type="number"
step="0.01"
placeholder="e.g. 45.5"
value={form.liters}
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">How many liters did you fill?</span>
</div>
<div className="flex flex-col gap-1">
<label htmlFor="cost" className="font-medium text-[var(--primary)]">Total Cost</label>
<input
id="cost"
name="cost"
type="number"
step="0.01"
placeholder="e.g. 300.00"
value={form.cost}
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">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
</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>
<ul className="space-y-2">
<ul className="space-y-3">
{fillups.map((fill) => (
<FillUpCard key={fill.id} fill={fill} /> // ✅
))}
+26 -16
View File
@@ -44,30 +44,40 @@ export default function MileagePage() {
};
return (
<main className="max-w-xl mx-auto p-6 space-y-6">
<h1 className="text-2xl font-bold">Mileage Log</h1>
<main className="max-w-xl mx-auto p-6 space-y-6 bg-[var(--muted)] rounded-xl shadow">
<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">
<input
type="number"
value={mileage}
onChange={(e) => setMileage(e.target.value)}
placeholder="Odometer (e.g. 102300)"
className="flex-1 border px-3 py-2 rounded"
required
/>
<button type="submit" className="bg-blue-600 text-white px-4 py-2 rounded">
<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">
<div className="flex-1 flex flex-col gap-1 w-full">
<label htmlFor="mileage" className="font-medium text-[var(--primary)]">Odometer</label>
<input
id="mileage"
type="number"
value={mileage}
onChange={(e) => setMileage(e.target.value)}
placeholder="e.g. 102300"
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
</button>
</form>
{error && <p className="text-red-500">{error}</p>}
{error && <p className="text-red-500 text-center">{error}</p>}
<ul className="space-y-2">
{entries.map((entry) => (
<li key={entry.id} className="border rounded p-3">
<p className="font-medium">{entry.mileage} km</p>
<p className="text-sm text-gray-500">{new Date(entry.date).toLocaleString()}</p>
<li key={entry.id} className="border rounded p-3 bg-white shadow-sm flex items-center gap-4">
<span className="text-[var(--primary)] text-xl"></span>
<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>
))}
</ul>
+9 -12
View File
@@ -22,33 +22,30 @@ export default async function CarDetailPage({ params }: CarDetailPageProps) {
});
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 (
<main className="max-w-3xl mx-auto p-6 space-y-4">
<h1 className="text-3xl font-bold">{car.name}</h1>
<p className="text-gray-700">{car.make} {car.model} ({car.year})</p>
<p className="text-gray-600">Fuel Type: {car.fuelType}</p>
<div className="flex gap-4 mt-6">
<main className="max-w-3xl mx-auto p-8 space-y-6 bg-[var(--muted)] rounded-xl shadow">
<h1 className="text-3xl font-bold text-[var(--primary)]">{car.name}</h1>
<p className="text-lg text-gray-700">{car.make} {car.model} ({car.year})</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">
<Link
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
</Link>
<Link
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
</Link>
<Link
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
</Link>
+18 -13
View File
@@ -16,7 +16,7 @@ export default async function StatsPage({ params }: StatsProps) {
const car = await prisma.car.findFirst({
where: {
id: params.carId,
user: { email: session.user?.email! },
user: { email: session.user?.email || '' },
},
include: {
fillUps: {
@@ -30,9 +30,13 @@ export default async function StatsPage({ params }: StatsProps) {
if (fillUps.length < 2) {
return (
<main className="max-w-xl mx-auto p-6">
<h1 className="text-2xl font-bold mb-2">Not Enough Data</h1>
<p className="text-gray-600">Add at least 2 fill-ups to see statistics.</p>
<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">
<div className="flex flex-col items-center gap-2">
<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>
);
}
@@ -49,15 +53,16 @@ export default async function StatsPage({ params }: StatsProps) {
const costPerKm = totalCost / totalDistance;
return (
<main className="max-w-xl mx-auto p-6 space-y-4">
<h1 className="text-2xl font-bold">Fuel Stats</h1>
<div className="space-y-2">
<StatCard label="Total Distance" value={`${totalDistance.toFixed(0)} km`} />
<StatCard label="Total Fuel Used" value={`${totalLiters.toFixed(2)} L`} />
<StatCard label="Total Fuel Cost" value={`${totalCost.toFixed(2)} ${fillUps[0].currency}`} />
<StatCard label="Average Consumption" value={`${avgConsumption.toFixed(2)} L / 100km`} />
<StatCard label="Average Cost / km" value={`${costPerKm.toFixed(2)} ${fillUps[0].currency}`} />
<main className="max-w-2xl mx-auto p-8 space-y-8 bg-[var(--muted)] rounded-xl shadow">
<h1 className="text-3xl font-bold text-[var(--primary)] flex items-center gap-2">
<span role="img" aria-label="stats">📊</span> Fuel Stats
</h1>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
<StatCard label="Total Distance" value={`${totalDistance.toFixed(0)} km`} icon="🛣️" color="primary" />
<StatCard label="Total Fuel Used" value={`${totalLiters.toFixed(2)} L`} icon="⛽" color="secondary" />
<StatCard label="Total Fuel Cost" value={`${totalCost.toFixed(2)} ${fillUps[0].currency}`} icon="💸" color="accent" />
<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>
</main>
);
+10 -12
View File
@@ -22,13 +22,11 @@ export default function AddCarPage() {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const res = await fetch('/api/cars', {
method: 'POST',
body: JSON.stringify(form),
headers: { 'Content-Type': 'application/json' },
});
if (res.ok) {
router.push('/dashboard');
} else {
@@ -38,16 +36,16 @@ export default function AddCarPage() {
};
return (
<main className="max-w-md mx-auto p-6 space-y-4">
<h1 className="text-2xl font-bold">Add New Car</h1>
<form onSubmit={handleSubmit} className="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 text-[var(--primary)]">Add New Car</h1>
<form onSubmit={handleSubmit} className="space-y-5">
<input
name="name"
value={form.name}
onChange={handleChange}
required
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
name="make"
@@ -55,7 +53,7 @@ export default function AddCarPage() {
onChange={handleChange}
required
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
name="model"
@@ -63,7 +61,7 @@ export default function AddCarPage() {
onChange={handleChange}
required
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
name="year"
@@ -72,22 +70,22 @@ export default function AddCarPage() {
onChange={handleChange}
required
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
name="fuelType"
value={form.fuelType}
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) => (
<option key={type} value={type}>{type}</option>
))}
</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
</button>
{error && <p className="text-red-500">{error}</p>}
{error && <p className="text-red-500 text-center">{error}</p>}
</form>
</main>
);
+10 -8
View File
@@ -1,6 +1,6 @@
import Link from 'next/link';
import { getSession } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
import Link from 'next/link';
import { redirect } from 'next/navigation';
import CarCard from '@/components/CarCard';
@@ -15,22 +15,24 @@ export default async function DashboardPage() {
});
return (
<main className="max-w-4xl mx-auto p-6 space-y-6">
<div className="flex justify-between items-center">
<h1 className="text-2xl font-bold">Your Cars</h1>
<Link href="/dashboard/cars/new" className="bg-blue-600 text-white px-4 py-2 rounded">
<main className="flex flex-col gap-8 max-w-5xl mx-auto p-6">
<div className="flex flex-col sm:flex-row justify-between items-center gap-4">
<h1 className="text-3xl font-bold text-[var(--primary)]">Your Cars</h1>
<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
</Link>
</div>
{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) => (
<CarCard key={car.id} car={car} />
))}
</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>
);
+17
View File
@@ -3,6 +3,11 @@
:root {
--background: #ffffff;
--foreground: #171717;
--primary: #2563eb; /* blue-600 */
--secondary: #22c55e; /* green-500 */
--accent: #a21caf; /* purple-700 */
--muted: #f3f4f6; /* gray-100 */
--border: #e5e7eb; /* gray-200 */
}
@theme inline {
@@ -16,6 +21,8 @@
:root {
--background: #0a0a0a;
--foreground: #ededed;
--muted: #171717;
--border: #232323;
}
}
@@ -23,4 +30,14 @@ body {
background: var(--background);
color: var(--foreground);
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 { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import Link from "next/link";
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -13,8 +14,8 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "Car Fuel Tracker",
description: "Track your car's fuel and mileage easily.",
};
export default function RootLayout({
@@ -25,9 +26,21 @@ export default function RootLayout({
return (
<html lang="en">
<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>
</html>
);
+35 -89
View File
@@ -1,102 +1,48 @@
import Image from "next/image";
import Link from "next/link";
export default function Home() {
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)]">
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
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]">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold">
src/app/page.tsx
</code>
.
</li>
<li className="tracking-[-.01em]">
Save and see your changes instantly.
</li>
</ol>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<div className="flex flex-col min-h-screen bg-[var(--background)] text-[var(--foreground)]">
<section className="flex-1 flex flex-col items-center justify-center gap-10 p-8 sm:p-20">
<div className="flex flex-col items-center gap-4">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={120}
height={30}
priority
/>
<h1 className="text-4xl font-bold tracking-tight text-[var(--primary)] mb-2">
Welcome to Car Fuel Tracker
</h1>
<p className="text-lg text-gray-500 max-w-xl text-center">
Track your car's fuel fill-ups, mileage, and stats with a beautiful,
simple dashboard.
</p>
</div>
<div className="flex gap-4 flex-wrap justify-center">
<Link
href="/dashboard"
className="rounded-lg bg-[var(--primary)] text-white px-6 py-3 font-semibold shadow hover:bg-blue-700 transition"
>
Go to Dashboard
</Link>
<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"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
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://nextjs.org/docs"
target="_blank"
rel="noopener noreferrer"
>
<Image
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
Next.js Docs
</a>
</div>
</main>
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
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>
</section>
<footer className="w-full py-4 flex items-center justify-center border-t border-[var(--border)] bg-[var(--muted)] text-sm text-gray-500">
<span>
&copy; {new Date().getFullYear()} Car Fuel Tracker. Built with Next.js.
</span>
</footer>
</div>
);
+6 -6
View File
@@ -1,13 +1,13 @@
import Link from 'next/link';
import { Car } from '@prisma/client';
import Link from 'next/link';
export default function CarCard({ car }: { car: Car }) {
return (
<Link href={`/dashboard/cars/${car.id}`}>
<div className="border rounded-xl p-4 shadow-sm hover:shadow-md transition bg-gray-50">
<h2 className="text-xl font-bold text-black">{car.name}</h2>
<p className="text-gray-600">{car.make} {car.model} ({car.year})</p>
<p className="text-sm text-gray-500 mt-1">Fuel Type: {car.fuelType}</p>
<Link href={`/dashboard/cars/${car.id}`} className="block group">
<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-[var(--primary)] group-hover:underline">{car.name}</h2>
<p className="text-gray-700">{car.make} {car.model} <span className="text-gray-400">({car.year})</span></p>
<p className="text-sm text-[var(--secondary)] mt-1 font-semibold">Fuel Type: {car.fuelType}</p>
</div>
</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 (
<div className="border rounded-xl p-4 shadow-sm bg-white">
<p className="text-sm text-gray-500">{label}</p>
<p className="text-2xl font-semibold">{value}</p>
<div className="border rounded-xl p-4 shadow-sm bg-white flex items-center gap-4">
{icon && <span className={`text-2xl ${color ? `text-[var(--${color})]` : ''}`}>{icon}</span>}
<div>
<p className="text-sm text-gray-500">{label}</p>
<p className={`text-2xl font-semibold ${valueClass}`}>{value}</p>
</div>
</div>
);
}