Begin dark mode!

This commit is contained in:
2025-07-12 21:46:26 -04:00
parent 07f190bce2
commit fa4bd886b3
23 changed files with 2189 additions and 1030 deletions

View File

@@ -49,23 +49,25 @@ function RegisterForm() {
}
return (
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-green-50 to-emerald-100 p-4">
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-green-50 to-emerald-100 p-4 dark:from-gray-900 dark:to-gray-800">
<div className="w-full max-w-md space-y-8">
{/* Logo and Welcome */}
<div className="space-y-4 text-center">
<Logo size="lg" className="mx-auto" />
<div>
<h1 className="text-2xl font-bold text-gray-900">Join beenvoice</h1>
<p className="mt-2 text-gray-600">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
Join beenvoice
</h1>
<p className="mt-2 text-gray-600 dark:text-gray-300">
Create your account to get started
</p>
</div>
</div>
{/* Registration Form */}
<Card className="border-0 shadow-xl">
<Card className="border-0 shadow-xl dark:bg-gray-800">
<CardHeader className="space-y-1">
<CardTitle className="text-center text-xl">
<CardTitle className="text-center text-xl dark:text-white">
Create Account
</CardTitle>
</CardHeader>
@@ -75,7 +77,7 @@ function RegisterForm() {
<div className="space-y-2">
<Label htmlFor="firstName">First Name</Label>
<div className="relative">
<User className="absolute top-3 left-3 h-4 w-4 text-gray-400" />
<User className="absolute top-3 left-3 h-4 w-4 text-gray-400 dark:text-gray-500" />
<Input
id="firstName"
type="text"
@@ -91,7 +93,7 @@ function RegisterForm() {
<div className="space-y-2">
<Label htmlFor="lastName">Last Name</Label>
<div className="relative">
<User className="absolute top-3 left-3 h-4 w-4 text-gray-400" />
<User className="absolute top-3 left-3 h-4 w-4 text-gray-400 dark:text-gray-500" />
<Input
id="lastName"
type="text"
@@ -107,7 +109,7 @@ function RegisterForm() {
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<div className="relative">
<Mail className="absolute top-3 left-3 h-4 w-4 text-gray-400" />
<Mail className="absolute top-3 left-3 h-4 w-4 text-gray-400 dark:text-gray-500" />
<Input
id="email"
type="email"
@@ -122,7 +124,7 @@ function RegisterForm() {
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<div className="relative">
<Lock className="absolute top-3 left-3 h-4 w-4 text-gray-400" />
<Lock className="absolute top-3 left-3 h-4 w-4 text-gray-400 dark:text-gray-500" />
<Input
id="password"
type="password"
@@ -134,7 +136,7 @@ function RegisterForm() {
placeholder="Create a password"
/>
</div>
<p className="text-xs text-gray-500">
<p className="text-xs text-gray-500 dark:text-gray-400">
Must be at least 6 characters
</p>
</div>
@@ -150,10 +152,12 @@ function RegisterForm() {
</Button>
</form>
<div className="mt-6 text-center text-sm">
<span className="text-gray-600">Already have an account? </span>
<span className="text-gray-600 dark:text-gray-300">
Already have an account?{" "}
</span>
<Link
href="/auth/signin"
className="font-medium text-green-600 hover:text-green-700"
className="font-medium text-green-600 hover:text-green-700 dark:text-green-400 dark:hover:text-green-300"
>
Sign in here
</Link>
@@ -163,8 +167,10 @@ function RegisterForm() {
{/* Features */}
<div className="space-y-4 text-center">
<p className="text-sm text-gray-500">Start invoicing like a pro</p>
<div className="flex justify-center space-x-6 text-xs text-gray-400">
<p className="text-sm text-gray-500 dark:text-gray-400">
Start invoicing like a pro
</p>
<div className="flex justify-center space-x-6 text-xs text-gray-400 dark:text-gray-500">
<span> Free to start</span>
<span> No credit card</span>
<span> Cancel anytime</span>
@@ -179,15 +185,17 @@ export default function RegisterPage() {
return (
<Suspense
fallback={
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-green-50 to-emerald-100 p-4">
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-green-50 to-emerald-100 p-4 dark:from-gray-900 dark:to-gray-800">
<div className="w-full max-w-md space-y-8">
<div className="space-y-4 text-center">
<Logo size="lg" className="mx-auto" />
<div>
<h1 className="text-2xl font-bold text-gray-900">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
Join beenvoice
</h1>
<p className="mt-2 text-gray-600">Loading...</p>
<p className="mt-2 text-gray-600 dark:text-gray-300">
Loading...
</p>
</div>
</div>
</div>

View File

@@ -42,30 +42,34 @@ function SignInForm() {
}
return (
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-green-50 to-emerald-100 p-4">
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-green-50 to-emerald-100 p-4 dark:from-gray-900 dark:to-gray-800">
<div className="w-full max-w-md space-y-8">
{/* Logo and Welcome */}
<div className="space-y-4 text-center">
<Logo size="lg" className="mx-auto" />
<div>
<h1 className="text-2xl font-bold text-gray-900">Welcome back</h1>
<p className="mt-2 text-gray-600">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
Welcome back
</h1>
<p className="mt-2 text-gray-600 dark:text-gray-300">
Sign in to your beenvoice account
</p>
</div>
</div>
{/* Sign In Form */}
<Card className="border-0 shadow-xl">
<Card className="border-0 shadow-xl dark:bg-gray-800">
<CardHeader className="space-y-1">
<CardTitle className="text-center text-xl">Sign In</CardTitle>
<CardTitle className="text-center text-xl dark:text-white">
Sign In
</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSignIn} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<div className="relative">
<Mail className="absolute top-3 left-3 h-4 w-4 text-gray-400" />
<Mail className="absolute top-3 left-3 h-4 w-4 text-gray-400 dark:text-gray-500" />
<Input
id="email"
type="email"
@@ -81,7 +85,7 @@ function SignInForm() {
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<div className="relative">
<Lock className="absolute top-3 left-3 h-4 w-4 text-gray-400" />
<Lock className="absolute top-3 left-3 h-4 w-4 text-gray-400 dark:text-gray-500" />
<Input
id="password"
type="password"
@@ -105,12 +109,12 @@ function SignInForm() {
</Button>
</form>
<div className="mt-6 text-center text-sm">
<span className="text-gray-600">
<span className="text-gray-600 dark:text-gray-300">
Don&apos;t have an account?{" "}
</span>
<Link
href="/auth/register"
className="font-medium text-green-600 hover:text-green-700"
className="font-medium text-green-600 hover:text-green-700 dark:text-green-400 dark:hover:text-green-300"
>
Create one now
</Link>
@@ -120,10 +124,10 @@ function SignInForm() {
{/* Features */}
<div className="space-y-4 text-center">
<p className="text-sm text-gray-500">
<p className="text-sm text-gray-500 dark:text-gray-400">
Simple invoicing for freelancers and small businesses
</p>
<div className="flex justify-center space-x-6 text-xs text-gray-400">
<div className="flex justify-center space-x-6 text-xs text-gray-400 dark:text-gray-500">
<span> Easy client management</span>
<span> Professional invoices</span>
<span> Payment tracking</span>
@@ -138,15 +142,17 @@ export default function SignInPage() {
return (
<Suspense
fallback={
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-green-50 to-emerald-100 p-4">
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-green-50 to-emerald-100 p-4 dark:from-gray-900 dark:to-gray-800">
<div className="w-full max-w-md space-y-8">
<div className="space-y-4 text-center">
<Logo size="lg" className="mx-auto" />
<div>
<h1 className="text-2xl font-bold text-gray-900">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
Welcome back
</h1>
<p className="mt-2 text-gray-600">Loading...</p>
<p className="mt-2 text-gray-600 dark:text-gray-300">
Loading...
</p>
</div>
</div>
</div>

View File

@@ -3,21 +3,26 @@
import { api } from "~/trpc/react";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { Button } from "~/components/ui/button";
import {
Users,
FileText,
TrendingUp,
import {
Users,
FileText,
TrendingUp,
Calendar,
Plus,
ArrowRight
ArrowRight,
} from "lucide-react";
import Link from "next/link";
import { DashboardStatsSkeleton, DashboardActivitySkeleton } from "~/components/ui/skeleton";
import {
DashboardStatsSkeleton,
DashboardActivitySkeleton,
} from "~/components/ui/skeleton";
// Client component for dashboard stats
export function DashboardStats() {
const { data: clients, isLoading: clientsLoading } = api.clients.getAll.useQuery();
const { data: invoices, isLoading: invoicesLoading } = api.invoices.getAll.useQuery();
const { data: clients, isLoading: clientsLoading } =
api.clients.getAll.useQuery();
const { data: invoices, isLoading: invoicesLoading } =
api.invoices.getAll.useQuery();
if (clientsLoading || invoicesLoading) {
return <DashboardStatsSkeleton />;
@@ -25,8 +30,12 @@ export function DashboardStats() {
const totalClients = clients?.length ?? 0;
const totalInvoices = invoices?.length ?? 0;
const totalRevenue = invoices?.reduce((sum, invoice) => sum + invoice.totalAmount, 0) ?? 0;
const pendingInvoices = invoices?.filter(invoice => invoice.status === "sent" || invoice.status === "draft").length ?? 0;
const totalRevenue =
invoices?.reduce((sum, invoice) => sum + invoice.totalAmount, 0) ?? 0;
const pendingInvoices =
invoices?.filter(
(invoice) => invoice.status === "sent" || invoice.status === "draft",
).length ?? 0;
// Calculate month-over-month changes (simplified)
const lastMonthClients = 0; // This would need historical data
@@ -34,62 +43,85 @@ export function DashboardStats() {
const lastMonthRevenue = 0;
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm hover:shadow-2xl transition-all duration-300">
<div className="mb-8 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm transition-all duration-300 hover:shadow-2xl dark:bg-gray-800/80">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-gray-700">Total Clients</CardTitle>
<div className="p-2 bg-emerald-100 rounded-lg">
<Users className="h-4 w-4 text-emerald-600" />
<CardTitle className="text-sm font-medium text-gray-700 dark:text-gray-300">
Total Clients
</CardTitle>
<div className="rounded-lg bg-emerald-100 p-2 dark:bg-emerald-900/30">
<Users className="h-4 w-4 text-emerald-600 dark:text-emerald-400" />
</div>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-emerald-600">{totalClients}</div>
<p className="text-xs text-gray-500">
{totalClients > lastMonthClients ? "+" : ""}{totalClients - lastMonthClients} from last month
<div className="text-3xl font-bold text-emerald-600 dark:text-emerald-400">
{totalClients}
</div>
<p className="text-xs text-gray-500 dark:text-gray-400">
{totalClients > lastMonthClients ? "+" : ""}
{totalClients - lastMonthClients} from last month
</p>
</CardContent>
</Card>
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm hover:shadow-2xl transition-all duration-300">
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm transition-all duration-300 hover:shadow-2xl dark:bg-gray-800/80">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-gray-700">Total Invoices</CardTitle>
<div className="p-2 bg-blue-100 rounded-lg">
<FileText className="h-4 w-4 text-blue-600" />
<CardTitle className="text-sm font-medium text-gray-700 dark:text-gray-300">
Total Invoices
</CardTitle>
<div className="rounded-lg bg-blue-100 p-2 dark:bg-blue-900/30">
<FileText className="h-4 w-4 text-blue-600 dark:text-blue-400" />
</div>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-blue-600">{totalInvoices}</div>
<p className="text-xs text-gray-500">
{totalInvoices > lastMonthInvoices ? "+" : ""}{totalInvoices - lastMonthInvoices} from last month
<div className="text-3xl font-bold text-blue-600 dark:text-blue-400">
{totalInvoices}
</div>
<p className="text-xs text-gray-500 dark:text-gray-400">
{totalInvoices > lastMonthInvoices ? "+" : ""}
{totalInvoices - lastMonthInvoices} from last month
</p>
</CardContent>
</Card>
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm hover:shadow-2xl transition-all duration-300">
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm transition-all duration-300 hover:shadow-2xl dark:bg-gray-800/80">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-gray-700">Revenue</CardTitle>
<div className="p-2 bg-teal-100 rounded-lg">
<TrendingUp className="h-4 w-4 text-teal-600" />
<CardTitle className="text-sm font-medium text-gray-700 dark:text-gray-300">
Revenue
</CardTitle>
<div className="rounded-lg bg-teal-100 p-2 dark:bg-teal-900/30">
<TrendingUp className="h-4 w-4 text-teal-600 dark:text-teal-400" />
</div>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-teal-600">${totalRevenue.toFixed(2)}</div>
<p className="text-xs text-gray-500">
{totalRevenue > lastMonthRevenue ? "+" : ""}{((totalRevenue - lastMonthRevenue) / (lastMonthRevenue || 1) * 100).toFixed(1)}% from last month
<div className="text-3xl font-bold text-teal-600 dark:text-teal-400">
${totalRevenue.toFixed(2)}
</div>
<p className="text-xs text-gray-500 dark:text-gray-400">
{totalRevenue > lastMonthRevenue ? "+" : ""}
{(
((totalRevenue - lastMonthRevenue) / (lastMonthRevenue || 1)) *
100
).toFixed(1)}
% from last month
</p>
</CardContent>
</Card>
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm hover:shadow-2xl transition-all duration-300">
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm transition-all duration-300 hover:shadow-2xl dark:bg-gray-800/80">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-gray-700">Pending Invoices</CardTitle>
<div className="p-2 bg-orange-100 rounded-lg">
<Calendar className="h-4 w-4 text-orange-600" />
<CardTitle className="text-sm font-medium text-gray-700 dark:text-gray-300">
Pending Invoices
</CardTitle>
<div className="rounded-lg bg-orange-100 p-2 dark:bg-orange-900/30">
<Calendar className="h-4 w-4 text-orange-600 dark:text-orange-400" />
</div>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-orange-600">{pendingInvoices}</div>
<p className="text-xs text-gray-500">
<div className="text-3xl font-bold text-orange-600 dark:text-orange-400">
{pendingInvoices}
</div>
<p className="text-xs text-gray-500 dark:text-gray-400">
Due this month
</p>
</CardContent>
@@ -101,34 +133,34 @@ export function DashboardStats() {
// Client component for dashboard cards
export function DashboardCards() {
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm hover:shadow-2xl transition-all duration-300">
<div className="mb-8 grid grid-cols-1 gap-8 lg:grid-cols-2">
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm transition-all duration-300 hover:shadow-2xl dark:bg-gray-800/80">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-emerald-700">
<div className="p-2 bg-emerald-100 rounded-lg">
<CardTitle className="flex items-center gap-2 text-emerald-700 dark:text-emerald-400">
<div className="rounded-lg bg-emerald-100 p-2 dark:bg-emerald-900/30">
<Users className="h-5 w-5" />
</div>
Manage Clients
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-gray-600">
<p className="text-gray-600 dark:text-gray-300">
Add new clients and manage your existing client relationships.
</p>
<div className="flex gap-3">
<Button
<Button
asChild
className="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700 text-white font-medium shadow-lg hover:shadow-xl transition-all duration-200"
className="bg-gradient-to-r from-emerald-600 to-teal-600 font-medium text-white shadow-lg transition-all duration-200 hover:from-emerald-700 hover:to-teal-700 hover:shadow-xl"
>
<Link href="/dashboard/clients/new">
<Plus className="mr-2 h-4 w-4" />
Add Client
</Link>
</Button>
<Button
variant="outline"
<Button
variant="outline"
asChild
className="border-gray-300 text-gray-700 hover:bg-gray-50 font-medium"
className="border-gray-300 font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800"
>
<Link href="/dashboard/clients">
View All Clients
@@ -139,33 +171,33 @@ export function DashboardCards() {
</CardContent>
</Card>
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm hover:shadow-2xl transition-all duration-300">
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm transition-all duration-300 hover:shadow-2xl dark:bg-gray-800/80">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-emerald-700">
<div className="p-2 bg-emerald-100 rounded-lg">
<CardTitle className="flex items-center gap-2 text-emerald-700 dark:text-emerald-400">
<div className="rounded-lg bg-emerald-100 p-2 dark:bg-emerald-900/30">
<FileText className="h-5 w-5" />
</div>
Create Invoices
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-gray-600">
<p className="text-gray-600 dark:text-gray-300">
Generate professional invoices and track payments.
</p>
<div className="flex gap-3">
<Button
<Button
asChild
className="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700 text-white font-medium shadow-lg hover:shadow-xl transition-all duration-200"
className="bg-gradient-to-r from-emerald-600 to-teal-600 font-medium text-white shadow-lg transition-all duration-200 hover:from-emerald-700 hover:to-teal-700 hover:shadow-xl"
>
<Link href="/dashboard/invoices/new">
<Plus className="mr-2 h-4 w-4" />
New Invoice
</Link>
</Button>
<Button
variant="outline"
<Button
variant="outline"
asChild
className="border-gray-300 text-gray-700 hover:bg-gray-50 font-medium"
className="border-gray-300 font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800"
>
<Link href="/dashboard/invoices">
View All Invoices
@@ -190,42 +222,60 @@ export function DashboardActivity() {
const recentInvoices = invoices?.slice(0, 5) ?? [];
return (
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm">
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
<CardHeader>
<CardTitle className="text-emerald-700">Recent Activity</CardTitle>
<CardTitle className="text-emerald-700 dark:text-emerald-400">
Recent Activity
</CardTitle>
</CardHeader>
<CardContent>
{recentInvoices.length === 0 ? (
<div className="text-center py-12 text-gray-500">
<div className="p-4 bg-gray-100 rounded-full w-20 h-20 mx-auto mb-4 flex items-center justify-center">
<FileText className="h-8 w-8 text-gray-400" />
<div className="py-12 text-center text-gray-500 dark:text-gray-400">
<div className="mx-auto mb-4 flex h-20 w-20 items-center justify-center rounded-full bg-gray-100 p-4 dark:bg-gray-700">
<FileText className="h-8 w-8 text-gray-400 dark:text-gray-500" />
</div>
<p className="text-lg font-medium mb-2">No recent activity</p>
<p className="text-sm">Start by adding your first client or creating an invoice</p>
<p className="mb-2 text-lg font-medium dark:text-gray-300">
No recent activity
</p>
<p className="text-sm dark:text-gray-400">
Start by adding your first client or creating an invoice
</p>
</div>
) : (
<div className="space-y-4">
{recentInvoices.map((invoice) => (
<div key={invoice.id} className="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
<div
key={invoice.id}
className="flex items-center justify-between rounded-lg bg-gray-50 p-4 dark:bg-gray-700"
>
<div className="flex items-center gap-3">
<div className="p-2 bg-emerald-100 rounded-lg">
<FileText className="h-4 w-4 text-emerald-600" />
<div className="rounded-lg bg-emerald-100 p-2 dark:bg-emerald-900/30">
<FileText className="h-4 w-4 text-emerald-600 dark:text-emerald-400" />
</div>
<div>
<p className="font-medium text-gray-900">Invoice #{invoice.invoiceNumber}</p>
<p className="text-sm text-gray-500">
{invoice.client?.name ?? "Unknown Client"} ${invoice.totalAmount.toFixed(2)}
<p className="font-medium text-gray-900 dark:text-white">
Invoice #{invoice.invoiceNumber}
</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
{invoice.client?.name ?? "Unknown Client"} $
{invoice.totalAmount.toFixed(2)}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<span className={`px-2 py-1 text-xs font-medium rounded-full ${
invoice.status === "paid" ? "bg-green-100 text-green-800" :
invoice.status === "sent" ? "bg-blue-100 text-blue-800" :
invoice.status === "overdue" ? "bg-red-100 text-red-800" :
"bg-gray-100 text-gray-800"
}`}>
{invoice.status.charAt(0).toUpperCase() + invoice.status.slice(1)}
<span
className={`rounded-full px-2 py-1 text-xs font-medium ${
invoice.status === "paid"
? "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400"
: invoice.status === "sent"
? "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400"
: invoice.status === "overdue"
? "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400"
: "bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300"
}`}
>
{invoice.status.charAt(0).toUpperCase() +
invoice.status.slice(1)}
</span>
<Button variant="ghost" size="sm" asChild>
<Link href={`/dashboard/invoices/${invoice.id}`}>
@@ -240,4 +290,4 @@ export function DashboardActivity() {
</CardContent>
</Card>
);
}
}

View File

@@ -4,17 +4,27 @@ import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { Button } from "~/components/ui/button";
import { Badge } from "~/components/ui/badge";
import Link from "next/link";
import { Edit, Mail, Phone, MapPin, Building, Calendar, DollarSign } from "lucide-react";
import {
Edit,
Mail,
Phone,
MapPin,
Building,
Calendar,
DollarSign,
} from "lucide-react";
interface ClientDetailPageProps {
params: Promise<{ id: string }>;
}
export default async function ClientDetailPage({ params }: ClientDetailPageProps) {
export default async function ClientDetailPage({
params,
}: ClientDetailPageProps) {
const { id } = await params;
const client = await api.clients.getById({ id });
if (!client) {
notFound();
}
@@ -34,20 +44,26 @@ export default async function ClientDetailPage({ params }: ClientDetailPageProps
}).format(amount);
};
const totalInvoiced = client.invoices?.reduce((sum, invoice) => sum + invoice.totalAmount, 0) || 0;
const paidInvoices = client.invoices?.filter(invoice => invoice.status === "paid").length || 0;
const pendingInvoices = client.invoices?.filter(invoice => invoice.status === "sent").length || 0;
const totalInvoiced =
client.invoices?.reduce((sum, invoice) => sum + invoice.totalAmount, 0) ||
0;
const paidInvoices =
client.invoices?.filter((invoice) => invoice.status === "paid").length || 0;
const pendingInvoices =
client.invoices?.filter((invoice) => invoice.status === "sent").length || 0;
return (
<div className="p-4 md:p-6 md:ml-72 md:mr-4">
<div className="max-w-4xl mx-auto space-y-6">
<div className="p-4 md:mr-4 md:ml-72 md:p-6">
<div className="mx-auto max-w-4xl space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-transparent">
<h1 className="bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-3xl font-bold text-transparent">
{client.name}
</h1>
<p className="text-muted-foreground">Client Details</p>
<p className="text-muted-foreground dark:text-gray-300">
Client Details
</p>
</div>
<Link href={`/clients/${client.id}/edit`}>
<Button className="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700">
@@ -57,39 +73,47 @@ export default async function ClientDetailPage({ params }: ClientDetailPageProps
</Link>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
{/* Client Information Card */}
<div className="lg:col-span-2">
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm">
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
<CardHeader>
<CardTitle className="flex items-center space-x-2 text-emerald-700">
<CardTitle className="flex items-center space-x-2 text-emerald-700 dark:text-emerald-400">
<Building className="h-5 w-5" />
<span>Contact Information</span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Basic Info */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
{client.email && (
<div className="flex items-center space-x-3">
<div className="p-2 bg-emerald-100 rounded-lg">
<div className="rounded-lg bg-emerald-100 p-2 dark:bg-emerald-900/30">
<Mail className="h-4 w-4 text-emerald-600" />
</div>
<div>
<p className="text-sm font-medium text-gray-500">Email</p>
<p className="text-sm">{client.email}</p>
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">
Email
</p>
<p className="text-sm dark:text-gray-300">
{client.email}
</p>
</div>
</div>
)}
{client.phone && (
<div className="flex items-center space-x-3">
<div className="p-2 bg-emerald-100 rounded-lg">
<div className="rounded-lg bg-emerald-100 p-2 dark:bg-emerald-900/30">
<Phone className="h-4 w-4 text-emerald-600" />
</div>
<div>
<p className="text-sm font-medium text-gray-500">Phone</p>
<p className="text-sm">{client.phone}</p>
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">
Phone
</p>
<p className="text-sm dark:text-gray-300">
{client.phone}
</p>
</div>
</div>
)}
@@ -99,19 +123,23 @@ export default async function ClientDetailPage({ params }: ClientDetailPageProps
{(client.addressLine1 ?? client.city ?? client.state) && (
<div className="space-y-4">
<div className="flex items-center space-x-3">
<div className="p-2 bg-emerald-100 rounded-lg">
<div className="rounded-lg bg-emerald-100 p-2 dark:bg-emerald-900/30">
<MapPin className="h-4 w-4 text-emerald-600" />
</div>
<div>
<p className="text-sm font-medium text-gray-500">Address</p>
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">
Address
</p>
</div>
</div>
<div className="ml-11 space-y-1 text-sm">
<div className="ml-11 space-y-1 text-sm dark:text-gray-300">
{client.addressLine1 && <p>{client.addressLine1}</p>}
{client.addressLine2 && <p>{client.addressLine2}</p>}
{(client.city ?? client.state ?? client.postalCode) && (
<p>
{[client.city, client.state, client.postalCode].filter(Boolean).join(", ")}
{[client.city, client.state, client.postalCode]
.filter(Boolean)
.join(", ")}
</p>
)}
{client.country && <p>{client.country}</p>}
@@ -121,12 +149,16 @@ export default async function ClientDetailPage({ params }: ClientDetailPageProps
{/* Client Since */}
<div className="flex items-center space-x-3">
<div className="p-2 bg-emerald-100 rounded-lg">
<div className="rounded-lg bg-emerald-100 p-2 dark:bg-emerald-900/30">
<Calendar className="h-4 w-4 text-emerald-600" />
</div>
<div>
<p className="text-sm font-medium text-gray-500">Client Since</p>
<p className="text-sm">{formatDate(client.createdAt)}</p>
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">
Client Since
</p>
<p className="text-sm dark:text-gray-300">
{formatDate(client.createdAt)}
</p>
</div>
</div>
</CardContent>
@@ -135,9 +167,9 @@ export default async function ClientDetailPage({ params }: ClientDetailPageProps
{/* Stats Card */}
<div className="space-y-6">
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm">
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
<CardHeader>
<CardTitle className="flex items-center space-x-2 text-emerald-700">
<CardTitle className="flex items-center space-x-2 text-emerald-700 dark:text-emerald-400">
<DollarSign className="h-5 w-5" />
<span>Invoice Summary</span>
</CardTitle>
@@ -147,17 +179,27 @@ export default async function ClientDetailPage({ params }: ClientDetailPageProps
<p className="text-2xl font-bold text-emerald-600">
{formatCurrency(totalInvoiced)}
</p>
<p className="text-sm text-gray-500">Total Invoiced</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
Total Invoiced
</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="text-center">
<p className="text-lg font-semibold text-green-600">{paidInvoices}</p>
<p className="text-xs text-gray-500">Paid</p>
<p className="text-lg font-semibold text-green-600">
{paidInvoices}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
Paid
</p>
</div>
<div className="text-center">
<p className="text-lg font-semibold text-orange-600">{pendingInvoices}</p>
<p className="text-xs text-gray-500">Pending</p>
<p className="text-lg font-semibold text-orange-600">
{pendingInvoices}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
Pending
</p>
</div>
</div>
</CardContent>
@@ -165,24 +207,38 @@ export default async function ClientDetailPage({ params }: ClientDetailPageProps
{/* Recent Invoices */}
{client.invoices && client.invoices.length > 0 && (
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm">
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
<CardHeader>
<CardTitle className="text-lg">Recent Invoices</CardTitle>
<CardTitle className="text-lg dark:text-white">
Recent Invoices
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{client.invoices.slice(0, 3).map((invoice) => (
<div key={invoice.id} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div
key={invoice.id}
className="flex items-center justify-between rounded-lg bg-gray-50 p-3 dark:bg-gray-700"
>
<div>
<p className="font-medium text-sm">{invoice.invoiceNumber}</p>
<p className="text-xs text-gray-500">{formatDate(invoice.issueDate)}</p>
<p className="text-sm font-medium dark:text-white">
{invoice.invoiceNumber}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
{formatDate(invoice.issueDate)}
</p>
</div>
<div className="text-right">
<p className="font-medium text-sm">{formatCurrency(invoice.totalAmount)}</p>
<Badge
<p className="text-sm font-medium dark:text-white">
{formatCurrency(invoice.totalAmount)}
</p>
<Badge
variant={
invoice.status === "paid" ? "default" :
invoice.status === "sent" ? "secondary" : "outline"
invoice.status === "paid"
? "default"
: invoice.status === "sent"
? "secondary"
: "outline"
}
className="text-xs"
>
@@ -200,4 +256,4 @@ export default async function ClientDetailPage({ params }: ClientDetailPageProps
</div>
</div>
);
}
}

View File

@@ -27,14 +27,14 @@ export default async function InvoicePage({
<h1 className="bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-3xl font-bold text-transparent">
Invoice Details
</h1>
<p className="mt-1 text-lg text-gray-600">
<p className="mt-1 text-lg text-gray-600 dark:text-gray-300">
View and manage invoice information.
</p>
</div>
<div className="relative flex rounded-lg border border-gray-200 bg-gray-100 p-1">
<div className="relative flex rounded-lg border border-gray-200 bg-gray-100 p-1 dark:border-gray-700 dark:bg-gray-800">
<div
className={`absolute top-1 bottom-1 rounded-md bg-white shadow-sm transition-all duration-300 ease-in-out ${
className={`absolute top-1 bottom-1 rounded-md bg-white shadow-sm transition-all duration-300 ease-in-out dark:bg-gray-700 ${
mode === "view" ? "left-1 w-10" : "left-11 w-10"
}`}
/>
@@ -43,7 +43,7 @@ export default async function InvoicePage({
className={`relative z-10 rounded-md px-3 py-2 transition-all duration-200 ${
mode === "view"
? "text-emerald-600"
: "text-gray-600 hover:bg-gray-50 hover:text-gray-800"
: "text-gray-600 hover:bg-gray-50 hover:text-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-300"
}`}
>
<Eye className="h-4 w-4" />
@@ -53,7 +53,7 @@ export default async function InvoicePage({
className={`relative z-10 rounded-md px-3 py-2 transition-all duration-200 ${
mode === "edit"
? "text-emerald-600"
: "text-gray-600 hover:bg-gray-50 hover:text-gray-800"
: "text-gray-600 hover:bg-gray-50 hover:text-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-300"
}`}
>
<Edit className="h-4 w-4" />

View File

@@ -16,7 +16,7 @@ export default async function DashboardPage() {
<h1 className="bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-4xl font-bold text-transparent">
Welcome back, {session?.user?.name?.split(" ")[0] ?? "User"}!
</h1>
<p className="mt-2 text-lg text-gray-600">
<p className="mt-2 text-lg text-gray-600 dark:text-gray-300">
Here&apos;s what&apos;s happening with your invoicing business
</p>
</div>

View File

@@ -8,7 +8,8 @@ import { Toaster } from "~/components/ui/toaster";
export const metadata: Metadata = {
title: "beenvoice - Invoicing Made Simple",
description: "Simple and efficient invoicing for freelancers and small businesses",
description:
"Simple and efficient invoicing for freelancers and small businesses",
icons: [{ rel: "icon", url: "/favicon.ico" }],
};
@@ -21,8 +22,8 @@ export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en" className={`${geist.variable}`}>
<body className="relative min-h-screen font-sans antialiased overflow-x-hidden bg-gradient-to-br from-emerald-100 via-white via-60% to-teal-100 before:content-[''] before:fixed before:inset-0 before:z-0 before:pointer-events-none before:bg-[radial-gradient(ellipse_at_80%_0%,rgba(16,185,129,0.10)_0%,transparent_60%)]">
<html lang="en" className={`${geist.variable}`}>
<body className="relative min-h-screen overflow-x-hidden bg-gradient-to-br from-emerald-100 via-white via-60% to-teal-100 font-sans antialiased before:pointer-events-none before:fixed before:inset-0 before:z-0 before:bg-[radial-gradient(ellipse_at_80%_0%,rgba(16,185,129,0.10)_0%,transparent_60%)] before:content-[''] dark:from-gray-900 dark:via-gray-800 dark:to-gray-900 dark:before:bg-[radial-gradient(ellipse_at_80%_0%,rgba(34,197,94,0.15)_0%,transparent_60%)]">
<TRPCReactProvider>{children}</TRPCReactProvider>
<Toaster />
</body>

View File

@@ -23,19 +23,26 @@ import {
export default function HomePage() {
return (
<div className="min-h-screen bg-gradient-to-br from-green-50 to-emerald-100">
<div className="min-h-screen bg-gradient-to-br from-green-50 to-emerald-100 dark:from-gray-900 dark:to-gray-800">
<AuthRedirect />
{/* Header */}
<header className="border-b border-green-200 bg-white/80 backdrop-blur-sm">
<header className="border-b border-green-200 bg-white/80 backdrop-blur-sm dark:border-gray-700 dark:bg-gray-900/80">
<div className="container mx-auto px-4 py-4">
<div className="flex items-center justify-between">
<Logo />
<div className="flex items-center space-x-4">
<Link href="/auth/signin">
<Button variant="ghost">Sign In</Button>
<Button
variant="ghost"
className="dark:text-gray-300 dark:hover:bg-gray-800"
>
Sign In
</Button>
</Link>
<Link href="/auth/register">
<Button>Get Started</Button>
<Button className="dark:bg-green-600 dark:hover:bg-green-700">
Get Started
</Button>
</Link>
</div>
</div>
@@ -45,23 +52,33 @@ export default function HomePage() {
{/* Hero Section */}
<section className="px-4 py-20">
<div className="container mx-auto max-w-4xl text-center">
<h1 className="mb-6 text-5xl font-bold text-gray-900 md:text-6xl">
<h1 className="mb-6 text-5xl font-bold text-gray-900 md:text-6xl dark:text-white">
Simple Invoicing for
<span className="text-green-600"> Freelancers</span>
<span className="text-green-600 dark:text-green-400">
{" "}
Freelancers
</span>
</h1>
<p className="mx-auto mb-8 max-w-2xl text-xl text-gray-600">
<p className="mx-auto mb-8 max-w-2xl text-xl text-gray-600 dark:text-gray-300">
Create professional invoices, manage clients, and get paid faster
with beenvoice. The invoicing app that works as hard as you do.
</p>
<div className="flex flex-col justify-center gap-4 sm:flex-row">
<Link href="/auth/register">
<Button size="lg" className="px-8 py-6 text-lg">
<Button
size="lg"
className="px-8 py-6 text-lg dark:bg-green-600 dark:hover:bg-green-700"
>
Start Free Trial
<ArrowRight className="ml-2 h-5 w-5" />
</Button>
</Link>
<Link href="#features">
<Button variant="outline" size="lg" className="px-8 py-6 text-lg">
<Button
variant="outline"
size="lg"
className="px-8 py-6 text-lg dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800"
>
See How It Works
</Button>
</Link>
@@ -70,28 +87,30 @@ export default function HomePage() {
</section>
{/* Features Section */}
<section id="features" className="bg-white px-4 py-20">
<section id="features" className="bg-white px-4 py-20 dark:bg-gray-800">
<div className="container mx-auto max-w-6xl">
<div className="mb-16 text-center">
<h2 className="mb-4 text-4xl font-bold text-gray-900">
<h2 className="mb-4 text-4xl font-bold text-gray-900 dark:text-white">
Everything you need to invoice like a pro
</h2>
<p className="mx-auto max-w-2xl text-xl text-gray-600">
<p className="mx-auto max-w-2xl text-xl text-gray-600 dark:text-gray-300">
Powerful features designed for freelancers and small businesses
</p>
</div>
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
<Card className="border-0 shadow-lg">
<Card className="border-0 shadow-lg dark:bg-gray-700">
<CardHeader>
<Users className="mb-4 h-12 w-12 text-green-600" />
<CardTitle>Client Management</CardTitle>
<CardDescription>
<Users className="mb-4 h-12 w-12 text-green-600 dark:text-green-400" />
<CardTitle className="dark:text-white">
Client Management
</CardTitle>
<CardDescription className="dark:text-gray-300">
Keep all your client information organized in one place
</CardDescription>
</CardHeader>
<CardContent>
<ul className="space-y-2 text-sm text-gray-600">
<ul className="space-y-2 text-sm text-gray-600 dark:text-gray-300">
<li className="flex items-center">
<CheckCircle className="mr-2 h-4 w-4 text-green-500" />
Store contact details and addresses
@@ -108,16 +127,18 @@ export default function HomePage() {
</CardContent>
</Card>
<Card className="border-0 shadow-lg">
<Card className="border-0 shadow-lg dark:bg-gray-700">
<CardHeader>
<FileText className="mb-4 h-12 w-12 text-green-600" />
<CardTitle>Professional Invoices</CardTitle>
<CardDescription>
<FileText className="mb-4 h-12 w-12 text-green-600 dark:text-green-400" />
<CardTitle className="dark:text-white">
Professional Invoices
</CardTitle>
<CardDescription className="dark:text-gray-300">
Create beautiful, detailed invoices with line items
</CardDescription>
</CardHeader>
<CardContent>
<ul className="space-y-2 text-sm text-gray-600">
<ul className="space-y-2 text-sm text-gray-600 dark:text-gray-300">
<li className="flex items-center">
<CheckCircle className="mr-2 h-4 w-4 text-green-500" />
Add multiple line items with dates
@@ -134,16 +155,18 @@ export default function HomePage() {
</CardContent>
</Card>
<Card className="border-0 shadow-lg">
<Card className="border-0 shadow-lg dark:bg-gray-700">
<CardHeader>
<DollarSign className="mb-4 h-12 w-12 text-green-600" />
<CardTitle>Payment Tracking</CardTitle>
<CardDescription>
<DollarSign className="mb-4 h-12 w-12 text-green-600 dark:text-green-400" />
<CardTitle className="dark:text-white">
Payment Tracking
</CardTitle>
<CardDescription className="dark:text-gray-300">
Monitor invoice status and track payments
</CardDescription>
</CardHeader>
<CardContent>
<ul className="space-y-2 text-sm text-gray-600">
<ul className="space-y-2 text-sm text-gray-600 dark:text-gray-300">
<li className="flex items-center">
<CheckCircle className="mr-2 h-4 w-4 text-green-500" />
Track draft, sent, paid, and overdue status
@@ -164,19 +187,21 @@ export default function HomePage() {
</section>
{/* Benefits Section */}
<section className="bg-gray-50 px-4 py-20">
<section className="bg-gray-50 px-4 py-20 dark:bg-gray-900">
<div className="container mx-auto max-w-4xl text-center">
<h2 className="mb-16 text-4xl font-bold text-gray-900">
<h2 className="mb-16 text-4xl font-bold text-gray-900 dark:text-white">
Why choose beenvoice?
</h2>
<div className="grid gap-12 md:grid-cols-2">
<div className="space-y-6">
<div className="flex items-start space-x-4">
<Zap className="mt-1 h-8 w-8 text-green-600" />
<Zap className="mt-1 h-8 w-8 text-green-600 dark:text-green-400" />
<div className="text-left">
<h3 className="mb-2 text-xl font-semibold">Lightning Fast</h3>
<p className="text-gray-600">
<h3 className="mb-2 text-xl font-semibold dark:text-white">
Lightning Fast
</h3>
<p className="text-gray-600 dark:text-gray-300">
Create invoices in seconds, not minutes. Our streamlined
interface gets you back to work faster.
</p>
@@ -184,12 +209,12 @@ export default function HomePage() {
</div>
<div className="flex items-start space-x-4">
<Shield className="mt-1 h-8 w-8 text-green-600" />
<Shield className="mt-1 h-8 w-8 text-green-600 dark:text-green-400" />
<div className="text-left">
<h3 className="mb-2 text-xl font-semibold">
<h3 className="mb-2 text-xl font-semibold dark:text-white">
Secure & Private
</h3>
<p className="text-gray-600">
<p className="text-gray-600 dark:text-gray-300">
Your data is encrypted and secure. We never share your
information with third parties.
</p>
@@ -199,12 +224,12 @@ export default function HomePage() {
<div className="space-y-6">
<div className="flex items-start space-x-4">
<Star className="mt-1 h-8 w-8 text-green-600" />
<Star className="mt-1 h-8 w-8 text-green-600 dark:text-green-400" />
<div className="text-left">
<h3 className="mb-2 text-xl font-semibold">
<h3 className="mb-2 text-xl font-semibold dark:text-white">
Professional Quality
</h3>
<p className="text-gray-600">
<p className="text-gray-600 dark:text-gray-300">
Generate invoices that look professional and build trust
with your clients.
</p>
@@ -212,10 +237,12 @@ export default function HomePage() {
</div>
<div className="flex items-start space-x-4">
<Clock className="mt-1 h-8 w-8 text-green-600" />
<Clock className="mt-1 h-8 w-8 text-green-600 dark:text-green-400" />
<div className="text-left">
<h3 className="mb-2 text-xl font-semibold">Save Time</h3>
<p className="text-gray-600">
<h3 className="mb-2 text-xl font-semibold dark:text-white">
Save Time
</h3>
<p className="text-gray-600 dark:text-gray-300">
Automated calculations, templates, and client management
save you hours every month.
</p>
@@ -227,39 +254,49 @@ export default function HomePage() {
</section>
{/* CTA Section */}
<section className="bg-green-600 px-4 py-20">
<section className="bg-green-600 px-4 py-20 dark:bg-green-700">
<div className="container mx-auto max-w-2xl text-center">
<h2 className="mb-4 text-4xl font-bold text-white">
Ready to get started?
</h2>
<p className="mb-8 text-xl text-green-100">
<p className="mb-8 text-xl text-green-100 dark:text-green-200">
Join thousands of freelancers who trust beenvoice for their
invoicing needs.
</p>
<Link href="/auth/register">
<Button size="lg" variant="secondary" className="px-8 py-6 text-lg">
<Button
size="lg"
variant="secondary"
className="px-8 py-6 text-lg dark:bg-white dark:text-green-700 dark:hover:bg-gray-100"
>
Start Your Free Trial
<ArrowRight className="ml-2 h-5 w-5" />
</Button>
</Link>
<p className="mt-4 text-sm text-green-200">
<p className="mt-4 text-sm text-green-200 dark:text-green-300">
No credit card required Cancel anytime
</p>
</div>
</section>
{/* Footer */}
<footer className="bg-gray-900 px-4 py-12 text-white">
<footer className="bg-gray-900 px-4 py-12 text-white dark:bg-black">
<div className="container mx-auto text-center">
<Logo className="mx-auto mb-4" />
<p className="mb-4 text-gray-400">
<p className="mb-4 text-gray-400 dark:text-gray-500">
Simple invoicing for freelancers and small businesses
</p>
<div className="flex justify-center space-x-6 text-sm text-gray-400">
<Link href="/auth/signin" className="hover:text-white">
<div className="flex justify-center space-x-6 text-sm text-gray-400 dark:text-gray-500">
<Link
href="/auth/signin"
className="hover:text-white dark:hover:text-gray-300"
>
Sign In
</Link>
<Link href="/auth/register" className="hover:text-white">
<Link
href="/auth/register"
className="hover:text-white dark:hover:text-gray-300"
>
Register
</Link>
</div>

View File

@@ -8,55 +8,54 @@ import { SidebarTrigger } from "./SidebarTrigger";
export function Navbar() {
const { data: session } = useSession();
return (
<header className="fixed top-4 left-4 right-4 md:top-6 md:left-6 md:right-6 z-30">
<div className="bg-white/60 backdrop-blur-md shadow-2xl rounded-xl border-0">
<div className="flex h-14 md:h-16 items-center justify-between px-4 md:px-8">
<header className="fixed top-4 right-4 left-4 z-30 md:top-6 md:right-6 md:left-6">
<div className="rounded-xl border-0 bg-white/60 shadow-2xl backdrop-blur-md dark:bg-gray-900/60">
<div className="flex h-14 items-center justify-between px-4 md:h-16 md:px-8">
<div className="flex items-center gap-4 md:gap-6">
<SidebarTrigger />
<Link href="/dashboard" className="flex items-center gap-2">
<Logo size="md" />
</Link>
</div>
<Link href="/dashboard" className="flex items-center gap-2">
<Logo size="md" />
</Link>
</div>
<div className="flex items-center gap-2 md:gap-4">
{session?.user ? (
<>
<span className="text-xs md:text-sm text-gray-700 hidden sm:inline font-medium">
{session?.user ? (
<>
<span className="hidden text-xs font-medium text-gray-700 sm:inline md:text-sm dark:text-gray-300">
{session.user.name ?? session.user.email}
</span>
<Button
variant="outline"
size="sm"
<Button
variant="outline"
size="sm"
onClick={() => signOut({ callbackUrl: "/" })}
className="border-gray-300 text-gray-700 hover:bg-gray-50 text-xs md:text-sm"
className="border-gray-300 text-xs text-gray-700 hover:bg-gray-50 md:text-sm dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800"
>
Sign Out
</Button>
</>
) : (
<>
<Link href="/auth/signin">
<Button
variant="ghost"
Sign Out
</Button>
</>
) : (
<>
<Link href="/auth/signin">
<Button
variant="ghost"
size="sm"
className="text-gray-700 hover:bg-gray-100 text-xs md:text-sm"
className="text-xs text-gray-700 hover:bg-gray-100 md:text-sm dark:text-gray-300 dark:hover:bg-gray-800"
>
Sign In
</Button>
</Link>
<Link href="/auth/register">
<Button
</Link>
<Link href="/auth/register">
<Button
size="sm"
className="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700 text-white font-medium text-xs md:text-sm"
className="bg-gradient-to-r from-emerald-600 to-teal-600 text-xs font-medium text-white hover:from-emerald-700 hover:to-teal-700 md:text-sm dark:from-emerald-500 dark:to-teal-500 dark:hover:from-emerald-600 dark:hover:to-teal-600"
>
Register
</Button>
</Link>
</>
)}
</Link>
</>
)}
</div>
</div>
</div>
</header>
);
}
}

View File

@@ -2,7 +2,13 @@
import Link from "next/link";
import { usePathname } from "next/navigation";
import { Settings, LayoutDashboard, Users, FileText, Building } from "lucide-react";
import {
Settings,
LayoutDashboard,
Users,
FileText,
Building,
} from "lucide-react";
const navLinks = [
{ name: "Dashboard", href: "/dashboard", icon: LayoutDashboard },
@@ -15,43 +21,47 @@ export function Sidebar() {
const pathname = usePathname();
return (
<aside className="hidden md:flex flex-col justify-between fixed left-6 top-28 bottom-6 w-64 z-20 bg-white/60 backdrop-blur-md shadow-2xl rounded-xl border-0 p-8">
<nav className="flex flex-col gap-1">
<div className="mb-2 text-xs font-semibold text-gray-400 tracking-wider uppercase">Main</div>
{navLinks.map((link) => {
const Icon = link.icon;
return (
<Link
key={link.href}
href={link.href}
aria-current={pathname === link.href ? "page" : undefined}
className={`flex items-center gap-3 rounded-lg px-4 py-2 text-base font-medium transition-all duration-200 ${
pathname === link.href
? "bg-emerald-100 text-emerald-700 shadow-lg"
: "text-gray-700 hover:bg-gray-100"
}`}
>
<Icon className="h-5 w-5" />
{link.name}
</Link>
);
})}
</nav>
<div>
<div className="border-t border-gray-200 my-4" />
<div className="mb-2 text-xs font-semibold text-gray-400 tracking-wider uppercase">Account</div>
<Link
href="/dashboard/settings"
className={`flex items-center gap-3 rounded-lg px-4 py-2 text-base font-medium transition-all duration-200 ${
pathname === "/dashboard/settings"
? "bg-emerald-100 text-emerald-700 shadow-lg"
: "text-gray-700 hover:bg-gray-100"
}`}
>
<Settings className="h-5 w-5" />
Settings
</Link>
<aside className="fixed top-28 bottom-6 left-6 z-20 hidden w-64 flex-col justify-between rounded-xl border-0 bg-white/60 p-8 shadow-2xl backdrop-blur-md md:flex dark:bg-gray-900/60">
<nav className="flex flex-col gap-1">
<div className="mb-2 text-xs font-semibold tracking-wider text-gray-400 uppercase dark:text-gray-500">
Main
</div>
</aside>
{navLinks.map((link) => {
const Icon = link.icon;
return (
<Link
key={link.href}
href={link.href}
aria-current={pathname === link.href ? "page" : undefined}
className={`flex items-center gap-3 rounded-lg px-4 py-2 text-base font-medium transition-all duration-200 ${
pathname === link.href
? "bg-emerald-100 text-emerald-700 shadow-lg dark:bg-emerald-900/30 dark:text-emerald-400"
: "text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800"
}`}
>
<Icon className="h-5 w-5" />
{link.name}
</Link>
);
})}
</nav>
<div>
<div className="my-4 border-t border-gray-200 dark:border-gray-700" />
<div className="mb-2 text-xs font-semibold tracking-wider text-gray-400 uppercase dark:text-gray-500">
Account
</div>
<Link
href="/dashboard/settings"
className={`flex items-center gap-3 rounded-lg px-4 py-2 text-base font-medium transition-all duration-200 ${
pathname === "/dashboard/settings"
? "bg-emerald-100 text-emerald-700 shadow-lg dark:bg-emerald-900/30 dark:text-emerald-400"
: "text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800"
}`}
>
<Settings className="h-5 w-5" />
Settings
</Link>
</div>
</aside>
);
}
}

View File

@@ -1,8 +1,20 @@
"use client";
import { Sheet, SheetContent, SheetTrigger, SheetHeader, SheetTitle } from "~/components/ui/sheet";
import {
Sheet,
SheetContent,
SheetTrigger,
SheetHeader,
SheetTitle,
} from "~/components/ui/sheet";
import { Button } from "~/components/ui/button";
import { MenuIcon, Settings, LayoutDashboard, Users, FileText } from "lucide-react";
import {
MenuIcon,
Settings,
LayoutDashboard,
Users,
FileText,
} from "lucide-react";
import { useState } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
@@ -20,26 +32,28 @@ export function SidebarTrigger() {
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button
variant="outline"
size="icon"
<Button
variant="outline"
size="icon"
aria-label="Open sidebar"
className="md:hidden bg-white/80 backdrop-blur-sm border-gray-200 shadow-lg hover:bg-white h-8 w-8"
className="h-8 w-8 border-gray-200 bg-white/80 shadow-lg backdrop-blur-sm hover:bg-white md:hidden dark:border-gray-600 dark:bg-gray-900/80 dark:hover:bg-gray-800"
>
<MenuIcon className="h-4 w-4" />
</Button>
</SheetTrigger>
<SheetContent
side="left"
className="p-0 w-80 max-w-[85vw] bg-white/95 border-0 backdrop-blur-sm"
<SheetContent
side="left"
className="w-80 max-w-[85vw] border-0 bg-white/95 p-0 backdrop-blur-sm dark:bg-gray-900/95"
>
<SheetHeader className="p-4 border-b border-gray-200">
<SheetTitle>Navigation</SheetTitle>
<SheetHeader className="border-b border-gray-200 p-4 dark:border-gray-700">
<SheetTitle className="dark:text-white">Navigation</SheetTitle>
</SheetHeader>
{/* Navigation */}
<nav className="flex-1 flex flex-col gap-1 p-4">
<div className="mb-2 text-xs font-semibold text-gray-400 tracking-wider uppercase">Main</div>
<nav className="flex flex-1 flex-col gap-1 p-4">
<div className="mb-2 text-xs font-semibold tracking-wider text-gray-400 uppercase dark:text-gray-500">
Main
</div>
{navLinks.map((link) => {
const Icon = link.icon;
return (
@@ -48,9 +62,9 @@ export function SidebarTrigger() {
href={link.href}
aria-current={pathname === link.href ? "page" : undefined}
className={`flex items-center gap-3 rounded-lg px-3 py-3 text-base font-medium transition-all duration-200 ${
pathname === link.href
? "bg-emerald-100 text-emerald-700 shadow-lg"
: "text-gray-700 hover:bg-gray-100"
pathname === link.href
? "bg-emerald-100 text-emerald-700 shadow-lg dark:bg-emerald-900/30 dark:text-emerald-400"
: "text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800"
}`}
onClick={() => setOpen(false)}
>
@@ -59,15 +73,17 @@ export function SidebarTrigger() {
</Link>
);
})}
<div className="border-t border-gray-200 my-4" />
<div className="mb-2 text-xs font-semibold text-gray-400 tracking-wider uppercase">Account</div>
<div className="my-4 border-t border-gray-200 dark:border-gray-700" />
<div className="mb-2 text-xs font-semibold tracking-wider text-gray-400 uppercase dark:text-gray-500">
Account
</div>
<Link
href="/dashboard/settings"
className={`flex items-center gap-3 rounded-lg px-3 py-3 text-base font-medium transition-all duration-200 ${
pathname === "/dashboard/settings"
? "bg-emerald-100 text-emerald-700 shadow-lg"
: "text-gray-700 hover:bg-gray-100"
? "bg-emerald-100 text-emerald-700 shadow-lg dark:bg-emerald-900/30 dark:text-emerald-400"
: "text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800"
}`}
onClick={() => setOpen(false)}
>
@@ -78,4 +94,4 @@ export function SidebarTrigger() {
</SheetContent>
</Sheet>
);
}
}

View File

@@ -1,6 +1,16 @@
"use client";
import { Building, Mail, MapPin, Phone, Save, Globe, BadgeDollarSign, Image, Star } from "lucide-react";
import {
Building,
Mail,
MapPin,
Phone,
Save,
Globe,
BadgeDollarSign,
Image,
Star,
} from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
@@ -38,10 +48,11 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
const [loading, setLoading] = useState(false);
// Fetch business data if editing
const { data: business, isLoading: isLoadingBusiness } = api.businesses.getById.useQuery(
{ id: businessId! },
{ enabled: mode === "edit" && !!businessId }
);
const { data: business, isLoading: isLoadingBusiness } =
api.businesses.getById.useQuery(
{ id: businessId! },
{ enabled: mode === "edit" && !!businessId },
);
const createBusiness = api.businesses.create.useMutation({
onSuccess: () => {
@@ -102,12 +113,12 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
};
const handleInputChange = (field: string, value: string | boolean) => {
setFormData(prev => ({ ...prev, [field]: value }));
setFormData((prev) => ({ ...prev, [field]: value }));
};
// Phone number formatting (reuse from client-form)
const formatPhoneNumber = (value: string) => {
const phoneNumber = value.replace(/\D/g, '');
const phoneNumber = value.replace(/\D/g, "");
if (phoneNumber.length <= 3) {
return phoneNumber;
} else if (phoneNumber.length <= 6) {
@@ -174,7 +185,7 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
{ value: "WA", label: "Washington" },
{ value: "WV", label: "West Virginia" },
{ value: "WI", label: "Wisconsin" },
{ value: "WY", label: "Wyoming" }
{ value: "WY", label: "Wyoming" },
];
const MOST_USED_COUNTRIES = [
@@ -184,27 +195,223 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
{ value: "Australia", label: "Australia" },
{ value: "Germany", label: "Germany" },
{ value: "France", label: "France" },
{ value: "India", label: "India" }
{ value: "India", label: "India" },
];
const ALL_COUNTRIES = [
"Afghanistan", "Albania", "Algeria", "Andorra", "Angola", "Antigua and Barbuda", "Argentina", "Armenia", "Australia", "Austria", "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh", "Barbados", "Belarus", "Belgium", "Belize", "Benin", "Bhutan", "Bolivia", "Bosnia and Herzegovina", "Botswana", "Brazil", "Brunei", "Bulgaria", "Burkina Faso", "Burundi", "Cabo Verde", "Cambodia", "Cameroon", "Canada", "Central African Republic", "Chad", "Chile", "China", "Colombia", "Comoros", "Congo", "Costa Rica", "Croatia", "Cuba", "Cyprus", "Czech Republic", "Democratic Republic of the Congo", "Denmark", "Djibouti", "Dominica", "Dominican Republic", "East Timor", "Ecuador", "Egypt", "El Salvador", "Equatorial Guinea", "Eritrea", "Estonia", "Eswatini", "Ethiopia", "Fiji", "Finland", "France", "Gabon", "Gambia", "Georgia", "Germany", "Ghana", "Greece", "Grenada", "Guatemala", "Guinea", "Guinea-Bissau", "Guyana", "Haiti", "Honduras", "Hungary", "Iceland", "India", "Indonesia", "Iran", "Iraq", "Ireland", "Israel", "Italy", "Ivory Coast", "Jamaica", "Japan", "Jordan", "Kazakhstan", "Kenya", "Kiribati", "Kuwait", "Kyrgyzstan", "Laos", "Latvia", "Lebanon", "Lesotho", "Liberia", "Libya", "Liechtenstein", "Lithuania", "Luxembourg", "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands", "Mauritania", "Mauritius", "Mexico", "Micronesia", "Moldova", "Monaco", "Mongolia", "Montenegro", "Morocco", "Mozambique", "Myanmar", "Namibia", "Nauru", "Nepal", "Netherlands", "New Zealand", "Nicaragua", "Niger", "Nigeria", "North Korea", "North Macedonia", "Norway", "Oman", "Pakistan", "Palau", "Palestine", "Panama", "Papua New Guinea", "Paraguay", "Peru", "Philippines", "Poland", "Portugal", "Qatar", "Romania", "Russia", "Rwanda", "Saint Kitts and Nevis", "Saint Lucia", "Saint Vincent and the Grenadines", "Samoa", "San Marino", "Sao Tome and Principe", "Saudi Arabia", "Senegal", "Serbia", "Seychelles", "Sierra Leone", "Singapore", "Slovakia", "Slovenia", "Solomon Islands", "Somalia", "South Africa", "South Korea", "South Sudan", "Spain", "Sri Lanka", "Sudan", "Suriname", "Sweden", "Switzerland", "Syria", "Taiwan", "Tajikistan", "Tanzania", "Thailand", "Togo", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey", "Turkmenistan", "Tuvalu", "Uganda", "Ukraine", "United Arab Emirates", "United Kingdom", "United States", "Uruguay", "Uzbekistan", "Vanuatu", "Vatican City", "Venezuela", "Vietnam", "Yemen", "Zambia", "Zimbabwe"
"Afghanistan",
"Albania",
"Algeria",
"Andorra",
"Angola",
"Antigua and Barbuda",
"Argentina",
"Armenia",
"Australia",
"Austria",
"Azerbaijan",
"Bahamas",
"Bahrain",
"Bangladesh",
"Barbados",
"Belarus",
"Belgium",
"Belize",
"Benin",
"Bhutan",
"Bolivia",
"Bosnia and Herzegovina",
"Botswana",
"Brazil",
"Brunei",
"Bulgaria",
"Burkina Faso",
"Burundi",
"Cabo Verde",
"Cambodia",
"Cameroon",
"Canada",
"Central African Republic",
"Chad",
"Chile",
"China",
"Colombia",
"Comoros",
"Congo",
"Costa Rica",
"Croatia",
"Cuba",
"Cyprus",
"Czech Republic",
"Democratic Republic of the Congo",
"Denmark",
"Djibouti",
"Dominica",
"Dominican Republic",
"East Timor",
"Ecuador",
"Egypt",
"El Salvador",
"Equatorial Guinea",
"Eritrea",
"Estonia",
"Eswatini",
"Ethiopia",
"Fiji",
"Finland",
"France",
"Gabon",
"Gambia",
"Georgia",
"Germany",
"Ghana",
"Greece",
"Grenada",
"Guatemala",
"Guinea",
"Guinea-Bissau",
"Guyana",
"Haiti",
"Honduras",
"Hungary",
"Iceland",
"India",
"Indonesia",
"Iran",
"Iraq",
"Ireland",
"Israel",
"Italy",
"Ivory Coast",
"Jamaica",
"Japan",
"Jordan",
"Kazakhstan",
"Kenya",
"Kiribati",
"Kuwait",
"Kyrgyzstan",
"Laos",
"Latvia",
"Lebanon",
"Lesotho",
"Liberia",
"Libya",
"Liechtenstein",
"Lithuania",
"Luxembourg",
"Madagascar",
"Malawi",
"Malaysia",
"Maldives",
"Mali",
"Malta",
"Marshall Islands",
"Mauritania",
"Mauritius",
"Mexico",
"Micronesia",
"Moldova",
"Monaco",
"Mongolia",
"Montenegro",
"Morocco",
"Mozambique",
"Myanmar",
"Namibia",
"Nauru",
"Nepal",
"Netherlands",
"New Zealand",
"Nicaragua",
"Niger",
"Nigeria",
"North Korea",
"North Macedonia",
"Norway",
"Oman",
"Pakistan",
"Palau",
"Palestine",
"Panama",
"Papua New Guinea",
"Paraguay",
"Peru",
"Philippines",
"Poland",
"Portugal",
"Qatar",
"Romania",
"Russia",
"Rwanda",
"Saint Kitts and Nevis",
"Saint Lucia",
"Saint Vincent and the Grenadines",
"Samoa",
"San Marino",
"Sao Tome and Principe",
"Saudi Arabia",
"Senegal",
"Serbia",
"Seychelles",
"Sierra Leone",
"Singapore",
"Slovakia",
"Slovenia",
"Solomon Islands",
"Somalia",
"South Africa",
"South Korea",
"South Sudan",
"Spain",
"Sri Lanka",
"Sudan",
"Suriname",
"Sweden",
"Switzerland",
"Syria",
"Taiwan",
"Tajikistan",
"Tanzania",
"Thailand",
"Togo",
"Tonga",
"Trinidad and Tobago",
"Tunisia",
"Turkey",
"Turkmenistan",
"Tuvalu",
"Uganda",
"Ukraine",
"United Arab Emirates",
"United Kingdom",
"United States",
"Uruguay",
"Uzbekistan",
"Vanuatu",
"Vatican City",
"Venezuela",
"Vietnam",
"Yemen",
"Zambia",
"Zimbabwe",
];
const OTHER_COUNTRIES = ALL_COUNTRIES
.filter(c => !MOST_USED_COUNTRIES.some(mc => mc.value === c))
.map(country => ({ value: country, label: country }))
const OTHER_COUNTRIES = ALL_COUNTRIES.filter(
(c) => !MOST_USED_COUNTRIES.some((mc) => mc.value === c),
)
.map((country) => ({ value: country, label: country }))
.sort((a, b) => a.label.localeCompare(b.label));
const ALL_COUNTRIES_OPTIONS = [
{ value: "__placeholder__", label: "Select country" },
...MOST_USED_COUNTRIES,
...OTHER_COUNTRIES
...MOST_USED_COUNTRIES,
...OTHER_COUNTRIES,
];
if (mode === "edit" && isLoadingBusiness) {
return (
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm w-full my-8 px-0">
<Card className="my-8 w-full border-0 bg-white/80 px-0 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
<CardContent className="p-8">
<FormSkeleton />
</CardContent>
@@ -213,18 +420,23 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
}
return (
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm w-full my-8 px-0">
<Card className="my-8 w-full border-0 bg-white/80 px-0 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
<CardContent>
<form onSubmit={handleSubmit} className="space-y-8">
{/* Basic Information Section */}
<div className="space-y-6">
<div className="flex items-center space-x-2 text-emerald-700">
<div className="flex items-center space-x-2 text-emerald-700 dark:text-emerald-400">
<Building className="h-5 w-5" />
<h3 className="text-lg font-semibold">Business Information</h3>
<h3 className="text-lg font-semibold dark:text-white">
Business Information
</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="name" className="text-sm font-medium text-gray-700">
<Label
htmlFor="name"
className="text-sm font-medium text-gray-700 dark:text-gray-300"
>
Business Name *
</Label>
<Input
@@ -233,22 +445,25 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
onChange={(e) => handleInputChange("name", e.target.value)}
required
placeholder="Enter business name"
className="h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
className="h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
/>
</div>
<div className="space-y-2">
<Label htmlFor="email" className="text-sm font-medium text-gray-700">
<Label
htmlFor="email"
className="text-sm font-medium text-gray-700 dark:text-gray-300"
>
Email Address
</Label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<Mail className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform text-gray-400" />
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => handleInputChange("email", e.target.value)}
placeholder="business@example.com"
className="h-12 pl-10 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
className="h-12 border-gray-200 pl-10 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
/>
</div>
</div>
@@ -257,40 +472,50 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
{/* Contact Information Section */}
<div className="space-y-6">
<div className="flex items-center space-x-2 text-emerald-700">
<div className="flex items-center space-x-2 text-emerald-700 dark:text-emerald-400">
<Phone className="h-5 w-5" />
<h3 className="text-lg font-semibold">Contact Information</h3>
<h3 className="text-lg font-semibold dark:text-white">
Contact Information
</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="phone" className="text-sm font-medium text-gray-700">
<Label
htmlFor="phone"
className="text-sm font-medium text-gray-700 dark:text-gray-300"
>
Phone Number
</Label>
<div className="relative">
<Phone className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<Phone className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform text-gray-400" />
<Input
id="phone"
type="tel"
value={formData.phone}
onChange={(e) => handlePhoneChange(e.target.value)}
placeholder="(555) 123-4567"
className="h-12 pl-10 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
className="h-12 border-gray-200 pl-10 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="website" className="text-sm font-medium text-gray-700">
<Label
htmlFor="website"
className="text-sm font-medium text-gray-700 dark:text-gray-300"
>
Website
</Label>
<div className="relative">
<Globe className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<Globe className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform text-gray-400" />
<Input
id="website"
type="url"
value={formData.website}
onChange={(e) => handleInputChange("website", e.target.value)}
onChange={(e) =>
handleInputChange("website", e.target.value)
}
placeholder="https://yourbusiness.com"
className="h-12 pl-10 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
className="h-12 border-gray-200 pl-10 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
/>
</div>
</div>
@@ -299,37 +524,52 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
{/* Address Section */}
<div className="space-y-6">
<div className="flex items-center space-x-2 text-emerald-700">
<div className="flex items-center space-x-2 text-emerald-700 dark:text-emerald-400">
<MapPin className="h-5 w-5" />
<h3 className="text-lg font-semibold">Address</h3>
<h3 className="text-lg font-semibold dark:text-white">
Address Information
</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="addressLine1" className="text-sm font-medium text-gray-700">
<Label
htmlFor="addressLine1"
className="text-sm font-medium text-gray-700 dark:text-gray-300"
>
Address Line 1
</Label>
<Input
id="addressLine1"
value={formData.addressLine1}
onChange={(e) => handleInputChange("addressLine1", e.target.value)}
onChange={(e) =>
handleInputChange("addressLine1", e.target.value)
}
placeholder="123 Main St"
className="h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
className="h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
/>
</div>
<div className="space-y-2">
<Label htmlFor="addressLine2" className="text-sm font-medium text-gray-700">
<Label
htmlFor="addressLine2"
className="text-sm font-medium text-gray-700 dark:text-gray-300"
>
Address Line 2
</Label>
<Input
id="addressLine2"
value={formData.addressLine2}
onChange={(e) => handleInputChange("addressLine2", e.target.value)}
onChange={(e) =>
handleInputChange("addressLine2", e.target.value)
}
placeholder="Suite 100"
className="h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
className="h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
/>
</div>
<div className="space-y-2">
<Label htmlFor="city" className="text-sm font-medium text-gray-700">
<Label
htmlFor="city"
className="text-sm font-medium text-gray-700 dark:text-gray-300"
>
City
</Label>
<Input
@@ -337,11 +577,14 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
value={formData.city}
onChange={(e) => handleInputChange("city", e.target.value)}
placeholder="City"
className="h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
className="h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
/>
</div>
<div className="space-y-2">
<Label htmlFor="state" className="text-sm font-medium text-gray-700">
<Label
htmlFor="state"
className="text-sm font-medium text-gray-700 dark:text-gray-300"
>
State/Province
</Label>
<SearchableSelect
@@ -353,19 +596,27 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
/>
</div>
<div className="space-y-2">
<Label htmlFor="postalCode" className="text-sm font-medium text-gray-700">
<Label
htmlFor="postalCode"
className="text-sm font-medium text-gray-700 dark:text-gray-300"
>
Postal Code
</Label>
<Input
id="postalCode"
value={formData.postalCode}
onChange={(e) => handleInputChange("postalCode", e.target.value)}
onChange={(e) =>
handleInputChange("postalCode", e.target.value)
}
placeholder="ZIP or postal code"
className="h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
className="h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
/>
</div>
<div className="space-y-2">
<Label htmlFor="country" className="text-sm font-medium text-gray-700">
<Label
htmlFor="country"
className="text-sm font-medium text-gray-700 dark:text-gray-300"
>
Country
</Label>
<SearchableSelect
@@ -381,13 +632,18 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
{/* Tax, Logo, Default Section */}
<div className="space-y-6">
<div className="flex items-center space-x-2 text-emerald-700">
<div className="flex items-center space-x-2 text-emerald-700 dark:text-emerald-400">
<BadgeDollarSign className="h-5 w-5" />
<h3 className="text-lg font-semibold">Other Details</h3>
<h3 className="text-lg font-semibold dark:text-white">
Business Details
</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="taxId" className="text-sm font-medium text-gray-700">
<Label
htmlFor="taxId"
className="text-sm font-medium text-gray-700 dark:text-gray-300"
>
Tax ID / VAT Number
</Label>
<Input
@@ -395,40 +651,51 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
value={formData.taxId}
onChange={(e) => handleInputChange("taxId", e.target.value)}
placeholder="Tax ID or VAT number"
className="h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
className="h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
/>
</div>
<div className="space-y-2">
<Label htmlFor="logoUrl" className="text-sm font-medium text-gray-700">
<Label
htmlFor="logoUrl"
className="text-sm font-medium text-gray-700 dark:text-gray-300"
>
Logo URL
</Label>
<div className="relative">
<Image className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<Image className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform text-gray-400" />
<Input
id="logoUrl"
value={formData.logoUrl}
onChange={(e) => handleInputChange("logoUrl", e.target.value)}
onChange={(e) =>
handleInputChange("logoUrl", e.target.value)
}
placeholder="https://yourbusiness.com/logo.png"
className="h-12 pl-10 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
className="h-12 border-gray-200 pl-10 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
/>
</div>
</div>
<div className="flex items-center space-x-2 mt-4">
<div className="mt-4 flex items-center space-x-2">
<input
id="isDefault"
type="checkbox"
checked={formData.isDefault}
onChange={(e) => handleInputChange("isDefault", e.target.checked)}
className="h-5 w-5 text-emerald-600 border-gray-300 rounded focus:ring-emerald-500"
onChange={(e) =>
handleInputChange("isDefault", e.target.checked)
}
className="h-5 w-5 rounded border-gray-300 text-emerald-600 focus:ring-emerald-500"
/>
<Label htmlFor="isDefault" className="text-sm font-medium text-gray-700 flex items-center">
<Star className="h-4 w-4 mr-1 text-yellow-400" /> Set as default business
<Label
htmlFor="isDefault"
className="flex items-center text-sm font-medium text-gray-700"
>
<Star className="mr-1 h-4 w-4 text-yellow-400" /> Set as
default business
</Label>
</div>
</div>
</div>
<div className="flex justify-end gap-4 mt-8">
<div className="mt-8 flex justify-end gap-4">
<Button
type="button"
variant="outline"
@@ -440,7 +707,7 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
</Button>
<Button
type="submit"
className="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700 text-white font-medium shadow-lg hover:shadow-xl"
className="bg-gradient-to-r from-emerald-600 to-teal-600 font-medium text-white shadow-lg hover:from-emerald-700 hover:to-teal-700 hover:shadow-xl"
disabled={loading}
>
<Save className="mr-2 h-5 w-5" />
@@ -451,4 +718,4 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
</CardContent>
</Card>
);
}
}

View File

@@ -33,10 +33,11 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
const [loading, setLoading] = useState(false);
// Fetch client data if editing
const { data: client, isLoading: isLoadingClient } = api.clients.getById.useQuery(
{ id: clientId! },
{ enabled: mode === "edit" && !!clientId }
);
const { data: client, isLoading: isLoadingClient } =
api.clients.getById.useQuery(
{ id: clientId! },
{ enabled: mode === "edit" && !!clientId },
);
const createClient = api.clients.create.useMutation({
onSuccess: () => {
@@ -94,12 +95,12 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
};
const handleInputChange = (field: string, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
setFormData((prev) => ({ ...prev, [field]: value }));
};
// Phone number formatting
const formatPhoneNumber = (value: string) => {
const phoneNumber = value.replace(/\D/g, '');
const phoneNumber = value.replace(/\D/g, "");
if (phoneNumber.length <= 3) {
return phoneNumber;
} else if (phoneNumber.length <= 6) {
@@ -115,22 +116,272 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
};
const US_STATES = [
"", "AL", "AK", "AZ", "AR", "CA", "CO", "CT", "DE", "FL", "GA", "HI", "ID", "IL", "IN", "IA", "KS", "KY", "LA", "ME", "MD", "MA", "MI", "MN", "MS", "MO", "MT", "NE", "NV", "NH", "NJ", "NM", "NY", "NC", "ND", "OH", "OK", "OR", "PA", "RI", "SC", "SD", "TN", "TX", "UT", "VT", "VA", "WA", "WV", "WI", "WY"
"",
"AL",
"AK",
"AZ",
"AR",
"CA",
"CO",
"CT",
"DE",
"FL",
"GA",
"HI",
"ID",
"IL",
"IN",
"IA",
"KS",
"KY",
"LA",
"ME",
"MD",
"MA",
"MI",
"MN",
"MS",
"MO",
"MT",
"NE",
"NV",
"NH",
"NJ",
"NM",
"NY",
"NC",
"ND",
"OH",
"OK",
"OR",
"PA",
"RI",
"SC",
"SD",
"TN",
"TX",
"UT",
"VT",
"VA",
"WA",
"WV",
"WI",
"WY",
];
const MOST_USED_COUNTRIES = [
"United States", "United Kingdom", "Canada", "Australia", "Germany", "France", "India"
"United States",
"United Kingdom",
"Canada",
"Australia",
"Germany",
"France",
"India",
];
const ALL_COUNTRIES = [
"Afghanistan", "Albania", "Algeria", "Andorra", "Angola", "Antigua and Barbuda", "Argentina", "Armenia", "Australia", "Austria", "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh", "Barbados", "Belarus", "Belgium", "Belize", "Benin", "Bhutan", "Bolivia", "Bosnia and Herzegovina", "Botswana", "Brazil", "Brunei", "Bulgaria", "Burkina Faso", "Burundi", "Cabo Verde", "Cambodia", "Cameroon", "Canada", "Central African Republic", "Chad", "Chile", "China", "Colombia", "Comoros", "Congo", "Costa Rica", "Croatia", "Cuba", "Cyprus", "Czech Republic", "Denmark", "Djibouti", "Dominica", "Dominican Republic", "East Timor", "Ecuador", "Egypt", "El Salvador", "Equatorial Guinea", "Eritrea", "Estonia", "Eswatini", "Ethiopia", "Fiji", "Finland", "France", "Gabon", "Gambia", "Georgia", "Germany", "Ghana", "Greece", "Grenada", "Guatemala", "Guinea", "Guinea-Bissau", "Guyana", "Haiti", "Honduras", "Hungary", "Iceland", "India", "Indonesia", "Iran", "Iraq", "Ireland", "Israel", "Italy", "Ivory Coast", "Jamaica", "Japan", "Jordan", "Kazakhstan", "Kenya", "Kiribati", "Kuwait", "Kyrgyzstan", "Laos", "Latvia", "Lebanon", "Lesotho", "Liberia", "Libya", "Liechtenstein", "Lithuania", "Luxembourg", "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands", "Mauritania", "Mauritius", "Mexico", "Micronesia", "Moldova", "Monaco", "Mongolia", "Montenegro", "Morocco", "Mozambique", "Myanmar", "Namibia", "Nauru", "Nepal", "Netherlands", "New Zealand", "Nicaragua", "Niger", "Nigeria", "North Korea", "North Macedonia", "Norway", "Oman", "Pakistan", "Palau", "Palestine", "Panama", "Papua New Guinea", "Paraguay", "Peru", "Philippines", "Poland", "Portugal", "Qatar", "Romania", "Russia", "Rwanda", "Saint Kitts and Nevis", "Saint Lucia", "Saint Vincent and the Grenadines", "Samoa", "San Marino", "Sao Tome and Principe", "Saudi Arabia", "Senegal", "Serbia", "Seychelles", "Sierra Leone", "Singapore", "Slovakia", "Slovenia", "Solomon Islands", "Somalia", "South Africa", "South Korea", "South Sudan", "Spain", "Sri Lanka", "Sudan", "Suriname", "Sweden", "Switzerland", "Syria", "Taiwan", "Tajikistan", "Tanzania", "Thailand", "Togo", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey", "Turkmenistan", "Tuvalu", "Uganda", "Ukraine", "United Arab Emirates", "United Kingdom", "United States", "Uruguay", "Uzbekistan", "Vanuatu", "Vatican City", "Venezuela", "Vietnam", "Yemen", "Zambia", "Zimbabwe"
"Afghanistan",
"Albania",
"Algeria",
"Andorra",
"Angola",
"Antigua and Barbuda",
"Argentina",
"Armenia",
"Australia",
"Austria",
"Azerbaijan",
"Bahamas",
"Bahrain",
"Bangladesh",
"Barbados",
"Belarus",
"Belgium",
"Belize",
"Benin",
"Bhutan",
"Bolivia",
"Bosnia and Herzegovina",
"Botswana",
"Brazil",
"Brunei",
"Bulgaria",
"Burkina Faso",
"Burundi",
"Cabo Verde",
"Cambodia",
"Cameroon",
"Canada",
"Central African Republic",
"Chad",
"Chile",
"China",
"Colombia",
"Comoros",
"Congo",
"Costa Rica",
"Croatia",
"Cuba",
"Cyprus",
"Czech Republic",
"Denmark",
"Djibouti",
"Dominica",
"Dominican Republic",
"East Timor",
"Ecuador",
"Egypt",
"El Salvador",
"Equatorial Guinea",
"Eritrea",
"Estonia",
"Eswatini",
"Ethiopia",
"Fiji",
"Finland",
"France",
"Gabon",
"Gambia",
"Georgia",
"Germany",
"Ghana",
"Greece",
"Grenada",
"Guatemala",
"Guinea",
"Guinea-Bissau",
"Guyana",
"Haiti",
"Honduras",
"Hungary",
"Iceland",
"India",
"Indonesia",
"Iran",
"Iraq",
"Ireland",
"Israel",
"Italy",
"Ivory Coast",
"Jamaica",
"Japan",
"Jordan",
"Kazakhstan",
"Kenya",
"Kiribati",
"Kuwait",
"Kyrgyzstan",
"Laos",
"Latvia",
"Lebanon",
"Lesotho",
"Liberia",
"Libya",
"Liechtenstein",
"Lithuania",
"Luxembourg",
"Madagascar",
"Malawi",
"Malaysia",
"Maldives",
"Mali",
"Malta",
"Marshall Islands",
"Mauritania",
"Mauritius",
"Mexico",
"Micronesia",
"Moldova",
"Monaco",
"Mongolia",
"Montenegro",
"Morocco",
"Mozambique",
"Myanmar",
"Namibia",
"Nauru",
"Nepal",
"Netherlands",
"New Zealand",
"Nicaragua",
"Niger",
"Nigeria",
"North Korea",
"North Macedonia",
"Norway",
"Oman",
"Pakistan",
"Palau",
"Palestine",
"Panama",
"Papua New Guinea",
"Paraguay",
"Peru",
"Philippines",
"Poland",
"Portugal",
"Qatar",
"Romania",
"Russia",
"Rwanda",
"Saint Kitts and Nevis",
"Saint Lucia",
"Saint Vincent and the Grenadines",
"Samoa",
"San Marino",
"Sao Tome and Principe",
"Saudi Arabia",
"Senegal",
"Serbia",
"Seychelles",
"Sierra Leone",
"Singapore",
"Slovakia",
"Slovenia",
"Solomon Islands",
"Somalia",
"South Africa",
"South Korea",
"South Sudan",
"Spain",
"Sri Lanka",
"Sudan",
"Suriname",
"Sweden",
"Switzerland",
"Syria",
"Taiwan",
"Tajikistan",
"Tanzania",
"Thailand",
"Togo",
"Tonga",
"Trinidad and Tobago",
"Tunisia",
"Turkey",
"Turkmenistan",
"Tuvalu",
"Uganda",
"Ukraine",
"United Arab Emirates",
"United Kingdom",
"United States",
"Uruguay",
"Uzbekistan",
"Vanuatu",
"Vatican City",
"Venezuela",
"Vietnam",
"Yemen",
"Zambia",
"Zimbabwe",
];
const OTHER_COUNTRIES = ALL_COUNTRIES.filter(
c => !MOST_USED_COUNTRIES.includes(c)
(c) => !MOST_USED_COUNTRIES.includes(c),
).sort();
if (mode === "edit" && isLoadingClient) {
return (
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm w-full my-8 px-0">
<Card className="my-8 w-full border-0 bg-white/80 px-0 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
<CardContent className="p-8">
<FormSkeleton />
</CardContent>
@@ -139,18 +390,23 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
}
return (
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm w-full my-8 px-0">
<Card className="my-8 w-full border-0 bg-white/80 px-0 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
<CardContent>
<form onSubmit={handleSubmit} className="space-y-8">
{/* Basic Information Section */}
<div className="space-y-6">
<div className="flex items-center space-x-2 text-emerald-700">
<div className="flex items-center space-x-2 text-emerald-700 dark:text-emerald-400">
<Building className="h-5 w-5" />
<h3 className="text-lg font-semibold">Business Information</h3>
<h3 className="text-lg font-semibold dark:text-white">
Business Information
</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="name" className="text-sm font-medium text-gray-700">
<Label
htmlFor="name"
className="text-sm font-medium text-gray-700 dark:text-gray-300"
>
Business Name / Full Name *
</Label>
<Input
@@ -159,22 +415,25 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
onChange={(e) => handleInputChange("name", e.target.value)}
required
placeholder="Enter business name or full name"
className="h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
className="h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
/>
</div>
<div className="space-y-2">
<Label htmlFor="email" className="text-sm font-medium text-gray-700">
<Label
htmlFor="email"
className="text-sm font-medium text-gray-700 dark:text-gray-300"
>
Email Address
</Label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<Mail className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform text-gray-400 dark:text-gray-500" />
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => handleInputChange("email", e.target.value)}
placeholder="business@example.com"
className="h-12 pl-10 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
className="h-12 border-gray-200 pl-10 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
/>
</div>
</div>
@@ -183,17 +442,22 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
{/* Contact Information Section */}
<div className="space-y-6">
<div className="flex items-center space-x-2 text-emerald-700">
<div className="flex items-center space-x-2 text-emerald-700 dark:text-emerald-400">
<Phone className="h-5 w-5" />
<h3 className="text-lg font-semibold">Contact Information</h3>
<h3 className="text-lg font-semibold dark:text-white">
Contact Information
</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="phone" className="text-sm font-medium text-gray-700">
<Label
htmlFor="phone"
className="text-sm font-medium text-gray-700 dark:text-gray-300"
>
Phone Number
</Label>
<div className="relative">
<Phone className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<Phone className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform text-gray-400 dark:text-gray-500" />
<Input
id="phone"
type="tel"
@@ -201,49 +465,66 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
onChange={(e) => handlePhoneChange(e.target.value)}
placeholder="(555) 123-4567"
maxLength={14}
className="h-12 pl-10 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
className="h-12 border-gray-200 pl-10 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
/>
</div>
<p className="text-xs text-gray-500">Format: (555) 123-4567</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
Format: (555) 123-4567
</p>
</div>
</div>
</div>
{/* Address Section */}
<div className="space-y-6">
<div className="flex items-center space-x-2 text-emerald-700">
<div className="flex items-center space-x-2 text-emerald-700 dark:text-emerald-400">
<MapPin className="h-5 w-5" />
<h3 className="text-lg font-semibold">Address Information</h3>
<h3 className="text-lg font-semibold dark:text-white">
Address Information
</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="addressLine1" className="text-sm font-medium text-gray-700">
<Label
htmlFor="addressLine1"
className="text-sm font-medium text-gray-700 dark:text-gray-300"
>
Address Line 1
</Label>
<Input
id="addressLine1"
value={formData.addressLine1}
onChange={(e) => handleInputChange("addressLine1", e.target.value)}
onChange={(e) =>
handleInputChange("addressLine1", e.target.value)
}
placeholder="123 Main Street"
className="h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
className="h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
/>
</div>
<div className="space-y-2">
<Label htmlFor="addressLine2" className="text-sm font-medium text-gray-700">
<Label
htmlFor="addressLine2"
className="text-sm font-medium text-gray-700 dark:text-gray-300"
>
Address Line 2
</Label>
<Input
id="addressLine2"
value={formData.addressLine2}
onChange={(e) => handleInputChange("addressLine2", e.target.value)}
onChange={(e) =>
handleInputChange("addressLine2", e.target.value)
}
placeholder="Suite 100"
className="h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
className="h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="grid grid-cols-1 gap-6 md:grid-cols-3">
<div className="space-y-2">
<Label htmlFor="city" className="text-sm font-medium text-gray-700">
<Label
htmlFor="city"
className="text-sm font-medium text-gray-700 dark:text-gray-300"
>
City
</Label>
<Input
@@ -255,14 +536,17 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
/>
</div>
<div className="space-y-2">
<Label htmlFor="state" className="text-sm font-medium text-gray-700">
<Label
htmlFor="state"
className="text-sm font-medium text-gray-700 dark:text-gray-300"
>
State / Province
</Label>
<select
id="state"
value={formData.state}
onChange={(e) => handleInputChange("state", e.target.value)}
className="h-12 w-full rounded-md border border-gray-200 bg-white px-3 py-2 text-sm focus:border-emerald-500 focus:outline-none focus:ring-1 focus:ring-emerald-500"
className="h-12 w-full rounded-md border border-gray-200 bg-white px-3 py-2 text-sm focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white"
>
{US_STATES.map((state) => (
<option key={state} value={state}>
@@ -272,28 +556,36 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
</select>
</div>
<div className="space-y-2">
<Label htmlFor="postalCode" className="text-sm font-medium text-gray-700">
<Label
htmlFor="postalCode"
className="text-sm font-medium text-gray-700 dark:text-gray-300"
>
Postal Code
</Label>
<Input
id="postalCode"
value={formData.postalCode}
onChange={(e) => handleInputChange("postalCode", e.target.value)}
onChange={(e) =>
handleInputChange("postalCode", e.target.value)
}
placeholder="12345"
className="h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
className="h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="country" className="text-sm font-medium text-gray-700">
Country
</Label>
<select
id="country"
value={formData.country}
</div>
<div className="space-y-2">
<Label
htmlFor="country"
className="text-sm font-medium text-gray-700 dark:text-gray-300"
>
Country
</Label>
<select
id="country"
value={formData.country}
onChange={(e) => handleInputChange("country", e.target.value)}
className="h-12 w-full rounded-md border border-gray-200 bg-white px-3 py-2 text-sm focus:border-emerald-500 focus:outline-none focus:ring-1 focus:ring-emerald-500"
>
className="h-12 w-full rounded-md border border-gray-200 bg-white px-3 py-2 text-sm focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white"
>
<option value="">Select Country</option>
<optgroup label="Most Used">
{MOST_USED_COUNTRIES.map((country) => (
@@ -309,16 +601,16 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
</option>
))}
</optgroup>
</select>
</select>
</div>
</div>
{/* Submit Button */}
<div className="flex gap-3 pt-6">
<Button
type="submit"
<Button
type="submit"
disabled={loading}
className="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700 text-white font-medium shadow-lg hover:shadow-xl transition-all duration-200"
className="bg-gradient-to-r from-emerald-600 to-teal-600 font-medium text-white shadow-lg transition-all duration-200 hover:from-emerald-700 hover:to-teal-700 hover:shadow-xl"
>
{loading ? (
<>
@@ -327,22 +619,22 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
</>
) : (
<>
<Save className="mr-2 h-4 w-4" />
<Save className="mr-2 h-4 w-4" />
{mode === "create" ? "Create Client" : "Update Client"}
</>
)}
</Button>
<Button
type="button"
variant="outline"
<Button
type="button"
variant="outline"
onClick={() => router.push("/dashboard/clients")}
className="border-gray-300 text-gray-700 hover:bg-gray-50 font-medium"
>
Cancel
</Button>
className="border-gray-300 font-medium text-gray-700 hover:bg-gray-50"
>
Cancel
</Button>
</div>
</form>
</CardContent>
</Card>
);
}
}

View File

@@ -15,14 +15,14 @@ import {
AlertCircle,
Settings,
User,
Mail
Mail,
} from "lucide-react";
export function DarkModeTest() {
return (
<div className="min-h-screen p-8 space-y-8">
<div className="min-h-screen space-y-8 p-8">
{/* Header */}
<div className="text-center space-y-4">
<div className="space-y-4 text-center">
<h1 className="text-4xl font-bold text-gray-900 dark:text-white">
Dark Mode Test Suite
</h1>
@@ -53,11 +53,17 @@ export function DarkModeTest() {
</CardHeader>
<CardContent className="space-y-3">
<div className="space-y-2">
<p className="text-sm text-gray-500 dark:text-gray-400">Text Colors:</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
Text Colors:
</p>
<div className="text-gray-900 dark:text-white">Primary Text</div>
<div className="text-gray-700 dark:text-gray-300">Secondary Text</div>
<div className="text-gray-700 dark:text-gray-300">
Secondary Text
</div>
<div className="text-gray-500 dark:text-gray-400">Muted Text</div>
<div className="text-green-600 dark:text-green-400">Success Text</div>
<div className="text-green-600 dark:text-green-400">
Success Text
</div>
<div className="text-red-600 dark:text-red-400">Error Text</div>
</div>
</CardContent>
@@ -71,10 +77,18 @@ export function DarkModeTest() {
<CardContent className="space-y-3">
<div className="flex flex-wrap gap-2">
<Button size="sm">Default</Button>
<Button variant="secondary" size="sm">Secondary</Button>
<Button variant="outline" size="sm">Outline</Button>
<Button variant="ghost" size="sm">Ghost</Button>
<Button variant="destructive" size="sm">Destructive</Button>
<Button variant="secondary" size="sm">
Secondary
</Button>
<Button variant="outline" size="sm">
Outline
</Button>
<Button variant="ghost" size="sm">
Ghost
</Button>
<Button variant="destructive" size="sm">
Destructive
</Button>
</div>
</CardContent>
</Card>
@@ -100,7 +114,7 @@ export function DarkModeTest() {
<Label htmlFor="test-select">Test Select</Label>
<select
id="test-select"
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 dark:bg-input/30"
className="border-input placeholder:text-muted-foreground focus-visible:ring-ring dark:bg-input/30 flex h-9 w-full rounded-md border bg-transparent px-3 py-1 text-sm shadow-xs transition-colors focus-visible:ring-1 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
>
<option value="">Select an option</option>
<option value="1">Option 1</option>
@@ -125,19 +139,27 @@ export function DarkModeTest() {
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm">
<Check className="h-4 w-4 text-green-500" />
<span className="text-gray-700 dark:text-gray-300">Success Status</span>
<span className="text-gray-700 dark:text-gray-300">
Success Status
</span>
</div>
<div className="flex items-center gap-2 text-sm">
<X className="h-4 w-4 text-red-500" />
<span className="text-gray-700 dark:text-gray-300">Error Status</span>
<span className="text-gray-700 dark:text-gray-300">
Error Status
</span>
</div>
<div className="flex items-center gap-2 text-sm">
<Info className="h-4 w-4 text-blue-500" />
<span className="text-gray-700 dark:text-gray-300">Info Status</span>
<span className="text-gray-700 dark:text-gray-300">
Info Status
</span>
</div>
<div className="flex items-center gap-2 text-sm">
<AlertCircle className="h-4 w-4 text-yellow-500" />
<span className="text-gray-700 dark:text-gray-300">Warning Status</span>
<span className="text-gray-700 dark:text-gray-300">
Warning Status
</span>
</div>
</div>
</CardContent>
@@ -150,14 +172,20 @@ export function DarkModeTest() {
</CardHeader>
<CardContent className="space-y-3">
<div className="space-y-2">
<div className="p-3 bg-gray-50 dark:bg-gray-700 rounded-md">
<p className="text-sm text-gray-700 dark:text-gray-300">Light Background</p>
<div className="rounded-md bg-gray-50 p-3 dark:bg-gray-700">
<p className="text-sm text-gray-700 dark:text-gray-300">
Light Background
</p>
</div>
<div className="p-3 bg-gray-100 dark:bg-gray-600 rounded-md">
<p className="text-sm text-gray-700 dark:text-gray-300">Medium Background</p>
<div className="rounded-md bg-gray-100 p-3 dark:bg-gray-600">
<p className="text-sm text-gray-700 dark:text-gray-300">
Medium Background
</p>
</div>
<div className="p-3 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-md">
<p className="text-sm text-gray-700 dark:text-gray-300">Card Background</p>
<div className="rounded-md border border-gray-200 bg-white p-3 dark:border-gray-600 dark:bg-gray-800">
<p className="text-sm text-gray-700 dark:text-gray-300">
Card Background
</p>
</div>
</div>
</CardContent>
@@ -172,19 +200,27 @@ export function DarkModeTest() {
<div className="grid grid-cols-4 gap-4">
<div className="flex flex-col items-center gap-1">
<User className="h-6 w-6 text-gray-700 dark:text-gray-300" />
<span className="text-xs text-gray-500 dark:text-gray-400">Default</span>
<span className="text-xs text-gray-500 dark:text-gray-400">
Default
</span>
</div>
<div className="flex flex-col items-center gap-1">
<Settings className="h-6 w-6 text-green-600 dark:text-green-400" />
<span className="text-xs text-gray-500 dark:text-gray-400">Success</span>
<span className="text-xs text-gray-500 dark:text-gray-400">
Success
</span>
</div>
<div className="flex flex-col items-center gap-1">
<AlertCircle className="h-6 w-6 text-red-600 dark:text-red-400" />
<span className="text-xs text-gray-500 dark:text-gray-400">Error</span>
<span className="text-xs text-gray-500 dark:text-gray-400">
Error
</span>
</div>
<div className="flex flex-col items-center gap-1">
<Info className="h-6 w-6 text-blue-600 dark:text-blue-400" />
<span className="text-xs text-gray-500 dark:text-gray-400">Info</span>
<span className="text-xs text-gray-500 dark:text-gray-400">
Info
</span>
</div>
</div>
</CardContent>
@@ -199,16 +235,28 @@ export function DarkModeTest() {
<CardContent>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<div className="space-y-1">
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">Dark Mode Method:</p>
<p className="text-sm text-gray-500 dark:text-gray-400">Media Query (@media (prefers-color-scheme: dark))</p>
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">
Dark Mode Method:
</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
Media Query (@media (prefers-color-scheme: dark))
</p>
</div>
<div className="space-y-1">
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">Tailwind Config:</p>
<p className="text-sm text-gray-500 dark:text-gray-400">darkMode: "media"</p>
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">
Tailwind Config:
</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
darkMode: &quot;media&quot;
</p>
</div>
<div className="space-y-1">
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">CSS Variables:</p>
<p className="text-sm text-gray-500 dark:text-gray-400">oklch() color space</p>
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">
CSS Variables:
</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
oklch() color space
</p>
</div>
</div>
</CardContent>
@@ -217,10 +265,15 @@ export function DarkModeTest() {
{/* Instructions */}
<Card className="border-blue-200 dark:border-blue-800 dark:bg-gray-800">
<CardHeader>
<CardTitle className="text-blue-700 dark:text-blue-300">Testing Instructions</CardTitle>
<CardTitle className="text-blue-700 dark:text-blue-300">
Testing Instructions
</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm text-blue-600 dark:text-blue-400">
<p> Change your system theme between light and dark to test automatic switching</p>
<p>
Change your system theme between light and dark to test automatic
switching
</p>
<p> All UI elements should adapt colors automatically</p>
<p> Text should remain readable in both modes</p>
<p> Icons and buttons should have appropriate contrast</p>

View File

@@ -1,6 +1,13 @@
"use client";
import { Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbPage, BreadcrumbSeparator } from "~/components/ui/breadcrumb";
import {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
} from "~/components/ui/breadcrumb";
import { usePathname } from "next/navigation";
import Link from "next/link";
import { ChevronRight } from "lucide-react";
@@ -10,61 +17,69 @@ import { format } from "date-fns";
import { Skeleton } from "~/components/ui/skeleton";
function isUUID(str: string) {
return /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(str);
return /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(
str,
);
}
export function DashboardBreadcrumbs() {
const pathname = usePathname();
const segments = pathname.split('/').filter(Boolean);
const segments = pathname.split("/").filter(Boolean);
// Find clientId if present
let clientId: string | undefined = undefined;
if (segments[1] === "clients" && segments[2] && isUUID(segments[2])) {
clientId = segments[2];
}
const { data: client, isLoading: clientLoading } = api.clients.getById.useQuery(
{ id: clientId ?? "" },
{ enabled: !!clientId }
);
const { data: client, isLoading: clientLoading } =
api.clients.getById.useQuery(
{ id: clientId ?? "" },
{ enabled: !!clientId },
);
// Find invoiceId if present
let invoiceId: string | undefined = undefined;
if (segments[1] === "invoices" && segments[2] && isUUID(segments[2])) {
invoiceId = segments[2];
}
const { data: invoice, isLoading: invoiceLoading } = api.invoices.getById.useQuery(
{ id: invoiceId ?? "" },
{ enabled: !!invoiceId }
);
const { data: invoice, isLoading: invoiceLoading } =
api.invoices.getById.useQuery(
{ id: invoiceId ?? "" },
{ enabled: !!invoiceId },
);
// Generate breadcrumb items based on pathname
const breadcrumbs = React.useMemo(() => {
const items = [];
for (let i = 0; i < segments.length; i++) {
const segment = segments[i];
const path = `/${segments.slice(0, i + 1).join('/')}`;
if (segment === 'dashboard') continue;
const path = `/${segments.slice(0, i + 1).join("/")}`;
if (segment === "dashboard") continue;
let label: string | React.ReactElement = segment ?? "";
if (segment === 'clients') label = 'Clients';
if (isUUID(segment ?? "") && clientLoading) label = <Skeleton className="h-5 w-24 inline-block align-middle" />;
if (segment === "clients") label = "Clients";
if (isUUID(segment ?? "") && clientLoading)
label = <Skeleton className="inline-block h-5 w-24 align-middle" />;
else if (isUUID(segment ?? "") && client) label = client.name ?? "";
if (isUUID(segment ?? "") && invoiceLoading) label = <Skeleton className="h-5 w-24 inline-block align-middle" />;
if (isUUID(segment ?? "") && invoiceLoading)
label = <Skeleton className="inline-block h-5 w-24 align-middle" />;
else if (isUUID(segment ?? "") && invoice) {
const issueDate = new Date(invoice.issueDate);
label = format(issueDate, "MMM dd, yyyy");
}
if (segment === 'invoices') label = 'Invoices';
if (segment === 'new') label = 'New';
if (segment === "invoices") label = "Invoices";
if (segment === "new") label = "New";
// Only show 'Edit' if not the last segment
if (segment === 'edit' && i !== segments.length - 1) label = 'Edit';
if (segment === "edit" && i !== segments.length - 1) label = "Edit";
// Don't show 'edit' as the last breadcrumb, just show the client name
if (segment === 'edit' && i === segments.length - 1 && client) continue;
if (segment === 'import') label = 'Import';
if (segment === "edit" && i === segments.length - 1 && client) continue;
if (segment === "import") label = "Import";
items.push({
label,
href: path,
isLast: i === segments.length - 1 || (segment === 'edit' && i === segments.length - 1 && client),
isLast:
i === segments.length - 1 ||
(segment === "edit" && i === segments.length - 1 && client),
});
}
return items;
@@ -77,7 +92,12 @@ export function DashboardBreadcrumbs() {
<BreadcrumbList className="flex-wrap">
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link href="/dashboard" className="text-sm sm:text-base">Dashboard</Link>
<Link
href="/dashboard"
className="text-sm sm:text-base dark:text-gray-300"
>
Dashboard
</Link>
</BreadcrumbLink>
</BreadcrumbItem>
{breadcrumbs.map((crumb) => (
@@ -87,10 +107,17 @@ export function DashboardBreadcrumbs() {
</BreadcrumbSeparator>
<BreadcrumbItem>
{crumb.isLast ? (
<BreadcrumbPage className="text-sm sm:text-base">{crumb.label}</BreadcrumbPage>
<BreadcrumbPage className="text-sm sm:text-base dark:text-white">
{crumb.label}
</BreadcrumbPage>
) : (
<BreadcrumbLink asChild>
<Link href={crumb.href} className="text-sm sm:text-base">{crumb.label}</Link>
<Link
href={crumb.href}
className="text-sm sm:text-base dark:text-gray-300"
>
{crumb.label}
</Link>
</BreadcrumbLink>
)}
</BreadcrumbItem>
@@ -99,4 +126,4 @@ export function DashboardBreadcrumbs() {
</BreadcrumbList>
</Breadcrumb>
);
}
}

View File

@@ -17,9 +17,7 @@ import {
sortableKeyboardCoordinates,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import {
useSortable,
} from "@dnd-kit/sortable";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { Input } from "~/components/ui/input";
import { Button } from "~/components/ui/button";
@@ -47,15 +45,19 @@ interface EditableInvoiceItemsProps {
onRemoveItem: (index: number) => void;
}
function SortableItem({
item,
index,
onItemChange,
onRemove
}: {
item: InvoiceItem;
index: number;
onItemChange: (index: number, field: string, value: any) => void;
function SortableItem({
item,
index,
onItemChange,
onRemove,
}: {
item: InvoiceItem;
index: number;
onItemChange: (
index: number,
field: string,
value: string | number | Date,
) => void;
onRemove: (index: number) => void;
}) {
const {
@@ -72,7 +74,7 @@ function SortableItem({
transition,
};
const handleItemChange = (field: string, value: any) => {
const handleItemChange = (field: string, value: string | number | Date) => {
onItemChange(index, field, value);
};
@@ -80,17 +82,17 @@ function SortableItem({
<div
ref={setNodeRef}
style={style}
className={`grid grid-cols-12 gap-2 items-center p-4 border border-gray-200 rounded-lg hover:border-emerald-300 transition-colors ${
className={`grid grid-cols-12 items-center gap-2 rounded-lg border border-gray-200 p-4 transition-colors hover:border-emerald-300 dark:border-gray-700 dark:hover:border-emerald-500 ${
isDragging ? "opacity-50 shadow-lg" : ""
}`}
>
{/* Drag Handle */}
<div className="col-span-1 flex items-center justify-center h-10">
<div className="col-span-1 flex h-10 items-center justify-center">
<button
type="button"
{...attributes}
{...listeners}
className="p-2 text-gray-400 hover:text-gray-600 cursor-grab active:cursor-grabbing rounded hover:bg-gray-100 transition-colors"
className="cursor-grab rounded p-2 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600 active:cursor-grabbing dark:text-gray-500 dark:hover:bg-gray-800 dark:hover:text-gray-400"
>
<GripVertical className="h-4 w-4" />
</button>
@@ -102,10 +104,10 @@ function SortableItem({
<PopoverTrigger asChild>
<Button
variant="outline"
className="w-full justify-between font-normal h-10 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500 text-sm"
className="h-10 w-full justify-between border-gray-200 text-sm font-normal focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600"
>
{item.date ? format(item.date, "MMM dd") : "Date"}
<CalendarIcon className="h-4 w-4 text-gray-400" />
<CalendarIcon className="h-4 w-4 text-gray-400 dark:text-gray-500" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto overflow-hidden p-0" align="start">
@@ -114,23 +116,23 @@ function SortableItem({
selected={item.date}
captionLayout="dropdown"
onSelect={(selectedDate: Date | undefined) => {
handleItemChange("date", selectedDate || new Date())
handleItemChange("date", selectedDate ?? new Date());
}}
/>
</PopoverContent>
</Popover>
</div>
{/* Description */}
<div className="col-span-4">
<Input
value={item.description}
onChange={e => handleItemChange("description", e.target.value)}
onChange={(e) => handleItemChange("description", e.target.value)}
placeholder="Work description"
className="h-10 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
className="h-10 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
/>
</div>
{/* Hours */}
<div className="col-span-1">
<Input
@@ -138,12 +140,12 @@ function SortableItem({
step="0.25"
min="0"
value={item.hours}
onChange={e => handleItemChange("hours", e.target.value)}
onChange={(e) => handleItemChange("hours", e.target.value)}
placeholder="0"
className="h-10 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
className="h-10 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
/>
</div>
{/* Rate */}
<div className="col-span-2">
<Input
@@ -151,19 +153,19 @@ function SortableItem({
step="0.01"
min="0"
value={item.rate}
onChange={e => handleItemChange("rate", e.target.value)}
onChange={(e) => handleItemChange("rate", e.target.value)}
placeholder="0.00"
className="h-10 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
className="h-10 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
/>
</div>
{/* Amount */}
<div className="col-span-1">
<div className="h-10 flex items-center px-3 border border-gray-200 rounded-md bg-gray-50 text-gray-700 font-medium">
<div className="flex h-10 items-center rounded-md border border-gray-200 bg-gray-50 px-3 font-medium text-gray-700 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300">
${item.amount.toFixed(2)}
</div>
</div>
{/* Remove Button */}
<div className="col-span-1">
<Button
@@ -171,7 +173,7 @@ function SortableItem({
onClick={() => onRemove(index)}
variant="outline"
size="sm"
className="h-10 w-10 p-0 border-red-200 text-red-700 hover:bg-red-50"
className="h-10 w-10 border-red-200 p-0 text-red-700 hover:bg-red-50 dark:border-red-800 dark:text-red-400 dark:hover:bg-red-900/20"
>
<Trash2 className="h-4 w-4" />
</Button>
@@ -180,7 +182,11 @@ function SortableItem({
);
}
export function EditableInvoiceItems({ items, onItemsChange, onRemoveItem }: EditableInvoiceItemsProps) {
export function EditableInvoiceItems({
items,
onItemsChange,
onRemoveItem,
}: EditableInvoiceItemsProps) {
const [isClient, setIsClient] = useState(false);
useEffect(() => {
@@ -191,35 +197,48 @@ export function EditableInvoiceItems({ items, onItemsChange, onRemoveItem }: Edi
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
}),
);
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (active.id !== over?.id) {
const oldIndex = items.findIndex(item => item.id === active.id);
const newIndex = items.findIndex(item => item.id === over?.id);
const oldIndex = items.findIndex((item) => item.id === active.id);
const newIndex = items.findIndex((item) => item.id === over?.id);
const newItems = arrayMove(items, oldIndex, newIndex);
onItemsChange(newItems);
}
};
const handleItemChange = (index: number, field: string, value: any) => {
const handleItemChange = (
index: number,
field: string,
value: string | number | Date,
) => {
const newItems = [...items];
if (field === "hours" || field === "rate") {
if (newItems[index]) {
newItems[index][field as "hours" | "rate"] = parseFloat(value) || 0;
const numValue =
typeof value === "string"
? parseFloat(value)
: typeof value === "number"
? value
: 0;
newItems[index][field] = numValue || 0;
newItems[index].amount = newItems[index].hours * newItems[index].rate;
}
} else if (field === "date") {
if (newItems[index]) {
newItems[index][field as "date"] = value;
const dateValue =
value instanceof Date ? value : new Date(String(value));
newItems[index].date = dateValue;
}
} else {
if (newItems[index]) {
newItems[index][field as "description"] = value;
const stringValue = typeof value === "string" ? value : String(value);
newItems[index].description = stringValue;
}
}
onItemsChange(newItems);
@@ -229,28 +248,31 @@ export function EditableInvoiceItems({ items, onItemsChange, onRemoveItem }: Edi
if (!isClient) {
return (
<div className="space-y-3">
{items.map((item, index) => (
<div key={item.id} className="grid grid-cols-12 gap-2 items-center p-4 border border-gray-200 rounded-lg animate-pulse">
<div className="col-span-1 flex items-center justify-center h-10">
<div className="w-4 h-4 bg-gray-300 rounded"></div>
{items.map((item, _index) => (
<div
key={item.id}
className="grid animate-pulse grid-cols-12 items-center gap-2 rounded-lg border border-gray-200 p-4"
>
<div className="col-span-1 flex h-10 items-center justify-center">
<div className="h-4 w-4 rounded bg-gray-300"></div>
</div>
<div className="col-span-2">
<div className="h-10 bg-gray-300 rounded"></div>
<div className="h-10 rounded bg-gray-300"></div>
</div>
<div className="col-span-4">
<div className="h-10 bg-gray-300 rounded"></div>
<div className="h-10 rounded bg-gray-300"></div>
</div>
<div className="col-span-1">
<div className="h-10 bg-gray-300 rounded"></div>
<div className="h-10 rounded bg-gray-300"></div>
</div>
<div className="col-span-2">
<div className="h-10 bg-gray-300 rounded"></div>
<div className="h-10 rounded bg-gray-300"></div>
</div>
<div className="col-span-1">
<div className="h-10 bg-gray-300 rounded"></div>
<div className="h-10 rounded bg-gray-300"></div>
</div>
<div className="col-span-1">
<div className="h-10 w-10 bg-gray-300 rounded"></div>
<div className="h-10 w-10 rounded bg-gray-300"></div>
</div>
</div>
))}
@@ -264,7 +286,10 @@ export function EditableInvoiceItems({ items, onItemsChange, onRemoveItem }: Edi
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext items={items.map(item => item.id)} strategy={verticalListSortingStrategy}>
<SortableContext
items={items.map((item) => item.id)}
strategy={verticalListSortingStrategy}
>
<div className="space-y-3">
{items.map((item, index) => (
<SortableItem
@@ -279,4 +304,4 @@ export function EditableInvoiceItems({ items, onItemsChange, onRemoveItem }: Edi
</SortableContext>
</DndContext>
);
}
}

View File

@@ -11,21 +11,27 @@ import { DatePicker } from "~/components/ui/date-picker";
import { Badge } from "~/components/ui/badge";
import { Separator } from "~/components/ui/separator";
import { SearchableSelect } from "~/components/ui/select";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "~/components/ui/select";
import { toast } from "sonner";
import {
Calendar,
FileText,
User,
Plus,
Trash2,
DollarSign,
Clock,
import {
Calendar,
FileText,
User,
Plus,
Trash2,
DollarSign,
Clock,
Edit3,
Save,
X,
AlertCircle,
Building
Building,
} from "lucide-react";
import { useRouter } from "next/navigation";
import { format } from "date-fns";
@@ -33,10 +39,27 @@ import { FormSkeleton } from "~/components/ui/skeleton";
import { EditableInvoiceItems } from "~/components/editable-invoice-items";
const STATUS_OPTIONS = [
{ value: "draft", label: "Draft", color: "bg-gray-100 text-gray-800" },
{ value: "sent", label: "Sent", color: "bg-blue-100 text-blue-800" },
{ value: "paid", label: "Paid", color: "bg-green-100 text-green-800" },
{ value: "overdue", label: "Overdue", color: "bg-red-100 text-red-800" },
{
value: "draft",
label: "Draft",
color: "bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300",
},
{
value: "sent",
label: "Sent",
color: "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400",
},
{
value: "paid",
label: "Paid",
color:
"bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400",
},
{
value: "overdue",
label: "Overdue",
color: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400",
},
] as const;
interface InvoiceFormProps {
@@ -46,7 +69,7 @@ interface InvoiceFormProps {
export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
const router = useRouter();
const [formData, setFormData] = useState({
invoiceNumber: `INV-${new Date().toISOString().slice(0, 10).replace(/-/g, '')}-${Date.now().toString().slice(-6)}`,
invoiceNumber: `INV-${new Date().toISOString().slice(0, 10).replace(/-/g, "")}-${Date.now().toString().slice(-6)}`,
businessId: "",
clientId: "",
issueDate: new Date(),
@@ -55,21 +78,28 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
notes: "",
taxRate: 0,
items: [
{ id: crypto.randomUUID(), date: new Date(), description: "", hours: 0, rate: 0, amount: 0 },
{
id: crypto.randomUUID(),
date: new Date(),
description: "",
hours: 0,
rate: 0,
amount: 0,
},
],
});
const [loading, setLoading] = useState(false);
const [defaultRate, setDefaultRate] = useState(0);
// Fetch clients and businesses for dropdowns
const { data: clients, isLoading: loadingClients } = api.clients.getAll.useQuery();
const { data: businesses, isLoading: loadingBusinesses } = api.businesses.getAll.useQuery();
const { data: clients, isLoading: loadingClients } =
api.clients.getAll.useQuery();
const { data: businesses, isLoading: loadingBusinesses } =
api.businesses.getAll.useQuery();
// Fetch existing invoice data if editing
const { data: existingInvoice, isLoading: loadingInvoice } = api.invoices.getById.useQuery(
{ id: invoiceId! },
{ enabled: !!invoiceId }
);
const { data: existingInvoice, isLoading: loadingInvoice } =
api.invoices.getById.useQuery({ id: invoiceId! }, { enabled: !!invoiceId });
// Populate form with existing data when editing
React.useEffect(() => {
@@ -83,16 +113,25 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
status: existingInvoice.status as "draft" | "sent" | "paid" | "overdue",
notes: existingInvoice.notes ?? "",
taxRate: existingInvoice.taxRate,
items: existingInvoice.items?.map(item => ({
items: existingInvoice.items?.map((item) => ({
id: crypto.randomUUID(),
date: new Date(item.date),
description: item.description,
hours: item.hours,
rate: item.rate,
amount: item.amount,
})) || [{ id: crypto.randomUUID(), date: new Date(), description: "", hours: 0, rate: 0, amount: 0 }],
})) || [
{
id: crypto.randomUUID(),
date: new Date(),
description: "",
hours: 0,
rate: 0,
amount: 0,
},
],
});
// Set default rate from first item
if (existingInvoice.items?.[0]) {
setDefaultRate(existingInvoice.items[0].rate);
@@ -102,7 +141,10 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
// Calculate totals
const totals = React.useMemo(() => {
const subtotal = formData.items.reduce((sum, item) => sum + (item.hours * item.rate), 0);
const subtotal = formData.items.reduce(
(sum, item) => sum + item.hours * item.rate,
0,
);
const taxAmount = (subtotal * formData.taxRate) / 100;
const total = subtotal + taxAmount;
return {
@@ -112,15 +154,20 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
};
}, [formData.items, formData.taxRate]);
// Add new item
const addItem = () => {
setFormData((prev) => ({
...prev,
items: [
...prev.items,
{ id: crypto.randomUUID(), date: new Date(), description: "", hours: 0, rate: defaultRate, amount: 0 },
{
id: crypto.randomUUID(),
date: new Date(),
description: "",
hours: 0,
rate: defaultRate,
amount: 0,
},
],
}));
};
@@ -139,7 +186,7 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
const applyDefaultRate = () => {
setFormData((prev) => ({
...prev,
items: prev.items.map(item => ({
items: prev.items.map((item) => ({
...item,
rate: defaultRate,
amount: item.hours * defaultRate,
@@ -171,29 +218,29 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
// Handle form submit
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Validate form
if (!formData.businessId) {
toast.error("Please select a business");
return;
}
if (!formData.clientId) {
toast.error("Please select a client");
return;
}
if (formData.items.some(item => !item.description.trim())) {
if (formData.items.some((item) => !item.description.trim())) {
toast.error("Please fill in all item descriptions");
return;
}
if (formData.items.some(item => item.hours <= 0)) {
if (formData.items.some((item) => item.hours <= 0)) {
toast.error("Please enter valid hours for all items");
return;
}
if (formData.items.some(item => item.rate <= 0)) {
if (formData.items.some((item) => item.rate <= 0)) {
toast.error("Please enter valid rates for all items");
return;
}
@@ -231,16 +278,16 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
return (
<div className="space-y-6 pb-20">
{/* Invoice Details Card Skeleton */}
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm">
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
<CardHeader>
<div className="h-6 bg-gray-300 rounded w-48 animate-pulse"></div>
<div className="h-6 w-48 animate-pulse rounded bg-gray-300 dark:bg-gray-600"></div>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 lg:gap-6">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 lg:gap-6 xl:grid-cols-4">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="space-y-2">
<div className="h-4 bg-gray-300 rounded w-24 animate-pulse"></div>
<div className="h-10 bg-gray-300 rounded animate-pulse"></div>
<div className="h-4 w-24 animate-pulse rounded bg-gray-300 dark:bg-gray-600"></div>
<div className="h-10 animate-pulse rounded bg-gray-300 dark:bg-gray-600"></div>
</div>
))}
</div>
@@ -248,27 +295,36 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
</Card>
{/* Invoice Items Card Skeleton */}
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm">
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
<CardHeader>
<div className="flex items-center justify-between">
<div className="h-6 bg-gray-300 rounded w-32 animate-pulse"></div>
<div className="h-10 bg-gray-300 rounded w-24 animate-pulse"></div>
<div className="h-6 w-32 animate-pulse rounded bg-gray-300 dark:bg-gray-600"></div>
<div className="h-10 w-24 animate-pulse rounded bg-gray-300 dark:bg-gray-600"></div>
</div>
</CardHeader>
<CardContent className="space-y-4">
{/* Items Table Header Skeleton */}
<div className="grid grid-cols-12 gap-2 px-4 py-3 bg-gray-50 rounded-lg">
<div className="grid grid-cols-12 gap-2 rounded-lg bg-gray-50 px-4 py-3 dark:bg-gray-700">
{Array.from({ length: 8 }).map((_, i) => (
<div key={i} className="h-4 bg-gray-300 rounded animate-pulse"></div>
<div
key={i}
className="h-4 animate-pulse rounded bg-gray-300 dark:bg-gray-600"
></div>
))}
</div>
{/* Items Skeleton */}
<div className="space-y-3">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="grid grid-cols-12 gap-2 items-center p-4 border border-gray-200 rounded-lg animate-pulse">
<div
key={i}
className="grid animate-pulse grid-cols-12 items-center gap-2 rounded-lg border border-gray-200 p-4 dark:border-gray-700"
>
{Array.from({ length: 8 }).map((_, j) => (
<div key={j} className="h-10 bg-gray-300 rounded"></div>
<div
key={j}
className="h-10 rounded bg-gray-300 dark:bg-gray-600"
></div>
))}
</div>
))}
@@ -278,12 +334,12 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
{/* Form Controls Bar Skeleton */}
<div className="mt-6">
<div className="bg-white/90 rounded-2xl border border-gray-200 shadow-sm p-4">
<div className="rounded-2xl border border-gray-200 bg-white/90 p-4 shadow-sm dark:border-gray-700 dark:bg-gray-800/90">
<div className="flex items-center justify-between">
<div className="h-4 bg-gray-300 rounded w-32 animate-pulse"></div>
<div className="h-4 w-32 animate-pulse rounded bg-gray-300 dark:bg-gray-600"></div>
<div className="flex items-center gap-3">
<div className="h-10 bg-gray-300 rounded w-20 animate-pulse"></div>
<div className="h-10 bg-gray-300 rounded w-32 animate-pulse"></div>
<div className="h-10 w-20 animate-pulse rounded bg-gray-300 dark:bg-gray-600"></div>
<div className="h-10 w-32 animate-pulse rounded bg-gray-300 dark:bg-gray-600"></div>
</div>
</div>
</div>
@@ -292,24 +348,26 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
);
}
const selectedClient = clients?.find(c => c.id === formData.clientId);
const selectedBusiness = businesses?.find(b => b.id === formData.businessId);
const selectedClient = clients?.find((c) => c.id === formData.clientId);
const selectedBusiness = businesses?.find(
(b) => b.id === formData.businessId,
);
// Show loading state while fetching clients
if (loadingClients) {
return (
<div className="space-y-6 pb-20">
{/* Invoice Details Card Skeleton */}
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm">
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm">
<CardHeader>
<div className="h-6 bg-gray-300 rounded w-48 animate-pulse"></div>
<div className="h-6 w-48 animate-pulse rounded bg-gray-300"></div>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 lg:gap-6">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 lg:gap-6 xl:grid-cols-4">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="space-y-2">
<div className="h-4 bg-gray-300 rounded w-24 animate-pulse"></div>
<div className="h-10 bg-gray-300 rounded animate-pulse"></div>
<div className="h-4 w-24 animate-pulse rounded bg-gray-300"></div>
<div className="h-10 animate-pulse rounded bg-gray-300"></div>
</div>
))}
</div>
@@ -317,27 +375,33 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
</Card>
{/* Invoice Items Card Skeleton */}
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm">
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
<CardHeader>
<div className="flex items-center justify-between">
<div className="h-6 bg-gray-300 rounded w-32 animate-pulse"></div>
<div className="h-10 bg-gray-300 rounded w-24 animate-pulse"></div>
<div className="h-6 w-32 animate-pulse rounded bg-gray-300"></div>
<div className="h-10 w-24 animate-pulse rounded bg-gray-300"></div>
</div>
</CardHeader>
<CardContent className="space-y-4">
{/* Items Table Header Skeleton */}
<div className="grid grid-cols-12 gap-2 px-4 py-3 bg-gray-50 rounded-lg">
<div className="grid grid-cols-12 gap-2 rounded-lg bg-gray-50 px-4 py-3">
{Array.from({ length: 8 }).map((_, i) => (
<div key={i} className="h-4 bg-gray-300 rounded animate-pulse"></div>
<div
key={i}
className="h-4 animate-pulse rounded bg-gray-300"
></div>
))}
</div>
{/* Items Skeleton */}
<div className="space-y-3">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="grid grid-cols-12 gap-2 items-center p-4 border border-gray-200 rounded-lg animate-pulse">
<div
key={i}
className="grid animate-pulse grid-cols-12 items-center gap-2 rounded-lg border border-gray-200 p-4"
>
{Array.from({ length: 8 }).map((_, j) => (
<div key={j} className="h-10 bg-gray-300 rounded"></div>
<div key={j} className="h-10 rounded bg-gray-300"></div>
))}
</div>
))}
@@ -347,12 +411,12 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
{/* Form Controls Bar Skeleton */}
<div className="mt-6">
<div className="bg-white/90 rounded-2xl border border-gray-200 shadow-sm p-4">
<div className="rounded-2xl border border-gray-200 bg-white/90 p-4 shadow-sm">
<div className="flex items-center justify-between">
<div className="h-4 bg-gray-300 rounded w-32 animate-pulse"></div>
<div className="h-4 w-32 animate-pulse rounded bg-gray-300"></div>
<div className="flex items-center gap-3">
<div className="h-10 bg-gray-300 rounded w-20 animate-pulse"></div>
<div className="h-10 bg-gray-300 rounded w-32 animate-pulse"></div>
<div className="h-10 w-20 animate-pulse rounded bg-gray-300"></div>
<div className="h-10 w-32 animate-pulse rounded bg-gray-300"></div>
</div>
</div>
</div>
@@ -364,65 +428,96 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
return (
<form id="invoice-form" onSubmit={handleSubmit} className="space-y-6 pb-20">
{/* Invoice Details Card */}
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm">
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-emerald-700">
<FileText className="h-5 w-5" />
<CardTitle className="flex items-center gap-2 text-emerald-700 dark:text-emerald-400">
<FileText className="h-5 w-5" />
Invoice Details
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 lg:gap-6">
<div className="space-y-2">
<Label htmlFor="invoiceNumber" className="text-sm font-medium text-gray-700">
Invoice Number
</Label>
<Input
id="invoiceNumber"
value={formData.invoiceNumber}
className="h-10 border-gray-200 bg-gray-50"
placeholder="Auto-generated"
readOnly
/>
</div>
<div className="space-y-2">
<Label htmlFor="businessId" className="text-sm font-medium text-gray-700">
Business *
</Label>
<SearchableSelect
value={formData.businessId}
onValueChange={(value) => setFormData(f => ({ ...f, businessId: value }))}
options={businesses?.map(business => ({ value: business.id, label: business.name })) ?? []}
placeholder="Select a business"
searchPlaceholder="Search businesses..."
disabled={loadingBusinesses}
/>
</div>
<div className="space-y-2">
<Label htmlFor="clientId" className="text-sm font-medium text-gray-700">
Client *
</Label>
<SearchableSelect
value={formData.clientId}
onValueChange={(value) => setFormData(f => ({ ...f, clientId: value }))}
options={clients?.map(client => ({ value: client.id, label: client.name })) ?? []}
placeholder="Select a client"
searchPlaceholder="Search clients..."
disabled={loadingClients}
/>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 lg:gap-6 xl:grid-cols-4">
<div className="space-y-2">
<Label
htmlFor="invoiceNumber"
className="text-sm font-medium text-gray-700 dark:text-gray-300"
>
Invoice Number
</Label>
<Input
id="invoiceNumber"
value={formData.invoiceNumber}
className="h-10 border-gray-200 bg-gray-50 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Auto-generated"
readOnly
/>
</div>
<div className="space-y-2">
<Label htmlFor="status" className="text-sm font-medium text-gray-700">
<Label
htmlFor="businessId"
className="text-sm font-medium text-gray-700 dark:text-gray-300"
>
Business *
</Label>
<SearchableSelect
value={formData.businessId}
onValueChange={(value) =>
setFormData((f) => ({ ...f, businessId: value }))
}
options={
businesses?.map((business) => ({
value: business.id,
label: business.name,
})) ?? []
}
placeholder="Select a business"
searchPlaceholder="Search businesses..."
disabled={loadingBusinesses}
/>
</div>
<div className="space-y-2">
<Label
htmlFor="clientId"
className="text-sm font-medium text-gray-700 dark:text-gray-300"
>
Client *
</Label>
<SearchableSelect
value={formData.clientId}
onValueChange={(value) =>
setFormData((f) => ({ ...f, clientId: value }))
}
options={
clients?.map((client) => ({
value: client.id,
label: client.name,
})) ?? []
}
placeholder="Select a client"
searchPlaceholder="Search clients..."
disabled={loadingClients}
/>
</div>
<div className="space-y-2">
<Label
htmlFor="status"
className="text-sm font-medium text-gray-700 dark:text-gray-300"
>
Status
</Label>
<Select
value={formData.status}
onValueChange={(value) => setFormData(f => ({ ...f, status: value as "draft" | "sent" | "paid" | "overdue" }))}
onValueChange={(value) =>
setFormData((f) => ({
...f,
status: value as "draft" | "sent" | "paid" | "overdue",
}))
}
>
<SelectTrigger className="h-10 border-gray-200 bg-gray-50">
<SelectTrigger className="h-10 border-gray-200 bg-gray-50 dark:border-gray-600 dark:bg-gray-700">
<SelectValue placeholder="Select status" />
</SelectTrigger>
<SelectContent>
@@ -434,90 +529,119 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="issueDate" className="text-sm font-medium text-gray-700">
<div className="space-y-2">
<Label
htmlFor="issueDate"
className="text-sm font-medium text-gray-700 dark:text-gray-300"
>
Issue Date *
</Label>
<DatePicker
date={formData.issueDate}
onDateChange={date => setFormData(f => ({ ...f, issueDate: date ?? new Date() }))}
placeholder="Select issue date"
required
/>
</div>
</Label>
<DatePicker
date={formData.issueDate}
onDateChange={(date) =>
setFormData((f) => ({ ...f, issueDate: date ?? new Date() }))
}
placeholder="Select issue date"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="dueDate" className="text-sm font-medium text-gray-700">
<div className="space-y-2">
<Label
htmlFor="dueDate"
className="text-sm font-medium text-gray-700 dark:text-gray-300"
>
Due Date *
</Label>
<DatePicker
date={formData.dueDate}
onDateChange={date => setFormData(f => ({ ...f, dueDate: date ?? new Date() }))}
placeholder="Select due date"
required
/>
</div>
</Label>
<DatePicker
date={formData.dueDate}
onDateChange={(date) =>
setFormData((f) => ({ ...f, dueDate: date ?? new Date() }))
}
placeholder="Select due date"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="defaultRate" className="text-sm font-medium text-gray-700">
<div className="space-y-2">
<Label
htmlFor="defaultRate"
className="text-sm font-medium text-gray-700 dark:text-gray-300"
>
Default Rate ($/hr)
</Label>
</Label>
<div className="flex gap-2">
<Input
id="defaultRate"
type="number"
step="0.01"
value={defaultRate}
onChange={e => setDefaultRate(parseFloat(e.target.value) || 0)}
onChange={(e) =>
setDefaultRate(parseFloat(e.target.value) || 0)
}
placeholder="0.00"
className="h-10 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
className="h-10 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
/>
<Button
type="button"
onClick={applyDefaultRate}
variant="outline"
size="sm"
className="h-10 border-emerald-200 text-emerald-700 hover:bg-emerald-50"
className="h-10 border-emerald-200 text-emerald-700 hover:bg-emerald-50 dark:border-emerald-800 dark:text-emerald-400 dark:hover:bg-emerald-900/20"
>
Apply
</Button>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="taxRate" className="text-sm font-medium text-gray-700">
Tax Rate (%)
</Label>
<Input
id="taxRate"
type="number"
step="0.01"
min="0"
max="100"
value={formData.taxRate}
onChange={e => setFormData(f => ({ ...f, taxRate: parseFloat(e.target.value) || 0 }))}
placeholder="0.00"
className="h-10 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
/>
</div>
<div className="space-y-2">
<Label
htmlFor="taxRate"
className="text-sm font-medium text-gray-700 dark:text-gray-300"
>
Tax Rate (%)
</Label>
<Input
id="taxRate"
type="number"
step="0.01"
min="0"
max="100"
value={formData.taxRate}
onChange={(e) =>
setFormData((f) => ({
...f,
taxRate: parseFloat(e.target.value) || 0,
}))
}
placeholder="0.00"
className="h-10 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
/>
</div>
</div>
{selectedBusiness && (
<div className="p-4 bg-emerald-50 rounded-lg border border-emerald-200">
<div className="flex items-center gap-2 text-emerald-700 mb-2">
<div className="rounded-lg border border-emerald-200 bg-emerald-50 p-4 dark:border-emerald-800 dark:bg-emerald-900/20">
<div className="mb-2 flex items-center gap-2 text-emerald-700 dark:text-emerald-400">
<Building className="h-4 w-4" />
<span className="font-medium">Business Information</span>
</div>
<div className="text-sm text-gray-700">
<div className="text-sm text-gray-700 dark:text-gray-300">
<p className="font-medium">{selectedBusiness.name}</p>
{selectedBusiness.email && <p>{selectedBusiness.email}</p>}
{selectedBusiness.phone && <p>{selectedBusiness.phone}</p>}
{selectedBusiness.addressLine1 && (
<p>{selectedBusiness.addressLine1}</p>
)}
{(selectedBusiness.city ?? selectedBusiness.state ?? selectedBusiness.postalCode) && (
{(selectedBusiness.city ??
selectedBusiness.state ??
selectedBusiness.postalCode) && (
<p>
{[selectedBusiness.city, selectedBusiness.state, selectedBusiness.postalCode]
{[
selectedBusiness.city,
selectedBusiness.state,
selectedBusiness.postalCode,
]
.filter(Boolean)
.join(", ")}
</p>
@@ -527,12 +651,12 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
)}
{selectedClient && (
<div className="p-4 bg-emerald-50 rounded-lg border border-emerald-200">
<div className="flex items-center gap-2 text-emerald-700 mb-2">
<div className="rounded-lg border border-emerald-200 bg-emerald-50 p-4 dark:border-emerald-800 dark:bg-emerald-900/20">
<div className="mb-2 flex items-center gap-2 text-emerald-700 dark:text-emerald-400">
<User className="h-4 w-4" />
<span className="font-medium">Client Information</span>
</div>
<div className="text-sm text-gray-700">
<div className="text-sm text-gray-700 dark:text-gray-300">
<p className="font-medium">{selectedClient.name}</p>
{selectedClient.email && <p>{selectedClient.email}</p>}
{selectedClient.phone && <p>{selectedClient.phone}</p>}
@@ -541,25 +665,30 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
)}
<div className="space-y-2">
<Label htmlFor="notes" className="text-sm font-medium text-gray-700">
<Label
htmlFor="notes"
className="text-sm font-medium text-gray-700 dark:text-gray-300"
>
Notes
</Label>
<textarea
id="notes"
value={formData.notes}
onChange={e => setFormData(f => ({ ...f, notes: e.target.value }))}
className="w-full rounded-md border border-gray-200 bg-white px-3 py-2 text-gray-700 focus:border-emerald-500 focus:ring-emerald-500 min-h-[80px] resize-none"
onChange={(e) =>
setFormData((f) => ({ ...f, notes: e.target.value }))
}
className="min-h-[80px] w-full resize-none rounded-md border border-gray-200 bg-white px-3 py-2 text-gray-700 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Additional notes, terms, or special instructions..."
/>
</div>
/>
</div>
</CardContent>
</Card>
{/* Invoice Items Card */}
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm">
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="flex items-center gap-2 text-emerald-700">
<CardTitle className="flex items-center gap-2 text-emerald-700 dark:text-emerald-400">
<Clock className="h-5 w-5" />
Invoice Items
</CardTitle>
@@ -572,11 +701,11 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
<Plus className="mr-2 h-4 w-4" />
Add Item
</Button>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
{/* Items Table Header */}
<div className="grid grid-cols-12 gap-2 px-4 py-3 bg-gray-50 rounded-lg font-medium text-sm text-gray-700 items-center">
<div className="grid grid-cols-12 items-center gap-2 rounded-lg bg-gray-50 px-4 py-3 text-sm font-medium text-gray-700 dark:bg-gray-700 dark:text-gray-300">
<div className="col-span-1 text-center"></div>
<div className="col-span-2">Date</div>
<div className="col-span-4">Description</div>
@@ -584,55 +713,64 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
<div className="col-span-2">Rate ($)</div>
<div className="col-span-1">Amount</div>
<div className="col-span-1"></div>
</div>
</div>
{/* Items */}
<EditableInvoiceItems
items={formData.items}
onItemsChange={(newItems) => setFormData(prev => ({ ...prev, items: newItems }))}
onItemsChange={(newItems) =>
setFormData((prev) => ({ ...prev, items: newItems }))
}
onRemoveItem={removeItem}
/>
{/* Validation Messages */}
{formData.items.some(item => !item.description.trim()) && (
<div className="flex items-center gap-2 text-amber-600 text-sm">
{formData.items.some((item) => !item.description.trim()) && (
<div className="flex items-center gap-2 text-sm text-amber-600">
<AlertCircle className="h-4 w-4" />
Please fill in all item descriptions
</div>
</div>
)}
{formData.items.some(item => item.hours <= 0) && (
<div className="flex items-center gap-2 text-amber-600 text-sm">
{formData.items.some((item) => item.hours <= 0) && (
<div className="flex items-center gap-2 text-sm text-amber-600">
<AlertCircle className="h-4 w-4" />
Please enter valid hours for all items
</div>
)}
{formData.items.some(item => item.rate <= 0) && (
<div className="flex items-center gap-2 text-amber-600 text-sm">
</div>
)}
{formData.items.some((item) => item.rate <= 0) && (
<div className="flex items-center gap-2 text-sm text-amber-600">
<AlertCircle className="h-4 w-4" />
Please enter valid rates for all items
</div>
</div>
)}
<Separator />
{/* Totals */}
<div className="flex justify-end">
<div className="text-right space-y-2">
<div className="space-y-2 text-right">
<div className="space-y-1">
<div className="text-sm text-gray-600">Subtotal: ${totals.subtotal.toFixed(2)}</div>
<div className="text-sm text-gray-600">
Subtotal: ${totals.subtotal.toFixed(2)}
</div>
{formData.taxRate > 0 && (
<div className="text-sm text-gray-600">
Tax ({formData.taxRate}%): ${totals.taxAmount.toFixed(2)}
</div>
)}
</div>
<div className="text-lg font-medium text-gray-700">Total Amount</div>
<div className="text-3xl font-bold text-emerald-600">${totals.total.toFixed(2)}</div>
<div className="text-sm text-gray-500">
{formData.items.length} item{formData.items.length !== 1 ? 's' : ''}
</div>
<div className="text-lg font-medium text-gray-700 dark:text-gray-300">
Total Amount
</div>
<div className="text-3xl font-bold text-emerald-600 dark:text-emerald-400">
${totals.total.toFixed(2)}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
{formData.items.length} item
{formData.items.length !== 1 ? "s" : ""}
</div>
</div>
</div>
</CardContent>
@@ -640,34 +778,37 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
{/* Form Controls Bar */}
<div className="mt-6">
<div className="bg-white/90 rounded-2xl border border-gray-200 shadow-sm p-4">
<div className="rounded-2xl border border-gray-200 bg-white/90 p-4 shadow-sm dark:border-gray-700 dark:bg-gray-800/90">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-sm text-gray-600">
<div className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-300">
<div className="flex items-center gap-1">
<div className="w-2 h-2 bg-emerald-500 rounded-full"></div>
<div className="h-2 w-2 rounded-full bg-emerald-500"></div>
<span>Ready to save</span>
</div>
{formData.items.length > 0 && (
<span className="text-gray-400"></span>
<span className="text-gray-400 dark:text-gray-500"></span>
)}
{formData.items.length > 0 && (
<span>{formData.items.length} item{formData.items.length !== 1 ? 's' : ''}</span>
<span>
{formData.items.length} item
{formData.items.length !== 1 ? "s" : ""}
</span>
)}
</div>
</div>
<div className="flex items-center gap-3">
<Button
type="button"
variant="outline"
<Button
type="button"
variant="outline"
onClick={() => router.push("/dashboard/invoices")}
className="border-gray-300 text-gray-700 hover:bg-gray-50 font-medium"
>
Cancel
</Button>
className="border-gray-300 font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800"
>
Cancel
</Button>
<Button
type="submit"
disabled={loading}
className="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700 text-white font-medium shadow-lg hover:shadow-xl transition-all duration-200"
className="bg-gradient-to-r from-emerald-600 to-teal-600 font-medium text-white shadow-lg transition-all duration-200 hover:from-emerald-700 hover:to-teal-700 hover:shadow-xl"
>
{loading ? (
<>
@@ -685,6 +826,6 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
</div>
</div>
</div>
</form>
</form>
);
}
}

View File

@@ -42,16 +42,25 @@ interface InvoiceViewProps {
}
const statusConfig = {
draft: { label: "Draft", color: "bg-gray-100 text-gray-800", icon: FileText },
sent: { label: "Sent", color: "bg-blue-100 text-blue-800", icon: Send },
draft: {
label: "Draft",
color: "bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300",
icon: FileText,
},
sent: {
label: "Sent",
color: "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400",
icon: Send,
},
paid: {
label: "Paid",
color: "bg-green-100 text-green-800",
color:
"bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400",
icon: DollarSign,
},
overdue: {
label: "Overdue",
color: "bg-red-100 text-red-800",
color: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400",
icon: AlertCircle,
},
} as const;
@@ -144,10 +153,10 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
return (
<div className="py-12 text-center">
<FileText className="mx-auto mb-4 h-12 w-12 text-gray-400" />
<h3 className="mb-2 text-lg font-medium text-gray-900">
<h3 className="mb-2 text-lg font-medium text-gray-900 dark:text-white">
Invoice not found
</h3>
<p className="mb-4 text-gray-500">
<p className="mb-4 text-gray-500 dark:text-gray-400">
The invoice you&apos;re looking for doesn&apos;t exist or has been
deleted.
</p>
@@ -165,9 +174,9 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
<div className="space-y-6">
{/* Status Alert */}
{isOverdue && (
<Card className="border-red-200 bg-red-50">
<Card className="border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-900/20">
<CardContent className="p-4">
<div className="flex items-center gap-2 text-red-700">
<div className="flex items-center gap-2 text-red-700 dark:text-red-400">
<AlertCircle className="h-5 w-5" />
<span className="font-medium">This invoice is overdue</span>
</div>
@@ -179,32 +188,38 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
{/* Main Content */}
<div className="space-y-6 lg:col-span-2">
{/* Invoice Header Card */}
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm">
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
<CardContent>
<div className="flex items-start justify-between">
<div className="space-y-4">
<div className="flex items-center gap-3">
<div className="rounded-lg bg-emerald-100 p-2">
<FileText className="h-6 w-6 text-emerald-600" />
<div className="rounded-lg bg-emerald-100 p-2 dark:bg-emerald-900/30">
<FileText className="h-6 w-6 text-emerald-600 dark:text-emerald-400" />
</div>
<div>
<h2 className="text-2xl font-bold text-gray-900">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
{invoice.invoiceNumber}
</h2>
<p className="text-gray-600">Professional Invoice</p>
<p className="text-gray-600 dark:text-gray-300">
Professional Invoice
</p>
</div>
</div>
<div className="grid grid-cols-2 gap-6 text-sm">
<div>
<span className="text-gray-500">Issue Date</span>
<p className="font-medium text-gray-900">
<span className="text-gray-500 dark:text-gray-400">
Issue Date
</span>
<p className="font-medium text-gray-900 dark:text-white">
{formatDate(invoice.issueDate)}
</p>
</div>
<div>
<span className="text-gray-500">Due Date</span>
<p className="font-medium text-gray-900">
<span className="text-gray-500 dark:text-gray-400">
Due Date
</span>
<p className="font-medium text-gray-900 dark:text-white">
{formatDate(invoice.dueDate)}
</p>
</div>
@@ -221,7 +236,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
.label
}
</Badge>
<div className="text-3xl font-bold text-emerald-600">
<div className="text-3xl font-bold text-emerald-600 dark:text-emerald-400">
{formatCurrency(invoice.totalAmount)}
</div>
<Button
@@ -247,38 +262,38 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
</Card>
{/* Client Information */}
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm">
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-emerald-700">
<CardTitle className="flex items-center gap-2 text-emerald-700 dark:text-emerald-400">
<User className="h-5 w-5" />
Bill To
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<h3 className="text-lg font-semibold text-gray-900">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{invoice.client?.name}
</h3>
</div>
<div className="grid grid-cols-1 gap-4 text-sm md:grid-cols-2">
{invoice.client?.email && (
<div className="flex items-center gap-2 text-gray-600">
<Mail className="h-4 w-4 text-gray-400" />
<div className="flex items-center gap-2 text-gray-600 dark:text-gray-300">
<Mail className="h-4 w-4 text-gray-400 dark:text-gray-500" />
{invoice.client.email}
</div>
)}
{invoice.client?.phone && (
<div className="flex items-center gap-2 text-gray-600">
<Phone className="h-4 w-4 text-gray-400" />
<div className="flex items-center gap-2 text-gray-600 dark:text-gray-300">
<Phone className="h-4 w-4 text-gray-400 dark:text-gray-500" />
{invoice.client.phone}
</div>
)}
{(invoice.client?.addressLine1 ??
invoice.client?.city ??
invoice.client?.state) && (
<div className="flex items-start gap-2 text-gray-600 md:col-span-2">
<MapPin className="mt-0.5 h-4 w-4 flex-shrink-0 text-gray-400" />
<div className="flex items-start gap-2 text-gray-600 md:col-span-2 dark:text-gray-300">
<MapPin className="mt-0.5 h-4 w-4 flex-shrink-0 text-gray-400 dark:text-gray-500" />
<div>
{invoice.client?.addressLine1 && (
<div>{invoice.client.addressLine1}</div>
@@ -310,31 +325,31 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
</Card>
{/* Invoice Items */}
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm">
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-emerald-700">
<CardTitle className="flex items-center gap-2 text-emerald-700 dark:text-emerald-400">
<Clock className="h-5 w-5" />
Invoice Items
</CardTitle>
</CardHeader>
<CardContent>
<div className="overflow-hidden rounded-lg border border-gray-200">
<div className="overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
<table className="w-full">
<thead className="bg-gray-50">
<thead className="bg-gray-50 dark:bg-gray-700">
<tr>
<th className="px-4 py-3 text-left text-sm font-semibold text-gray-700">
<th className="px-4 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-300">
Date
</th>
<th className="px-4 py-3 text-left text-sm font-semibold text-gray-700">
<th className="px-4 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-300">
Description
</th>
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-700">
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-700 dark:text-gray-300">
Hours
</th>
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-700">
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-700 dark:text-gray-300">
Rate
</th>
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-700">
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-700 dark:text-gray-300">
Amount
</th>
</tr>
@@ -343,21 +358,21 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
{invoice.items?.map((item, index) => (
<tr
key={item.id || index}
className="border-t border-gray-100 hover:bg-gray-50"
className="border-t border-gray-100 hover:bg-gray-50 dark:border-gray-600 dark:hover:bg-gray-700"
>
<td className="px-4 py-3 text-sm text-gray-900">
<td className="px-4 py-3 text-sm text-gray-900 dark:text-gray-300">
{formatDate(item.date)}
</td>
<td className="px-4 py-3 text-sm text-gray-900">
<td className="px-4 py-3 text-sm text-gray-900 dark:text-gray-300">
{item.description}
</td>
<td className="px-4 py-3 text-right text-sm text-gray-900">
<td className="px-4 py-3 text-right text-sm text-gray-900 dark:text-gray-300">
{item.hours}
</td>
<td className="px-4 py-3 text-right text-sm text-gray-900">
<td className="px-4 py-3 text-right text-sm text-gray-900 dark:text-gray-300">
{formatCurrency(item.rate)}
</td>
<td className="px-4 py-3 text-right text-sm font-medium text-gray-900">
<td className="px-4 py-3 text-right text-sm font-medium text-gray-900 dark:text-gray-300">
{formatCurrency(item.amount)}
</td>
</tr>
@@ -370,12 +385,14 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
{/* Notes */}
{invoice.notes && (
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm">
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
<CardHeader>
<CardTitle className="text-emerald-700">Notes</CardTitle>
<CardTitle className="text-emerald-700 dark:text-emerald-400">
Notes
</CardTitle>
</CardHeader>
<CardContent>
<p className="whitespace-pre-wrap text-gray-700">
<p className="whitespace-pre-wrap text-gray-700 dark:text-gray-300">
{invoice.notes}
</p>
</CardContent>
@@ -386,9 +403,11 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
{/* Sidebar */}
<div className="space-y-6">
{/* Status Actions */}
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm">
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
<CardHeader>
<CardTitle className="text-emerald-700">Status Actions</CardTitle>
<CardTitle className="text-emerald-700 dark:text-emerald-400">
Status Actions
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{invoice.status === "draft" && (
@@ -426,41 +445,47 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
{invoice.status === "paid" && (
<div className="py-4 text-center">
<DollarSign className="mx-auto mb-2 h-8 w-8 text-green-600" />
<p className="font-medium text-green-600">Invoice Paid</p>
<DollarSign className="mx-auto mb-2 h-8 w-8 text-green-600 dark:text-green-400" />
<p className="font-medium text-green-600 dark:text-green-400">
Invoice Paid
</p>
</div>
)}
</CardContent>
</Card>
{/* Invoice Summary */}
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm">
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
<CardHeader>
<CardTitle className="text-emerald-700">Summary</CardTitle>
<CardTitle className="text-emerald-700 dark:text-emerald-400">
Summary
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-3">
<div className="flex justify-between text-sm">
<span className="text-gray-600">Subtotal</span>
<span className="font-medium">
<span className="text-gray-600 dark:text-gray-300">
Subtotal
</span>
<span className="font-medium dark:text-white">
{formatCurrency(invoice.totalAmount)}
</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600">Tax</span>
<span className="font-medium">$0.00</span>
<span className="text-gray-600 dark:text-gray-300">Tax</span>
<span className="font-medium dark:text-white">$0.00</span>
</div>
<Separator />
<div className="flex justify-between text-lg font-bold">
<span>Total</span>
<span className="text-emerald-600">
<span className="dark:text-white">Total</span>
<span className="text-emerald-600 dark:text-emerald-400">
{formatCurrency(invoice.totalAmount)}
</span>
</div>
</div>
<div className="border-t border-gray-200 pt-4 text-center">
<p className="text-sm text-gray-500">
<div className="border-t border-gray-200 pt-4 text-center dark:border-gray-700">
<p className="text-sm text-gray-500 dark:text-gray-400">
{invoice.items?.length ?? 0} item
{invoice.items?.length !== 1 ? "s" : ""}
</p>
@@ -469,15 +494,17 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
</Card>
{/* Danger Zone */}
<Card className="border-0 border-red-200 bg-white/80 shadow-xl backdrop-blur-sm">
<Card className="border-0 border-red-200 bg-white/80 shadow-xl backdrop-blur-sm dark:border-red-800 dark:bg-gray-800/80">
<CardHeader>
<CardTitle className="text-red-700">Danger Zone</CardTitle>
<CardTitle className="text-red-700 dark:text-red-400">
Danger Zone
</CardTitle>
</CardHeader>
<CardContent>
<Button
onClick={handleDelete}
variant="outline"
className="w-full border-red-200 text-red-700 hover:bg-red-50"
className="w-full border-red-200 text-red-700 hover:bg-red-50 dark:border-red-800 dark:text-red-400 dark:hover:bg-red-900/20"
>
<Trash2 className="mr-2 h-4 w-4" />
Delete Invoice
@@ -489,12 +516,12 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
{/* Delete Confirmation Dialog */}
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<DialogContent className="border-0 bg-white/95 shadow-2xl backdrop-blur-sm">
<DialogContent className="border-0 bg-white/95 shadow-2xl backdrop-blur-sm dark:bg-gray-800/95">
<DialogHeader>
<DialogTitle className="text-xl font-bold text-gray-800">
<DialogTitle className="text-xl font-bold text-gray-800 dark:text-white">
Delete Invoice
</DialogTitle>
<DialogDescription className="text-gray-600">
<DialogDescription className="text-gray-600 dark:text-gray-300">
Are you sure you want to delete this invoice? This action cannot
be undone and will permanently remove the invoice and all its
data.
@@ -504,7 +531,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
<Button
variant="outline"
onClick={() => setDeleteDialogOpen(false)}
className="border-gray-300 text-gray-700 hover:bg-gray-50"
className="border-gray-300 text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800"
>
Cancel
</Button>

View File

@@ -1,27 +1,32 @@
"use client"
"use client";
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon, Search } from "lucide-react"
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import {
CheckIcon,
ChevronDownIcon,
ChevronUpIcon,
Search,
} from "lucide-react";
import { cn } from "~/lib/utils"
import { cn } from "~/lib/utils";
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />
return <SelectPrimitive.Root data-slot="select" {...props} />;
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
}
function SelectTrigger({
@@ -30,15 +35,15 @@ function SelectTrigger({
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default"
size?: "sm" | "default";
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"flex w-full items-center justify-between gap-2 rounded-md border border-gray-200 bg-gray-50 px-3 h-10 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-emerald-500 focus-visible:ring-emerald-500 focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[placeholder]:text-muted-foreground",
className
"data-[placeholder]:text-muted-foreground flex h-10 w-full items-center justify-between gap-2 rounded-md border border-gray-200 bg-gray-50 px-3 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-emerald-500 focus-visible:ring-[3px] focus-visible:ring-emerald-500 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-700 dark:text-white",
className,
)}
{...props}
>
@@ -47,7 +52,7 @@ function SelectTrigger({
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
);
}
function SelectContent({
@@ -64,7 +69,7 @@ function SelectContent({
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
className,
)}
position={position}
{...props}
@@ -74,7 +79,7 @@ function SelectContent({
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
)}
>
{children}
@@ -82,7 +87,7 @@ function SelectContent({
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
);
}
function SelectLabel({
@@ -95,7 +100,7 @@ function SelectLabel({
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
)
);
}
function SelectItem({
@@ -108,7 +113,7 @@ function SelectItem({
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
className,
)}
{...props}
>
@@ -119,7 +124,7 @@ function SelectItem({
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
);
}
function SelectSeparator({
@@ -132,7 +137,7 @@ function SelectSeparator({
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
)
);
}
function SelectScrollUpButton({
@@ -144,13 +149,13 @@ function SelectScrollUpButton({
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
className,
)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
)
);
}
function SelectScrollDownButton({
@@ -162,13 +167,13 @@ function SelectScrollDownButton({
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
className,
)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
)
);
}
// Enhanced SelectContent with search functionality
@@ -208,7 +213,7 @@ function SelectContentWithSearch({
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-96 min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-hidden rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
className,
)}
position={position}
onEscapeKeyDown={(e) => {
@@ -226,11 +231,11 @@ function SelectContentWithSearch({
{...props}
>
{onSearchChange && (
<div className="flex items-center px-3 py-2 border-b">
<div className="border-border flex items-center border-b px-3 py-2">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<input
ref={searchInputRef}
className="flex h-8 w-full rounded-md bg-transparent py-2 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50 border-0 focus:ring-0 focus:outline-none"
className="placeholder:text-muted-foreground text-foreground flex h-8 w-full rounded-md border-0 bg-transparent py-2 text-sm outline-none focus:ring-0 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
placeholder={searchPlaceholder}
value={searchValue}
onChange={(e) => onSearchChange(e.target.value)}
@@ -240,7 +245,11 @@ function SelectContentWithSearch({
e.stopPropagation();
}
// Prevent arrow keys from moving focus away from search
if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) {
if (
["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(
e.key,
)
) {
e.stopPropagation();
}
}}
@@ -255,7 +264,9 @@ function SelectContentWithSearch({
<SelectScrollUpButton />
<SelectPrimitive.Viewport className="p-1">
{filteredOptions && filteredOptions.length === 0 ? (
<div className="px-3 py-2 text-sm text-muted-foreground select-none">No results found</div>
<div className="text-muted-foreground px-3 py-2 text-sm select-none">
No results found
</div>
) : (
children
)}
@@ -263,7 +274,7 @@ function SelectContentWithSearch({
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
);
}
// Searchable Select component
@@ -284,21 +295,21 @@ function SearchableSelect({
options,
searchPlaceholder = "Search...",
className,
disabled
disabled,
}: SearchableSelectProps) {
const [searchValue, setSearchValue] = React.useState("");
const [isOpen, setIsOpen] = React.useState(false);
const filteredOptions = React.useMemo(() => {
if (!searchValue) return options;
return options.filter(option =>
option.label.toLowerCase().includes(searchValue.toLowerCase())
return options.filter((option) =>
option.label.toLowerCase().includes(searchValue.toLowerCase()),
);
}, [options, searchValue]);
// Convert empty string to placeholder value for display
const displayValue = value === "" ? "__placeholder__" : value;
// Convert placeholder value back to empty string when selected
const handleValueChange = (newValue: string) => {
const actualValue = newValue === "__placeholder__" ? "" : newValue;
@@ -309,9 +320,9 @@ function SearchableSelect({
};
return (
<Select
value={displayValue}
onValueChange={handleValueChange}
<Select
value={displayValue}
onValueChange={handleValueChange}
disabled={disabled}
open={isOpen}
onOpenChange={setIsOpen}
@@ -353,4 +364,4 @@ export {
SelectTrigger,
SelectValue,
SearchableSelect,
}
};

View File

@@ -1,4 +1,4 @@
import { cn } from "~/lib/utils"
import { cn } from "~/lib/utils";
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
@@ -7,37 +7,43 @@ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
className={cn("bg-muted animate-pulse rounded-md", className)}
{...props}
/>
)
);
}
// Dashboard skeleton components
export function DashboardStatsSkeleton() {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div className="mb-8 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="shadow-xl border-0 bg-white/80 backdrop-blur-sm rounded-xl p-6">
<div className="flex items-center justify-between mb-4">
<div
key={i}
className="rounded-xl border-0 bg-white/80 p-6 shadow-xl backdrop-blur-sm dark:bg-gray-800/80"
>
<div className="mb-4 flex items-center justify-between">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-8 w-8 rounded-lg" />
</div>
<Skeleton className="h-8 w-16 mb-2" />
<Skeleton className="mb-2 h-8 w-16" />
<Skeleton className="h-3 w-32" />
</div>
))}
</div>
)
);
}
export function DashboardCardsSkeleton() {
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
<div className="mb-8 grid grid-cols-1 gap-8 lg:grid-cols-2">
{Array.from({ length: 2 }).map((_, i) => (
<div key={i} className="shadow-xl border-0 bg-white/80 backdrop-blur-sm rounded-xl p-6">
<div className="flex items-center gap-2 mb-4">
<div
key={i}
className="rounded-xl border-0 bg-white/80 p-6 shadow-xl backdrop-blur-sm dark:bg-gray-800/80"
>
<div className="mb-4 flex items-center gap-2">
<Skeleton className="h-8 w-8 rounded-lg" />
<Skeleton className="h-6 w-32" />
</div>
<Skeleton className="h-4 w-full mb-4" />
<Skeleton className="mb-4 h-4 w-full" />
<div className="flex gap-3">
<Skeleton className="h-10 w-24" />
<Skeleton className="h-10 w-32" />
@@ -45,20 +51,20 @@ export function DashboardCardsSkeleton() {
</div>
))}
</div>
)
);
}
export function DashboardActivitySkeleton() {
return (
<div className="shadow-xl border-0 bg-white/80 backdrop-blur-sm rounded-xl p-6">
<Skeleton className="h-6 w-32 mb-6" />
<div className="text-center py-12">
<Skeleton className="h-20 w-20 rounded-full mx-auto mb-4" />
<Skeleton className="h-6 w-48 mx-auto mb-2" />
<Skeleton className="h-4 w-64 mx-auto" />
<div className="rounded-xl border-0 bg-white/80 p-6 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
<Skeleton className="mb-6 h-6 w-32" />
<div className="py-12 text-center">
<Skeleton className="mx-auto mb-4 h-20 w-20 rounded-full" />
<Skeleton className="mx-auto mb-2 h-6 w-48" />
<Skeleton className="mx-auto h-4 w-64" />
</div>
</div>
)
);
}
// Table skeleton components
@@ -66,17 +72,17 @@ export function TableSkeleton({ rows = 5 }: { rows?: number }) {
return (
<div className="space-y-4">
{/* Search and filters */}
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex flex-col gap-4 sm:flex-row">
<Skeleton className="h-10 w-64" />
<div className="flex gap-2">
<Skeleton className="h-10 w-24" />
<Skeleton className="h-10 w-24" />
</div>
</div>
{/* Table */}
<div className="border rounded-lg">
<div className="p-4 border-b">
<div className="rounded-lg border">
<div className="border-b p-4">
<div className="flex items-center justify-between">
<Skeleton className="h-4 w-32" />
<div className="flex gap-2">
@@ -85,7 +91,7 @@ export function TableSkeleton({ rows = 5 }: { rows?: number }) {
</div>
</div>
</div>
<div className="p-4">
<div className="space-y-3">
{Array.from({ length: rows }).map((_, i) => (
@@ -101,7 +107,7 @@ export function TableSkeleton({ rows = 5 }: { rows?: number }) {
</div>
</div>
</div>
{/* Pagination */}
<div className="flex items-center justify-between">
<Skeleton className="h-4 w-32" />
@@ -112,7 +118,7 @@ export function TableSkeleton({ rows = 5 }: { rows?: number }) {
</div>
</div>
</div>
)
);
}
// Form skeleton components
@@ -121,36 +127,36 @@ export function FormSkeleton() {
<div className="space-y-6">
<div className="space-y-4">
<div>
<Skeleton className="h-4 w-20 mb-2" />
<Skeleton className="mb-2 h-4 w-20" />
<Skeleton className="h-10 w-full" />
</div>
<div>
<Skeleton className="h-4 w-24 mb-2" />
<Skeleton className="mb-2 h-4 w-24" />
<Skeleton className="h-10 w-full" />
</div>
<div>
<Skeleton className="h-4 w-16 mb-2" />
<Skeleton className="mb-2 h-4 w-16" />
<Skeleton className="h-10 w-full" />
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<Skeleton className="h-4 w-20 mb-2" />
<Skeleton className="mb-2 h-4 w-20" />
<Skeleton className="h-10 w-full" />
</div>
<div>
<Skeleton className="h-4 w-16 mb-2" />
<Skeleton className="mb-2 h-4 w-16" />
<Skeleton className="h-10 w-full" />
</div>
</div>
<div className="flex gap-3">
<Skeleton className="h-10 w-24" />
<Skeleton className="h-10 w-24" />
</div>
</div>
)
);
}
// Invoice view skeleton
@@ -158,16 +164,16 @@ export function InvoiceViewSkeleton() {
return (
<div className="space-y-6">
{/* Header */}
<div className="flex justify-between items-start">
<div className="flex items-start justify-between">
<div className="space-y-2">
<Skeleton className="h-8 w-48" />
<Skeleton className="h-4 w-64" />
</div>
<Skeleton className="h-10 w-32" />
</div>
{/* Client info */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<div className="space-y-3">
<Skeleton className="h-5 w-24" />
<Skeleton className="h-4 w-full" />
@@ -180,10 +186,10 @@ export function InvoiceViewSkeleton() {
<Skeleton className="h-4 w-3/4" />
</div>
</div>
{/* Items table */}
<div className="border rounded-lg">
<div className="p-4 border-b">
<div className="rounded-lg border">
<div className="border-b p-4">
<Skeleton className="h-5 w-32" />
</div>
<div className="p-4">
@@ -200,7 +206,7 @@ export function InvoiceViewSkeleton() {
</div>
</div>
</div>
{/* Total */}
<div className="flex justify-end">
<div className="space-y-2">
@@ -209,7 +215,7 @@ export function InvoiceViewSkeleton() {
</div>
</div>
</div>
)
);
}
export { Skeleton }
export { Skeleton };

View File

@@ -104,10 +104,10 @@ interface Business {
}
const statusColors = {
draft: "bg-gray-100 text-gray-800",
sent: "bg-blue-100 text-blue-800",
paid: "bg-green-100 text-green-800",
overdue: "bg-red-100 text-red-800",
draft: "bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300",
sent: "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400",
paid: "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400",
overdue: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400",
} as const;
const statusLabels = {
@@ -503,7 +503,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
/>
</TableHead>
<TableHead
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50"
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800"
onClick={() => handleSort("name")}
>
<div className="flex items-center gap-1">
@@ -515,10 +515,10 @@ export function UniversalTable({ resource }: UniversalTableProps) {
)}
</div>
</TableHead>
<TableHead className="px-4 py-4 text-base font-semibold text-gray-700">
<TableHead className="px-4 py-4 text-base font-semibold text-gray-700 dark:text-gray-300">
Email
</TableHead>
<TableHead className="px-4 py-4 text-base font-semibold text-gray-700">
<TableHead className="px-4 py-4 text-base font-semibold text-gray-700 dark:text-gray-300">
Phone
</TableHead>
<TableHead className="w-8 px-4 py-4"></TableHead>
@@ -536,7 +536,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
/>
</TableHead>
<TableHead
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50"
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800"
onClick={() => handleSort("invoiceNumber")}
>
<div className="flex items-center gap-1">
@@ -549,7 +549,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
</div>
</TableHead>
<TableHead
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50"
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800"
onClick={() => handleSort("client.name")}
>
<div className="flex items-center gap-1">
@@ -562,7 +562,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
</div>
</TableHead>
<TableHead
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50"
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800"
onClick={() => handleSort("status")}
>
<div className="flex items-center gap-1">
@@ -575,7 +575,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
</div>
</TableHead>
<TableHead
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50"
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800"
onClick={() => handleSort("totalAmount")}
>
<div className="flex items-center gap-1">
@@ -588,7 +588,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
</div>
</TableHead>
<TableHead
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50"
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800"
onClick={() => handleSort("dueDate")}
>
<div className="flex items-center gap-1">
@@ -615,21 +615,21 @@ export function UniversalTable({ resource }: UniversalTableProps) {
/>
</TableHead>
<TableHead
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50"
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800"
onClick={() => handleSort("name")}
>
Name
</TableHead>
<TableHead className="px-4 py-4 text-base font-semibold text-gray-700">
<TableHead className="px-4 py-4 text-base font-semibold text-gray-700 dark:text-gray-300">
Email
</TableHead>
<TableHead className="px-4 py-4 text-base font-semibold text-gray-700">
<TableHead className="px-4 py-4 text-base font-semibold text-gray-700 dark:text-gray-300">
Phone
</TableHead>
<TableHead className="px-4 py-4 text-base font-semibold text-gray-700">
<TableHead className="px-4 py-4 text-base font-semibold text-gray-700 dark:text-gray-300">
Website
</TableHead>
<TableHead className="px-4 py-4 text-base font-semibold text-gray-700">
<TableHead className="px-4 py-4 text-base font-semibold text-gray-700 dark:text-gray-300">
Default
</TableHead>
<TableHead className="w-8 px-4 py-4"></TableHead>
@@ -691,7 +691,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
<TableRow>
<TableCell
colSpan={colSpan}
className="py-12 text-center text-gray-500"
className="py-12 text-center text-gray-500 dark:text-gray-400"
>
<div className="flex flex-col items-center gap-2">
{resource === "clients" ? (
@@ -701,8 +701,10 @@ export function UniversalTable({ resource }: UniversalTableProps) {
) : (
<FileText className="mb-2 h-8 w-8 text-emerald-400" />
)}
<div className="text-lg font-semibold">No {resource} found</div>
<div className="mb-2 text-gray-500">
<div className="text-lg font-semibold dark:text-gray-300">
No {resource} found
</div>
<div className="mb-2 text-gray-500 dark:text-gray-400">
Get started by adding your first{" "}
{getSingularResourceName(resource).toLowerCase()}.
</div>
@@ -728,7 +730,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
<TableRow
key={client.id}
data-selected={selected.includes(client.id)}
className="group cursor-pointer transition-colors hover:bg-emerald-50/60"
className="group cursor-pointer transition-colors hover:bg-emerald-50/60 dark:hover:bg-emerald-900/20"
onClick={(e) => {
if (
(e.target as HTMLElement).closest(
@@ -750,7 +752,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
className="data-[state=checked]:border-emerald-600 data-[state=checked]:bg-emerald-600"
/>
</TableCell>
<TableCell className="px-4 py-4 text-base font-medium text-gray-900 group-hover:text-emerald-700">
<TableCell className="px-4 py-4 text-base font-medium text-gray-900 group-hover:text-emerald-700 dark:text-white dark:group-hover:text-emerald-400">
<Link
href={`/dashboard/clients/${client.id}/edit`}
className="hover:underline"
@@ -758,10 +760,10 @@ export function UniversalTable({ resource }: UniversalTableProps) {
{client.name}
</Link>
</TableCell>
<TableCell className="px-4 py-4 text-gray-700">
<TableCell className="px-4 py-4 text-gray-700 dark:text-gray-300">
{client.email}
</TableCell>
<TableCell className="px-4 py-4 text-gray-700">
<TableCell className="px-4 py-4 text-gray-700 dark:text-gray-300">
{client.phone}
</TableCell>
<TableCell
@@ -797,7 +799,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
<TableRow
key={invoice.id}
data-selected={selected.includes(invoice.id)}
className="group cursor-pointer transition-colors hover:bg-emerald-50/60"
className="group cursor-pointer transition-colors hover:bg-emerald-50/60 dark:hover:bg-emerald-900/20"
onClick={(e) => {
if (
(e.target as HTMLElement).closest(
@@ -819,7 +821,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
className="data-[state=checked]:border-emerald-600 data-[state=checked]:bg-emerald-600"
/>
</TableCell>
<TableCell className="px-4 py-4 text-base font-medium text-gray-900 group-hover:text-emerald-700">
<TableCell className="px-4 py-4 text-base font-medium text-gray-900 group-hover:text-emerald-700 dark:text-white dark:group-hover:text-emerald-400">
<Link
href={`/dashboard/invoices/${invoice.id}`}
className="hover:underline"
@@ -827,7 +829,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
{invoice.invoiceNumber}
</Link>
</TableCell>
<TableCell className="px-4 py-4 text-gray-700">
<TableCell className="px-4 py-4 text-gray-700 dark:text-gray-300">
{invoice.client?.name}
</TableCell>
<TableCell className="px-4 py-4">
@@ -837,10 +839,10 @@ export function UniversalTable({ resource }: UniversalTableProps) {
{statusLabels[invoice.status]}
</span>
</TableCell>
<TableCell className="px-4 py-4 font-medium text-gray-700">
<TableCell className="px-4 py-4 font-medium text-gray-700 dark:text-gray-300">
{formatCurrency(invoice.totalAmount)}
</TableCell>
<TableCell className="px-4 py-4 text-gray-700">
<TableCell className="px-4 py-4 text-gray-700 dark:text-gray-300">
{formatDate(invoice.dueDate)}
</TableCell>
<TableCell
@@ -885,7 +887,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
<TableRow
key={business.id}
data-selected={selected.includes(business.id)}
className="group cursor-pointer transition-colors hover:bg-emerald-50/60"
className="group cursor-pointer transition-colors hover:bg-emerald-50/60 dark:hover:bg-emerald-900/20"
onClick={(e) => {
if (
(e.target as HTMLElement).closest(
@@ -907,7 +909,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
className="data-[state=checked]:border-emerald-600 data-[state=checked]:bg-emerald-600"
/>
</TableCell>
<TableCell className="px-4 py-4 text-base font-medium text-gray-900 group-hover:text-emerald-700">
<TableCell className="px-4 py-4 text-base font-medium text-gray-900 group-hover:text-emerald-700 dark:text-white dark:group-hover:text-emerald-400">
<Link
href={`/dashboard/businesses/${business.id}/edit`}
className="hover:underline"
@@ -915,18 +917,18 @@ export function UniversalTable({ resource }: UniversalTableProps) {
{business.name}
</Link>
</TableCell>
<TableCell className="px-4 py-4 text-gray-700">
<TableCell className="px-4 py-4 text-gray-700 dark:text-gray-300">
{business.email}
</TableCell>
<TableCell className="px-4 py-4 text-gray-700">
<TableCell className="px-4 py-4 text-gray-700 dark:text-gray-300">
{business.phone}
</TableCell>
<TableCell className="px-4 py-4 text-gray-700">
<TableCell className="px-4 py-4 text-gray-700 dark:text-gray-300">
{business.website}
</TableCell>
<TableCell className="px-4 py-4 text-gray-700">
<TableCell className="px-4 py-4 text-gray-700 dark:text-gray-300">
{business.isDefault ? (
<span className="rounded-full bg-emerald-100 px-2 py-1 text-xs font-medium text-emerald-800">
<span className="rounded-full bg-emerald-100 px-2 py-1 text-xs font-medium text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-400">
Default
</span>
) : (
@@ -968,7 +970,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
return (
<div className="w-full">
{/* Controls */}
<div className="mb-4 flex flex-wrap items-center gap-3 rounded-lg border border-gray-200 bg-white/90 p-4 shadow-sm">
<div className="mb-4 flex flex-wrap items-center gap-3 rounded-lg border border-gray-200 bg-white/90 p-4 shadow-sm dark:border-gray-700 dark:bg-gray-800/90">
{/* Left side - View controls and filters */}
<div className="flex items-center gap-2">
<Button
@@ -996,7 +998,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-48">
<DropdownMenuItem className="font-medium text-gray-700">
<DropdownMenuItem className="font-medium text-gray-700 dark:text-gray-300">
Filters
</DropdownMenuItem>
{resource === "invoices" && (
@@ -1005,7 +1007,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
onClick={() => setStatusFilter("all")}
className={
statusFilter === "all"
? "bg-emerald-50 text-emerald-700"
? "bg-emerald-50 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400"
: ""
}
>
@@ -1015,7 +1017,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
onClick={() => setStatusFilter("draft")}
className={
statusFilter === "draft"
? "bg-emerald-50 text-emerald-700"
? "bg-emerald-50 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400"
: ""
}
>
@@ -1025,7 +1027,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
onClick={() => setStatusFilter("sent")}
className={
statusFilter === "sent"
? "bg-emerald-50 text-emerald-700"
? "bg-emerald-50 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400"
: ""
}
>
@@ -1035,7 +1037,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
onClick={() => setStatusFilter("paid")}
className={
statusFilter === "paid"
? "bg-emerald-50 text-emerald-700"
? "bg-emerald-50 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400"
: ""
}
>
@@ -1045,7 +1047,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
onClick={() => setStatusFilter("overdue")}
className={
statusFilter === "overdue"
? "bg-emerald-50 text-emerald-700"
? "bg-emerald-50 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400"
: ""
}
>
@@ -1065,7 +1067,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
placeholder={`Search ${resource}...`}
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-48 sm:w-64"
className="w-48 sm:w-64 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
/>
<Button variant="outline" size="icon">
<Search className="h-4 w-4" />
@@ -1075,7 +1077,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
{/* Batch actions */}
{selected.length > 0 && (
<>
<span className="hidden text-sm text-gray-500 sm:inline">
<span className="hidden text-sm text-gray-500 sm:inline dark:text-gray-400">
{selected.length} selected
</span>
{resource === "invoices" && (
@@ -1124,7 +1126,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
</div>
{/* Table View */}
{view === "table" && (
<div className="overflow-hidden rounded-2xl border border-gray-200 bg-white/90 shadow-xl">
<div className="overflow-hidden rounded-2xl border border-gray-200 bg-white/90 shadow-xl dark:border-gray-700 dark:bg-gray-800/90">
<Table className="w-full">
<TableHeader>
<TableRow>{renderTableHeaders()}</TableRow>
@@ -1135,9 +1137,9 @@ export function UniversalTable({ resource }: UniversalTableProps) {
)}
{/* Pagination Controls */}
{view === "table" && totalPages > 1 && (
<div className="mt-4 mb-4 flex flex-wrap items-center justify-between gap-3 rounded-2xl border border-gray-200 bg-white/90 p-4 shadow-sm">
<div className="mt-4 mb-4 flex flex-wrap items-center justify-between gap-3 rounded-2xl border border-gray-200 bg-white/90 p-4 shadow-sm dark:border-gray-700 dark:bg-gray-800/90">
{/* Left side - Page info and items per page */}
<div className="flex items-center gap-3 text-sm text-gray-600">
<div className="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
<span className="hidden sm:inline">
Showing {startIndex + 1} to{" "}
{Math.min(endIndex, filteredAndSortedData.length)} of{" "}
@@ -1154,7 +1156,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
setItemsPerPage(Number(e.target.value));
setCurrentPage(1);
}}
className="h-8 w-20 rounded-md border border-gray-300 bg-white px-2 py-1 text-sm focus:border-emerald-500 focus:ring-emerald-500 sm:w-28"
className="h-8 w-20 rounded-md border border-gray-300 bg-white px-2 py-1 text-sm focus:border-emerald-500 focus:ring-emerald-500 sm:w-28 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
>
<option value={5}>5</option>
<option value={10}>10</option>
@@ -1220,7 +1222,10 @@ export function UniversalTable({ resource }: UniversalTableProps) {
pageNum === currentPage + 2
) {
return (
<span key={pageNum} className="px-1 text-gray-400 sm:px-2">
<span
key={pageNum}
className="px-1 text-gray-400 sm:px-2 dark:text-gray-500"
>
...
</span>
);
@@ -1251,7 +1256,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
Array.from({ length: 6 }).map((_, index) => (
<div
key={`skeleton-card-${index}`}
className="flex flex-col gap-2 rounded-2xl border border-gray-200 bg-white/90 p-4 shadow-xl"
className="flex flex-col gap-2 rounded-2xl border border-gray-200 bg-white/90 p-4 shadow-xl dark:border-gray-700 dark:bg-gray-800/90"
>
<Skeleton className="h-6 w-32" />
<Skeleton className="h-4 w-40" />
@@ -1259,7 +1264,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
</div>
))
) : filteredAndSortedData.length === 0 ? (
<div className="col-span-full flex flex-col items-center py-16 text-gray-500">
<div className="col-span-full flex flex-col items-center py-16 text-gray-500 dark:text-gray-400">
{resource === "clients" ? (
<UserPlus className="mb-2 h-8 w-8 text-emerald-400" />
) : resource === "businesses" ? (
@@ -1267,8 +1272,10 @@ export function UniversalTable({ resource }: UniversalTableProps) {
) : (
<FileText className="mb-2 h-8 w-8 text-emerald-400" />
)}
<div className="text-lg font-semibold">No {resource} found</div>
<div className="mb-2 text-gray-500">
<div className="text-lg font-semibold dark:text-gray-300">
No {resource} found
</div>
<div className="mb-2 text-gray-500 dark:text-gray-400">
Get started by adding your first{" "}
{getSingularResourceName(resource).toLowerCase()}.
</div>
@@ -1289,13 +1296,17 @@ export function UniversalTable({ resource }: UniversalTableProps) {
return (
<div
key={client.id}
className="flex cursor-pointer flex-col gap-2 rounded-2xl border border-gray-200 bg-white/90 p-4 shadow-xl transition-colors hover:bg-emerald-50/60"
className="flex cursor-pointer flex-col gap-2 rounded-2xl border border-gray-200 bg-white/90 p-4 shadow-xl transition-colors hover:bg-emerald-50/60 dark:border-gray-700 dark:bg-gray-800/90 dark:hover:bg-emerald-900/20"
>
<div className="text-lg font-semibold text-gray-900 group-hover:text-emerald-700">
<div className="text-lg font-semibold text-gray-900 group-hover:text-emerald-700 dark:text-white dark:group-hover:text-emerald-400">
{client.name}
</div>
<div className="text-sm text-gray-700">{client.email}</div>
<div className="text-sm text-gray-700">{client.phone}</div>
<div className="text-sm text-gray-700 dark:text-gray-300">
{client.email}
</div>
<div className="text-sm text-gray-700 dark:text-gray-300">
{client.phone}
</div>
</div>
);
} else if (resource === "invoices") {
@@ -1303,15 +1314,15 @@ export function UniversalTable({ resource }: UniversalTableProps) {
return (
<div
key={invoice.id}
className="flex cursor-pointer flex-col gap-2 rounded-2xl border border-gray-200 bg-white/90 p-4 shadow-xl transition-colors hover:bg-emerald-50/60"
className="flex cursor-pointer flex-col gap-2 rounded-2xl border border-gray-200 bg-white/90 p-4 shadow-xl transition-colors hover:bg-emerald-50/60 dark:border-gray-700 dark:bg-gray-800/90 dark:hover:bg-emerald-900/20"
>
<div className="text-lg font-semibold text-gray-900 group-hover:text-emerald-700">
<div className="text-lg font-semibold text-gray-900 group-hover:text-emerald-700 dark:text-white dark:group-hover:text-emerald-400">
{invoice.invoiceNumber}
</div>
<div className="text-sm text-gray-700">
<div className="text-sm text-gray-700 dark:text-gray-300">
{invoice.client?.name}
</div>
<div className="text-sm text-gray-700">
<div className="text-sm text-gray-700 dark:text-gray-300">
{formatCurrency(invoice.totalAmount)}
</div>
</div>
@@ -1321,15 +1332,15 @@ export function UniversalTable({ resource }: UniversalTableProps) {
return (
<div
key={business.id}
className="flex cursor-pointer flex-col gap-2 rounded-2xl border border-gray-200 bg-white/90 p-4 shadow-xl transition-colors hover:bg-emerald-50/60"
className="flex cursor-pointer flex-col gap-2 rounded-2xl border border-gray-200 bg-white/90 p-4 shadow-xl transition-colors hover:bg-emerald-50/60 dark:border-gray-700 dark:bg-gray-800/90 dark:hover:bg-emerald-900/20"
>
<div className="text-lg font-semibold text-gray-900 group-hover:text-emerald-700">
<div className="text-lg font-semibold text-gray-900 group-hover:text-emerald-700 dark:text-white dark:group-hover:text-emerald-400">
{business.name}
</div>
<div className="text-sm text-gray-700">
<div className="text-sm text-gray-700 dark:text-gray-300">
{business.email}
</div>
<div className="text-sm text-gray-700">
<div className="text-sm text-gray-700 dark:text-gray-300">
{business.phone}
</div>
</div>
@@ -1341,15 +1352,15 @@ export function UniversalTable({ resource }: UniversalTableProps) {
)}
{/* Confirmation Dialog */}
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<DialogContent className="border-0 bg-white/95 shadow-2xl backdrop-blur-sm">
<DialogContent className="border-0 bg-white/95 shadow-2xl backdrop-blur-sm dark:bg-gray-800/95">
<DialogHeader>
<DialogTitle className="text-xl font-bold text-gray-800">
<DialogTitle className="text-xl font-bold text-gray-800 dark:text-white">
Delete{" "}
{resource.slice(0, -1).charAt(0).toUpperCase() +
resource.slice(0, -1).slice(1)}
{itemToDelete === "batch" ? "s" : ""}
</DialogTitle>
<DialogDescription className="text-gray-600">
<DialogDescription className="text-gray-600 dark:text-gray-300">
{itemToDelete === "batch"
? `Are you sure you want to delete the selected ${resource}? This action cannot be undone.`
: `Are you sure you want to delete this ${resource.slice(0, -1)}? This action cannot be undone.`}
@@ -1359,7 +1370,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
<Button
variant="outline"
onClick={() => setDeleteDialogOpen(false)}
className="border-gray-300 text-gray-700 hover:bg-gray-50"
className="border-gray-300 text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800"
>
Cancel
</Button>

View File

@@ -1,10 +1,9 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme {
--font-sans: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif,
--font-sans:
var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}
@@ -28,6 +27,7 @@
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
@@ -63,6 +63,7 @@
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.985 0 0);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
@@ -81,45 +82,155 @@
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
@media (prefers-color-scheme: dark) {
:root {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--destructive-foreground: oklch(0.985 0 0);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
@apply bg-background text-foreground font-sans antialiased;
}
/* Improved form elements for dark mode */
input[type="text"],
input[type="email"],
input[type="password"],
input[type="tel"],
input[type="url"],
input[type="search"],
input[type="number"],
textarea,
select {
@apply bg-background text-foreground border-input;
}
input::placeholder,
textarea::placeholder {
@apply text-muted-foreground;
}
/* Better focus states */
input:focus,
textarea:focus,
select:focus {
@apply ring-ring border-ring;
}
/* Selection styling */
::selection {
@apply bg-primary/20 text-foreground;
}
/* Scrollbar styling for dark mode */
::-webkit-scrollbar {
@apply h-2 w-2;
}
::-webkit-scrollbar-track {
@apply bg-muted/20;
}
::-webkit-scrollbar-thumb {
@apply bg-muted-foreground/30 rounded-full;
}
::-webkit-scrollbar-thumb:hover {
@apply bg-muted-foreground/50;
}
/* Firefox scrollbar */
* {
scrollbar-width: thin;
scrollbar-color: hsl(var(--muted-foreground) / 0.3) hsl(var(--muted) / 0.2);
}
/* Better link styling */
a {
@apply text-primary hover:text-primary/80 transition-colors;
}
/* Code elements */
code {
@apply bg-muted/50 text-foreground rounded px-1 py-0.5 font-mono text-sm;
}
pre {
@apply bg-muted/50 text-foreground overflow-auto rounded-lg p-4;
}
/* Better table styling */
table {
@apply w-full border-collapse;
}
th {
@apply bg-muted/50 text-muted-foreground border-border border-b p-2 text-left font-medium;
}
td {
@apply border-border border-b p-2;
}
/* Modal and dialog backdrops */
.backdrop {
@apply bg-background/80 backdrop-blur-sm;
}
/* Better disabled state styling */
:disabled {
@apply cursor-not-allowed opacity-50;
}
/* Focus visible for keyboard navigation */
:focus-visible {
@apply ring-ring ring-offset-background ring-2 ring-offset-2 outline-none;
}
/* Better checkbox and radio styling */
input[type="checkbox"],
input[type="radio"] {
@apply border-input bg-background;
}
input[type="checkbox"]:checked,
input[type="radio"]:checked {
@apply bg-primary border-primary;
}
}

View File

@@ -2,7 +2,7 @@ import type { Config } from "tailwindcss";
import animate from "tailwindcss-animate";
export default {
darkMode: "class",
darkMode: "media",
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
@@ -11,7 +11,12 @@ export default {
theme: {
extend: {
fontFamily: {
sans: ["var(--font-geist-sans)", "ui-sans-serif", "system-ui", "sans-serif"],
sans: [
"var(--font-geist-sans)",
"ui-sans-serif",
"system-ui",
"sans-serif",
],
},
colors: {
border: "hsl(var(--border))",
@@ -73,4 +78,4 @@ export default {
future: {
hoverOnlyWhenSupported: true,
},
} satisfies Config;
} satisfies Config;