mirror of
https://github.com/EdiFarcas/Car-Fuel-Tracking-App.git
synced 2026-06-22 07:00:55 +03:00
Logic + UI upgrade
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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} /> // ✅
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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"
|
© {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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user