mirror of
https://github.com/EdiFarcas/Car-Fuel-Tracking-App.git
synced 2026-06-28 17:00:42 +03:00
First protorype
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
import NextAuth, { AuthOptions } from "next-auth";
|
||||
import CredentialsProvider from "next-auth/providers/credentials";
|
||||
import { PrismaAdapter } from "@auth/prisma-adapter";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import bcrypt from "bcryptjs";
|
||||
|
||||
export const authOptions: AuthOptions = {
|
||||
adapter: PrismaAdapter(prisma),
|
||||
providers: [
|
||||
CredentialsProvider({
|
||||
name: "Credentials",
|
||||
credentials: {
|
||||
email: { label: "Email", type: "text" },
|
||||
password: { label: "Password", type: "password" },
|
||||
},
|
||||
async authorize(credentials) {
|
||||
// Check for missing credentials
|
||||
if (!credentials?.email || !credentials?.password) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find user by email
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: credentials.email },
|
||||
});
|
||||
|
||||
// Validate password
|
||||
if (user && await bcrypt.compare(credentials.password, user.hashedPassword)) {
|
||||
return user;
|
||||
}
|
||||
|
||||
// Return null if authentication fails
|
||||
return null;
|
||||
},
|
||||
}),
|
||||
],
|
||||
session: { strategy: "jwt" },
|
||||
pages: {
|
||||
signIn: "/auth/login",
|
||||
},
|
||||
};
|
||||
|
||||
const handler = NextAuth(authOptions);
|
||||
export { handler as GET, handler as POST };
|
||||
@@ -0,0 +1,27 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { email, password } = await req.json();
|
||||
|
||||
if (!email || !password) {
|
||||
return NextResponse.json({ message: 'Email and password are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const existingUser = await prisma.user.findUnique({ where: { email } });
|
||||
if (existingUser) {
|
||||
return NextResponse.json({ message: 'Email already exists' }, { status: 400 });
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
email,
|
||||
hashedPassword,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ message: 'User registered successfully' }, { status: 200 });
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const session = await getSession();
|
||||
if (!session) return NextResponse.json({ message: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const body = await req.json();
|
||||
const { name, make, model, year, fuelType } = body;
|
||||
|
||||
if (!name || !make || !model || !year || !fuelType) {
|
||||
return NextResponse.json({ message: 'Missing fields' }, { status: 400 });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: session.user?.email! },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ message: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
await prisma.car.create({
|
||||
data: {
|
||||
name,
|
||||
make,
|
||||
model,
|
||||
year: parseInt(year),
|
||||
fuelType,
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ message: 'Car added successfully' }, { status: 200 });
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
|
||||
export async function GET(req: Request) {
|
||||
const session = await getSession();
|
||||
if (!session) return NextResponse.json({ message: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const { searchParams } = new URL(req.url);
|
||||
const carId = searchParams.get('carId');
|
||||
|
||||
const fillUps = await prisma.fillUp.findMany({
|
||||
where: {
|
||||
car: {
|
||||
id: carId || '',
|
||||
user: { email: session.user?.email! },
|
||||
},
|
||||
},
|
||||
orderBy: { date: 'desc' },
|
||||
});
|
||||
|
||||
return NextResponse.json(fillUps);
|
||||
}
|
||||
|
||||
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 } = await req.json();
|
||||
if (!carId || !mileage || !liters || !cost || !currency) {
|
||||
return NextResponse.json({ message: 'Missing fields' }, { status: 400 });
|
||||
}
|
||||
|
||||
const car = await prisma.car.findFirst({
|
||||
where: {
|
||||
id: carId,
|
||||
user: { email: session.user?.email! },
|
||||
},
|
||||
});
|
||||
|
||||
if (!car) return NextResponse.json({ message: 'Car not found' }, { status: 404 });
|
||||
|
||||
const fillUp = await prisma.fillUp.create({
|
||||
data: {
|
||||
mileage: parseInt(mileage),
|
||||
liters: parseFloat(liters),
|
||||
cost: parseFloat(cost),
|
||||
currency,
|
||||
carId,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(fillUp);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
|
||||
// GET: Return mileage entries for a car
|
||||
export async function GET(req: Request) {
|
||||
const session = await getSession();
|
||||
if (!session) return NextResponse.json({ message: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const { searchParams } = new URL(req.url);
|
||||
const carId = searchParams.get('carId');
|
||||
|
||||
const entries = await prisma.mileageEntry.findMany({
|
||||
where: {
|
||||
car: {
|
||||
id: carId || '',
|
||||
user: {
|
||||
email: session.user?.email!,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { date: 'desc' },
|
||||
});
|
||||
|
||||
return NextResponse.json(entries);
|
||||
}
|
||||
|
||||
// POST: Add mileage entry
|
||||
export async function POST(req: Request) {
|
||||
const session = await getSession();
|
||||
if (!session) return NextResponse.json({ message: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const { carId, mileage } = await req.json();
|
||||
if (!carId || !mileage) {
|
||||
return NextResponse.json({ message: 'Missing fields' }, { status: 400 });
|
||||
}
|
||||
|
||||
const car = await prisma.car.findFirst({
|
||||
where: {
|
||||
id: carId,
|
||||
user: { email: session.user?.email! },
|
||||
},
|
||||
});
|
||||
|
||||
if (!car) return NextResponse.json({ message: 'Car not found' }, { status: 404 });
|
||||
|
||||
const entry = await prisma.mileageEntry.create({
|
||||
data: {
|
||||
mileage: parseInt(mileage),
|
||||
carId,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(entry);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { signIn } from 'next-auth/react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function LoginPage() {
|
||||
const router = useRouter();
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const res = await signIn('credentials', {
|
||||
redirect: false,
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
if (res?.ok) {
|
||||
router.push('/dashboard');
|
||||
} else {
|
||||
setError('Invalid credentials');
|
||||
}
|
||||
};
|
||||
|
||||
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">
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
value={email}
|
||||
required
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full border px-3 py-2 rounded"
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
required
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full border px-3 py-2 rounded"
|
||||
/>
|
||||
<button type="submit" className="w-full bg-blue-600 text-white py-2 rounded">
|
||||
Sign In
|
||||
</button>
|
||||
{error && <p className="text-red-500">{error}</p>}
|
||||
</form>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function RegisterPage() {
|
||||
const router = useRouter();
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
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 {
|
||||
const data = await res.json();
|
||||
setError(data.message || 'Registration failed');
|
||||
}
|
||||
};
|
||||
|
||||
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">
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
value={email}
|
||||
required
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full border px-3 py-2 rounded"
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
required
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full border px-3 py-2 rounded"
|
||||
/>
|
||||
<button type="submit" className="w-full bg-green-600 text-white py-2 rounded">
|
||||
Register
|
||||
</button>
|
||||
{error && <p className="text-red-500">{error}</p>}
|
||||
</form>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams } from 'next/navigation';
|
||||
import FillUpCard from '@/components/FillUpCard';
|
||||
|
||||
interface FillUp {
|
||||
id: string;
|
||||
mileage: number;
|
||||
liters: number;
|
||||
cost: number;
|
||||
currency: string;
|
||||
date: string;
|
||||
}
|
||||
|
||||
const currencies = ['EUR', 'USD', 'RON', 'GBP'];
|
||||
|
||||
export default function FillUpsPage() {
|
||||
const { carId } = useParams();
|
||||
const [fillups, setFillups] = useState<FillUp[]>([]);
|
||||
const [form, setForm] = useState({
|
||||
mileage: '',
|
||||
liters: '',
|
||||
cost: '',
|
||||
currency: 'EUR',
|
||||
});
|
||||
const [error, setError] = useState('');
|
||||
|
||||
// Fetch fill-up entries
|
||||
useEffect(() => {
|
||||
fetch(`/api/fillups?carId=${carId}`)
|
||||
.then((res) => res.json())
|
||||
.then(setFillups);
|
||||
}, [carId]);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
setForm({ ...form, [e.target.name]: e.target.value });
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const res = await fetch('/api/fillups', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ ...form, carId }),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const newFill = await res.json();
|
||||
setFillups((prev) => [newFill, ...prev]);
|
||||
setForm({ mileage: '', liters: '', cost: '', currency: 'EUR' });
|
||||
setError('');
|
||||
} else {
|
||||
const data = await res.json();
|
||||
setError(data.message || 'Error adding fill-up');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="max-w-xl mx-auto p-6 space-y-6">
|
||||
<h1 className="text-2xl font-bold">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">
|
||||
➕ Add Fill-Up
|
||||
</button>
|
||||
{error && <p className="text-red-500">{error}</p>}
|
||||
</form>
|
||||
|
||||
<ul className="space-y-2">
|
||||
{fillups.map((fill) => (
|
||||
<FillUpCard key={fill.id} fill={fill} /> // ✅
|
||||
))}
|
||||
</ul>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
|
||||
interface MileageEntry {
|
||||
id: string;
|
||||
mileage: number;
|
||||
date: string;
|
||||
}
|
||||
|
||||
export default function MileagePage() {
|
||||
const { carId } = useParams();
|
||||
const router = useRouter();
|
||||
|
||||
const [entries, setEntries] = useState<MileageEntry[]>([]);
|
||||
const [mileage, setMileage] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
// Fetch existing entries
|
||||
useEffect(() => {
|
||||
fetch(`/api/mileage?carId=${carId}`)
|
||||
.then((res) => res.json())
|
||||
.then((data) => setEntries(data));
|
||||
}, [carId]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const res = await fetch('/api/mileage', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ carId, mileage }),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const newEntry = await res.json();
|
||||
setEntries((prev) => [newEntry, ...prev]);
|
||||
setMileage('');
|
||||
} else {
|
||||
const data = await res.json();
|
||||
setError(data.message || 'Error adding mileage');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="max-w-xl mx-auto p-6 space-y-6">
|
||||
<h1 className="text-2xl font-bold">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">
|
||||
➕ Add
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{error && <p className="text-red-500">{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>
|
||||
))}
|
||||
</ul>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { redirect } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface CarDetailPageProps {
|
||||
params: {
|
||||
carId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default async function CarDetailPage({ params }: CarDetailPageProps) {
|
||||
const session = await getSession();
|
||||
if (!session) redirect('/auth/login');
|
||||
|
||||
const userEmail = session.user?.email!;
|
||||
const car = await prisma.car.findFirst({
|
||||
where: {
|
||||
id: params.carId,
|
||||
user: { email: userEmail },
|
||||
},
|
||||
});
|
||||
|
||||
if (!car) {
|
||||
return <div className="p-6">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">
|
||||
<Link
|
||||
href={`/dashboard/cars/${car.id}/mileage`}
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded"
|
||||
>
|
||||
➕ Add Mileage
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href={`/dashboard/cars/${car.id}/fillups`}
|
||||
className="bg-green-600 text-white px-4 py-2 rounded"
|
||||
>
|
||||
➕ Add Fill-Up
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href={`/dashboard/cars/${car.id}/stats`}
|
||||
className="bg-purple-600 text-white px-4 py-2 rounded"
|
||||
>
|
||||
📊 View Stats
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { redirect } from 'next/navigation';
|
||||
import StatCard from '@/components/StatCard';
|
||||
|
||||
interface StatsProps {
|
||||
params: {
|
||||
carId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default async function StatsPage({ params }: StatsProps) {
|
||||
const session = await getSession();
|
||||
if (!session) redirect('/auth/login');
|
||||
|
||||
const car = await prisma.car.findFirst({
|
||||
where: {
|
||||
id: params.carId,
|
||||
user: { email: session.user?.email! },
|
||||
},
|
||||
include: {
|
||||
fillUps: {
|
||||
orderBy: { mileage: 'asc' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!car) return <div className="p-6">Car not found or unauthorized.</div>;
|
||||
const fillUps = car.fillUps;
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
// Compute stats
|
||||
const first = fillUps[0];
|
||||
const last = fillUps[fillUps.length - 1];
|
||||
|
||||
const totalDistance = last.mileage - first.mileage;
|
||||
const totalLiters = fillUps.slice(1).reduce((sum, f) => sum + f.liters, 0); // ignore first fill
|
||||
const totalCost = fillUps.slice(1).reduce((sum, f) => sum + f.cost, 0);
|
||||
|
||||
const avgConsumption = (totalLiters / totalDistance) * 100; // L/100km
|
||||
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}`} />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
const fuelTypes = ['GASOLINE', 'DIESEL', 'LPG'];
|
||||
|
||||
export default function AddCarPage() {
|
||||
const router = useRouter();
|
||||
const [form, setForm] = useState({
|
||||
name: '',
|
||||
make: '',
|
||||
model: '',
|
||||
year: '',
|
||||
fuelType: 'GASOLINE',
|
||||
});
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
setForm({ ...form, [e.target.name]: e.target.value });
|
||||
};
|
||||
|
||||
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 {
|
||||
const data = await res.json();
|
||||
setError(data.message || 'Failed to add car');
|
||||
}
|
||||
};
|
||||
|
||||
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">
|
||||
<input
|
||||
name="name"
|
||||
value={form.name}
|
||||
onChange={handleChange}
|
||||
required
|
||||
placeholder="Car Name"
|
||||
className="w-full border px-3 py-2 rounded"
|
||||
/>
|
||||
<input
|
||||
name="make"
|
||||
value={form.make}
|
||||
onChange={handleChange}
|
||||
required
|
||||
placeholder="Make (e.g. BMW)"
|
||||
className="w-full border px-3 py-2 rounded"
|
||||
/>
|
||||
<input
|
||||
name="model"
|
||||
value={form.model}
|
||||
onChange={handleChange}
|
||||
required
|
||||
placeholder="Model (e.g. 320i)"
|
||||
className="w-full border px-3 py-2 rounded"
|
||||
/>
|
||||
<input
|
||||
name="year"
|
||||
type="number"
|
||||
value={form.year}
|
||||
onChange={handleChange}
|
||||
required
|
||||
placeholder="Year"
|
||||
className="w-full border px-3 py-2 rounded"
|
||||
/>
|
||||
<select
|
||||
name="fuelType"
|
||||
value={form.fuelType}
|
||||
onChange={handleChange}
|
||||
className="w-full border px-3 py-2 rounded"
|
||||
>
|
||||
{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">
|
||||
Save Car
|
||||
</button>
|
||||
{error && <p className="text-red-500">{error}</p>}
|
||||
</form>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
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';
|
||||
|
||||
export default async function DashboardPage() {
|
||||
const session = await getSession();
|
||||
if (!session) redirect('/auth/login');
|
||||
|
||||
const userEmail = session.user.email!;
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: userEmail },
|
||||
include: { cars: true },
|
||||
});
|
||||
|
||||
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">
|
||||
+ Add Car
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{user?.cars.length ? (
|
||||
<ul className="grid gap-4">
|
||||
{user.cars.map((car) => (
|
||||
<CarCard key={car.id} car={car} />
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p>No cars added yet.</p>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import Link from 'next/link';
|
||||
import { Car } from '@prisma/client';
|
||||
|
||||
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>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { FillUp } from '@prisma/client';
|
||||
|
||||
export default function FillUpCard({ fill }: { fill: FillUp }) {
|
||||
return (
|
||||
<div className="border rounded p-3 bg-white shadow-sm">
|
||||
<div className="font-semibold text-black">{fill.mileage} km - {fill.liters} L</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
{fill.cost} {fill.currency} — {new Date(fill.date).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 text-gray-900">
|
||||
<main className="max-w-4xl mx-auto p-6">{children}</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export default function StatCard({ label, value }: { label: string; value: string }) {
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
|
||||
|
||||
export function getSession() {
|
||||
return getServerSession(authOptions);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const globalForPrisma = globalThis as unknown as {
|
||||
prisma: PrismaClient | undefined;
|
||||
};
|
||||
|
||||
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
|
||||
Reference in New Issue
Block a user