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

View File

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

View File

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

View File

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

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"> <h1 className="bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-3xl font-bold text-transparent">
Invoice Details Invoice Details
</h1> </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. View and manage invoice information.
</p> </p>
</div> </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 <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" 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 ${ className={`relative z-10 rounded-md px-3 py-2 transition-all duration-200 ${
mode === "view" mode === "view"
? "text-emerald-600" ? "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" /> <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 ${ className={`relative z-10 rounded-md px-3 py-2 transition-all duration-200 ${
mode === "edit" mode === "edit"
? "text-emerald-600" ? "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" /> <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"> <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"}! Welcome back, {session?.user?.name?.split(" ")[0] ?? "User"}!
</h1> </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 Here&apos;s what&apos;s happening with your invoicing business
</p> </p>
</div> </div>

View File

@@ -8,7 +8,8 @@ import { Toaster } from "~/components/ui/toaster";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "beenvoice - Invoicing Made Simple", 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" }], icons: [{ rel: "icon", url: "/favicon.ico" }],
}; };
@@ -22,7 +23,7 @@ export default function RootLayout({
}: Readonly<{ children: React.ReactNode }>) { }: Readonly<{ children: React.ReactNode }>) {
return ( return (
<html lang="en" className={`${geist.variable}`}> <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%)]"> <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> <TRPCReactProvider>{children}</TRPCReactProvider>
<Toaster /> <Toaster />
</body> </body>

View File

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

View File

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

View File

@@ -2,7 +2,13 @@
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; 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 = [ const navLinks = [
{ name: "Dashboard", href: "/dashboard", icon: LayoutDashboard }, { name: "Dashboard", href: "/dashboard", icon: LayoutDashboard },
@@ -15,43 +21,47 @@ export function Sidebar() {
const pathname = usePathname(); const pathname = usePathname();
return ( 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"> <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"> <nav className="flex flex-col gap-1">
<div className="mb-2 text-xs font-semibold text-gray-400 tracking-wider uppercase">Main</div> <div className="mb-2 text-xs font-semibold tracking-wider text-gray-400 uppercase dark:text-gray-500">
{navLinks.map((link) => { Main
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>
</div> </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"; "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 { 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 { useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
@@ -24,22 +36,24 @@ export function SidebarTrigger() {
variant="outline" variant="outline"
size="icon" size="icon"
aria-label="Open sidebar" 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" /> <MenuIcon className="h-4 w-4" />
</Button> </Button>
</SheetTrigger> </SheetTrigger>
<SheetContent <SheetContent
side="left" side="left"
className="p-0 w-80 max-w-[85vw] bg-white/95 border-0 backdrop-blur-sm" 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"> <SheetHeader className="border-b border-gray-200 p-4 dark:border-gray-700">
<SheetTitle>Navigation</SheetTitle> <SheetTitle className="dark:text-white">Navigation</SheetTitle>
</SheetHeader> </SheetHeader>
{/* Navigation */} {/* Navigation */}
<nav className="flex-1 flex flex-col gap-1 p-4"> <nav className="flex flex-1 flex-col gap-1 p-4">
<div className="mb-2 text-xs font-semibold text-gray-400 tracking-wider uppercase">Main</div> <div className="mb-2 text-xs font-semibold tracking-wider text-gray-400 uppercase dark:text-gray-500">
Main
</div>
{navLinks.map((link) => { {navLinks.map((link) => {
const Icon = link.icon; const Icon = link.icon;
return ( return (
@@ -49,8 +63,8 @@ export function SidebarTrigger() {
aria-current={pathname === link.href ? "page" : undefined} 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 ${ className={`flex items-center gap-3 rounded-lg px-3 py-3 text-base font-medium transition-all duration-200 ${
pathname === link.href pathname === link.href
? "bg-emerald-100 text-emerald-700 shadow-lg" ? "bg-emerald-100 text-emerald-700 shadow-lg dark:bg-emerald-900/30 dark:text-emerald-400"
: "text-gray-700 hover:bg-gray-100" : "text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800"
}`} }`}
onClick={() => setOpen(false)} onClick={() => setOpen(false)}
> >
@@ -60,14 +74,16 @@ export function SidebarTrigger() {
); );
})} })}
<div className="border-t border-gray-200 my-4" /> <div className="my-4 border-t border-gray-200 dark:border-gray-700" />
<div className="mb-2 text-xs font-semibold text-gray-400 tracking-wider uppercase">Account</div> <div className="mb-2 text-xs font-semibold tracking-wider text-gray-400 uppercase dark:text-gray-500">
Account
</div>
<Link <Link
href="/dashboard/settings" href="/dashboard/settings"
className={`flex items-center gap-3 rounded-lg px-3 py-3 text-base font-medium transition-all duration-200 ${ className={`flex items-center gap-3 rounded-lg px-3 py-3 text-base font-medium transition-all duration-200 ${
pathname === "/dashboard/settings" pathname === "/dashboard/settings"
? "bg-emerald-100 text-emerald-700 shadow-lg" ? "bg-emerald-100 text-emerald-700 shadow-lg dark:bg-emerald-900/30 dark:text-emerald-400"
: "text-gray-700 hover:bg-gray-100" : "text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800"
}`} }`}
onClick={() => setOpen(false)} onClick={() => setOpen(false)}
> >

View File

@@ -1,6 +1,16 @@
"use client"; "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 Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@@ -38,10 +48,11 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
// Fetch business data if editing // Fetch business data if editing
const { data: business, isLoading: isLoadingBusiness } = api.businesses.getById.useQuery( const { data: business, isLoading: isLoadingBusiness } =
{ id: businessId! }, api.businesses.getById.useQuery(
{ enabled: mode === "edit" && !!businessId } { id: businessId! },
); { enabled: mode === "edit" && !!businessId },
);
const createBusiness = api.businesses.create.useMutation({ const createBusiness = api.businesses.create.useMutation({
onSuccess: () => { onSuccess: () => {
@@ -102,12 +113,12 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
}; };
const handleInputChange = (field: string, value: string | boolean) => { const handleInputChange = (field: string, value: string | boolean) => {
setFormData(prev => ({ ...prev, [field]: value })); setFormData((prev) => ({ ...prev, [field]: value }));
}; };
// Phone number formatting (reuse from client-form) // Phone number formatting (reuse from client-form)
const formatPhoneNumber = (value: string) => { const formatPhoneNumber = (value: string) => {
const phoneNumber = value.replace(/\D/g, ''); const phoneNumber = value.replace(/\D/g, "");
if (phoneNumber.length <= 3) { if (phoneNumber.length <= 3) {
return phoneNumber; return phoneNumber;
} else if (phoneNumber.length <= 6) { } else if (phoneNumber.length <= 6) {
@@ -174,7 +185,7 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
{ value: "WA", label: "Washington" }, { value: "WA", label: "Washington" },
{ value: "WV", label: "West Virginia" }, { value: "WV", label: "West Virginia" },
{ value: "WI", label: "Wisconsin" }, { value: "WI", label: "Wisconsin" },
{ value: "WY", label: "Wyoming" } { value: "WY", label: "Wyoming" },
]; ];
const MOST_USED_COUNTRIES = [ const MOST_USED_COUNTRIES = [
@@ -184,27 +195,223 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
{ value: "Australia", label: "Australia" }, { value: "Australia", label: "Australia" },
{ value: "Germany", label: "Germany" }, { value: "Germany", label: "Germany" },
{ value: "France", label: "France" }, { value: "France", label: "France" },
{ value: "India", label: "India" } { value: "India", label: "India" },
]; ];
const ALL_COUNTRIES = [ 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 const OTHER_COUNTRIES = ALL_COUNTRIES.filter(
.filter(c => !MOST_USED_COUNTRIES.some(mc => mc.value === c)) (c) => !MOST_USED_COUNTRIES.some((mc) => mc.value === c),
.map(country => ({ value: country, label: country })) )
.map((country) => ({ value: country, label: country }))
.sort((a, b) => a.label.localeCompare(b.label)); .sort((a, b) => a.label.localeCompare(b.label));
const ALL_COUNTRIES_OPTIONS = [ const ALL_COUNTRIES_OPTIONS = [
{ value: "__placeholder__", label: "Select country" }, { value: "__placeholder__", label: "Select country" },
...MOST_USED_COUNTRIES, ...MOST_USED_COUNTRIES,
...OTHER_COUNTRIES ...OTHER_COUNTRIES,
]; ];
if (mode === "edit" && isLoadingBusiness) { if (mode === "edit" && isLoadingBusiness) {
return ( 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"> <CardContent className="p-8">
<FormSkeleton /> <FormSkeleton />
</CardContent> </CardContent>
@@ -213,18 +420,23 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
} }
return ( 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> <CardContent>
<form onSubmit={handleSubmit} className="space-y-8"> <form onSubmit={handleSubmit} className="space-y-8">
{/* Basic Information Section */} {/* Basic Information Section */}
<div className="space-y-6"> <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" /> <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>
<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"> <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 * Business Name *
</Label> </Label>
<Input <Input
@@ -233,22 +445,25 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
onChange={(e) => handleInputChange("name", e.target.value)} onChange={(e) => handleInputChange("name", e.target.value)}
required required
placeholder="Enter business name" 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>
<div className="space-y-2"> <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 Email Address
</Label> </Label>
<div className="relative"> <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 <Input
id="email" id="email"
type="email" type="email"
value={formData.email} value={formData.email}
onChange={(e) => handleInputChange("email", e.target.value)} onChange={(e) => handleInputChange("email", e.target.value)}
placeholder="business@example.com" 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>
</div> </div>
@@ -257,40 +472,50 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
{/* Contact Information Section */} {/* Contact Information Section */}
<div className="space-y-6"> <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" /> <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>
<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"> <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 Phone Number
</Label> </Label>
<div className="relative"> <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 <Input
id="phone" id="phone"
type="tel" type="tel"
value={formData.phone} value={formData.phone}
onChange={(e) => handlePhoneChange(e.target.value)} onChange={(e) => handlePhoneChange(e.target.value)}
placeholder="(555) 123-4567" 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> </div>
<div className="space-y-2"> <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 Website
</Label> </Label>
<div className="relative"> <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 <Input
id="website" id="website"
type="url" type="url"
value={formData.website} value={formData.website}
onChange={(e) => handleInputChange("website", e.target.value)} onChange={(e) =>
handleInputChange("website", e.target.value)
}
placeholder="https://yourbusiness.com" 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>
</div> </div>
@@ -299,37 +524,52 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
{/* Address Section */} {/* Address Section */}
<div className="space-y-6"> <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" /> <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>
<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"> <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 Address Line 1
</Label> </Label>
<Input <Input
id="addressLine1" id="addressLine1"
value={formData.addressLine1} value={formData.addressLine1}
onChange={(e) => handleInputChange("addressLine1", e.target.value)} onChange={(e) =>
handleInputChange("addressLine1", e.target.value)
}
placeholder="123 Main St" 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>
<div className="space-y-2"> <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 Address Line 2
</Label> </Label>
<Input <Input
id="addressLine2" id="addressLine2"
value={formData.addressLine2} value={formData.addressLine2}
onChange={(e) => handleInputChange("addressLine2", e.target.value)} onChange={(e) =>
handleInputChange("addressLine2", e.target.value)
}
placeholder="Suite 100" 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="space-y-2"> <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 City
</Label> </Label>
<Input <Input
@@ -337,11 +577,14 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
value={formData.city} value={formData.city}
onChange={(e) => handleInputChange("city", e.target.value)} onChange={(e) => handleInputChange("city", e.target.value)}
placeholder="City" 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>
<div className="space-y-2"> <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 State/Province
</Label> </Label>
<SearchableSelect <SearchableSelect
@@ -353,19 +596,27 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
/> />
</div> </div>
<div className="space-y-2"> <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 Postal Code
</Label> </Label>
<Input <Input
id="postalCode" id="postalCode"
value={formData.postalCode} value={formData.postalCode}
onChange={(e) => handleInputChange("postalCode", e.target.value)} onChange={(e) =>
handleInputChange("postalCode", e.target.value)
}
placeholder="ZIP or postal code" 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>
<div className="space-y-2"> <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 Country
</Label> </Label>
<SearchableSelect <SearchableSelect
@@ -381,13 +632,18 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
{/* Tax, Logo, Default Section */} {/* Tax, Logo, Default Section */}
<div className="space-y-6"> <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" /> <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>
<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"> <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 Tax ID / VAT Number
</Label> </Label>
<Input <Input
@@ -395,40 +651,51 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
value={formData.taxId} value={formData.taxId}
onChange={(e) => handleInputChange("taxId", e.target.value)} onChange={(e) => handleInputChange("taxId", e.target.value)}
placeholder="Tax ID or VAT number" 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>
<div className="space-y-2"> <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 Logo URL
</Label> </Label>
<div className="relative"> <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 <Input
id="logoUrl" id="logoUrl"
value={formData.logoUrl} value={formData.logoUrl}
onChange={(e) => handleInputChange("logoUrl", e.target.value)} onChange={(e) =>
handleInputChange("logoUrl", e.target.value)
}
placeholder="https://yourbusiness.com/logo.png" 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> </div>
<div className="flex items-center space-x-2 mt-4"> <div className="mt-4 flex items-center space-x-2">
<input <input
id="isDefault" id="isDefault"
type="checkbox" type="checkbox"
checked={formData.isDefault} checked={formData.isDefault}
onChange={(e) => handleInputChange("isDefault", e.target.checked)} onChange={(e) =>
className="h-5 w-5 text-emerald-600 border-gray-300 rounded focus:ring-emerald-500" 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"> <Label
<Star className="h-4 w-4 mr-1 text-yellow-400" /> Set as default business 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> </Label>
</div> </div>
</div> </div>
</div> </div>
<div className="flex justify-end gap-4 mt-8"> <div className="mt-8 flex justify-end gap-4">
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
@@ -440,7 +707,7 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
</Button> </Button>
<Button <Button
type="submit" 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} disabled={loading}
> >
<Save className="mr-2 h-5 w-5" /> <Save className="mr-2 h-5 w-5" />

View File

@@ -33,10 +33,11 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
// Fetch client data if editing // Fetch client data if editing
const { data: client, isLoading: isLoadingClient } = api.clients.getById.useQuery( const { data: client, isLoading: isLoadingClient } =
{ id: clientId! }, api.clients.getById.useQuery(
{ enabled: mode === "edit" && !!clientId } { id: clientId! },
); { enabled: mode === "edit" && !!clientId },
);
const createClient = api.clients.create.useMutation({ const createClient = api.clients.create.useMutation({
onSuccess: () => { onSuccess: () => {
@@ -94,12 +95,12 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
}; };
const handleInputChange = (field: string, value: string) => { const handleInputChange = (field: string, value: string) => {
setFormData(prev => ({ ...prev, [field]: value })); setFormData((prev) => ({ ...prev, [field]: value }));
}; };
// Phone number formatting // Phone number formatting
const formatPhoneNumber = (value: string) => { const formatPhoneNumber = (value: string) => {
const phoneNumber = value.replace(/\D/g, ''); const phoneNumber = value.replace(/\D/g, "");
if (phoneNumber.length <= 3) { if (phoneNumber.length <= 3) {
return phoneNumber; return phoneNumber;
} else if (phoneNumber.length <= 6) { } else if (phoneNumber.length <= 6) {
@@ -115,22 +116,272 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
}; };
const US_STATES = [ 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 = [ 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 = [ 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( const OTHER_COUNTRIES = ALL_COUNTRIES.filter(
c => !MOST_USED_COUNTRIES.includes(c) (c) => !MOST_USED_COUNTRIES.includes(c),
).sort(); ).sort();
if (mode === "edit" && isLoadingClient) { if (mode === "edit" && isLoadingClient) {
return ( 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"> <CardContent className="p-8">
<FormSkeleton /> <FormSkeleton />
</CardContent> </CardContent>
@@ -139,18 +390,23 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
} }
return ( 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> <CardContent>
<form onSubmit={handleSubmit} className="space-y-8"> <form onSubmit={handleSubmit} className="space-y-8">
{/* Basic Information Section */} {/* Basic Information Section */}
<div className="space-y-6"> <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" /> <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>
<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"> <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 * Business Name / Full Name *
</Label> </Label>
<Input <Input
@@ -159,22 +415,25 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
onChange={(e) => handleInputChange("name", e.target.value)} onChange={(e) => handleInputChange("name", e.target.value)}
required required
placeholder="Enter business name or full name" 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>
<div className="space-y-2"> <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 Email Address
</Label> </Label>
<div className="relative"> <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 <Input
id="email" id="email"
type="email" type="email"
value={formData.email} value={formData.email}
onChange={(e) => handleInputChange("email", e.target.value)} onChange={(e) => handleInputChange("email", e.target.value)}
placeholder="business@example.com" 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>
</div> </div>
@@ -183,17 +442,22 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
{/* Contact Information Section */} {/* Contact Information Section */}
<div className="space-y-6"> <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" /> <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>
<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"> <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 Phone Number
</Label> </Label>
<div className="relative"> <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 <Input
id="phone" id="phone"
type="tel" type="tel"
@@ -201,49 +465,66 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
onChange={(e) => handlePhoneChange(e.target.value)} onChange={(e) => handlePhoneChange(e.target.value)}
placeholder="(555) 123-4567" placeholder="(555) 123-4567"
maxLength={14} 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> </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> </div>
</div> </div>
{/* Address Section */} {/* Address Section */}
<div className="space-y-6"> <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" /> <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>
<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"> <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 Address Line 1
</Label> </Label>
<Input <Input
id="addressLine1" id="addressLine1"
value={formData.addressLine1} value={formData.addressLine1}
onChange={(e) => handleInputChange("addressLine1", e.target.value)} onChange={(e) =>
handleInputChange("addressLine1", e.target.value)
}
placeholder="123 Main Street" 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>
<div className="space-y-2"> <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 Address Line 2
</Label> </Label>
<Input <Input
id="addressLine2" id="addressLine2"
value={formData.addressLine2} value={formData.addressLine2}
onChange={(e) => handleInputChange("addressLine2", e.target.value)} onChange={(e) =>
handleInputChange("addressLine2", e.target.value)
}
placeholder="Suite 100" 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> </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"> <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 City
</Label> </Label>
<Input <Input
@@ -255,14 +536,17 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
/> />
</div> </div>
<div className="space-y-2"> <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 State / Province
</Label> </Label>
<select <select
id="state" id="state"
value={formData.state} value={formData.state}
onChange={(e) => handleInputChange("state", e.target.value)} 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) => ( {US_STATES.map((state) => (
<option key={state} value={state}> <option key={state} value={state}>
@@ -272,28 +556,36 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
</select> </select>
</div> </div>
<div className="space-y-2"> <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 Postal Code
</Label> </Label>
<Input <Input
id="postalCode" id="postalCode"
value={formData.postalCode} value={formData.postalCode}
onChange={(e) => handleInputChange("postalCode", e.target.value)} onChange={(e) =>
handleInputChange("postalCode", e.target.value)
}
placeholder="12345" 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> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="country" className="text-sm font-medium text-gray-700"> <Label
Country htmlFor="country"
</Label> className="text-sm font-medium text-gray-700 dark:text-gray-300"
<select >
id="country" Country
value={formData.country} </Label>
<select
id="country"
value={formData.country}
onChange={(e) => handleInputChange("country", e.target.value)} 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> <option value="">Select Country</option>
<optgroup label="Most Used"> <optgroup label="Most Used">
{MOST_USED_COUNTRIES.map((country) => ( {MOST_USED_COUNTRIES.map((country) => (
@@ -309,7 +601,7 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
</option> </option>
))} ))}
</optgroup> </optgroup>
</select> </select>
</div> </div>
</div> </div>
@@ -318,7 +610,7 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
<Button <Button
type="submit" type="submit"
disabled={loading} 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 ? ( {loading ? (
<> <>
@@ -327,19 +619,19 @@ 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"} {mode === "create" ? "Create Client" : "Update Client"}
</> </>
)} )}
</Button> </Button>
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
onClick={() => router.push("/dashboard/clients")} onClick={() => router.push("/dashboard/clients")}
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"
> >
Cancel Cancel
</Button> </Button>
</div> </div>
</form> </form>
</CardContent> </CardContent>

View File

@@ -15,14 +15,14 @@ import {
AlertCircle, AlertCircle,
Settings, Settings,
User, User,
Mail Mail,
} from "lucide-react"; } from "lucide-react";
export function DarkModeTest() { export function DarkModeTest() {
return ( return (
<div className="min-h-screen p-8 space-y-8"> <div className="min-h-screen space-y-8 p-8">
{/* Header */} {/* 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"> <h1 className="text-4xl font-bold text-gray-900 dark:text-white">
Dark Mode Test Suite Dark Mode Test Suite
</h1> </h1>
@@ -53,11 +53,17 @@ export function DarkModeTest() {
</CardHeader> </CardHeader>
<CardContent className="space-y-3"> <CardContent className="space-y-3">
<div className="space-y-2"> <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-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-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 className="text-red-600 dark:text-red-400">Error Text</div>
</div> </div>
</CardContent> </CardContent>
@@ -71,10 +77,18 @@ export function DarkModeTest() {
<CardContent className="space-y-3"> <CardContent className="space-y-3">
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<Button size="sm">Default</Button> <Button size="sm">Default</Button>
<Button variant="secondary" size="sm">Secondary</Button> <Button variant="secondary" size="sm">
<Button variant="outline" size="sm">Outline</Button> Secondary
<Button variant="ghost" size="sm">Ghost</Button> </Button>
<Button variant="destructive" size="sm">Destructive</Button> <Button variant="outline" size="sm">
Outline
</Button>
<Button variant="ghost" size="sm">
Ghost
</Button>
<Button variant="destructive" size="sm">
Destructive
</Button>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@@ -100,7 +114,7 @@ export function DarkModeTest() {
<Label htmlFor="test-select">Test Select</Label> <Label htmlFor="test-select">Test Select</Label>
<select <select
id="test-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="">Select an option</option>
<option value="1">Option 1</option> <option value="1">Option 1</option>
@@ -125,19 +139,27 @@ export function DarkModeTest() {
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center gap-2 text-sm"> <div className="flex items-center gap-2 text-sm">
<Check className="h-4 w-4 text-green-500" /> <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>
<div className="flex items-center gap-2 text-sm"> <div className="flex items-center gap-2 text-sm">
<X className="h-4 w-4 text-red-500" /> <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>
<div className="flex items-center gap-2 text-sm"> <div className="flex items-center gap-2 text-sm">
<Info className="h-4 w-4 text-blue-500" /> <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>
<div className="flex items-center gap-2 text-sm"> <div className="flex items-center gap-2 text-sm">
<AlertCircle className="h-4 w-4 text-yellow-500" /> <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>
</div> </div>
</CardContent> </CardContent>
@@ -150,14 +172,20 @@ export function DarkModeTest() {
</CardHeader> </CardHeader>
<CardContent className="space-y-3"> <CardContent className="space-y-3">
<div className="space-y-2"> <div className="space-y-2">
<div className="p-3 bg-gray-50 dark:bg-gray-700 rounded-md"> <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> <p className="text-sm text-gray-700 dark:text-gray-300">
Light Background
</p>
</div> </div>
<div className="p-3 bg-gray-100 dark:bg-gray-600 rounded-md"> <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> <p className="text-sm text-gray-700 dark:text-gray-300">
Medium Background
</p>
</div> </div>
<div className="p-3 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-md"> <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> <p className="text-sm text-gray-700 dark:text-gray-300">
Card Background
</p>
</div> </div>
</div> </div>
</CardContent> </CardContent>
@@ -172,19 +200,27 @@ export function DarkModeTest() {
<div className="grid grid-cols-4 gap-4"> <div className="grid grid-cols-4 gap-4">
<div className="flex flex-col items-center gap-1"> <div className="flex flex-col items-center gap-1">
<User className="h-6 w-6 text-gray-700 dark:text-gray-300" /> <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>
<div className="flex flex-col items-center gap-1"> <div className="flex flex-col items-center gap-1">
<Settings className="h-6 w-6 text-green-600 dark:text-green-400" /> <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>
<div className="flex flex-col items-center gap-1"> <div className="flex flex-col items-center gap-1">
<AlertCircle className="h-6 w-6 text-red-600 dark:text-red-400" /> <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>
<div className="flex flex-col items-center gap-1"> <div className="flex flex-col items-center gap-1">
<Info className="h-6 w-6 text-blue-600 dark:text-blue-400" /> <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>
</div> </div>
</CardContent> </CardContent>
@@ -199,16 +235,28 @@ export function DarkModeTest() {
<CardContent> <CardContent>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<div className="space-y-1"> <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 font-medium text-gray-700 dark:text-gray-300">
<p className="text-sm text-gray-500 dark:text-gray-400">Media Query (@media (prefers-color-scheme: dark))</p> 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>
<div className="space-y-1"> <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 font-medium text-gray-700 dark:text-gray-300">
<p className="text-sm text-gray-500 dark:text-gray-400">darkMode: "media"</p> Tailwind Config:
</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
darkMode: &quot;media&quot;
</p>
</div> </div>
<div className="space-y-1"> <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 font-medium text-gray-700 dark:text-gray-300">
<p className="text-sm text-gray-500 dark:text-gray-400">oklch() color space</p> CSS Variables:
</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
oklch() color space
</p>
</div> </div>
</div> </div>
</CardContent> </CardContent>
@@ -217,10 +265,15 @@ export function DarkModeTest() {
{/* Instructions */} {/* Instructions */}
<Card className="border-blue-200 dark:border-blue-800 dark:bg-gray-800"> <Card className="border-blue-200 dark:border-blue-800 dark:bg-gray-800">
<CardHeader> <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> </CardHeader>
<CardContent className="space-y-2 text-sm text-blue-600 dark:text-blue-400"> <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> All UI elements should adapt colors automatically</p>
<p> Text should remain readable in both modes</p> <p> Text should remain readable in both modes</p>
<p> Icons and buttons should have appropriate contrast</p> <p> Icons and buttons should have appropriate contrast</p>

View File

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

View File

@@ -17,9 +17,7 @@ import {
sortableKeyboardCoordinates, sortableKeyboardCoordinates,
verticalListSortingStrategy, verticalListSortingStrategy,
} from "@dnd-kit/sortable"; } from "@dnd-kit/sortable";
import { import { useSortable } from "@dnd-kit/sortable";
useSortable,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities"; import { CSS } from "@dnd-kit/utilities";
import { Input } from "~/components/ui/input"; import { Input } from "~/components/ui/input";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
@@ -51,11 +49,15 @@ function SortableItem({
item, item,
index, index,
onItemChange, onItemChange,
onRemove onRemove,
}: { }: {
item: InvoiceItem; item: InvoiceItem;
index: number; index: number;
onItemChange: (index: number, field: string, value: any) => void; onItemChange: (
index: number,
field: string,
value: string | number | Date,
) => void;
onRemove: (index: number) => void; onRemove: (index: number) => void;
}) { }) {
const { const {
@@ -72,7 +74,7 @@ function SortableItem({
transition, transition,
}; };
const handleItemChange = (field: string, value: any) => { const handleItemChange = (field: string, value: string | number | Date) => {
onItemChange(index, field, value); onItemChange(index, field, value);
}; };
@@ -80,17 +82,17 @@ function SortableItem({
<div <div
ref={setNodeRef} ref={setNodeRef}
style={style} 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" : "" isDragging ? "opacity-50 shadow-lg" : ""
}`} }`}
> >
{/* Drag Handle */} {/* 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 <button
type="button" type="button"
{...attributes} {...attributes}
{...listeners} {...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" /> <GripVertical className="h-4 w-4" />
</button> </button>
@@ -102,10 +104,10 @@ function SortableItem({
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
variant="outline" 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"} {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> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-auto overflow-hidden p-0" align="start"> <PopoverContent className="w-auto overflow-hidden p-0" align="start">
@@ -114,7 +116,7 @@ function SortableItem({
selected={item.date} selected={item.date}
captionLayout="dropdown" captionLayout="dropdown"
onSelect={(selectedDate: Date | undefined) => { onSelect={(selectedDate: Date | undefined) => {
handleItemChange("date", selectedDate || new Date()) handleItemChange("date", selectedDate ?? new Date());
}} }}
/> />
</PopoverContent> </PopoverContent>
@@ -125,9 +127,9 @@ function SortableItem({
<div className="col-span-4"> <div className="col-span-4">
<Input <Input
value={item.description} value={item.description}
onChange={e => handleItemChange("description", e.target.value)} onChange={(e) => handleItemChange("description", e.target.value)}
placeholder="Work description" 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> </div>
@@ -138,9 +140,9 @@ function SortableItem({
step="0.25" step="0.25"
min="0" min="0"
value={item.hours} value={item.hours}
onChange={e => handleItemChange("hours", e.target.value)} onChange={(e) => handleItemChange("hours", e.target.value)}
placeholder="0" 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> </div>
@@ -151,15 +153,15 @@ function SortableItem({
step="0.01" step="0.01"
min="0" min="0"
value={item.rate} value={item.rate}
onChange={e => handleItemChange("rate", e.target.value)} onChange={(e) => handleItemChange("rate", e.target.value)}
placeholder="0.00" 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> </div>
{/* Amount */} {/* Amount */}
<div className="col-span-1"> <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)} ${item.amount.toFixed(2)}
</div> </div>
</div> </div>
@@ -171,7 +173,7 @@ function SortableItem({
onClick={() => onRemove(index)} onClick={() => onRemove(index)}
variant="outline" variant="outline"
size="sm" 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" /> <Trash2 className="h-4 w-4" />
</Button> </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); const [isClient, setIsClient] = useState(false);
useEffect(() => { useEffect(() => {
@@ -191,35 +197,48 @@ export function EditableInvoiceItems({ items, onItemsChange, onRemoveItem }: Edi
useSensor(PointerSensor), useSensor(PointerSensor),
useSensor(KeyboardSensor, { useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates, coordinateGetter: sortableKeyboardCoordinates,
}) }),
); );
const handleDragEnd = (event: DragEndEvent) => { const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event; const { active, over } = event;
if (active.id !== over?.id) { if (active.id !== over?.id) {
const oldIndex = items.findIndex(item => item.id === active.id); const oldIndex = items.findIndex((item) => item.id === active.id);
const newIndex = items.findIndex(item => item.id === over?.id); const newIndex = items.findIndex((item) => item.id === over?.id);
const newItems = arrayMove(items, oldIndex, newIndex); const newItems = arrayMove(items, oldIndex, newIndex);
onItemsChange(newItems); onItemsChange(newItems);
} }
}; };
const handleItemChange = (index: number, field: string, value: any) => { const handleItemChange = (
index: number,
field: string,
value: string | number | Date,
) => {
const newItems = [...items]; const newItems = [...items];
if (field === "hours" || field === "rate") { if (field === "hours" || field === "rate") {
if (newItems[index]) { 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; newItems[index].amount = newItems[index].hours * newItems[index].rate;
} }
} else if (field === "date") { } else if (field === "date") {
if (newItems[index]) { if (newItems[index]) {
newItems[index][field as "date"] = value; const dateValue =
value instanceof Date ? value : new Date(String(value));
newItems[index].date = dateValue;
} }
} else { } else {
if (newItems[index]) { if (newItems[index]) {
newItems[index][field as "description"] = value; const stringValue = typeof value === "string" ? value : String(value);
newItems[index].description = stringValue;
} }
} }
onItemsChange(newItems); onItemsChange(newItems);
@@ -229,28 +248,31 @@ export function EditableInvoiceItems({ items, onItemsChange, onRemoveItem }: Edi
if (!isClient) { if (!isClient) {
return ( return (
<div className="space-y-3"> <div className="space-y-3">
{items.map((item, index) => ( {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
<div className="col-span-1 flex items-center justify-center h-10"> key={item.id}
<div className="w-4 h-4 bg-gray-300 rounded"></div> 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>
<div className="col-span-2"> <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>
<div className="col-span-4"> <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>
<div className="col-span-1"> <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>
<div className="col-span-2"> <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>
<div className="col-span-1"> <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>
<div className="col-span-1"> <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>
</div> </div>
))} ))}
@@ -264,7 +286,10 @@ export function EditableInvoiceItems({ items, onItemsChange, onRemoveItem }: Edi
collisionDetection={closestCenter} collisionDetection={closestCenter}
onDragEnd={handleDragEnd} 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"> <div className="space-y-3">
{items.map((item, index) => ( {items.map((item, index) => (
<SortableItem <SortableItem

View File

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

View File

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

View File

@@ -1,27 +1,32 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select" import * as SelectPrimitive from "@radix-ui/react-select";
import { CheckIcon, ChevronDownIcon, ChevronUpIcon, Search } from "lucide-react" import {
CheckIcon,
ChevronDownIcon,
ChevronUpIcon,
Search,
} from "lucide-react";
import { cn } from "~/lib/utils" import { cn } from "~/lib/utils";
function Select({ function Select({
...props ...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) { }: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} /> return <SelectPrimitive.Root data-slot="select" {...props} />;
} }
function SelectGroup({ function SelectGroup({
...props ...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) { }: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} /> return <SelectPrimitive.Group data-slot="select-group" {...props} />;
} }
function SelectValue({ function SelectValue({
...props ...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) { }: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} /> return <SelectPrimitive.Value data-slot="select-value" {...props} />;
} }
function SelectTrigger({ function SelectTrigger({
@@ -30,15 +35,15 @@ function SelectTrigger({
children, children,
...props ...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & { }: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default" size?: "sm" | "default";
}) { }) {
return ( return (
<SelectPrimitive.Trigger <SelectPrimitive.Trigger
data-slot="select-trigger" data-slot="select-trigger"
data-size={size} data-size={size}
className={cn( 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", "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 className,
)} )}
{...props} {...props}
> >
@@ -47,7 +52,7 @@ function SelectTrigger({
<ChevronDownIcon className="size-4 opacity-50" /> <ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon> </SelectPrimitive.Icon>
</SelectPrimitive.Trigger> </SelectPrimitive.Trigger>
) );
} }
function SelectContent({ 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", "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" && 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", "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} position={position}
{...props} {...props}
@@ -74,7 +79,7 @@ function SelectContent({
className={cn( className={cn(
"p-1", "p-1",
position === "popper" && 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} {children}
@@ -82,7 +87,7 @@ function SelectContent({
<SelectScrollDownButton /> <SelectScrollDownButton />
</SelectPrimitive.Content> </SelectPrimitive.Content>
</SelectPrimitive.Portal> </SelectPrimitive.Portal>
) );
} }
function SelectLabel({ function SelectLabel({
@@ -95,7 +100,7 @@ function SelectLabel({
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)} className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props} {...props}
/> />
) );
} }
function SelectItem({ function SelectItem({
@@ -108,7 +113,7 @@ function SelectItem({
data-slot="select-item" data-slot="select-item"
className={cn( 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", "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} {...props}
> >
@@ -119,7 +124,7 @@ function SelectItem({
</span> </span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item> </SelectPrimitive.Item>
) );
} }
function SelectSeparator({ function SelectSeparator({
@@ -132,7 +137,7 @@ function SelectSeparator({
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)} className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props} {...props}
/> />
) );
} }
function SelectScrollUpButton({ function SelectScrollUpButton({
@@ -144,13 +149,13 @@ function SelectScrollUpButton({
data-slot="select-scroll-up-button" data-slot="select-scroll-up-button"
className={cn( className={cn(
"flex cursor-default items-center justify-center py-1", "flex cursor-default items-center justify-center py-1",
className className,
)} )}
{...props} {...props}
> >
<ChevronUpIcon className="size-4" /> <ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton> </SelectPrimitive.ScrollUpButton>
) );
} }
function SelectScrollDownButton({ function SelectScrollDownButton({
@@ -162,13 +167,13 @@ function SelectScrollDownButton({
data-slot="select-scroll-down-button" data-slot="select-scroll-down-button"
className={cn( className={cn(
"flex cursor-default items-center justify-center py-1", "flex cursor-default items-center justify-center py-1",
className className,
)} )}
{...props} {...props}
> >
<ChevronDownIcon className="size-4" /> <ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton> </SelectPrimitive.ScrollDownButton>
) );
} }
// Enhanced SelectContent with search functionality // 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", "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" && 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", "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} position={position}
onEscapeKeyDown={(e) => { onEscapeKeyDown={(e) => {
@@ -226,11 +231,11 @@ function SelectContentWithSearch({
{...props} {...props}
> >
{onSearchChange && ( {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" /> <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<input <input
ref={searchInputRef} 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} placeholder={searchPlaceholder}
value={searchValue} value={searchValue}
onChange={(e) => onSearchChange(e.target.value)} onChange={(e) => onSearchChange(e.target.value)}
@@ -240,7 +245,11 @@ function SelectContentWithSearch({
e.stopPropagation(); e.stopPropagation();
} }
// Prevent arrow keys from moving focus away from search // 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(); e.stopPropagation();
} }
}} }}
@@ -255,7 +264,9 @@ function SelectContentWithSearch({
<SelectScrollUpButton /> <SelectScrollUpButton />
<SelectPrimitive.Viewport className="p-1"> <SelectPrimitive.Viewport className="p-1">
{filteredOptions && filteredOptions.length === 0 ? ( {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 children
)} )}
@@ -263,7 +274,7 @@ function SelectContentWithSearch({
<SelectScrollDownButton /> <SelectScrollDownButton />
</SelectPrimitive.Content> </SelectPrimitive.Content>
</SelectPrimitive.Portal> </SelectPrimitive.Portal>
) );
} }
// Searchable Select component // Searchable Select component
@@ -284,15 +295,15 @@ function SearchableSelect({
options, options,
searchPlaceholder = "Search...", searchPlaceholder = "Search...",
className, className,
disabled disabled,
}: SearchableSelectProps) { }: SearchableSelectProps) {
const [searchValue, setSearchValue] = React.useState(""); const [searchValue, setSearchValue] = React.useState("");
const [isOpen, setIsOpen] = React.useState(false); const [isOpen, setIsOpen] = React.useState(false);
const filteredOptions = React.useMemo(() => { const filteredOptions = React.useMemo(() => {
if (!searchValue) return options; if (!searchValue) return options;
return options.filter(option => return options.filter((option) =>
option.label.toLowerCase().includes(searchValue.toLowerCase()) option.label.toLowerCase().includes(searchValue.toLowerCase()),
); );
}, [options, searchValue]); }, [options, searchValue]);
@@ -353,4 +364,4 @@ export {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
SearchableSelect, SearchableSelect,
} };

View File

@@ -1,4 +1,4 @@
import { cn } from "~/lib/utils" import { cn } from "~/lib/utils";
function Skeleton({ className, ...props }: React.ComponentProps<"div">) { function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
@@ -7,37 +7,43 @@ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
className={cn("bg-muted animate-pulse rounded-md", className)} className={cn("bg-muted animate-pulse rounded-md", className)}
{...props} {...props}
/> />
) );
} }
// Dashboard skeleton components // Dashboard skeleton components
export function DashboardStatsSkeleton() { export function DashboardStatsSkeleton() {
return ( 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) => ( {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
<div className="flex items-center justify-between mb-4"> 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-4 w-24" />
<Skeleton className="h-8 w-8 rounded-lg" /> <Skeleton className="h-8 w-8 rounded-lg" />
</div> </div>
<Skeleton className="h-8 w-16 mb-2" /> <Skeleton className="mb-2 h-8 w-16" />
<Skeleton className="h-3 w-32" /> <Skeleton className="h-3 w-32" />
</div> </div>
))} ))}
</div> </div>
) );
} }
export function DashboardCardsSkeleton() { export function DashboardCardsSkeleton() {
return ( 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) => ( {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
<div className="flex items-center gap-2 mb-4"> 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-8 w-8 rounded-lg" />
<Skeleton className="h-6 w-32" /> <Skeleton className="h-6 w-32" />
</div> </div>
<Skeleton className="h-4 w-full mb-4" /> <Skeleton className="mb-4 h-4 w-full" />
<div className="flex gap-3"> <div className="flex gap-3">
<Skeleton className="h-10 w-24" /> <Skeleton className="h-10 w-24" />
<Skeleton className="h-10 w-32" /> <Skeleton className="h-10 w-32" />
@@ -45,20 +51,20 @@ export function DashboardCardsSkeleton() {
</div> </div>
))} ))}
</div> </div>
) );
} }
export function DashboardActivitySkeleton() { export function DashboardActivitySkeleton() {
return ( return (
<div className="shadow-xl border-0 bg-white/80 backdrop-blur-sm rounded-xl p-6"> <div className="rounded-xl border-0 bg-white/80 p-6 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
<Skeleton className="h-6 w-32 mb-6" /> <Skeleton className="mb-6 h-6 w-32" />
<div className="text-center py-12"> <div className="py-12 text-center">
<Skeleton className="h-20 w-20 rounded-full mx-auto mb-4" /> <Skeleton className="mx-auto mb-4 h-20 w-20 rounded-full" />
<Skeleton className="h-6 w-48 mx-auto mb-2" /> <Skeleton className="mx-auto mb-2 h-6 w-48" />
<Skeleton className="h-4 w-64 mx-auto" /> <Skeleton className="mx-auto h-4 w-64" />
</div> </div>
</div> </div>
) );
} }
// Table skeleton components // Table skeleton components
@@ -66,7 +72,7 @@ export function TableSkeleton({ rows = 5 }: { rows?: number }) {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
{/* Search and filters */} {/* 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" /> <Skeleton className="h-10 w-64" />
<div className="flex gap-2"> <div className="flex gap-2">
<Skeleton className="h-10 w-24" /> <Skeleton className="h-10 w-24" />
@@ -75,8 +81,8 @@ export function TableSkeleton({ rows = 5 }: { rows?: number }) {
</div> </div>
{/* Table */} {/* Table */}
<div className="border rounded-lg"> <div className="rounded-lg border">
<div className="p-4 border-b"> <div className="border-b p-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Skeleton className="h-4 w-32" /> <Skeleton className="h-4 w-32" />
<div className="flex gap-2"> <div className="flex gap-2">
@@ -112,7 +118,7 @@ export function TableSkeleton({ rows = 5 }: { rows?: number }) {
</div> </div>
</div> </div>
</div> </div>
) );
} }
// Form skeleton components // Form skeleton components
@@ -121,26 +127,26 @@ export function FormSkeleton() {
<div className="space-y-6"> <div className="space-y-6">
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<Skeleton className="h-4 w-20 mb-2" /> <Skeleton className="mb-2 h-4 w-20" />
<Skeleton className="h-10 w-full" /> <Skeleton className="h-10 w-full" />
</div> </div>
<div> <div>
<Skeleton className="h-4 w-24 mb-2" /> <Skeleton className="mb-2 h-4 w-24" />
<Skeleton className="h-10 w-full" /> <Skeleton className="h-10 w-full" />
</div> </div>
<div> <div>
<Skeleton className="h-4 w-16 mb-2" /> <Skeleton className="mb-2 h-4 w-16" />
<Skeleton className="h-10 w-full" /> <Skeleton className="h-10 w-full" />
</div> </div>
</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> <div>
<Skeleton className="h-4 w-20 mb-2" /> <Skeleton className="mb-2 h-4 w-20" />
<Skeleton className="h-10 w-full" /> <Skeleton className="h-10 w-full" />
</div> </div>
<div> <div>
<Skeleton className="h-4 w-16 mb-2" /> <Skeleton className="mb-2 h-4 w-16" />
<Skeleton className="h-10 w-full" /> <Skeleton className="h-10 w-full" />
</div> </div>
</div> </div>
@@ -150,7 +156,7 @@ export function FormSkeleton() {
<Skeleton className="h-10 w-24" /> <Skeleton className="h-10 w-24" />
</div> </div>
</div> </div>
) );
} }
// Invoice view skeleton // Invoice view skeleton
@@ -158,7 +164,7 @@ export function InvoiceViewSkeleton() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Header */} {/* Header */}
<div className="flex justify-between items-start"> <div className="flex items-start justify-between">
<div className="space-y-2"> <div className="space-y-2">
<Skeleton className="h-8 w-48" /> <Skeleton className="h-8 w-48" />
<Skeleton className="h-4 w-64" /> <Skeleton className="h-4 w-64" />
@@ -167,7 +173,7 @@ export function InvoiceViewSkeleton() {
</div> </div>
{/* Client info */} {/* 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"> <div className="space-y-3">
<Skeleton className="h-5 w-24" /> <Skeleton className="h-5 w-24" />
<Skeleton className="h-4 w-full" /> <Skeleton className="h-4 w-full" />
@@ -182,8 +188,8 @@ export function InvoiceViewSkeleton() {
</div> </div>
{/* Items table */} {/* Items table */}
<div className="border rounded-lg"> <div className="rounded-lg border">
<div className="p-4 border-b"> <div className="border-b p-4">
<Skeleton className="h-5 w-32" /> <Skeleton className="h-5 w-32" />
</div> </div>
<div className="p-4"> <div className="p-4">
@@ -209,7 +215,7 @@ export function InvoiceViewSkeleton() {
</div> </div>
</div> </div>
</div> </div>
) );
} }
export { Skeleton } export { Skeleton };

View File

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

View File

@@ -1,10 +1,9 @@
@import "tailwindcss"; @import "tailwindcss";
@import "tw-animate-css"; @import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme { @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"; "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
} }
@@ -28,6 +27,7 @@
--color-accent: var(--accent); --color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground); --color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive); --color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border); --color-border: var(--border);
--color-input: var(--input); --color-input: var(--input);
--color-ring: var(--ring); --color-ring: var(--ring);
@@ -63,6 +63,7 @@
--accent: oklch(0.97 0 0); --accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0); --accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325); --destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.985 0 0);
--border: oklch(0.922 0 0); --border: oklch(0.922 0 0);
--input: oklch(0.922 0 0); --input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0); --ring: oklch(0.708 0 0);
@@ -81,45 +82,155 @@
--sidebar-ring: oklch(0.708 0 0); --sidebar-ring: oklch(0.708 0 0);
} }
.dark { @media (prefers-color-scheme: dark) {
--background: oklch(0.145 0 0); :root {
--foreground: oklch(0.985 0 0); --background: oklch(0.145 0 0);
--card: oklch(0.205 0 0); --foreground: oklch(0.985 0 0);
--card-foreground: oklch(0.985 0 0); --card: oklch(0.205 0 0);
--popover: oklch(0.205 0 0); --card-foreground: oklch(0.985 0 0);
--popover-foreground: oklch(0.985 0 0); --popover: oklch(0.205 0 0);
--primary: oklch(0.922 0 0); --popover-foreground: oklch(0.985 0 0);
--primary-foreground: oklch(0.205 0 0); --primary: oklch(0.922 0 0);
--secondary: oklch(0.269 0 0); --primary-foreground: oklch(0.205 0 0);
--secondary-foreground: oklch(0.985 0 0); --secondary: oklch(0.269 0 0);
--muted: oklch(0.269 0 0); --secondary-foreground: oklch(0.985 0 0);
--muted-foreground: oklch(0.708 0 0); --muted: oklch(0.269 0 0);
--accent: oklch(0.269 0 0); --muted-foreground: oklch(0.708 0 0);
--accent-foreground: oklch(0.985 0 0); --accent: oklch(0.269 0 0);
--destructive: oklch(0.704 0.191 22.216); --accent-foreground: oklch(0.985 0 0);
--border: oklch(1 0 0 / 10%); --destructive: oklch(0.704 0.191 22.216);
--input: oklch(1 0 0 / 15%); --destructive-foreground: oklch(0.985 0 0);
--ring: oklch(0.556 0 0); --border: oklch(1 0 0 / 10%);
--chart-1: oklch(0.488 0.243 264.376); --input: oklch(1 0 0 / 15%);
--chart-2: oklch(0.696 0.17 162.48); --ring: oklch(0.556 0 0);
--chart-3: oklch(0.769 0.188 70.08); --chart-1: oklch(0.488 0.243 264.376);
--chart-4: oklch(0.627 0.265 303.9); --chart-2: oklch(0.696 0.17 162.48);
--chart-5: oklch(0.645 0.246 16.439); --chart-3: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.205 0 0); --chart-4: oklch(0.627 0.265 303.9);
--sidebar-foreground: oklch(0.985 0 0); --chart-5: oklch(0.645 0.246 16.439);
--sidebar-primary: oklch(0.488 0.243 264.376); --sidebar: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0); --sidebar-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0); --sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-accent-foreground: oklch(0.985 0 0); --sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%); --sidebar-accent: oklch(0.269 0 0);
--sidebar-ring: oklch(0.556 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 { @layer base {
* { * {
@apply border-border outline-ring/50; @apply border-border outline-ring/50;
} }
body { 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"; import animate from "tailwindcss-animate";
export default { export default {
darkMode: "class", darkMode: "media",
content: [ content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}", "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}", "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
@@ -11,7 +11,12 @@ export default {
theme: { theme: {
extend: { extend: {
fontFamily: { 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: { colors: {
border: "hsl(var(--border))", border: "hsl(var(--border))",