Update date picker, mobile styling

This commit is contained in:
2025-07-16 03:27:56 -04:00
parent 76711d2c10
commit c6fa9c4ac1
41 changed files with 3522 additions and 1431 deletions

View File

@@ -33,6 +33,7 @@
"@types/bcryptjs": "^2.4.6", "@types/bcryptjs": "^2.4.6",
"@types/file-saver": "^2.0.7", "@types/file-saver": "^2.0.7",
"bcryptjs": "^3.0.2", "bcryptjs": "^3.0.2",
"chrono-node": "^2.8.3",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
@@ -641,6 +642,8 @@
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
"chrono-node": ["chrono-node@2.8.3", "", { "dependencies": { "dayjs": "^1.10.0" } }, "sha512-YukiXak31pshonVWaeJ9cZ4xxWIlbsyn5qYUkG5pQ+usZ6l22ASXDIk0kHUQkIBNOCLRevFkHJjnGKXwZNtyZw=="],
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
@@ -683,6 +686,8 @@
"date-fns-jalali": ["date-fns-jalali@4.1.0-0", "", {}, "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg=="], "date-fns-jalali": ["date-fns-jalali@4.1.0-0", "", {}, "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg=="],
"dayjs": ["dayjs@1.11.13", "", {}, "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="],
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],

1167
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -55,6 +55,7 @@
"@types/bcryptjs": "^2.4.6", "@types/bcryptjs": "^2.4.6",
"@types/file-saver": "^2.0.7", "@types/file-saver": "^2.0.7",
"bcryptjs": "^3.0.2", "bcryptjs": "^3.0.2",
"chrono-node": "^2.8.3",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",

View File

@@ -49,35 +49,29 @@ function RegisterForm() {
} }
return ( return (
<div className="bg-gradient-auth flex min-h-screen items-center justify-center p-4"> <div className="auth-container">
<div className="w-full max-w-md space-y-8"> <div className="auth-form-container">
{/* Logo and Welcome */} {/* Logo and Welcome */}
<div className="space-y-4 text-center"> <div className="auth-header">
<Logo size="lg" className="mx-auto" /> <Logo size="lg" className="mx-auto" />
<div> <div>
<h1 className="text-foreground text-2xl font-bold"> <h1 className="auth-title">Join beenvoice</h1>
Join beenvoice <p className="auth-subtitle">Create your account to get started</p>
</h1>
<p className="text-muted-foreground mt-2">
Create your account to get started
</p>
</div> </div>
</div> </div>
{/* Registration Form */} {/* Registration Form */}
<Card className="border-0 shadow-xl"> <Card className="auth-card">
<CardHeader className="space-y-1"> <CardHeader className="space-y-1">
<CardTitle className="text-center text-xl"> <CardTitle className="auth-card-title">Create Account</CardTitle>
Create Account
</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<form onSubmit={handleRegister} className="space-y-4"> <form onSubmit={handleRegister} className="auth-form">
<div className="grid grid-cols-2 gap-4"> <div className="auth-input-grid">
<div className="space-y-2"> <div className="auth-input-group">
<Label htmlFor="firstName">First Name</Label> <Label htmlFor="firstName">First Name</Label>
<div className="relative"> <div className="relative">
<User className="text-muted-foreground absolute top-3 left-3 h-4 w-4" /> <User className="auth-input-icon" />
<Input <Input
id="firstName" id="firstName"
type="text" type="text"
@@ -85,46 +79,46 @@ function RegisterForm() {
onChange={(e) => setFirstName(e.target.value)} onChange={(e) => setFirstName(e.target.value)}
required required
autoFocus autoFocus
className="pl-10" className="form-input-with-icon"
placeholder="First name" placeholder="First name"
/> />
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="auth-input-group">
<Label htmlFor="lastName">Last Name</Label> <Label htmlFor="lastName">Last Name</Label>
<div className="relative"> <div className="relative">
<User className="text-muted-foreground absolute top-3 left-3 h-4 w-4" /> <User className="auth-input-icon" />
<Input <Input
id="lastName" id="lastName"
type="text" type="text"
value={lastName} value={lastName}
onChange={(e) => setLastName(e.target.value)} onChange={(e) => setLastName(e.target.value)}
required required
className="pl-10" className="form-input-with-icon"
placeholder="Last name" placeholder="Last name"
/> />
</div> </div>
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="auth-input-group">
<Label htmlFor="email">Email</Label> <Label htmlFor="email">Email</Label>
<div className="relative"> <div className="relative">
<Mail className="text-muted-foreground absolute top-3 left-3 h-4 w-4" /> <Mail className="auth-input-icon" />
<Input <Input
id="email" id="email"
type="email" type="email"
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
required required
className="pl-10" className="form-input-with-icon"
placeholder="Enter your email" placeholder="Enter your email"
/> />
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="auth-input-group">
<Label htmlFor="password">Password</Label> <Label htmlFor="password">Password</Label>
<div className="relative"> <div className="relative">
<Lock className="text-muted-foreground absolute top-3 left-3 h-4 w-4" /> <Lock className="auth-input-icon" />
<Input <Input
id="password" id="password"
type="password" type="password"
@@ -132,15 +126,19 @@ function RegisterForm() {
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
required required
minLength={6} minLength={6}
className="pl-10" className="form-input-with-icon"
placeholder="Create a password" placeholder="Create a password"
/> />
</div> </div>
<p className="text-muted-foreground text-xs"> <p className="auth-password-help">
Must be at least 6 characters Must be at least 6 characters
</p> </p>
</div> </div>
<Button type="submit" className="w-full" disabled={loading}> <Button
type="submit"
className="auth-submit-btn"
disabled={loading}
>
{loading ? ( {loading ? (
"Creating account..." "Creating account..."
) : ( ) : (
@@ -151,14 +149,11 @@ function RegisterForm() {
)} )}
</Button> </Button>
</form> </form>
<div className="mt-6 text-center text-sm"> <div className="auth-footer-text">
<span className="text-muted-foreground"> <span className="text-muted-foreground">
Already have an account?{" "} Already have an account?{" "}
</span> </span>
<Link <Link href="/auth/signin" className="auth-footer-link">
href="/auth/signin"
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>
</div> </div>
@@ -166,11 +161,9 @@ function RegisterForm() {
</Card> </Card>
{/* Features */} {/* Features */}
<div className="space-y-4 text-center"> <div className="auth-features">
<p className="text-muted-foreground text-sm"> <p className="welcome-description">Start invoicing like a pro</p>
Start invoicing like a pro <div className="auth-features-list">
</p>
<div className="text-muted-foreground flex justify-center space-x-6 text-xs">
<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>
@@ -185,15 +178,13 @@ export default function RegisterPage() {
return ( return (
<Suspense <Suspense
fallback={ fallback={
<div className="bg-gradient-auth flex min-h-screen items-center justify-center p-4"> <div className="auth-container">
<div className="w-full max-w-md space-y-8"> <div className="auth-form-container">
<div className="space-y-4 text-center"> <div className="auth-header">
<Logo size="lg" className="mx-auto" /> <Logo size="lg" className="mx-auto" />
<div> <div>
<h1 className="text-foreground text-2xl font-bold"> <h1 className="auth-title">Join beenvoice</h1>
Join beenvoice <p className="auth-subtitle">Loading...</p>
</h1>
<p className="text-muted-foreground mt-2">Loading...</p>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -56,7 +56,7 @@ function SignInForm() {
</div> </div>
{/* Sign In Form */} {/* Sign In Form */}
<Card className="border-0 shadow-xl"> <Card className="card-primary">
<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">Sign In</CardTitle>
</CardHeader> </CardHeader>
@@ -65,7 +65,7 @@ function SignInForm() {
<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="text-muted-foreground absolute top-3 left-3 h-4 w-4" /> <Mail className="form-icon-left" />
<Input <Input
id="email" id="email"
type="email" type="email"
@@ -73,7 +73,7 @@ function SignInForm() {
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
required required
autoFocus autoFocus
className="pl-10" className="form-input-with-icon"
placeholder="Enter your email" placeholder="Enter your email"
/> />
</div> </div>
@@ -81,14 +81,14 @@ 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="text-muted-foreground absolute top-3 left-3 h-4 w-4" /> <Lock className="form-icon-left" />
<Input <Input
id="password" id="password"
type="password" type="password"
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
required required
className="pl-10" className="form-input-with-icon"
placeholder="Enter your password" placeholder="Enter your password"
/> />
</div> </div>
@@ -108,10 +108,7 @@ function SignInForm() {
<span className="text-muted-foreground"> <span className="text-muted-foreground">
Don&apos;t have an account?{" "} Don&apos;t have an account?{" "}
</span> </span>
<Link <Link href="/auth/register" className="nav-link-brand">
href="/auth/register"
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>
</div> </div>
@@ -120,10 +117,10 @@ function SignInForm() {
{/* Features */} {/* Features */}
<div className="space-y-4 text-center"> <div className="space-y-4 text-center">
<p className="text-muted-foreground text-sm"> <p className="welcome-description">
Simple invoicing for freelancers and small businesses Simple invoicing for freelancers and small businesses
</p> </p>
<div className="text-muted-foreground flex justify-center space-x-6 text-xs"> <div className="welcome-feature-list">
<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>

View File

@@ -49,7 +49,7 @@ export default async function BusinessDetailPage({
variant="gradient" variant="gradient"
> >
<Link href={`/dashboard/businesses/${business.id}/edit`}> <Link href={`/dashboard/businesses/${business.id}/edit`}>
<Button className="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700"> <Button variant="brand">
<Edit className="mr-2 h-4 w-4" /> <Edit className="mr-2 h-4 w-4" />
Edit Business Edit Business
</Button> </Button>
@@ -59,9 +59,9 @@ export default async function BusinessDetailPage({
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3"> <div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
{/* Business Information Card */} {/* Business Information Card */}
<div className="lg:col-span-2"> <div className="lg:col-span-2">
<Card className="border-0 shadow-xl backdrop-blur-sm"> <Card className="card-primary">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center space-x-2 text-green-600"> <CardTitle className="card-title-success">
<Building className="h-5 w-5" /> <Building className="h-5 w-5" />
<span>Business Information</span> <span>Business Information</span>
</CardTitle> </CardTitle>
@@ -71,50 +71,42 @@ export default async function BusinessDetailPage({
<div className="grid grid-cols-1 gap-6 md:grid-cols-2"> <div className="grid grid-cols-1 gap-6 md:grid-cols-2">
{business.email && ( {business.email && (
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<div className="rounded-lg bg-emerald-100 p-2 dark:bg-emerald-900/30"> <div className="icon-bg-emerald">
<Mail className="h-4 w-4 text-emerald-600" /> <Mail className="text-icon-emerald h-4 w-4" />
</div> </div>
<div> <div>
<p className="text-muted-foreground text-sm font-medium"> <p className="text-muted text-sm font-medium">Email</p>
Email <p className="text-accent text-sm">{business.email}</p>
</p>
<p className="text-foreground text-sm">
{business.email}
</p>
</div> </div>
</div> </div>
)} )}
{business.phone && ( {business.phone && (
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<div className="rounded-lg bg-emerald-100 p-2 dark:bg-emerald-900/30"> <div className="icon-bg-emerald">
<Phone className="h-4 w-4 text-emerald-600" /> <Phone className="text-icon-emerald h-4 w-4" />
</div> </div>
<div> <div>
<p className="text-muted-foreground text-sm font-medium"> <p className="text-muted text-sm font-medium">Phone</p>
Phone <p className="text-accent text-sm">{business.phone}</p>
</p>
<p className="text-foreground text-sm">
{business.phone}
</p>
</div> </div>
</div> </div>
)} )}
{business.website && ( {business.website && (
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<div className="rounded-lg bg-emerald-100 p-2 dark:bg-emerald-900/30"> <div className="icon-bg-emerald">
<Globe className="h-4 w-4 text-emerald-600" /> <Globe className="text-icon-emerald h-4 w-4" />
</div> </div>
<div> <div>
<p className="text-muted-foreground text-sm font-medium"> <p className="text-muted text-sm font-medium">
Website Website
</p> </p>
<a <a
href={business.website} href={business.website}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-foreground text-sm hover:text-emerald-600 hover:underline" className="link-primary text-sm"
> >
{business.website} {business.website}
</a> </a>
@@ -124,16 +116,12 @@ export default async function BusinessDetailPage({
{business.taxId && ( {business.taxId && (
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<div className="rounded-lg bg-emerald-100 p-2 dark:bg-emerald-900/30"> <div className="icon-bg-emerald">
<Hash className="h-4 w-4 text-emerald-600" /> <Hash className="text-icon-emerald h-4 w-4" />
</div> </div>
<div> <div>
<p className="text-muted-foreground text-sm font-medium"> <p className="text-muted text-sm font-medium">Tax ID</p>
Tax ID <p className="text-accent text-sm">{business.taxId}</p>
</p>
<p className="text-foreground text-sm">
{business.taxId}
</p>
</div> </div>
</div> </div>
)} )}
@@ -143,19 +131,21 @@ export default async function BusinessDetailPage({
{(business.addressLine1 ?? business.city ?? business.state) && ( {(business.addressLine1 ?? business.city ?? business.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="rounded-lg bg-emerald-100 p-2 dark:bg-emerald-900/30"> <div className="icon-bg-emerald">
<MapPin className="h-4 w-4 text-emerald-600" /> <MapPin className="text-icon-emerald h-4 w-4" />
</div> </div>
<div> <div>
<p className="text-muted-foreground text-sm font-medium"> <p className="text-muted text-sm font-medium">
Address Address
</p> </p>
</div> </div>
</div> </div>
<div className="text-foreground ml-11 space-y-1 text-sm"> <div className="text-accent ml-11 space-y-1 text-sm">
{business.addressLine1 && <p>{business.addressLine1}</p>} {business.addressLine1 && <p>{business.addressLine1}</p>}
{business.addressLine2 && <p>{business.addressLine2}</p>} {business.addressLine2 && <p>{business.addressLine2}</p>}
{(business.city ?? business.state ?? business.postalCode) && ( {(business.city ??
business.state ??
business.postalCode) && (
<p> <p>
{[business.city, business.state, business.postalCode] {[business.city, business.state, business.postalCode]
.filter(Boolean) .filter(Boolean)
@@ -169,14 +159,14 @@ export default async function BusinessDetailPage({
{/* Business Since */} {/* Business Since */}
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<div className="rounded-lg bg-emerald-100 p-2 dark:bg-emerald-900/30"> <div className="icon-bg-emerald">
<Calendar className="h-4 w-4 text-emerald-600" /> <Calendar className="text-icon-emerald h-4 w-4" />
</div> </div>
<div> <div>
<p className="text-sm font-medium text-gray-500 dark:text-gray-400"> <p className="text-muted text-sm font-medium">
Business Added Business Added
</p> </p>
<p className="text-sm dark:text-gray-300"> <p className="text-secondary text-sm">
{formatDate(business.createdAt)} {formatDate(business.createdAt)}
</p> </p>
</div> </div>
@@ -185,16 +175,12 @@ export default async function BusinessDetailPage({
{/* Default Business Badge */} {/* Default Business Badge */}
{business.isDefault && ( {business.isDefault && (
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<div className="rounded-lg bg-emerald-100 p-2 dark:bg-emerald-900/30"> <div className="icon-bg-emerald">
<Building className="h-4 w-4 text-emerald-600" /> <Building className="text-icon-emerald h-4 w-4" />
</div> </div>
<div> <div>
<p className="text-sm font-medium text-gray-500 dark:text-gray-400"> <p className="text-muted text-sm font-medium">Status</p>
Status <Badge className="badge-success">Default Business</Badge>
</p>
<Badge className="bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400">
Default Business
</Badge>
</div> </div>
</div> </div>
)} )}
@@ -204,23 +190,19 @@ export default async function BusinessDetailPage({
{/* Settings & Actions Card */} {/* Settings & Actions Card */}
<div className="space-y-6"> <div className="space-y-6">
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm dark:bg-gray-800/80"> <Card className="card-secondary">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center space-x-2 text-emerald-700 dark:text-emerald-400"> <CardTitle className="card-title-primary">
<Building className="h-5 w-5" /> <Building className="h-5 w-5" />
<span>Business Settings</span> <span>Business Settings</span>
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="text-center"> <div className="text-center">
<p className="text-sm text-gray-500 dark:text-gray-400"> <p className="text-muted text-sm">Default Business</p>
Default Business
</p>
<p className="text-lg font-semibold"> <p className="text-lg font-semibold">
{business.isDefault ? ( {business.isDefault ? (
<Badge className="bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400"> <Badge className="badge-success">Yes</Badge>
Yes
</Badge>
) : ( ) : (
<Badge variant="outline">No</Badge> <Badge variant="outline">No</Badge>
)} )}
@@ -228,7 +210,7 @@ export default async function BusinessDetailPage({
</div> </div>
<div className="space-y-3"> <div className="space-y-3">
<p className="text-sm font-medium text-gray-500 dark:text-gray-400"> <p className="text-muted text-sm font-medium">
Quick Actions Quick Actions
</p> </p>
<div className="space-y-2"> <div className="space-y-2">
@@ -258,20 +240,20 @@ export default async function BusinessDetailPage({
</Card> </Card>
{/* Information Card */} {/* Information Card */}
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm dark:bg-gray-800/80"> <Card className="card-secondary">
<CardHeader> <CardHeader>
<CardTitle className="text-lg dark:text-white"> <CardTitle className="text-accent text-lg">
About This Business About This Business
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-3 text-sm text-gray-600 dark:text-gray-300"> <div className="text-secondary space-y-3 text-sm">
<p> <p>
This business profile is used for generating invoices and This business profile is used for generating invoices and
represents your company information to clients. represents your company information to clients.
</p> </p>
{business.isDefault && ( {business.isDefault && (
<p className="text-emerald-600 dark:text-emerald-400"> <p className="text-icon-emerald">
This is your default business and will be automatically This is your default business and will be automatically
selected when creating new invoices. selected when creating new invoices.
</p> </p>

View File

@@ -72,9 +72,9 @@ export default async function ClientDetailPage({
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3"> <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="border-0 shadow-xl backdrop-blur-sm"> <Card className="card-primary">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center space-x-2 text-green-600"> <CardTitle className="client-section-title">
<Building className="h-5 w-5" /> <Building className="h-5 w-5" />
<span>Contact Information</span> <span>Contact Information</span>
</CardTitle> </CardTitle>
@@ -83,33 +83,25 @@ export default async function ClientDetailPage({
{/* Basic Info */} {/* Basic Info */}
<div className="grid grid-cols-1 gap-6 md:grid-cols-2"> <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="client-info-item">
<div className="rounded-lg bg-emerald-100 p-2 dark:bg-emerald-900/30"> <div className="client-info-icon">
<Mail className="h-4 w-4 text-emerald-600" /> <Mail className="client-info-icon-emerald" />
</div> </div>
<div> <div>
<p className="text-muted-foreground text-sm font-medium"> <p className="client-info-label">Email</p>
Email <p className="client-info-value">{client.email}</p>
</p>
<p className="text-foreground text-sm">
{client.email}
</p>
</div> </div>
</div> </div>
)} )}
{client.phone && ( {client.phone && (
<div className="flex items-center space-x-3"> <div className="client-info-item">
<div className="rounded-lg bg-emerald-100 p-2 dark:bg-emerald-900/30"> <div className="client-info-icon">
<Phone className="h-4 w-4 text-emerald-600" /> <Phone className="client-info-icon-emerald" />
</div> </div>
<div> <div>
<p className="text-muted-foreground text-sm font-medium"> <p className="client-info-label">Phone</p>
Phone <p className="client-info-value">{client.phone}</p>
</p>
<p className="text-foreground text-sm">
{client.phone}
</p>
</div> </div>
</div> </div>
)} )}
@@ -118,17 +110,15 @@ export default async function ClientDetailPage({
{/* Address */} {/* Address */}
{(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="client-info-item">
<div className="rounded-lg bg-emerald-100 p-2 dark:bg-emerald-900/30"> <div className="client-info-icon">
<MapPin className="h-4 w-4 text-emerald-600" /> <MapPin className="client-info-icon-emerald" />
</div> </div>
<div> <div>
<p className="text-muted-foreground text-sm font-medium"> <p className="client-info-label">Address</p>
Address
</p>
</div> </div>
</div> </div>
<div className="text-foreground ml-11 space-y-1 text-sm"> <div className="client-address-content">
{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) && (
@@ -144,15 +134,13 @@ export default async function ClientDetailPage({
)} )}
{/* Client Since */} {/* Client Since */}
<div className="flex items-center space-x-3"> <div className="client-info-item">
<div className="rounded-lg bg-emerald-100 p-2 dark:bg-emerald-900/30"> <div className="client-info-icon">
<Calendar className="h-4 w-4 text-emerald-600" /> <Calendar className="client-info-icon-emerald" />
</div> </div>
<div> <div>
<p className="text-sm font-medium text-gray-500 dark:text-gray-400"> <p className="client-info-label">Client Since</p>
Client Since <p className="client-info-value">
</p>
<p className="text-sm dark:text-gray-300">
{formatDate(client.createdAt)} {formatDate(client.createdAt)}
</p> </p>
</div> </div>
@@ -163,39 +151,31 @@ export default async function ClientDetailPage({
{/* Stats Card */} {/* Stats Card */}
<div className="space-y-6"> <div className="space-y-6">
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm dark:bg-gray-800/80"> <Card className="card-primary">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center space-x-2 text-emerald-700 dark:text-emerald-400"> <CardTitle className="client-stats-title">
<DollarSign className="h-5 w-5" /> <DollarSign className="h-5 w-5" />
<span>Invoice Summary</span> <span>Invoice Summary</span>
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="text-center"> <div className="text-center">
<p className="text-2xl font-bold text-emerald-600"> <p className="client-total-amount">
{formatCurrency(totalInvoiced)} {formatCurrency(totalInvoiced)}
</p> </p>
<p className="text-sm text-gray-500 dark:text-gray-400"> <p className="client-total-label">Total Invoiced</p>
Total Invoiced
</p>
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="client-stats-grid">
<div className="text-center"> <div className="text-center">
<p className="text-lg font-semibold text-green-600"> <p className="client-stat-value-paid">{paidInvoices}</p>
{paidInvoices} <p className="client-stat-label">Paid</p>
</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"> <p className="client-stat-value-pending">
{pendingInvoices} {pendingInvoices}
</p> </p>
<p className="text-xs text-gray-500 dark:text-gray-400"> <p className="client-stat-label">Pending</p>
Pending
</p>
</div> </div>
</div> </div>
</CardContent> </CardContent>
@@ -203,7 +183,7 @@ export default async function ClientDetailPage({
{/* Recent Invoices */} {/* Recent Invoices */}
{client.invoices && client.invoices.length > 0 && ( {client.invoices && client.invoices.length > 0 && (
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm dark:bg-gray-800/80"> <Card className="card-primary">
<CardHeader> <CardHeader>
<CardTitle className="text-lg dark:text-white"> <CardTitle className="text-lg dark:text-white">
Recent Invoices Recent Invoices
@@ -212,20 +192,17 @@ export default async function ClientDetailPage({
<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 <div key={invoice.id} className="invoice-item">
key={invoice.id}
className="flex items-center justify-between rounded-lg bg-gray-50 p-3 dark:bg-gray-700"
>
<div> <div>
<p className="text-sm font-medium dark:text-white"> <p className="invoice-item-title">
{invoice.invoiceNumber} {invoice.invoiceNumber}
</p> </p>
<p className="text-xs text-gray-500 dark:text-gray-400"> <p className="invoice-item-date">
{formatDate(invoice.issueDate)} {formatDate(invoice.issueDate)}
</p> </p>
</div> </div>
<div className="text-right"> <div className="text-right">
<p className="text-sm font-medium dark:text-white"> <p className="invoice-item-amount">
{formatCurrency(invoice.totalAmount)} {formatCurrency(invoice.totalAmount)}
</p> </p>
<Badge <Badge

View File

@@ -1,64 +0,0 @@
"use client";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "~/components/ui/dropdown-menu";
import { Button } from "~/components/ui/button";
import {
MoreHorizontal,
Edit,
Copy,
Send,
Trash2,
} from "lucide-react";
interface InvoiceActionsDropdownProps {
invoiceId: string;
}
export function InvoiceActionsDropdown({ invoiceId }: InvoiceActionsDropdownProps) {
const handleSendClick = () => {
const sendButton = document.querySelector(
"[data-testid='send-invoice-button']",
) as HTMLButtonElement;
sendButton?.click();
};
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="icon"
className="border-0 shadow-sm"
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuItem>
<Edit className="mr-2 h-4 w-4" />
Edit Invoice
</DropdownMenuItem>
<DropdownMenuItem>
<Copy className="mr-2 h-4 w-4" />
Duplicate
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleSendClick}>
<Send className="mr-2 h-4 w-4" />
Send to Client
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="text-red-600">
<Trash2 className="mr-2 h-4 w-4" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -66,7 +66,7 @@ const columns: ColumnDef<InvoiceItem>[] = [
accessorKey: "amount", accessorKey: "amount",
header: "Amount", header: "Amount",
cell: ({ row }) => ( cell: ({ row }) => (
<div className="text-right font-medium text-emerald-600"> <div className="text-icon-emerald text-right font-medium">
{formatCurrency(row.getValue("amount"))} {formatCurrency(row.getValue("amount"))}
</div> </div>
), ),

View File

@@ -85,7 +85,7 @@ async function InvoiceContent({ invoiceId }: { invoiceId: string }) {
{/* Left Column */} {/* Left Column */}
<div className="space-y-6 lg:col-span-2"> <div className="space-y-6 lg:col-span-2">
{/* Invoice Header */} {/* Invoice Header */}
<Card className="shadow-sm"> <Card className="card-primary">
<CardContent className="p-4 sm:p-6"> <CardContent className="p-4 sm:p-6">
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-start justify-between gap-6"> <div className="flex items-start justify-between gap-6">
@@ -120,7 +120,7 @@ async function InvoiceContent({ invoiceId }: { invoiceId: string }) {
{/* Overdue Alert */} {/* Overdue Alert */}
{isOverdue && ( {isOverdue && (
<Card className="border-destructive/20 bg-destructive/5 shadow-sm"> <Card className="border-destructive/20 bg-destructive/5 card-secondary">
<CardContent className="p-4"> <CardContent className="p-4">
<div className="text-destructive flex items-center gap-3"> <div className="text-destructive flex items-center gap-3">
<AlertTriangle className="h-5 w-5 flex-shrink-0" /> <AlertTriangle className="h-5 w-5 flex-shrink-0" />
@@ -143,7 +143,7 @@ async function InvoiceContent({ invoiceId }: { invoiceId: string }) {
{/* Client & Business Info */} {/* Client & Business Info */}
<div className="grid gap-4 sm:grid-cols-2"> <div className="grid gap-4 sm:grid-cols-2">
{/* Client Information */} {/* Client Information */}
<Card className="shadow-sm"> <Card className="card-primary">
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<User className="h-5 w-5" /> <User className="h-5 w-5" />
@@ -215,7 +215,7 @@ async function InvoiceContent({ invoiceId }: { invoiceId: string }) {
{/* Business Information */} {/* Business Information */}
{invoice.business && ( {invoice.business && (
<Card className="shadow-sm"> <Card className="card-primary">
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<Building className="h-5 w-5" /> <Building className="h-5 w-5" />
@@ -258,7 +258,7 @@ async function InvoiceContent({ invoiceId }: { invoiceId: string }) {
</div> </div>
{/* Invoice Items */} {/* Invoice Items */}
<Card className="shadow-sm"> <Card className="card-primary">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<FileText className="h-5 w-5" /> <FileText className="h-5 w-5" />
@@ -267,31 +267,34 @@ async function InvoiceContent({ invoiceId }: { invoiceId: string }) {
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
{invoice.items.map((item) => ( {invoice.items.map((item) => (
<div key={item.id} className="space-y-3 rounded-lg border p-4"> <Card key={item.id} className="card-secondary">
<div className="flex items-start justify-between gap-4"> <CardContent className="py-2">
<div className="min-w-0 flex-1"> <div className="flex items-start justify-between gap-4">
<p className="text-foreground mb-2 text-base font-medium"> <div className="min-w-0 flex-1">
{item.description} <p className="text-foreground mb-2 text-base font-medium">
</p> {item.description}
<div className="text-muted-foreground space-y-1 text-sm sm:space-y-0"> </p>
<span className="sm:inline"> <div className="text-muted-foreground text-sm">
{formatDate(item.date)} <span className="inline whitespace-nowrap">
</span> {formatDate(item.date).replace(/ /g, "\u00A0")}
<span className="block sm:inline sm:before:content-['_•_']"> </span>
{item.hours} hours <span className="inline whitespace-nowrap before:mx-2 before:content-['_|_']">
</span> {item.hours.toString().replace(/ /g, "\u00A0")}
<span className="block sm:inline sm:before:content-['_•_']"> &nbsp;hours
@ ${item.rate}/hr </span>
</span> <span className="inline whitespace-nowrap before:mx-2 before:content-['_|_']">
@&nbsp;${item.rate}/hr
</span>
</div>
</div>
<div className="flex-shrink-0 text-right">
<p className="text-primary text-lg font-semibold">
{formatCurrency(item.amount)}
</p>
</div> </div>
</div> </div>
<div className="flex-shrink-0 text-right"> </CardContent>
<p className="text-primary text-lg font-semibold"> </Card>
{formatCurrency(item.amount)}
</p>
</div>
</div>
</div>
))} ))}
{/* Totals */} {/* Totals */}
@@ -327,7 +330,7 @@ async function InvoiceContent({ invoiceId }: { invoiceId: string }) {
{/* Notes */} {/* Notes */}
{invoice.notes && ( {invoice.notes && (
<Card className="shadow-sm"> <Card className="card-primary">
<CardHeader> <CardHeader>
<CardTitle>Notes</CardTitle> <CardTitle>Notes</CardTitle>
</CardHeader> </CardHeader>
@@ -342,7 +345,7 @@ async function InvoiceContent({ invoiceId }: { invoiceId: string }) {
{/* Right Column - Actions */} {/* Right Column - Actions */}
<div className="space-y-6"> <div className="space-y-6">
<Card className="sticky top-6 shadow-sm"> <Card className="card-primary sticky top-6">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<Check className="h-5 w-5" /> <Check className="h-5 w-5" />

View File

@@ -64,7 +64,7 @@ function ImportStats() {
return ( return (
<Card <Card
key={stat.title} key={stat.title}
className="border-0 shadow-md transition-shadow hover:shadow-lg" className="card-primary transition-shadow hover:shadow-lg"
> >
<CardContent className="p-6"> <CardContent className="p-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@@ -92,19 +92,19 @@ function ImportStats() {
// File Upload Component // File Upload Component
function FileUploadArea() { function FileUploadArea() {
return ( return (
<Card className="border-0 shadow-lg"> <Card className="card-primary">
<CardHeader className="border-b"> <CardHeader className="border-b">
<CardTitle className="flex items-center gap-2 text-lg"> <CardTitle className="card-title-secondary">
<Upload className="h-5 w-5 text-emerald-600" /> <Upload className="text-icon-emerald h-5 w-5" />
Upload CSV File Upload CSV File
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="p-8"> <CardContent className="p-8">
<div className="mx-auto max-w-xl"> <div className="mx-auto max-w-xl">
{/* Drop Zone */} {/* Drop Zone */}
<div className="rounded-lg border-2 border-dashed border-emerald-300 bg-emerald-50/50 p-12 text-center transition-colors hover:border-emerald-400 hover:bg-emerald-50 dark:border-emerald-700 dark:bg-emerald-900/10 dark:hover:bg-emerald-900/20"> <div className="bg-upload-zone">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-emerald-100 dark:bg-emerald-900/30"> <div className="bg-brand-muted mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full">
<Upload className="h-8 w-8 text-emerald-600" /> <Upload className="text-icon-emerald h-8 w-8" />
</div> </div>
<h3 className="mb-2 text-lg font-semibold"> <h3 className="mb-2 text-lg font-semibold">
Drop your CSV file here Drop your CSV file here
@@ -112,10 +112,7 @@ function FileUploadArea() {
<p className="text-muted-foreground mb-4"> <p className="text-muted-foreground mb-4">
or click to browse and select a file or click to browse and select a file
</p> </p>
<Button <Button type="button" className="btn-brand-primary">
type="button"
className="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700"
>
<Upload className="mr-2 h-4 w-4" /> <Upload className="mr-2 h-4 w-4" />
Choose File Choose File
</Button> </Button>
@@ -128,11 +125,11 @@ function FileUploadArea() {
<div className="mt-6 hidden"> <div className="mt-6 hidden">
<div className="mb-2 flex items-center justify-between"> <div className="mb-2 flex items-center justify-between">
<span className="text-sm font-medium">Uploading...</span> <span className="text-sm font-medium">Uploading...</span>
<span className="text-sm text-emerald-600">75%</span> <span className="text-icon-emerald text-sm">75%</span>
</div> </div>
<div className="h-2 overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700"> <div className="bg-progress-track">
<div <div
className="h-full bg-gradient-to-r from-emerald-600 to-teal-600 transition-all duration-300" className="bg-brand-gradient h-full transition-all duration-300"
style={{ width: "75%" }} style={{ width: "75%" }}
></div> ></div>
</div> </div>
@@ -148,16 +145,16 @@ function FormatInstructions() {
return ( return (
<div className="grid gap-6 lg:grid-cols-2"> <div className="grid gap-6 lg:grid-cols-2">
{/* Required Format */} {/* Required Format */}
<Card className="border-0 shadow-lg"> <Card className="card-primary">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2 text-lg"> <CardTitle className="card-title-info">
<FileText className="h-5 w-5 text-blue-600" /> <FileText className="text-icon-blue h-5 w-5" />
Required CSV Format Required CSV Format
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="rounded-lg bg-gray-50 p-4 dark:bg-gray-800/50"> <div className="bg-muted-subtle rounded-lg p-4">
<p className="font-mono text-sm text-gray-700 dark:text-gray-300"> <p className="text-secondary font-mono text-sm">
client_name,client_email,invoice_number,issue_date,due_date,description,hours,rate,tax_rate client_name,client_email,invoice_number,issue_date,due_date,description,hours,rate,tax_rate
</p> </p>
</div> </div>
@@ -176,9 +173,7 @@ function FormatInstructions() {
{ field: "rate", desc: "Hourly rate (decimal)" }, { field: "rate", desc: "Hourly rate (decimal)" },
].map((col) => ( ].map((col) => (
<div key={col.field} className="flex items-start gap-3"> <div key={col.field} className="flex items-start gap-3">
<Badge variant="outline" className="text-xs"> <Badge className="badge-outline text-xs">{col.field}</Badge>
{col.field}
</Badge>
<span className="text-muted-foreground text-sm"> <span className="text-muted-foreground text-sm">
{col.desc} {col.desc}
</span> </span>
@@ -190,25 +185,19 @@ function FormatInstructions() {
<div className="pt-2"> <div className="pt-2">
<h4 className="mb-2 font-semibold">Optional Columns:</h4> <h4 className="mb-2 font-semibold">Optional Columns:</h4>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<Badge variant="secondary" className="text-xs"> <Badge className="badge-secondary text-xs">tax_rate</Badge>
tax_rate <Badge className="badge-secondary text-xs">notes</Badge>
</Badge> <Badge className="badge-secondary text-xs">client_phone</Badge>
<Badge variant="secondary" className="text-xs">
notes
</Badge>
<Badge variant="secondary" className="text-xs">
client_phone
</Badge>
</div> </div>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
{/* Sample Data & Download */} {/* Sample Data & Download */}
<Card className="border-0 shadow-lg"> <Card className="card-primary">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2 text-lg"> <CardTitle className="card-title-secondary">
<Download className="h-5 w-5 text-green-600" /> <Download className="text-icon-green h-5 w-5" />
Sample Template Sample Template
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@@ -218,14 +207,12 @@ function FormatInstructions() {
for importing invoices. for importing invoices.
</p> </p>
<div className="rounded-lg bg-green-50 p-4 dark:bg-green-900/20"> <div className="bg-green-subtle rounded-lg p-4">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<Info className="mt-0.5 h-5 w-5 text-green-600" /> <Info className="text-icon-green mt-0.5 h-5 w-5" />
<div> <div>
<p className="text-sm font-medium text-green-800 dark:text-green-400"> <p className="text-success text-sm font-medium">Pro Tip</p>
Pro Tip <p className="text-success text-sm">
</p>
<p className="text-sm text-green-700 dark:text-green-300">
The template includes sample data and formatting examples to The template includes sample data and formatting examples to
help you get started quickly. help you get started quickly.
</p> </p>
@@ -249,8 +236,8 @@ function FormatInstructions() {
<div className="space-y-2"> <div className="space-y-2">
<h4 className="text-sm font-semibold">Sample Row:</h4> <h4 className="text-sm font-semibold">Sample Row:</h4>
<div className="rounded-lg bg-gray-50 p-3 dark:bg-gray-800/50"> <div className="bg-muted-subtle rounded-lg p-3">
<p className="font-mono text-xs break-all text-gray-600 dark:text-gray-400"> <p className="text-muted font-mono text-xs break-all">
"Acme "Acme
Corp","john@acme.com","INV-001","2024-01-15","2024-02-14","Web Corp","john@acme.com","INV-001","2024-01-15","2024-02-14","Web
development work","40","75.00","8.5" development work","40","75.00","8.5"
@@ -266,10 +253,10 @@ function FormatInstructions() {
// Important Notes Section // Important Notes Section
function ImportantNotes() { function ImportantNotes() {
return ( return (
<Card className="border-0 border-l-4 border-l-amber-500 shadow-lg"> <Card className="card-primary border-l-4 border-l-amber-500">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2 text-lg"> <CardTitle className="card-title-warning">
<AlertCircle className="h-5 w-5 text-amber-600" /> <AlertCircle className="text-icon-amber h-5 w-5" />
Important Notes Important Notes
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@@ -331,7 +318,7 @@ function ImportHistory() {
const getStatusBadge = (status: string) => { const getStatusBadge = (status: string) => {
if (status === "completed") { if (status === "completed") {
return ( return (
<Badge className="bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400"> <Badge className="badge-success">
<CheckCircle className="mr-1 h-3 w-3" /> <CheckCircle className="mr-1 h-3 w-3" />
Completed Completed
</Badge> </Badge>
@@ -339,7 +326,7 @@ function ImportHistory() {
} }
if (status === "processing") { if (status === "processing") {
return ( return (
<Badge className="bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"> <Badge className="badge-features">
<RefreshCw className="mr-1 h-3 w-3" /> <RefreshCw className="mr-1 h-3 w-3" />
Processing Processing
</Badge> </Badge>
@@ -354,10 +341,10 @@ function ImportHistory() {
}; };
return ( return (
<Card className="border-0 shadow-lg"> <Card className="card-primary">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2 text-lg"> <CardTitle className="card-title-purple">
<FileText className="h-5 w-5 text-purple-600" /> <FileText className="text-icon-purple h-5 w-5" />
Recent Imports Recent Imports
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@@ -382,8 +369,8 @@ function ImportHistory() {
> >
<td className="p-4"> <td className="p-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="rounded-lg bg-purple-100 p-2 dark:bg-purple-900/30"> <div className="icon-bg-purple-muted">
<FileSpreadsheet className="h-4 w-4 text-purple-600" /> <FileSpreadsheet className="text-icon-purple h-4 w-4" />
</div> </div>
<span className="font-medium">{item.filename}</span> <span className="font-medium">{item.filename}</span>
</div> </div>
@@ -397,7 +384,7 @@ function ImportHistory() {
</td> </td>
<td className="p-4 text-right"> <td className="p-4 text-right">
{item.errors > 0 ? ( {item.errors > 0 ? (
<span className="text-red-600">{item.errors}</span> <span className="status-text-error">{item.errors}</span>
) : ( ) : (
<span className="text-muted-foreground">0</span> <span className="text-muted-foreground">0</span>
)} )}
@@ -443,7 +430,7 @@ export default async function ImportPage() {
fallback={ fallback={
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4"> <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{[...Array(4)].map((_, i) => ( {[...Array(4)].map((_, i) => (
<Card key={i} className="border-0 shadow-md"> <Card key={i} className="card-primary">
<CardContent className="p-6"> <CardContent className="p-6">
<div className="animate-pulse"> <div className="animate-pulse">
<div className="bg-muted mb-2 h-4 w-1/2 rounded"></div> <div className="bg-muted mb-2 h-4 w-1/2 rounded"></div>

View File

@@ -94,7 +94,7 @@ function InvoiceItemCard({
}; };
return ( return (
<Card className="border-border/50 border p-3 shadow-sm"> <Card className="card-secondary">
<div className="space-y-3"> <div className="space-y-3">
{/* Header with item number and delete */} {/* Header with item number and delete */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@@ -106,7 +106,7 @@ function InvoiceItemCard({
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="h-6 w-6 p-0 text-red-500 hover:text-red-700" className="text-icon-red hover:text-error h-6 w-6 p-0"
> >
<Trash2 className="h-3 w-3" /> <Trash2 className="h-3 w-3" />
</Button> </Button>
@@ -123,7 +123,7 @@ function InvoiceItemCard({
<AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
onClick={() => onDelete(index)} onClick={() => onDelete(index)}
className="bg-red-600 hover:bg-red-700" className="btn-danger"
> >
Delete Delete
</AlertDialogAction> </AlertDialogAction>
@@ -142,7 +142,7 @@ function InvoiceItemCard({
/> />
{/* Date, Hours, Rate, Amount in compact grid */} {/* Date, Hours, Rate, Amount in compact grid */}
<div className="grid grid-cols-2 gap-2 text-sm sm:grid-cols-4"> <div className="grid grid-cols-2 gap-2 text-sm md:grid-cols-4">
<div className="space-y-1"> <div className="space-y-1">
<Label className="text-xs font-medium">Date</Label> <Label className="text-xs font-medium">Date</Label>
<DatePicker <DatePicker
@@ -150,7 +150,8 @@ function InvoiceItemCard({
onDateChange={(date) => onDateChange={(date) =>
handleFieldChange("date", date ?? new Date()) handleFieldChange("date", date ?? new Date())
} }
className="[&>button]:h-8 [&>button]:text-xs" size="sm"
className="w-full"
/> />
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
@@ -161,7 +162,6 @@ function InvoiceItemCard({
min={0} min={0}
step={0.25} step={0.25}
placeholder="0" placeholder="0"
className="text-xs"
/> />
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
@@ -173,13 +173,12 @@ function InvoiceItemCard({
step={0.25} step={0.25}
placeholder="0.00" placeholder="0.00"
prefix="$" prefix="$"
className="text-xs"
/> />
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<Label className="text-xs font-medium">Amount</Label> <Label className="text-xs font-medium">Amount</Label>
<div className="bg-muted/30 flex h-8 items-center rounded-md border px-2"> <div className="bg-muted/30 flex h-8 items-center rounded-md border px-2">
<span className="font-mono text-xs font-medium text-emerald-600"> <span className="amount-primary">
${(item.hours * item.rate).toFixed(2)} ${(item.hours * item.rate).toFixed(2)}
</span> </span>
</div> </div>
@@ -377,9 +376,9 @@ export default function NewInvoicePage() {
description="Loading form data..." description="Loading form data..."
variant="gradient" variant="gradient"
/> />
<Card className="shadow-xl"> <Card className="card-primary">
<CardContent className="flex items-center justify-center p-8"> <CardContent className="flex items-center justify-center p-8">
<Loader2 className="h-8 w-8 animate-spin text-emerald-600" /> <Loader2 className="text-icon-emerald h-8 w-8 animate-spin" />
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
@@ -394,25 +393,25 @@ export default function NewInvoicePage() {
variant="gradient" variant="gradient"
> >
<Link href="/dashboard/invoices"> <Link href="/dashboard/invoices">
<Button variant="outline" size="sm" className="w-full sm:w-auto"> <Button variant="outline" size="sm" className="w-full md:w-auto">
<ArrowLeft className="mr-2 h-4 w-4" /> <ArrowLeft className="mr-2 h-4 w-4" />
<span className="hidden sm:inline">Back to Invoices</span> <span className="hidden md:inline">Back to Invoices</span>
<span className="sm:hidden">Back</span> <span className="md:hidden">Back</span>
</Button> </Button>
</Link> </Link>
</PageHeader> </PageHeader>
<div className="space-y-6"> <div className="space-y-6">
{/* Invoice Header */} {/* Invoice Header */}
<Card className="shadow-lg"> <Card className="card-primary">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="card-title-secondary">
<FileText className="h-5 w-5 text-emerald-600" /> <FileText className="text-icon-emerald h-5 w-5" />
Invoice Details Invoice Details
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3"> <div className="grid grid-cols-1 gap-4 md:grid-cols-3">
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-sm font-medium">Invoice Number</Label> <Label className="text-sm font-medium">Invoice Number</Label>
<div className="bg-muted/30 flex h-10 items-center rounded-md border px-3"> <div className="bg-muted/30 flex h-10 items-center rounded-md border px-3">
@@ -453,15 +452,15 @@ export default function NewInvoicePage() {
</Card> </Card>
{/* Business & Client */} {/* Business & Client */}
<Card className="shadow-lg"> <Card className="card-primary">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="card-title-secondary">
<Building className="h-5 w-5 text-emerald-600" /> <Building className="text-icon-emerald h-5 w-5" />
Business & Client Business & Client
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2"> <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-sm font-medium">From Business</Label> <Label className="text-sm font-medium">From Business</Label>
<div className="relative"> <div className="relative">
@@ -484,7 +483,7 @@ export default function NewInvoicePage() {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span>{business.name}</span> <span>{business.name}</span>
{business.isDefault && ( {business.isDefault && (
<Badge variant="secondary" className="text-xs"> <Badge className="badge-secondary text-xs">
Default Default
</Badge> </Badge>
)} )}
@@ -495,11 +494,11 @@ export default function NewInvoicePage() {
</Select> </Select>
</div> </div>
{(!businesses || businesses.length === 0) && ( {(!businesses || businesses.length === 0) && (
<p className="text-sm text-red-600"> <p className="text-icon-red text-sm">
No businesses found.{" "} No businesses found.{" "}
<Link <Link
href="/dashboard/businesses/new" href="/dashboard/businesses/new"
className="underline hover:text-red-700" className="link-secondary"
> >
Create one first Create one first
</Link> </Link>
@@ -551,7 +550,7 @@ export default function NewInvoicePage() {
</Card> </Card>
{/* Line Items */} {/* Line Items */}
<Card className="shadow-lg"> <Card className="card-primary">
<CardHeader> <CardHeader>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
@@ -565,8 +564,8 @@ export default function NewInvoicePage() {
size="sm" size="sm"
className="shrink-0" className="shrink-0"
> >
<Plus className="h-4 w-4 sm:mr-2" /> <Plus className="h-4 w-4 md:mr-2" />
<span className="hidden sm:inline">Add Item</span> <span className="hidden md:inline">Add Item</span>
</Button> </Button>
</div> </div>
</CardHeader> </CardHeader>
@@ -585,7 +584,7 @@ export default function NewInvoicePage() {
</Card> </Card>
{/* Tax & Totals */} {/* Tax & Totals */}
<Card className="shadow-lg"> <Card className="card-primary">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<DollarSign className="h-5 w-5 text-emerald-600" /> <DollarSign className="h-5 w-5 text-emerald-600" />
@@ -595,22 +594,25 @@ export default function NewInvoicePage() {
<CardContent className="space-y-6"> <CardContent className="space-y-6">
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2"> <div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<Label className="text-sm font-medium">Tax Rate (%)</Label> <div className="space-y-2 md:col-span-1">
<NumberInput <Label className="text-sm font-medium">Tax Rate (%)</Label>
value={formData.taxRate} <NumberInput
onChange={(value) => value={formData.taxRate}
setFormData({ onChange={(value) =>
...formData, setFormData({
taxRate: value, ...formData,
}) taxRate: value,
} })
min={0} }
max={100} min={0}
step={0.01} max={100}
placeholder="0.00" step={0.01}
suffix="%" placeholder="0.00"
/> suffix="%"
width="full"
/>
</div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
@@ -659,21 +661,21 @@ export default function NewInvoicePage() {
{/* Action Buttons */} {/* Action Buttons */}
<div <div
ref={footerRef} ref={footerRef}
className="flex flex-col gap-3 border-t pt-6 sm:flex-row sm:justify-between" className="flex flex-col gap-3 border-t pt-6 md:flex-row md:justify-between"
> >
<Link href="/dashboard/invoices"> <Link href="/dashboard/invoices">
<Button variant="outline" className="w-full sm:w-auto"> <Button variant="outline" className="w-full md:w-auto">
<ArrowLeft className="mr-2 h-4 w-4" /> <ArrowLeft className="mr-2 h-4 w-4" />
Cancel Cancel
</Button> </Button>
</Link> </Link>
<div className="flex flex-col gap-3 sm:flex-row sm:items-center"> <div className="flex flex-col gap-3 md:flex-row md:items-center">
<Button <Button
onClick={handleSaveDraft} onClick={handleSaveDraft}
disabled={isLoading || !isFormValid()} disabled={isLoading || !isFormValid()}
variant="outline" variant="outline"
className="w-full sm:w-auto" className="w-full md:w-auto"
> >
{isLoading ? ( {isLoading ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-2 h-4 w-4 animate-spin" />
@@ -685,7 +687,7 @@ export default function NewInvoicePage() {
<Button <Button
onClick={handleCreateInvoice} onClick={handleCreateInvoice}
disabled={isLoading || !isFormValid()} disabled={isLoading || !isFormValid()}
className="w-full bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700 sm:w-auto" className="btn-brand-primary w-full md:w-auto"
> >
{isLoading ? ( {isLoading ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-2 h-4 w-4 animate-spin" />
@@ -708,8 +710,8 @@ export default function NewInvoicePage() {
className="border-border/40 hover:bg-accent/50" className="border-border/40 hover:bg-accent/50"
size="sm" size="sm"
> >
<ArrowLeft className="h-4 w-4 sm:mr-2" /> <ArrowLeft className="h-4 w-4 md:mr-2" />
<span className="hidden sm:inline">Cancel</span> <span className="hidden md:inline">Cancel</span>
</Button> </Button>
</Link> </Link>
<Button <Button
@@ -720,24 +722,24 @@ export default function NewInvoicePage() {
size="sm" size="sm"
> >
{isLoading ? ( {isLoading ? (
<Loader2 className="h-4 w-4 animate-spin sm:mr-2" /> <Loader2 className="h-4 w-4 animate-spin md:mr-2" />
) : ( ) : (
<Save className="h-4 w-4 sm:mr-2" /> <Save className="h-4 w-4 md:mr-2" />
)} )}
<span className="hidden sm:inline">Save Draft</span> <span className="hidden md:inline">Save Draft</span>
</Button> </Button>
<Button <Button
onClick={handleCreateInvoice} onClick={handleCreateInvoice}
disabled={isLoading || !isFormValid()} disabled={isLoading || !isFormValid()}
className="bg-gradient-to-r from-emerald-600 to-teal-600 shadow-md transition-all duration-200 hover:from-emerald-700 hover:to-teal-700 hover:shadow-lg" className="btn-brand-primary shadow-md"
size="sm" size="sm"
> >
{isLoading ? ( {isLoading ? (
<Loader2 className="h-4 w-4 animate-spin sm:mr-2" /> <Loader2 className="h-4 w-4 animate-spin md:mr-2" />
) : ( ) : (
<Send className="h-4 w-4 sm:mr-2" /> <Send className="h-4 w-4 md:mr-2" />
)} )}
<span className="hidden sm:inline">Create Invoice</span> <span className="hidden md:inline">Create Invoice</span>
</Button> </Button>
</FloatingActionBar> </FloatingActionBar>
</div> </div>

View File

@@ -28,10 +28,7 @@ export default async function InvoicesPage() {
<span>Import CSV</span> <span>Import CSV</span>
</Link> </Link>
</Button> </Button>
<Button <Button asChild className="btn-brand-primary shadow-md">
asChild
className="bg-gradient-to-r from-emerald-600 to-teal-600 shadow-md transition-all duration-200 hover:from-emerald-700 hover:to-teal-700 hover:shadow-lg"
>
<Link href="/dashboard/invoices/new"> <Link href="/dashboard/invoices/new">
<Plus className="mr-2 h-5 w-5" /> <Plus className="mr-2 h-5 w-5" />
<span>Create Invoice</span> <span>Create Invoice</span>

View File

@@ -8,7 +8,7 @@ export default function DashboardLayout({
children: React.ReactNode; children: React.ReactNode;
}) { }) {
return ( return (
<div className="bg-cosmic-gradient bg-nebula-overlay relative min-h-screen"> <div className="floating-orbs relative min-h-screen">
<Navbar /> <Navbar />
<Sidebar /> <Sidebar />
{/* Mobile layout - no left margin */} {/* Mobile layout - no left margin */}

View File

@@ -42,50 +42,46 @@ async function DashboardStats() {
title: "Total Clients", title: "Total Clients",
value: totalClients.toString(), value: totalClients.toString(),
icon: Users, icon: Users,
color: "text-blue-600 dark:text-blue-400", color: "text-icon-blue",
bgColor: "bg-blue-100 dark:bg-blue-900/20", bgColor: "bg-brand-muted-blue",
}, },
{ {
title: "Total Invoices", title: "Total Invoices",
value: totalInvoices.toString(), value: totalInvoices.toString(),
icon: FileText, icon: FileText,
color: "text-emerald-600 dark:text-emerald-400", color: "text-icon-emerald",
bgColor: "bg-emerald-100 dark:bg-emerald-900/20", bgColor: "bg-brand-muted",
}, },
{ {
title: "Total Revenue", title: "Total Revenue",
value: `$${totalRevenue.toLocaleString("en-US", { minimumFractionDigits: 2 })}`, value: `$${totalRevenue.toLocaleString("en-US", { minimumFractionDigits: 2 })}`,
icon: DollarSign, icon: DollarSign,
color: "text-teal-600 dark:text-teal-400", color: "text-icon-teal",
bgColor: "bg-teal-100 dark:bg-teal-900/20", bgColor: "bg-brand-muted-teal",
}, },
{ {
title: "Pending Invoices", title: "Pending Invoices",
value: pendingInvoices.toString(), value: pendingInvoices.toString(),
icon: Calendar, icon: Calendar,
color: "text-amber-600 dark:text-amber-400", color: "text-icon-amber",
bgColor: "bg-amber-100 dark:bg-amber-900/20", bgColor: "bg-brand-muted-amber",
}, },
]; ];
return ( return (
<Card className="mb-4 border-0 shadow-sm"> <Card className="card-primary mb-4">
<CardContent className="p-4 py-0"> <CardContent className="p-4 py-0">
<div className="grid grid-cols-2 gap-4 md:grid-cols-4"> <div className="stats-grid">
{stats.map((stat) => { {stats.map((stat) => {
const Icon = stat.icon; const Icon = stat.icon;
return ( return (
<div key={stat.title} className="flex items-center space-x-3"> <div key={stat.title} className="stats-item">
<div className={`rounded-lg p-2 ${stat.bgColor}`}> <div className={`icon-bg-small ${stat.bgColor}`}>
<Icon className={`h-4 w-4 ${stat.color}`} /> <Icon className={`h-4 w-4 ${stat.color}`} />
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<p className="text-muted-foreground text-xs font-medium"> <p className="stats-label">{stat.title}</p>
{stat.title} <p className={`stats-value ${stat.color}`}>{stat.value}</p>
</p>
<p className={`text-lg font-bold ${stat.color}`}>
{stat.value}
</p>
</div> </div>
</div> </div>
); );
@@ -99,38 +95,27 @@ async function DashboardStats() {
// Quick Actions Component // Quick Actions Component
function QuickActions() { function QuickActions() {
return ( return (
<Card className="border-0 shadow-sm"> <Card className="card-secondary">
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-lg"> <CardTitle className="quick-action-title">
<Plus className="h-5 w-5 text-emerald-600" /> <Plus className="quick-action-icon" />
Quick Actions Quick Actions
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-3"> <CardContent className="space-y-3">
<Button <Button asChild className="btn-brand-primary w-full shadow-sm">
asChild
className="w-full bg-gradient-to-r from-emerald-600 to-teal-600 text-white shadow-sm hover:from-emerald-700 hover:to-teal-700"
>
<Link href="/dashboard/invoices/new"> <Link href="/dashboard/invoices/new">
<FileText className="mr-2 h-4 w-4" /> <FileText className="mr-2 h-4 w-4" />
Create Invoice Create Invoice
</Link> </Link>
</Button> </Button>
<Button <Button asChild variant="outline" className="w-full shadow-sm">
asChild
variant="outline"
className="w-full border-0 shadow-sm"
>
<Link href="/dashboard/clients/new"> <Link href="/dashboard/clients/new">
<Users className="mr-2 h-4 w-4" /> <Users className="mr-2 h-4 w-4" />
Add Client Add Client
</Link> </Link>
</Button> </Button>
<Button <Button asChild variant="outline" className="w-full shadow-sm">
asChild
variant="outline"
className="w-full border-0 shadow-sm"
>
<Link href="/dashboard/businesses/new"> <Link href="/dashboard/businesses/new">
<TrendingUp className="mr-2 h-4 w-4" /> <TrendingUp className="mr-2 h-4 w-4" />
Add Business Add Business
@@ -153,7 +138,7 @@ async function RecentActivity() {
if (recentInvoices.length === 0) { if (recentInvoices.length === 0) {
return ( return (
<Card className="border-0 shadow-sm"> <Card className="card-primary">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<Calendar className="h-5 w-5" /> <Calendar className="h-5 w-5" />
@@ -161,15 +146,12 @@ async function RecentActivity() {
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="py-8 text-center"> <div className="recent-activity-empty">
<FileText className="text-muted-foreground mx-auto mb-4 h-12 w-12" /> <FileText className="recent-activity-icon" />
<p className="text-muted-foreground"> <p className="recent-activity-text">
No invoices yet. Create your first invoice to get started! No invoices yet. Create your first invoice to get started!
</p> </p>
<Button <Button asChild className="btn-brand-primary mt-4">
asChild
className="mt-4 bg-gradient-to-r from-emerald-600 to-teal-600 text-white hover:from-emerald-700 hover:to-teal-700"
>
<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" />
Create Invoice Create Invoice
@@ -182,10 +164,10 @@ async function RecentActivity() {
} }
return ( return (
<Card className="border-0 shadow-sm"> <Card className="card-primary">
<CardHeader className="flex flex-row items-center justify-between"> <CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<Calendar className="text-muted-foreground h-5 w-5" /> <Calendar className="text-muted h-5 w-5" />
Recent Activity Recent Activity
</CardTitle> </CardTitle>
<Button variant="outline" size="sm" asChild> <Button variant="outline" size="sm" asChild>
@@ -196,34 +178,36 @@ async function RecentActivity() {
</Button> </Button>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-4"> <div className="space-y-3">
{recentInvoices.map((invoice) => ( {recentInvoices.map((invoice) => (
<div <Card key={invoice.id} className="card-secondary">
key={invoice.id} <CardContent className="p-4">
className="hover:bg-muted/50 flex items-center justify-between rounded-lg border p-4 transition-colors" <div className="flex items-center justify-between">
> <div className="flex items-center space-x-3">
<div className="flex items-center space-x-4"> <div className="activity-icon">
<div className="rounded-lg bg-emerald-100 p-2 dark:bg-emerald-900/20"> <FileText className="text-icon-emerald h-4 w-4" />
<FileText className="h-4 w-4 text-emerald-600 dark:text-emerald-400" /> </div>
<div>
<p className="font-medium">
Invoice #{invoice.invoiceNumber}
</p>
<p className="text-muted text-sm">
{invoice.client?.name} $
{invoice.totalAmount.toFixed(2)}
</p>
</div>
</div>
<div className="flex items-center space-x-3">
<StatusBadge status={invoice.status as StatusType} />
<Button variant="ghost" size="sm" asChild>
<Link href={`/dashboard/invoices/${invoice.id}`}>
<Eye className="h-4 w-4" />
</Link>
</Button>
</div>
</div> </div>
<div> </CardContent>
<p className="font-medium"> </Card>
Invoice #{invoice.invoiceNumber}
</p>
<p className="text-muted-foreground text-sm">
{invoice.client?.name} ${invoice.totalAmount.toFixed(2)}
</p>
</div>
</div>
<div className="flex items-center space-x-3">
<StatusBadge status={invoice.status as StatusType} />
<Button variant="ghost" size="sm" asChild>
<Link href={`/dashboard/invoices/${invoice.id}`}>
<Eye className="h-4 w-4" />
</Link>
</Button>
</div>
</div>
))} ))}
</div> </div>
</CardContent> </CardContent>

View File

@@ -262,11 +262,11 @@ export function SettingsContent() {
{/* Profile & Account Overview */} {/* Profile & Account Overview */}
<div className="grid gap-6 lg:grid-cols-2"> <div className="grid gap-6 lg:grid-cols-2">
{/* Profile Section */} {/* Profile Section */}
<Card className="shadow-lg"> <Card className="card-primary">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<div className="rounded-lg bg-emerald-100 p-2"> <div className="icon-bg-emerald">
<User className="h-5 w-5 text-emerald-600" /> <User className="text-icon-emerald h-5 w-5" />
</div> </div>
Profile Information Profile Information
</CardTitle> </CardTitle>
@@ -301,7 +301,7 @@ export function SettingsContent() {
<Button <Button
type="submit" type="submit"
disabled={updateProfileMutation.isPending} disabled={updateProfileMutation.isPending}
className="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700" className="btn-brand-primary"
> >
{updateProfileMutation.isPending {updateProfileMutation.isPending
? "Updating..." ? "Updating..."
@@ -312,11 +312,11 @@ export function SettingsContent() {
</Card> </Card>
{/* Data Overview */} {/* Data Overview */}
<Card className="shadow-lg"> <Card className="card-primary">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<div className="rounded-lg bg-blue-100 p-2"> <div className="icon-bg-info">
<Database className="h-5 w-5 text-blue-600" /> <Database className="text-icon-blue h-5 w-5" />
</div> </div>
Account Data Account Data
</CardTitle> </CardTitle>
@@ -329,23 +329,24 @@ export function SettingsContent() {
{dataStatItems.map((item) => { {dataStatItems.map((item) => {
const Icon = item.icon; const Icon = item.icon;
return ( return (
<div <Card key={item.label} className="card-secondary">
key={item.label} <CardContent className="py-2">
className="flex items-center justify-between rounded-lg border p-3" <div className="flex items-center justify-between">
> <div className="flex items-center gap-3">
<div className="flex items-center gap-3"> <div className={`rounded-lg p-2 ${item.bgColor}`}>
<div className={`rounded-lg p-2 ${item.bgColor}`}> <Icon className={`h-4 w-4 ${item.color}`} />
<Icon className={`h-4 w-4 ${item.color}`} /> </div>
<span className="font-medium">{item.label}</span>
</div>
<Badge
variant="secondary"
className="text-lg font-semibold"
>
{item.value}
</Badge>
</div> </div>
<span className="font-medium">{item.label}</span> </CardContent>
</div> </Card>
<Badge
variant="secondary"
className="text-lg font-semibold"
>
{item.value}
</Badge>
</div>
); );
})} })}
</div> </div>
@@ -354,11 +355,11 @@ export function SettingsContent() {
</div> </div>
{/* Data Management */} {/* Data Management */}
<Card className="shadow-lg"> <Card className="card-primary">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<div className="rounded-lg bg-indigo-100 p-2"> <div className="bg-indigo-subtle rounded-lg p-2">
<Shield className="h-5 w-5 text-indigo-600" /> <Shield className="text-icon-indigo h-5 w-5" />
</div> </div>
Data Management Data Management
</CardTitle> </CardTitle>
@@ -418,7 +419,7 @@ export function SettingsContent() {
disabled={ disabled={
!importData.trim() || importDataMutation.isPending !importData.trim() || importDataMutation.isPending
} }
className="bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700" className="btn-brand-primary"
> >
{importDataMutation.isPending {importDataMutation.isPending
? "Importing..." ? "Importing..."
@@ -444,11 +445,11 @@ export function SettingsContent() {
</Card> </Card>
{/* Danger Zone */} {/* Danger Zone */}
<Card className="shadow-lg"> <Card className="card-primary">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<div className="rounded-lg bg-red-100 p-2"> <div className="icon-bg-error">
<AlertTriangle className="h-5 w-5 text-red-600" /> <AlertTriangle className="text-icon-red h-5 w-5" />
</div> </div>
Data Management Data Management
</CardTitle> </CardTitle>
@@ -515,7 +516,7 @@ export function SettingsContent() {
deleteConfirmText !== "delete all my data" || deleteConfirmText !== "delete all my data" ||
deleteDataMutation.isPending deleteDataMutation.isPending
} }
className="bg-red-600 hover:bg-red-700" className="btn-danger"
> >
{deleteDataMutation.isPending {deleteDataMutation.isPending
? "Deleting..." ? "Deleting..."

View File

@@ -29,7 +29,7 @@ export default function RootLayout({
}: Readonly<{ children: React.ReactNode }>) { }: Readonly<{ children: React.ReactNode }>) {
return ( return (
<html lang="en" className={`${geist.variable} ${azeretMono.variable}`}> <html lang="en" className={`${geist.variable} ${azeretMono.variable}`}>
<body className="bg-gradient-dashboard bg-radial-overlay relative min-h-screen overflow-x-hidden font-sans antialiased"> <body className="relative min-h-screen overflow-x-hidden font-sans antialiased">
<TRPCReactProvider>{children}</TRPCReactProvider> <TRPCReactProvider>{children}</TRPCReactProvider>
<Toaster /> <Toaster />
</body> </body>

View File

@@ -20,25 +20,19 @@ import {
export default function HomePage() { export default function HomePage() {
return ( return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-emerald-50 dark:from-slate-900 dark:via-slate-800 dark:to-emerald-900"> <div className="bg-page-gradient min-h-screen">
<AuthRedirect /> <AuthRedirect />
{/* Navigation */} {/* Navigation */}
<nav className="sticky top-0 z-50 border-b bg-white/80 backdrop-blur-xl dark:border-slate-700 dark:bg-slate-900/80"> <nav className="nav-sticky">
<div className="container mx-auto px-4"> <div className="container mx-auto px-4">
<div className="flex h-14 items-center justify-between sm:h-16"> <div className="flex h-14 items-center justify-between sm:h-16">
<Logo /> <Logo />
<div className="hidden items-center space-x-6 md:flex"> <div className="hidden items-center space-x-6 md:flex">
<a <a href="#features" className="nav-link">
href="#features"
className="text-slate-600 transition-colors hover:text-slate-900 dark:text-slate-300 dark:hover:text-slate-100"
>
Features Features
</a> </a>
<a <a href="#pricing" className="nav-link">
href="#pricing"
className="text-slate-600 transition-colors hover:text-slate-900 dark:text-slate-300 dark:hover:text-slate-100"
>
Pricing Pricing
</a> </a>
</div> </div>
@@ -53,10 +47,7 @@ export default function HomePage() {
</Button> </Button>
</Link> </Link>
<Link href="/auth/register"> <Link href="/auth/register">
<Button <Button size="sm" className="btn-brand-primary">
size="sm"
className="bg-gradient-to-r from-emerald-600 to-teal-600 shadow-lg shadow-emerald-500/25 transition-all duration-300 hover:shadow-xl hover:shadow-emerald-500/30"
>
<span className="hidden sm:inline">Get Started Free</span> <span className="hidden sm:inline">Get Started Free</span>
<span className="sm:hidden">Start Free</span> <span className="sm:hidden">Start Free</span>
</Button> </Button>
@@ -67,44 +58,35 @@ export default function HomePage() {
</nav> </nav>
{/* Hero Section */} {/* Hero Section */}
<section className="relative overflow-hidden bg-gradient-to-br from-blue-50 via-emerald-50 to-teal-50 px-4 pt-12 pb-16 sm:pt-20 dark:from-slate-800 dark:via-emerald-900/20 dark:to-teal-900/20"> <section className="bg-hero-gradient relative overflow-hidden px-4 pt-12 pb-16 sm:pt-20">
{/* Background decoration */} {/* Background decoration */}
<div className="absolute inset-0"> <div className="hero-overlay"></div>
<div className="absolute top-0 left-1/4 h-96 w-96 rounded-full bg-gradient-to-r from-emerald-400/20 to-blue-400/20 blur-3xl dark:from-emerald-500/10 dark:to-blue-500/10"></div> <div className="hero-orb-1"></div>
<div className="absolute top-32 right-1/4 h-80 w-80 rounded-full bg-gradient-to-r from-teal-400/20 to-emerald-400/20 blur-3xl dark:from-teal-500/10 dark:to-emerald-500/10"></div> <div className="hero-orb-2"></div>
<div className="absolute bottom-0 left-1/2 h-64 w-64 rounded-full bg-gradient-to-r from-blue-400/20 to-purple-400/20 blur-3xl dark:from-blue-500/10 dark:to-purple-500/10"></div> <div className="hero-orb-3"></div>
</div>
<div className="relative container mx-auto text-center"> <div className="relative container mx-auto text-center">
<div className="mx-auto max-w-4xl"> <div className="mx-auto max-w-4xl">
<Badge <Badge className="badge-brand mb-4 sm:mb-6">
variant="secondary"
className="mb-4 border-emerald-200 bg-emerald-100 text-emerald-800 sm:mb-6 dark:border-emerald-800 dark:bg-emerald-900 dark:text-emerald-200"
>
<Sparkles className="mr-1 h-3 w-3" /> <Sparkles className="mr-1 h-3 w-3" />
100% Free Forever 100% Free Forever
</Badge> </Badge>
<h1 className="mb-4 text-4xl font-bold tracking-tight text-slate-900 sm:mb-6 sm:text-6xl lg:text-7xl dark:text-slate-100"> <h1 className="mb-4 text-4xl font-bold tracking-tight text-white sm:mb-6 sm:text-6xl lg:text-7xl">
Simple Invoicing for Simple Invoicing for
<span className="block bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-transparent"> <span className="block text-emerald-100">Freelancers</span>
Freelancers
</span>
</h1> </h1>
<p className="mx-auto mb-6 max-w-2xl text-lg leading-relaxed text-slate-600 sm:mb-8 sm:text-xl dark:text-slate-300"> <p className="mx-auto mb-6 max-w-2xl text-lg leading-relaxed text-emerald-100 sm:mb-8 sm:text-xl">
Create professional invoices, manage clients, and track payments. Create professional invoices, manage clients, and track payments.
Built specifically for freelancers and small businesses Built specifically for freelancers and small businesses
<span className="bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text font-semibold text-transparent"> <span className="font-semibold text-white">completely free</span>.
completely free
</span>
.
</p> </p>
<div className="flex flex-col items-center gap-3 sm:flex-row sm:justify-center sm:gap-4"> <div className="btn-group">
<Link href="/auth/register"> <Link href="/auth/register">
<Button <Button
size="lg" size="lg"
className="group w-full bg-gradient-to-r from-emerald-500 to-teal-500 px-6 py-3 text-base font-semibold shadow-xl shadow-emerald-500/30 transition-all duration-300 hover:from-emerald-600 hover:to-teal-600 hover:shadow-2xl hover:shadow-emerald-500/40 sm:w-auto sm:px-8 sm:py-4 sm:text-lg" className="btn-brand-secondary group w-full px-6 py-3 text-base font-semibold text-emerald-700 shadow-xl transition-all duration-300 sm:w-auto sm:px-8 sm:py-4 sm:text-lg"
> >
Start Free Start Free
<ArrowRight className="ml-2 h-4 w-4 transition-transform group-hover:translate-x-1 sm:h-5 sm:w-5" /> <ArrowRight className="ml-2 h-4 w-4 transition-transform group-hover:translate-x-1 sm:h-5 sm:w-5" />
@@ -114,7 +96,7 @@ export default function HomePage() {
<Button <Button
variant="outline" variant="outline"
size="lg" size="lg"
className="group w-full border-emerald-200 bg-white/50 px-6 py-3 text-base text-emerald-700 backdrop-blur-sm hover:border-emerald-300 hover:bg-emerald-50 sm:w-auto sm:px-8 sm:py-4 sm:text-lg dark:border-emerald-700 dark:bg-emerald-900/20 dark:text-emerald-300 dark:hover:bg-emerald-900/40" className="btn-brand-secondary group w-full px-6 py-3 text-base sm:w-auto sm:px-8 sm:py-4 sm:text-lg"
> >
See Features See Features
<ChevronRight className="ml-2 h-4 w-4 transition-transform group-hover:translate-x-1 sm:h-5 sm:w-5" /> <ChevronRight className="ml-2 h-4 w-4 transition-transform group-hover:translate-x-1 sm:h-5 sm:w-5" />
@@ -122,14 +104,14 @@ export default function HomePage() {
</Link> </Link>
</div> </div>
<div className="mt-8 flex flex-col items-center justify-center gap-2 text-sm text-slate-600 sm:mt-12 sm:flex-row sm:gap-6 dark:text-slate-400"> <div className="mt-8 flex flex-col items-center justify-center gap-2 text-sm text-emerald-200 sm:mt-12 sm:flex-row sm:gap-6">
{[ {[
"No credit card required", "No credit card required",
"Setup in 2 minutes", "Setup in 2 minutes",
"Cancel anytime", "Cancel anytime",
].map((text, i) => ( ].map((text, i) => (
<div key={i} className="flex items-center gap-2"> <div key={i} className="flex items-center gap-2">
<Check className="h-4 w-4 text-emerald-500" /> <Check className="h-4 w-4 text-emerald-300" />
<span className="text-center">{text}</span> <span className="text-center">{text}</span>
</div> </div>
))} ))}
@@ -141,20 +123,20 @@ export default function HomePage() {
{/* Features Section */} {/* Features Section */}
<section <section
id="features" id="features"
className="bg-gradient-to-br from-white via-blue-50/30 to-emerald-50/50 py-16 sm:py-24 dark:from-slate-900 dark:via-slate-800/50 dark:to-emerald-900/20" className="bg-features-gradient relative overflow-hidden py-16 sm:py-24"
> >
<div className="container mx-auto px-4"> {/* Floating background elements */}
<div className="floating-decoration-1"></div>
<div className="floating-decoration-2"></div>
<div className="relative container mx-auto px-4">
<div className="mb-12 text-center sm:mb-16"> <div className="mb-12 text-center sm:mb-16">
<Badge <Badge className="badge-features mb-4">
variant="secondary"
className="mb-4 border-blue-200 bg-blue-100 text-blue-800 dark:border-blue-800 dark:bg-blue-900 dark:text-blue-200"
>
<Zap className="mr-1 h-3 w-3" /> <Zap className="mr-1 h-3 w-3" />
Supercharged Features Supercharged Features
</Badge> </Badge>
<h2 className="mb-4 text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl lg:text-5xl dark:text-slate-100"> <h2 className="mb-4 text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl lg:text-5xl dark:text-slate-100">
Everything you need to Everything you need to
<span className="block bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-transparent"> <span className="text-brand-gradient block">
invoice professionally invoice professionally
</span> </span>
</h2> </h2>
@@ -166,9 +148,9 @@ export default function HomePage() {
<div className="grid gap-6 sm:gap-8 md:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-6 sm:gap-8 md:grid-cols-2 lg:grid-cols-3">
{/* Feature 1 */} {/* Feature 1 */}
<Card className="group bg-white/70 shadow-lg backdrop-blur-sm transition-all duration-300 hover:bg-white/90 hover:shadow-xl dark:bg-slate-800/50 dark:hover:bg-slate-800/70"> <Card className="card-floating group">
<CardContent className="p-6 sm:p-8"> <CardContent className="p-6 sm:p-8">
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-r from-emerald-500 to-teal-500 text-white shadow-lg"> <div className="icon-bg-brand mb-4">
<Rocket className="h-6 w-6" /> <Rocket className="h-6 w-6" />
</div> </div>
<h3 className="mb-3 text-xl font-bold text-slate-900 dark:text-slate-100"> <h3 className="mb-3 text-xl font-bold text-slate-900 dark:text-slate-100">
@@ -178,17 +160,17 @@ export default function HomePage() {
Start creating invoices immediately. No complicated setup or Start creating invoices immediately. No complicated setup or
configuration required. configuration required.
</p> </p>
<ul className="space-y-2 text-sm text-slate-600 dark:text-slate-400"> <ul className="feature-list">
<li className="flex items-center gap-2"> <li className="feature-item">
<Check className="h-4 w-4 text-emerald-500" /> <Check className="feature-check" />
Simple client management Simple client management
</li> </li>
<li className="flex items-center gap-2"> <li className="feature-item">
<Check className="h-4 w-4 text-emerald-500" /> <Check className="feature-check" />
Professional templates Professional templates
</li> </li>
<li className="flex items-center gap-2"> <li className="feature-item">
<Check className="h-4 w-4 text-emerald-500" /> <Check className="feature-check" />
Easy invoice sending Easy invoice sending
</li> </li>
</ul> </ul>
@@ -196,9 +178,9 @@ export default function HomePage() {
</Card> </Card>
{/* Feature 2 */} {/* Feature 2 */}
<Card className="group bg-white/70 shadow-lg backdrop-blur-sm transition-all duration-300 hover:bg-white/90 hover:shadow-xl dark:bg-slate-800/50 dark:hover:bg-slate-800/70"> <Card className="card-floating group">
<CardContent className="p-6 sm:p-8"> <CardContent className="p-6 sm:p-8">
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-r from-blue-500 to-indigo-500 text-white shadow-lg"> <div className="icon-bg-blue mb-4">
<BarChart3 className="h-6 w-6" /> <BarChart3 className="h-6 w-6" />
</div> </div>
<h3 className="mb-3 text-xl font-bold text-slate-900 dark:text-slate-100"> <h3 className="mb-3 text-xl font-bold text-slate-900 dark:text-slate-100">
@@ -208,17 +190,17 @@ export default function HomePage() {
Keep track of invoice status and monitor which clients have Keep track of invoice status and monitor which clients have
paid. paid.
</p> </p>
<ul className="space-y-2 text-sm text-slate-600 dark:text-slate-400"> <ul className="feature-list">
<li className="flex items-center gap-2"> <li className="feature-item">
<Check className="h-4 w-4 text-emerald-500" /> <Check className="feature-check" />
Invoice status tracking Invoice status tracking
</li> </li>
<li className="flex items-center gap-2"> <li className="feature-item">
<Check className="h-4 w-4 text-emerald-500" /> <Check className="feature-check" />
Payment history Payment history
</li> </li>
<li className="flex items-center gap-2"> <li className="feature-item">
<Check className="h-4 w-4 text-emerald-500" /> <Check className="feature-check" />
Overdue notifications Overdue notifications
</li> </li>
</ul> </ul>
@@ -226,9 +208,9 @@ export default function HomePage() {
</Card> </Card>
{/* Feature 3 */} {/* Feature 3 */}
<Card className="group bg-white/70 shadow-lg backdrop-blur-sm transition-all duration-300 hover:bg-white/90 hover:shadow-xl dark:bg-slate-800/50 dark:hover:bg-slate-800/70"> <Card className="card-floating group">
<CardContent className="p-6 sm:p-8"> <CardContent className="p-6 sm:p-8">
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-r from-purple-500 to-pink-500 text-white shadow-lg"> <div className="icon-bg-purple mb-4">
<Globe className="h-6 w-6" /> <Globe className="h-6 w-6" />
</div> </div>
<h3 className="mb-3 text-xl font-bold text-slate-900 dark:text-slate-100"> <h3 className="mb-3 text-xl font-bold text-slate-900 dark:text-slate-100">
@@ -237,17 +219,17 @@ export default function HomePage() {
<p className="mb-4 text-slate-600 dark:text-slate-300"> <p className="mb-4 text-slate-600 dark:text-slate-300">
Everything you need to look professional and get paid on time. Everything you need to look professional and get paid on time.
</p> </p>
<ul className="space-y-2 text-sm text-slate-600"> <ul className="feature-list">
<li className="flex items-center gap-2"> <li className="feature-item">
<Check className="h-4 w-4 text-emerald-500" /> <Check className="feature-check" />
PDF generation PDF generation
</li> </li>
<li className="flex items-center gap-2"> <li className="feature-item">
<Check className="h-4 w-4 text-emerald-500" /> <Check className="feature-check" />
Custom tax rates Custom tax rates
</li> </li>
<li className="flex items-center gap-2"> <li className="feature-item">
<Check className="h-4 w-4 text-emerald-500" /> <Check className="feature-check" />
Professional numbering Professional numbering
</li> </li>
</ul> </ul>
@@ -260,9 +242,12 @@ export default function HomePage() {
{/* Pricing Section */} {/* Pricing Section */}
<section <section
id="pricing" id="pricing"
className="bg-gradient-to-br from-emerald-50 via-teal-50 to-blue-50 py-16 sm:py-24 dark:from-emerald-900/20 dark:via-teal-900/20 dark:to-blue-900/20" className="bg-features-gradient relative overflow-hidden py-16 sm:py-24"
> >
<div className="container mx-auto px-4"> {/* Floating background elements */}
<div className="floating-decoration-1"></div>
<div className="floating-decoration-2"></div>
<div className="relative container mx-auto px-4">
<div className="mb-12 text-center sm:mb-16"> <div className="mb-12 text-center sm:mb-16">
<h2 className="mb-4 text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl lg:text-5xl dark:text-slate-100"> <h2 className="mb-4 text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl lg:text-5xl dark:text-slate-100">
Simple, transparent pricing Simple, transparent pricing
@@ -276,13 +261,11 @@ export default function HomePage() {
<div className="mx-auto max-w-md"> <div className="mx-auto max-w-md">
<Card className="relative border-2 border-emerald-500 bg-white/90 shadow-2xl backdrop-blur-sm dark:border-emerald-400 dark:bg-slate-800/90"> <Card className="relative border-2 border-emerald-500 bg-white/90 shadow-2xl backdrop-blur-sm dark:border-emerald-400 dark:bg-slate-800/90">
<div className="absolute -top-4 left-1/2 -translate-x-1/2"> <div className="absolute -top-4 left-1/2 -translate-x-1/2">
<Badge className="bg-emerald-500 px-6 py-1 text-white dark:bg-emerald-600"> <Badge className="badge-success px-6 py-1">Forever Free</Badge>
Forever Free
</Badge>
</div> </div>
<CardContent className="p-6 text-center sm:p-8"> <CardContent className="p-6 text-center sm:p-8">
<div className="mb-6"> <div className="mb-6">
<div className="mb-2 bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-5xl font-bold text-transparent sm:text-6xl"> <div className="text-brand-gradient mb-2 text-5xl font-bold sm:text-6xl">
$0 $0
</div> </div>
<div className="text-slate-600 dark:text-slate-400"> <div className="text-slate-600 dark:text-slate-400">
@@ -311,7 +294,10 @@ export default function HomePage() {
</div> </div>
<Link href="/auth/register"> <Link href="/auth/register">
<Button className="w-full bg-gradient-to-r from-emerald-500 to-teal-500 py-3 text-base font-semibold shadow-lg shadow-emerald-500/30 transition-all duration-300 hover:from-emerald-600 hover:to-teal-600 hover:shadow-xl hover:shadow-emerald-500/40 sm:text-lg"> <Button
variant="brand"
className="w-full py-3 text-base font-semibold sm:text-lg"
>
Get Started Now Get Started Now
</Button> </Button>
</Link> </Link>
@@ -326,12 +312,15 @@ export default function HomePage() {
</section> </section>
{/* Why Choose */} {/* Why Choose */}
<section className="bg-gradient-to-br from-white via-emerald-50/30 to-teal-50/50 py-16 sm:py-24 dark:from-slate-900 dark:via-emerald-900/10 dark:to-teal-900/10"> <section className="bg-features-gradient relative overflow-hidden py-16 sm:py-24">
<div className="container mx-auto px-4"> {/* Floating background elements */}
<div className="floating-decoration-1"></div>
<div className="floating-decoration-2"></div>
<div className="relative container mx-auto px-4">
<div className="mb-12 text-center sm:mb-16"> <div className="mb-12 text-center sm:mb-16">
<h2 className="mb-4 text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl lg:text-5xl dark:text-slate-100"> <h2 className="mb-4 text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl lg:text-5xl dark:text-slate-100">
Why freelancers Why freelancers
<span className="block bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-transparent"> <span className="text-brand-gradient block">
choose BeenVoice choose BeenVoice
</span> </span>
</h2> </h2>
@@ -339,7 +328,7 @@ export default function HomePage() {
<div className="grid gap-6 sm:gap-8 md:grid-cols-3"> <div className="grid gap-6 sm:gap-8 md:grid-cols-3">
<div className="text-center"> <div className="text-center">
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-r from-emerald-500 to-teal-500 text-white shadow-lg"> <div className="icon-bg-emerald mb-4 inline-flex h-12 w-12 items-center justify-center rounded-xl text-white shadow-lg">
<Zap className="h-6 w-6" /> <Zap className="h-6 w-6" />
</div> </div>
<h3 className="mb-3 text-xl font-bold text-slate-900 dark:text-slate-100"> <h3 className="mb-3 text-xl font-bold text-slate-900 dark:text-slate-100">
@@ -351,7 +340,7 @@ export default function HomePage() {
</p> </p>
</div> </div>
<div className="text-center"> <div className="text-center">
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-r from-blue-500 to-indigo-500 text-white shadow-lg"> <div className="icon-bg-blue mb-4 inline-flex h-12 w-12 items-center justify-center rounded-xl text-white shadow-lg">
<Shield className="h-6 w-6" /> <Shield className="h-6 w-6" />
</div> </div>
<h3 className="mb-3 text-xl font-bold text-slate-900 dark:text-slate-100"> <h3 className="mb-3 text-xl font-bold text-slate-900 dark:text-slate-100">
@@ -363,7 +352,7 @@ export default function HomePage() {
</p> </p>
</div> </div>
<div className="text-center"> <div className="text-center">
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-r from-purple-500 to-pink-500 text-white shadow-lg"> <div className="icon-bg-purple mb-4">
<Clock className="h-6 w-6" /> <Clock className="h-6 w-6" />
</div> </div>
<h3 className="mb-3 text-xl font-bold text-slate-900 dark:text-slate-100"> <h3 className="mb-3 text-xl font-bold text-slate-900 dark:text-slate-100">
@@ -379,11 +368,11 @@ export default function HomePage() {
</section> </section>
{/* CTA Section */} {/* CTA Section */}
<section className="relative overflow-hidden bg-gradient-to-br from-emerald-500 via-teal-600 to-blue-700 py-16 sm:py-24"> <section className="bg-hero-gradient relative overflow-hidden py-16 sm:py-24">
<div className="absolute inset-0 bg-gradient-to-br from-emerald-500/95 via-teal-600/95 to-blue-700/95"></div> <div className="hero-overlay"></div>
<div className="absolute top-10 left-10 h-64 w-64 rounded-full bg-gradient-to-r from-white/20 to-emerald-300/20 blur-3xl"></div> <div className="hero-orb-1"></div>
<div className="absolute right-10 bottom-10 h-80 w-80 rounded-full bg-gradient-to-r from-teal-300/15 to-blue-300/15 blur-3xl"></div> <div className="hero-orb-2"></div>
<div className="absolute top-1/2 left-1/2 h-96 w-96 -translate-x-1/2 -translate-y-1/2 rounded-full bg-gradient-to-r from-emerald-400/10 to-teal-400/10 blur-3xl"></div> <div className="hero-orb-3"></div>
<div className="relative container mx-auto px-4 text-center"> <div className="relative container mx-auto px-4 text-center">
<div className="mx-auto max-w-3xl"> <div className="mx-auto max-w-3xl">
@@ -402,7 +391,7 @@ export default function HomePage() {
<Button <Button
size="lg" size="lg"
variant="secondary" variant="secondary"
className="group w-full bg-white px-6 py-3 text-base font-semibold text-emerald-700 shadow-xl transition-all duration-300 hover:bg-gradient-to-r hover:from-white hover:to-emerald-50 hover:shadow-2xl sm:w-auto sm:px-8 sm:py-4 sm:text-lg" className="btn-brand-secondary group w-full px-6 py-3 text-base font-semibold text-emerald-700 shadow-xl transition-all duration-300 sm:w-auto sm:px-8 sm:py-4 sm:text-lg"
> >
Start Your Success Story Start Your Success Story
<Rocket className="ml-2 h-4 w-4 transition-transform group-hover:translate-x-1 sm:h-5 sm:w-5" /> <Rocket className="ml-2 h-4 w-4 transition-transform group-hover:translate-x-1 sm:h-5 sm:w-5" />
@@ -429,41 +418,29 @@ export default function HomePage() {
</section> </section>
{/* Footer */} {/* Footer */}
<footer className="border-t bg-gradient-to-br from-slate-50 to-emerald-50/30 py-8 sm:py-12 dark:border-slate-700 dark:from-slate-900 dark:to-emerald-900/10"> <footer className="bg-features-gradient border-t py-8 sm:py-12 dark:border-slate-700">
<div className="container mx-auto px-4"> <div className="container mx-auto px-4">
<div className="text-center"> <div className="text-center">
<Logo className="mx-auto mb-4" /> <Logo className="mx-auto mb-4" />
<p className="mb-4 text-sm text-slate-600 sm:mb-6 sm:text-base dark:text-slate-400"> <p className="text-muted mb-4 text-sm sm:mb-6 sm:text-base">
Simple invoicing for freelancers. Free, forever. Simple invoicing for freelancers. Free, forever.
</p> </p>
<div className="flex flex-wrap items-center justify-center gap-4 text-sm text-slate-600 sm:gap-6 dark:text-slate-400"> <div className="text-muted flex flex-wrap items-center justify-center gap-4 text-sm sm:gap-6">
<Link <Link href="/auth/signin" className="link-primary">
href="/auth/signin"
className="transition-colors hover:text-emerald-600 dark:hover:text-emerald-400"
>
Sign In Sign In
</Link> </Link>
<Link <Link href="/auth/register" className="link-primary">
href="/auth/register" Register
className="transition-colors hover:text-emerald-600 dark:hover:text-emerald-400"
>
Get Started
</Link> </Link>
<a <a href="#features" className="link-primary">
href="#features"
className="transition-colors hover:text-emerald-600 dark:hover:text-emerald-400"
>
Features Features
</a> </a>
<a <a href="#pricing" className="link-primary">
href="#pricing"
className="transition-colors hover:text-emerald-600 dark:hover:text-emerald-400"
>
Pricing Pricing
</a> </a>
</div> </div>
<div className="mt-6 border-t pt-6 sm:mt-8 sm:pt-8"> <div className="mt-6 border-t pt-6 sm:mt-8 sm:pt-8">
<p className="text-sm text-slate-600 sm:text-base dark:text-slate-400"> <p className="text-muted text-sm sm:text-base">
&copy; 2024 BeenVoice. Built with for entrepreneurs. &copy; 2024 BeenVoice. Built with for entrepreneurs.
</p> </p>
</div> </div>

View File

@@ -10,7 +10,12 @@ interface AddressAutocompleteProps {
placeholder?: string; placeholder?: string;
} }
export function AddressAutocomplete({ value, onChange, onSelect, placeholder }: AddressAutocompleteProps) { export function AddressAutocomplete({
value,
onChange,
onSelect,
placeholder,
}: AddressAutocompleteProps) {
const [suggestions, setSuggestions] = useState<any[]>([]); const [suggestions, setSuggestions] = useState<any[]>([]);
const [showSuggestions, setShowSuggestions] = useState(false); const [showSuggestions, setShowSuggestions] = useState(false);
const timeoutRef = useRef<NodeJS.Timeout | null>(null); const timeoutRef = useRef<NodeJS.Timeout | null>(null);
@@ -20,7 +25,9 @@ export function AddressAutocomplete({ value, onChange, onSelect, placeholder }:
setSuggestions([]); setSuggestions([]);
return; return;
} }
const res = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}`); const res = await fetch(
`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}`,
);
const data = await res.json(); const data = await res.json();
setSuggestions(data); setSuggestions(data);
}; };
@@ -50,12 +57,12 @@ export function AddressAutocomplete({ value, onChange, onSelect, placeholder }:
onBlur={() => setTimeout(() => setShowSuggestions(false), 150)} onBlur={() => setTimeout(() => setShowSuggestions(false), 150)}
/> />
{showSuggestions && suggestions.length > 0 && ( {showSuggestions && suggestions.length > 0 && (
<Card className="absolute z-10 mt-1 w-full max-h-60 overflow-auto shadow-lg border bg-white"> <Card className="card-primary absolute z-10 mt-1 max-h-60 w-full overflow-auto">
<ul> <ul>
{suggestions.map((s, i) => ( {suggestions.map((s, i) => (
<li <li
key={s.place_id} key={s.place_id}
className="px-4 py-2 cursor-pointer hover:bg-muted text-sm" className="hover:bg-muted cursor-pointer px-4 py-2 text-sm"
onMouseDown={() => handleSelect(s.display_name)} onMouseDown={() => handleSelect(s.display_name)}
> >
{s.display_name} {s.display_name}

View File

@@ -438,9 +438,9 @@ export function CSVImportPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Global Client Selection */} {/* Global Client Selection */}
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm"> <Card className="card-primary">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2 text-emerald-800"> <CardTitle className="card-title-primary">
<Users className="h-5 w-5" /> <Users className="h-5 w-5" />
Default Client Default Client
</CardTitle> </CardTitle>
@@ -460,7 +460,7 @@ export function CSVImportPage() {
applyGlobalClient(newClientId); applyGlobalClient(newClientId);
} }
}} }}
className="h-12 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" className="h-12 w-full rounded-md border px-3 py-2"
disabled={loadingClients} disabled={loadingClients}
> >
<option value="">No default client (select individually)</option> <option value="">No default client (select individually)</option>
@@ -470,7 +470,7 @@ export function CSVImportPage() {
</option> </option>
))} ))}
</select> </select>
<p className="text-xs text-gray-500"> <p className="text-muted text-xs">
This client will be automatically selected for all uploaded files. This client will be automatically selected for all uploaded files.
You can still change individual files below. You can still change individual files below.
</p> </p>
@@ -479,9 +479,9 @@ export function CSVImportPage() {
</Card> </Card>
{/* File Upload Area */} {/* File Upload Area */}
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm"> <Card className="card-primary">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2 text-emerald-800"> <CardTitle className="card-title-primary">
<Upload className="h-5 w-5" /> <Upload className="h-5 w-5" />
Upload CSV Files Upload CSV Files
</CardTitle> </CardTitle>
@@ -500,32 +500,32 @@ export function CSVImportPage() {
{totalFiles > 0 && ( {totalFiles > 0 && (
<div className="grid grid-cols-2 gap-4 rounded-lg bg-emerald-50/50 p-4 md:grid-cols-4"> <div className="grid grid-cols-2 gap-4 rounded-lg bg-emerald-50/50 p-4 md:grid-cols-4">
<div className="text-center"> <div className="text-center">
<div className="text-2xl font-bold text-emerald-600"> <div className="text-icon-emerald text-2xl font-bold">
{totalFiles} {totalFiles}
</div> </div>
<div className="text-sm text-gray-600">Files</div> <div className="text-secondary text-sm">Files</div>
<div className="text-xs text-gray-500">of 50 max</div> <div className="text-muted text-xs">of 50 max</div>
</div> </div>
<div className="text-center"> <div className="text-center">
<div className="text-2xl font-bold text-emerald-600"> <div className="text-icon-emerald text-2xl font-bold">
{totalItems} {totalItems}
</div> </div>
<div className="text-sm text-gray-600">Total Items</div> <div className="text-secondary text-sm">Total Items</div>
</div> </div>
<div className="text-center"> <div className="text-center">
<div className="text-2xl font-bold text-emerald-600"> <div className="text-icon-emerald text-2xl font-bold">
{totalAmount.toLocaleString("en-US", { {totalAmount.toLocaleString("en-US", {
style: "currency", style: "currency",
currency: "USD", currency: "USD",
})} })}
</div> </div>
<div className="text-sm text-gray-600">Total Amount</div> <div className="text-secondary text-sm">Total Amount</div>
</div> </div>
<div className="text-center"> <div className="text-center">
<div className="text-2xl font-bold text-emerald-600"> <div className="text-icon-emerald text-2xl font-bold">
{readyFiles}/{totalFiles} {readyFiles}/{totalFiles}
</div> </div>
<div className="text-sm text-gray-600">Ready</div> <div className="text-secondary text-sm">Ready</div>
</div> </div>
</div> </div>
)} )}
@@ -534,9 +534,9 @@ export function CSVImportPage() {
{/* File List */} {/* File List */}
{files.length > 0 && ( {files.length > 0 && (
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm"> <Card className="card-primary">
<CardHeader> <CardHeader>
<CardTitle className="text-emerald-800">Uploaded Files</CardTitle> <CardTitle className="text-brand-light">Uploaded Files</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-4"> <div className="space-y-4">
@@ -547,12 +547,12 @@ export function CSVImportPage() {
> >
<div className="mb-4 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <div className="mb-4 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<FileText className="h-5 w-5 text-emerald-600" /> <FileText className="text-icon-emerald h-5 w-5" />
<div> <div>
<h3 className="truncate font-medium text-gray-900"> <h3 className="text-accent truncate font-medium">
{fileData.file.name} {fileData.file.name}
</h3> </h3>
<p className="text-sm text-gray-500"> <p className="text-muted text-sm">
{fileData.parsedItems.length} items {" "} {fileData.parsedItems.length} items {" "}
{fileData.parsedItems {fileData.parsedItems
.reduce((sum, item) => sum + item.hours, 0) .reduce((sum, item) => sum + item.hours, 0)
@@ -574,7 +574,7 @@ export function CSVImportPage() {
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => removeFile(index)} onClick={() => removeFile(index)}
className="text-red-600 hover:text-red-700" className="text-icon-red hover:text-error"
> >
<Trash2 className="mr-1 h-4 w-4" /> <Trash2 className="mr-1 h-4 w-4" />
Remove Remove
@@ -584,7 +584,7 @@ export function CSVImportPage() {
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-xs font-medium text-gray-700"> <Label className="text-secondary text-xs font-medium">
Invoice Number Invoice Number
</Label> </Label>
<Input <Input
@@ -614,7 +614,7 @@ export function CSVImportPage() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-xs font-medium text-gray-700"> <Label className="text-secondary text-xs font-medium">
Issue Date Issue Date
</Label> </Label>
<DatePicker <DatePicker
@@ -628,7 +628,7 @@ export function CSVImportPage() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-xs font-medium text-gray-700"> <Label className="text-secondary text-xs font-medium">
Due Date Due Date
</Label> </Label>
<DatePicker <DatePicker
@@ -646,18 +646,18 @@ export function CSVImportPage() {
{fileData.errors.length > 0 && ( {fileData.errors.length > 0 && (
<div className="mt-4 rounded-lg border border-red-200 bg-red-50 p-3"> <div className="mt-4 rounded-lg border border-red-200 bg-red-50 p-3">
<div className="mb-2 flex items-center gap-2"> <div className="mb-2 flex items-center gap-2">
<AlertCircle className="h-4 w-4 text-red-600" /> <AlertCircle className="text-icon-red h-4 w-4" />
<span className="text-sm font-medium text-red-800"> <span className="text-error text-sm font-medium">
Issues Found Issues Found
</span> </span>
</div> </div>
<ul className="space-y-1 text-sm text-red-700"> <ul className="text-error space-y-1 text-sm">
{fileData.errors.map((error, errorIndex) => ( {fileData.errors.map((error, errorIndex) => (
<li <li
key={errorIndex} key={errorIndex}
className="flex items-start gap-2" className="flex items-start gap-2"
> >
<span className="text-red-600"></span> <span className="text-icon-red"></span>
<span>{error}</span> <span>{error}</span>
</li> </li>
))} ))}
@@ -677,7 +677,7 @@ export function CSVImportPage() {
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{fileData.errors.length > 0 && ( {fileData.errors.length > 0 && (
<Badge variant="destructive" className="text-xs"> <Badge className="badge-error text-xs">
{fileData.errors.length} Error {fileData.errors.length} Error
{fileData.errors.length !== 1 ? "s" : ""} {fileData.errors.length !== 1 ? "s" : ""}
</Badge> </Badge>
@@ -712,7 +712,7 @@ export function CSVImportPage() {
{/* Batch Actions */} {/* Batch Actions */}
{files.length > 0 && ( {files.length > 0 && (
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm"> <Card className="card-primary">
<CardContent> <CardContent>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{isProcessing && ( {isProcessing && (
@@ -732,7 +732,7 @@ export function CSVImportPage() {
<Button <Button
onClick={processBatch} onClick={processBatch}
disabled={readyFiles === 0 || isProcessing} disabled={readyFiles === 0 || isProcessing}
className="bg-gradient-to-r from-emerald-600 to-teal-600 text-white hover:from-emerald-700 hover:to-teal-700" className="btn-brand-primary"
> >
{isProcessing {isProcessing
? "Processing..." ? "Processing..."
@@ -746,7 +746,7 @@ export function CSVImportPage() {
{/* Preview Modal */} {/* Preview Modal */}
<Dialog open={previewModalOpen} onOpenChange={setPreviewModalOpen}> <Dialog open={previewModalOpen} onOpenChange={setPreviewModalOpen}>
<DialogContent className="flex max-h-[90vh] max-w-4xl flex-col border-0 bg-white/95 shadow-2xl backdrop-blur-sm"> <DialogContent className="card-primary flex max-h-[90vh] max-w-4xl flex-col">
<DialogHeader className="flex-shrink-0"> <DialogHeader className="flex-shrink-0">
<DialogTitle className="flex items-center gap-2 text-xl font-bold text-gray-800"> <DialogTitle className="flex items-center gap-2 text-xl font-bold text-gray-800">
<FileText className="h-5 w-5 text-emerald-600" /> <FileText className="h-5 w-5 text-emerald-600" />
@@ -834,7 +834,7 @@ export function CSVImportPage() {
currency: "USD", currency: "USD",
})} })}
</td> </td>
<td className="p-2 text-right font-medium whitespace-nowrap text-gray-600"> <td className="text-secondary p-2 text-right font-medium whitespace-nowrap">
{item.amount.toLocaleString("en-US", { {item.amount.toLocaleString("en-US", {
style: "currency", style: "currency",
currency: "USD", currency: "USD",

View File

@@ -3,13 +3,35 @@
import { useState } from "react"; import { useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { api } from "~/trpc/react"; import { api } from "~/trpc/react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card"; import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "~/components/ui/card";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input"; import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label"; import { Label } from "~/components/ui/label";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "~/components/ui/dialog"; import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "~/components/ui/dialog";
import { toast } from "sonner"; import { toast } from "sonner";
import { Mail, Phone, MapPin, Edit, Trash2, Eye, Plus, Search } from "lucide-react"; import {
Mail,
Phone,
MapPin,
Edit,
Trash2,
Eye,
Plus,
Search,
} from "lucide-react";
export function ClientList() { export function ClientList() {
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
@@ -29,10 +51,12 @@ export function ClientList() {
}, },
}); });
const filteredClients = clients?.filter(client => const filteredClients =
client.name.toLowerCase().includes(searchTerm.toLowerCase()) || clients?.filter(
client.email?.toLowerCase().includes(searchTerm.toLowerCase()) (client) =>
) ?? []; client.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
client.email?.toLowerCase().includes(searchTerm.toLowerCase()),
) ?? [];
const handleDelete = (clientId: string) => { const handleDelete = (clientId: string) => {
setClientToDelete(clientId); setClientToDelete(clientId);
@@ -49,14 +73,14 @@ export function ClientList() {
return ( return (
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{[...Array(3)].map((_, i: number) => ( {[...Array(3)].map((_, i: number) => (
<Card key={i} className="shadow-xl border-0 bg-white/80 backdrop-blur-sm"> <Card key={i} className="card-primary">
<CardHeader> <CardHeader>
<div className="h-4 bg-gray-200 rounded animate-pulse" /> <div className="h-4 animate-pulse rounded bg-gray-200" />
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-2"> <div className="space-y-2">
<div className="h-3 bg-gray-200 rounded animate-pulse" /> <div className="h-3 animate-pulse rounded bg-gray-200" />
<div className="h-3 bg-gray-200 rounded w-2/3 animate-pulse" /> <div className="h-3 w-2/3 animate-pulse rounded bg-gray-200" />
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@@ -67,9 +91,9 @@ export function ClientList() {
if (!clients || clients.length === 0) { if (!clients || clients.length === 0) {
return ( return (
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm"> <Card className="card-primary">
<CardHeader className="text-center"> <CardHeader className="text-center">
<CardTitle className="text-2xl font-bold bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-transparent"> <CardTitle className="text-brand-gradient text-2xl font-bold">
No Clients Yet No Clients Yet
</CardTitle> </CardTitle>
<CardDescription className="text-lg"> <CardDescription className="text-lg">
@@ -78,9 +102,7 @@ export function ClientList() {
</CardHeader> </CardHeader>
<CardContent className="text-center"> <CardContent className="text-center">
<Link href="/dashboard/clients/new"> <Link href="/dashboard/clients/new">
<Button <Button variant="brand" className="h-12 w-full">
className="w-full h-12 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"
>
<Plus className="mr-2 h-4 w-4" /> <Plus className="mr-2 h-4 w-4" />
Add Your First Client Add Your First Client
</Button> </Button>
@@ -92,24 +114,24 @@ export function ClientList() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4"> <div className="flex flex-col items-start gap-4 sm:flex-row sm:items-center">
<div className="flex-1 relative"> <div className="relative flex-1">
<Label htmlFor="search" className="sr-only">Search clients</Label> <Label htmlFor="search" className="sr-only">
Search clients
</Label>
<div className="relative"> <div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" /> <Search className="text-muted absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform" />
<Input <Input
id="search" id="search"
placeholder="Search by name or email..." placeholder="Search by name or email..."
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500" className="h-12 pl-10"
/> />
</div> </div>
</div> </div>
<Link href="/dashboard/clients/new"> <Link href="/dashboard/clients/new">
<Button <Button variant="brand" className="h-12 w-full sm:w-auto">
className="w-full sm:w-auto h-12 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"
>
<Plus className="mr-2 h-4 w-4" /> <Plus className="mr-2 h-4 w-4" />
Add Client Add Client
</Button> </Button>
@@ -118,20 +140,23 @@ export function ClientList() {
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{filteredClients.map((client) => ( {filteredClients.map((client) => (
<Card key={client.id} className="shadow-xl border-0 bg-white/80 backdrop-blur-sm hover:shadow-2xl transition-all duration-300 group"> <Card
key={client.id}
className="group card-primary transition-all duration-300 hover:shadow-lg"
>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center justify-between text-lg"> <CardTitle className="flex items-center justify-between text-lg">
<span className="font-semibold text-gray-800 group-hover:text-emerald-600 transition-colors"> <span className="text-accent group-hover:text-icon-emerald font-semibold transition-colors">
{client.name} {client.name}
</span> </span>
<div className="flex space-x-1 opacity-0 group-hover:opacity-100 transition-opacity"> <div className="flex space-x-1 opacity-0 transition-opacity group-hover:opacity-100">
<Link href={`/clients/${client.id}`}> <Link href={`/clients/${client.id}`}>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0 hover:bg-emerald-100"> <Button variant="ghost" size="sm" className="h-8 w-8 p-0">
<Eye className="h-4 w-4" /> <Eye className="h-4 w-4" />
</Button> </Button>
</Link> </Link>
<Link href={`/clients/${client.id}/edit`}> <Link href={`/clients/${client.id}/edit`}>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0 hover:bg-emerald-100"> <Button variant="ghost" size="sm" className="h-8 w-8 p-0">
<Edit className="h-4 w-4" /> <Edit className="h-4 w-4" />
</Button> </Button>
</Link> </Link>
@@ -139,7 +164,7 @@ export function ClientList() {
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => handleDelete(client.id)} onClick={() => handleDelete(client.id)}
className="h-8 w-8 p-0 hover:bg-red-100 hover:text-red-600" className="hover:bg-error-subtle hover:text-icon-red h-8 w-8 p-0"
> >
<Trash2 className="h-4 w-4" /> <Trash2 className="h-4 w-4" />
</Button> </Button>
@@ -148,32 +173,34 @@ export function ClientList() {
</CardHeader> </CardHeader>
<CardContent className="space-y-3"> <CardContent className="space-y-3">
{client.email && ( {client.email && (
<div className="flex items-center text-sm text-gray-600"> <div className="text-secondary flex items-center text-sm">
<div className="p-1.5 bg-emerald-100 rounded mr-3"> <div className="bg-brand-muted mr-3 rounded p-1.5">
<Mail className="h-3 w-3 text-emerald-600" /> <Mail className="text-icon-emerald h-3 w-3" />
</div> </div>
{client.email} {client.email}
</div> </div>
)} )}
{client.phone && ( {client.phone && (
<div className="flex items-center text-sm text-gray-600"> <div className="text-secondary flex items-center text-sm">
<div className="p-1.5 bg-blue-100 rounded mr-3"> <div className="bg-brand-muted-blue mr-3 rounded p-1.5">
<Phone className="h-3 w-3 text-blue-600" /> <Phone className="text-icon-blue h-3 w-3" />
</div> </div>
{client.phone} {client.phone}
</div> </div>
)} )}
{(client.addressLine1 ?? client.city ?? client.state) && ( {(client.addressLine1 ?? client.city ?? client.state) && (
<div className="flex items-start text-sm text-gray-600"> <div className="text-secondary flex items-start text-sm">
<div className="p-1.5 bg-teal-100 rounded mr-3 mt-0.5 flex-shrink-0"> <div className="bg-brand-muted-teal mt-0.5 mr-3 flex-shrink-0 rounded p-1.5">
<MapPin className="h-3 w-3 text-teal-600" /> <MapPin className="text-icon-teal h-3 w-3" />
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
{client.addressLine1 && <div>{client.addressLine1}</div>} {client.addressLine1 && <div>{client.addressLine1}</div>}
{client.addressLine2 && <div>{client.addressLine2}</div>} {client.addressLine2 && <div>{client.addressLine2}</div>}
{(client.city ?? client.state ?? client.postalCode) && ( {(client.city ?? client.state ?? client.postalCode) && (
<div> <div>
{[client.city, client.state, client.postalCode].filter(Boolean).join(", ")} {[client.city, client.state, client.postalCode]
.filter(Boolean)
.join(", ")}
</div> </div>
)} )}
{client.country && <div>{client.country}</div>} {client.country && <div>{client.country}</div>}
@@ -186,18 +213,21 @@ export function ClientList() {
</div> </div>
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}> <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<DialogContent className="bg-white/95 backdrop-blur-sm border-0 shadow-2xl"> <DialogContent className="card-primary">
<DialogHeader> <DialogHeader>
<DialogTitle className="text-xl font-bold text-gray-800">Delete Client</DialogTitle> <DialogTitle className="text-accent text-xl font-bold">
<DialogDescription className="text-gray-600"> Delete Client
Are you sure you want to delete this client? This action cannot be undone. </DialogTitle>
<DialogDescription className="text-secondary">
Are you sure you want to delete this client? This action cannot be
undone.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<DialogFooter> <DialogFooter>
<Button <Button
variant="outline" variant="outline"
onClick={() => setDeleteDialogOpen(false)} onClick={() => setDeleteDialogOpen(false)}
className="border-gray-300 text-gray-700 hover:bg-gray-50" className="text-secondary"
> >
Cancel Cancel
</Button> </Button>

View File

@@ -14,11 +14,12 @@ import {
Eye, Eye,
DollarSign, DollarSign,
User, User,
Calendar Calendar,
} from "lucide-react"; } from "lucide-react";
export function CurrentOpenInvoiceCard() { export function CurrentOpenInvoiceCard() {
const { data: currentInvoice, isLoading } = api.invoices.getCurrentOpen.useQuery(); const { data: currentInvoice, isLoading } =
api.invoices.getCurrentOpen.useQuery();
const formatCurrency = (amount: number) => { const formatCurrency = (amount: number) => {
return new Intl.NumberFormat("en-US", { return new Intl.NumberFormat("en-US", {
@@ -36,10 +37,10 @@ export function CurrentOpenInvoiceCard() {
if (isLoading) { if (isLoading) {
return ( return (
<Card className="border-0 shadow-sm"> <Card className="card-primary">
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-lg"> <CardTitle className="card-title-secondary">
<FileText className="h-5 w-5 text-emerald-600" /> <FileText className="text-icon-emerald h-5 w-5" />
Current Open Invoice Current Open Invoice
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@@ -57,20 +58,21 @@ export function CurrentOpenInvoiceCard() {
if (!currentInvoice) { if (!currentInvoice) {
return ( return (
<Card className="border-0 shadow-sm"> <Card className="card-primary">
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-lg"> <CardTitle className="card-title-secondary">
<FileText className="h-5 w-5 text-emerald-600" /> <FileText className="text-icon-emerald h-5 w-5" />
Current Open Invoice Current Open Invoice
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="text-center py-6"> <div className="py-6 text-center">
<FileText className="mx-auto mb-3 h-8 w-8 text-muted-foreground" /> <FileText className="text-muted-foreground mx-auto mb-3 h-8 w-8" />
<p className="text-muted-foreground text-sm mb-4"> <p className="text-muted-foreground mb-4 text-sm">
No open invoice found. Create a new invoice to start tracking your time. No open invoice found. Create a new invoice to start tracking your
time.
</p> </p>
<Button asChild className="bg-gradient-to-r from-emerald-600 to-teal-600 text-white hover:from-emerald-700 hover:to-teal-700"> <Button asChild variant="brand">
<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" />
Create New Invoice Create New Invoice
@@ -82,14 +84,15 @@ export function CurrentOpenInvoiceCard() {
); );
} }
const totalHours = currentInvoice.items?.reduce((sum, item) => sum + item.hours, 0) ?? 0; const totalHours =
currentInvoice.items?.reduce((sum, item) => sum + item.hours, 0) ?? 0;
const totalAmount = currentInvoice.totalAmount; const totalAmount = currentInvoice.totalAmount;
return ( return (
<Card className="border-0 shadow-sm"> <Card className="card-primary">
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-lg"> <CardTitle className="card-title-secondary">
<FileText className="h-5 w-5 text-emerald-600" /> <FileText className="text-icon-emerald h-5 w-5" />
Current Open Invoice Current Open Invoice
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@@ -97,15 +100,13 @@ export function CurrentOpenInvoiceCard() {
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Badge variant="secondary" className="text-xs"> <Badge className="badge-secondary text-xs">
{currentInvoice.invoiceNumber} {currentInvoice.invoiceNumber}
</Badge> </Badge>
<Badge variant="outline" className="text-xs"> <Badge className="badge-outline text-xs">Draft</Badge>
Draft
</Badge>
</div> </div>
<div className="text-right"> <div className="text-right">
<p className="text-sm font-medium text-emerald-600"> <p className="text-icon-emerald text-sm font-medium">
{formatCurrency(totalAmount)} {formatCurrency(totalAmount)}
</p> </p>
</div> </div>
@@ -113,19 +114,21 @@ export function CurrentOpenInvoiceCard() {
<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">
<User className="h-3 w-3 text-muted-foreground" /> <User className="text-muted-foreground h-3 w-3" />
<span className="text-muted-foreground">Client:</span> <span className="text-muted-foreground">Client:</span>
<span className="font-medium">{currentInvoice.client?.name}</span> <span className="font-medium">{currentInvoice.client?.name}</span>
</div> </div>
<div className="flex items-center gap-2 text-sm"> <div className="flex items-center gap-2 text-sm">
<Calendar className="h-3 w-3 text-muted-foreground" /> <Calendar className="text-muted-foreground h-3 w-3" />
<span className="text-muted-foreground">Due:</span> <span className="text-muted-foreground">Due:</span>
<span className="font-medium">{formatDate(currentInvoice.dueDate)}</span> <span className="font-medium">
{formatDate(currentInvoice.dueDate)}
</span>
</div> </div>
<div className="flex items-center gap-2 text-sm"> <div className="flex items-center gap-2 text-sm">
<Clock className="h-3 w-3 text-muted-foreground" /> <Clock className="text-muted-foreground h-3 w-3" />
<span className="text-muted-foreground">Hours:</span> <span className="text-muted-foreground">Hours:</span>
<span className="font-medium">{totalHours.toFixed(1)}h</span> <span className="font-medium">{totalHours.toFixed(1)}h</span>
</div> </div>
@@ -139,7 +142,7 @@ export function CurrentOpenInvoiceCard() {
View View
</Link> </Link>
</Button> </Button>
<Button asChild size="sm" className="flex-1 bg-gradient-to-r from-emerald-600 to-teal-600 text-white hover:from-emerald-700 hover:to-teal-700"> <Button asChild variant="brand" size="sm" className="flex-1">
<Link href={`/dashboard/invoices/${currentInvoice.id}/edit`}> <Link href={`/dashboard/invoices/${currentInvoice.id}/edit`}>
<Edit className="mr-2 h-3 w-3" /> <Edit className="mr-2 h-3 w-3" />
Continue Continue

View File

@@ -108,8 +108,8 @@ export function DataTable<TData, TValue>({
}; };
checkMobile(); checkMobile();
window.addEventListener('resize', checkMobile); window.addEventListener("resize", checkMobile);
return () => window.removeEventListener('resize', checkMobile); return () => window.removeEventListener("resize", checkMobile);
}, []); }, []);
// Create responsive columns that properly hide on mobile // Create responsive columns that properly hide on mobile
@@ -118,9 +118,23 @@ export function DataTable<TData, TValue>({
...column, ...column,
// Add a meta property to control responsive visibility // Add a meta property to control responsive visibility
meta: { meta: {
...((column as ColumnDef<TData, TValue> & { meta?: { headerClassName?: string; cellClassName?: string } }).meta ?? {}), ...((
headerClassName: (column as ColumnDef<TData, TValue> & { meta?: { headerClassName?: string; cellClassName?: string } }).meta?.headerClassName ?? "", column as ColumnDef<TData, TValue> & {
cellClassName: (column as ColumnDef<TData, TValue> & { meta?: { headerClassName?: string; cellClassName?: string } }).meta?.cellClassName ?? "", meta?: { headerClassName?: string; cellClassName?: string };
}
).meta ?? {}),
headerClassName:
(
column as ColumnDef<TData, TValue> & {
meta?: { headerClassName?: string; cellClassName?: string };
}
).meta?.headerClassName ?? "",
cellClassName:
(
column as ColumnDef<TData, TValue> & {
meta?: { headerClassName?: string; cellClassName?: string };
}
).meta?.cellClassName ?? "",
}, },
})); }));
}, [columns]); }, [columns]);
@@ -163,10 +177,11 @@ export function DataTable<TData, TValue>({
const handleRowClick = (row: TData, event: React.MouseEvent) => { const handleRowClick = (row: TData, event: React.MouseEvent) => {
// Don't trigger row click if clicking on action buttons or their children // Don't trigger row click if clicking on action buttons or their children
const target = event.target as HTMLElement; const target = event.target as HTMLElement;
const isActionButton = target.closest('[data-action-button="true"]') ?? const isActionButton =
target.closest('button') ?? target.closest('[data-action-button="true"]') ??
target.closest('a') ?? target.closest("button") ??
target.closest('[role="button"]'); target.closest("a") ??
target.closest('[role="button"]');
if (isActionButton) { if (isActionButton) {
return; return;
@@ -200,7 +215,7 @@ export function DataTable<TData, TValue>({
{/* Filter Bar Card */} {/* Filter Bar Card */}
{(showSearch || filterableColumns.length > 0 || showColumnVisibility) && ( {(showSearch || filterableColumns.length > 0 || showColumnVisibility) && (
<Card className="border-0 py-2 shadow-sm"> <Card className="card-primary py-2">
<CardContent className="px-3 py-0"> <CardContent className="px-3 py-0">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{showSearch && ( {showSearch && (
@@ -300,7 +315,7 @@ export function DataTable<TData, TValue>({
)} )}
{/* Table Content Card */} {/* Table Content Card */}
<Card className="overflow-hidden border-0 p-0 shadow-sm"> <Card className="card-primary overflow-hidden p-0">
<div className="w-full overflow-x-auto"> <div className="w-full overflow-x-auto">
<Table> <Table>
<TableHeader> <TableHeader>
@@ -310,7 +325,9 @@ export function DataTable<TData, TValue>({
className="bg-muted/50 hover:bg-muted/50" className="bg-muted/50 hover:bg-muted/50"
> >
{headerGroup.headers.map((header) => { {headerGroup.headers.map((header) => {
const meta = header.column.columnDef.meta as { headerClassName?: string; cellClassName?: string } | undefined; const meta = header.column.columnDef.meta as
| { headerClassName?: string; cellClassName?: string }
| undefined;
return ( return (
<TableHead <TableHead
key={header.id} key={header.id}
@@ -339,12 +356,16 @@ export function DataTable<TData, TValue>({
data-state={row.getIsSelected() && "selected"} data-state={row.getIsSelected() && "selected"}
className={cn( className={cn(
"hover:bg-muted/20 data-[state=selected]:bg-muted/50 border-b transition-colors", "hover:bg-muted/20 data-[state=selected]:bg-muted/50 border-b transition-colors",
onRowClick && "cursor-pointer" onRowClick && "cursor-pointer",
)} )}
onClick={(event) => onRowClick && handleRowClick(row.original, event)} onClick={(event) =>
onRowClick && handleRowClick(row.original, event)
}
> >
{row.getVisibleCells().map((cell) => { {row.getVisibleCells().map((cell) => {
const meta = cell.column.columnDef.meta as { headerClassName?: string; cellClassName?: string } | undefined; const meta = cell.column.columnDef.meta as
| { headerClassName?: string; cellClassName?: string }
| undefined;
return ( return (
<TableCell <TableCell
key={cell.id} key={cell.id}
@@ -379,7 +400,7 @@ export function DataTable<TData, TValue>({
{/* Pagination Bar Card */} {/* Pagination Bar Card */}
{showPagination && ( {showPagination && (
<Card className="border-0 py-2 shadow-sm"> <Card className="card-primary py-2">
<CardContent className="px-3 py-0"> <CardContent className="px-3 py-0">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -540,7 +561,7 @@ export function DataTableSkeleton({
return ( return (
<div className="space-y-4"> <div className="space-y-4">
{/* Filter bar skeleton */} {/* Filter bar skeleton */}
<Card className="border-0 py-2 shadow-sm"> <Card className="card-primary py-2">
<CardContent className="px-3 py-0"> <CardContent className="px-3 py-0">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="bg-muted/30 h-9 w-full flex-1 animate-pulse rounded-md sm:max-w-sm"></div> <div className="bg-muted/30 h-9 w-full flex-1 animate-pulse rounded-md sm:max-w-sm"></div>
@@ -550,7 +571,7 @@ export function DataTableSkeleton({
</Card> </Card>
{/* Table skeleton */} {/* Table skeleton */}
<Card className="overflow-hidden border-0 p-0 shadow-sm"> <Card className="card-primary overflow-hidden p-0">
<div className="w-full overflow-x-auto"> <div className="w-full overflow-x-auto">
<Table> <Table>
<TableHeader> <TableHeader>
@@ -562,16 +583,16 @@ export function DataTableSkeleton({
<TableHead className="h-12 px-3 text-left align-middle sm:h-14 sm:px-4"> <TableHead className="h-12 px-3 text-left align-middle sm:h-14 sm:px-4">
<div className="bg-muted/30 h-4 w-14 animate-pulse rounded sm:w-20 lg:w-24"></div> <div className="bg-muted/30 h-4 w-14 animate-pulse rounded sm:w-20 lg:w-24"></div>
</TableHead> </TableHead>
<TableHead className="hidden sm:table-cell h-12 px-3 text-left align-middle sm:h-14 sm:px-4"> <TableHead className="hidden h-12 px-3 text-left align-middle sm:table-cell sm:h-14 sm:px-4">
<div className="bg-muted/30 h-4 w-14 animate-pulse rounded sm:w-20 lg:w-24"></div> <div className="bg-muted/30 h-4 w-14 animate-pulse rounded sm:w-20 lg:w-24"></div>
</TableHead> </TableHead>
<TableHead className="hidden sm:table-cell h-12 px-3 text-left align-middle sm:h-14 sm:px-4"> <TableHead className="hidden h-12 px-3 text-left align-middle sm:table-cell sm:h-14 sm:px-4">
<div className="bg-muted/30 h-4 w-16 animate-pulse rounded sm:w-20 lg:w-24"></div> <div className="bg-muted/30 h-4 w-16 animate-pulse rounded sm:w-20 lg:w-24"></div>
</TableHead> </TableHead>
<TableHead className="h-12 px-3 text-left align-middle sm:h-14 sm:px-4"> <TableHead className="h-12 px-3 text-left align-middle sm:h-14 sm:px-4">
<div className="bg-muted/30 h-4 w-10 animate-pulse rounded sm:w-12 lg:w-16"></div> <div className="bg-muted/30 h-4 w-10 animate-pulse rounded sm:w-12 lg:w-16"></div>
</TableHead> </TableHead>
<TableHead className="hidden lg:table-cell h-12 px-3 text-left align-middle sm:h-14 sm:px-4"> <TableHead className="hidden h-12 px-3 text-left align-middle sm:h-14 sm:px-4 lg:table-cell">
<div className="bg-muted/30 h-4 w-20 animate-pulse rounded"></div> <div className="bg-muted/30 h-4 w-20 animate-pulse rounded"></div>
</TableHead> </TableHead>
</TableRow> </TableRow>
@@ -588,11 +609,11 @@ export function DataTableSkeleton({
<div className="bg-muted/30 h-4 w-14 animate-pulse rounded sm:w-20 lg:w-24"></div> <div className="bg-muted/30 h-4 w-14 animate-pulse rounded sm:w-20 lg:w-24"></div>
</TableCell> </TableCell>
{/* Status (sm+) */} {/* Status (sm+) */}
<TableCell className="hidden sm:table-cell px-3 py-3 align-middle sm:px-4 sm:py-4"> <TableCell className="hidden px-3 py-3 align-middle sm:table-cell sm:px-4 sm:py-4">
<div className="bg-muted/30 h-4 w-14 animate-pulse rounded sm:w-20 lg:w-24"></div> <div className="bg-muted/30 h-4 w-14 animate-pulse rounded sm:w-20 lg:w-24"></div>
</TableCell> </TableCell>
{/* Amount (sm+) */} {/* Amount (sm+) */}
<TableCell className="hidden sm:table-cell px-3 py-3 align-middle sm:px-4 sm:py-4"> <TableCell className="hidden px-3 py-3 align-middle sm:table-cell sm:px-4 sm:py-4">
<div className="bg-muted/30 h-4 w-16 animate-pulse rounded sm:w-20 lg:w-24"></div> <div className="bg-muted/30 h-4 w-16 animate-pulse rounded sm:w-20 lg:w-24"></div>
</TableCell> </TableCell>
{/* Actions */} {/* Actions */}
@@ -600,7 +621,7 @@ export function DataTableSkeleton({
<div className="bg-muted/30 h-4 w-10 animate-pulse rounded sm:w-12 lg:w-16"></div> <div className="bg-muted/30 h-4 w-10 animate-pulse rounded sm:w-12 lg:w-16"></div>
</TableCell> </TableCell>
{/* Extra (lg+) */} {/* Extra (lg+) */}
<TableCell className="hidden lg:table-cell px-3 py-3 align-middle sm:px-4 sm:py-4"> <TableCell className="hidden px-3 py-3 align-middle sm:px-4 sm:py-4 lg:table-cell">
<div className="bg-muted/30 h-4 w-20 animate-pulse rounded"></div> <div className="bg-muted/30 h-4 w-20 animate-pulse rounded"></div>
</TableCell> </TableCell>
</TableRow> </TableRow>
@@ -611,7 +632,7 @@ export function DataTableSkeleton({
</Card> </Card>
{/* Pagination skeleton */} {/* Pagination skeleton */}
<Card className="border-0 py-2 shadow-sm"> <Card className="card-primary py-2">
<CardContent className="px-3 py-0"> <CardContent className="px-3 py-0">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">

View File

@@ -21,14 +21,11 @@ import { 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";
import { Trash2, GripVertical, CalendarIcon } from "lucide-react"; import { Label } from "~/components/ui/label";
import { format } from "date-fns"; import { DatePicker } from "~/components/ui/date-picker";
import { Calendar } from "~/components/ui/calendar"; import { NumberInput } from "~/components/ui/number-input";
import { import { Textarea } from "~/components/ui/textarea";
Popover, import { Trash2, GripVertical, ChevronUp, ChevronDown } from "lucide-react";
PopoverContent,
PopoverTrigger,
} from "~/components/ui/popover";
interface InvoiceItem { interface InvoiceItem {
id: string; id: string;
@@ -50,6 +47,10 @@ function SortableItem({
index, index,
onItemChange, onItemChange,
onRemove, onRemove,
onMoveUp,
onMoveDown,
canMoveUp,
canMoveDown,
}: { }: {
item: InvoiceItem; item: InvoiceItem;
index: number; index: number;
@@ -59,6 +60,10 @@ function SortableItem({
value: string | number | Date, value: string | number | Date,
) => void; ) => void;
onRemove: (index: number) => void; onRemove: (index: number) => void;
onMoveUp: (index: number) => void;
onMoveDown: (index: number) => void;
canMoveUp: boolean;
canMoveDown: boolean;
}) { }) {
const { const {
attributes, attributes,
@@ -82,101 +87,193 @@ function SortableItem({
<div <div
ref={setNodeRef} ref={setNodeRef}
style={style} style={style}
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 ${ className={`card-secondary rounded-lg transition-colors ${
isDragging ? "opacity-50 shadow-lg" : "" isDragging ? "opacity-50 shadow-lg" : ""
}`} }`}
> >
{/* Drag Handle */} {/* Desktop Layout - Hidden on Mobile */}
<div className="col-span-1 flex h-10 items-center justify-center"> <div className="hidden items-center gap-3 p-4 md:grid md:grid-cols-12">
<button {/* Drag Handle */}
type="button" <div className="col-span-1 flex items-center justify-center">
{...attributes} <button
{...listeners} type="button"
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" {...attributes}
> {...listeners}
<GripVertical className="h-4 w-4" /> className="text-muted-foreground hover:bg-muted hover:text-foreground cursor-grab rounded p-2 transition-colors active:cursor-grabbing"
</button> >
</div> <GripVertical className="h-4 w-4" />
</button>
</div>
{/* Date */} {/* Date */}
<div className="col-span-2"> <div className="col-span-2">
<Popover> <DatePicker
<PopoverTrigger asChild> date={item.date}
<Button onDateChange={(date) =>
variant="outline" handleItemChange("date", date ?? new Date())
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" }
> size="sm"
{item.date ? format(item.date, "MMM dd") : "Date"} className="w-full"
<CalendarIcon className="h-4 w-4 text-gray-400 dark:text-gray-500" /> />
</Button> </div>
</PopoverTrigger>
<PopoverContent className="w-auto overflow-hidden p-0" align="start">
<Calendar
mode="single"
selected={item.date}
captionLayout="dropdown"
onSelect={(selectedDate: Date | undefined) => {
handleItemChange("date", selectedDate ?? new Date());
}}
/>
</PopoverContent>
</Popover>
</div>
{/* Description */} {/* Description */}
<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 dark:border-gray-600 dark:bg-gray-700 dark:text-white" className="h-9"
/> />
</div> </div>
{/* Hours */} {/* Hours */}
<div className="col-span-1"> <div className="col-span-1">
<Input <NumberInput
type="number" value={item.hours}
step="0.25" onChange={(value) => handleItemChange("hours", value)}
min="0" min={0}
value={item.hours} step={0.25}
onChange={(e) => handleItemChange("hours", e.target.value)} placeholder="0"
placeholder="0" width="full"
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>
{/* Rate */} {/* Rate */}
<div className="col-span-2"> <div className="col-span-2">
<Input <NumberInput
type="number" value={item.rate}
step="0.01" onChange={(value) => handleItemChange("rate", value)}
min="0" min={0}
value={item.rate} step={0.01}
onChange={(e) => handleItemChange("rate", e.target.value)} placeholder="0.00"
placeholder="0.00" prefix="$"
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" width="full"
/> />
</div> </div>
{/* Amount */} {/* Amount */}
<div className="col-span-1"> <div className="col-span-1">
<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"> <div className="bg-muted/30 flex h-9 items-center rounded-md border px-3 font-medium text-emerald-600">
${item.amount.toFixed(2)} ${item.amount.toFixed(2)}
</div>
</div>
{/* Remove Button */}
<div className="col-span-1">
<Button
type="button"
onClick={() => onRemove(index)}
variant="ghost"
size="sm"
className="h-9 w-9 p-0 text-red-600 hover:bg-red-50 hover:text-red-700"
>
<Trash2 className="h-4 w-4" />
</Button>
</div> </div>
</div> </div>
{/* Remove Button */} {/* Mobile Layout - Visible on Mobile Only */}
<div className="col-span-1"> <div className="space-y-4 p-4 md:hidden">
<Button {/* Header with Item Number and Controls */}
type="button" <div className="flex items-center justify-between">
onClick={() => onRemove(index)} <span className="text-muted-foreground text-xs font-medium">
variant="outline" Item {index + 1}
size="sm" </span>
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" <div className="flex items-center gap-1">
> <Button
<Trash2 className="h-4 w-4" /> type="button"
</Button> onClick={() => onMoveUp(index)}
disabled={!canMoveUp}
variant="ghost"
size="sm"
className="h-6 w-6 p-0"
>
<ChevronUp className="h-3 w-3" />
</Button>
<Button
type="button"
onClick={() => onMoveDown(index)}
disabled={!canMoveDown}
variant="ghost"
size="sm"
className="h-6 w-6 p-0"
>
<ChevronDown className="h-3 w-3" />
</Button>
<Button
type="button"
onClick={() => onRemove(index)}
variant="ghost"
size="sm"
className="h-6 w-6 p-0 text-red-600 hover:bg-red-50 hover:text-red-700"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
</div>
{/* Description */}
<div className="space-y-1">
<Label className="text-xs font-medium">Description</Label>
<Textarea
value={item.description}
onChange={(e) => handleItemChange("description", e.target.value)}
placeholder="Description of work..."
className="min-h-[48px] resize-none text-sm"
rows={1}
/>
</div>
{/* Date */}
<div className="space-y-1">
<Label className="text-xs font-medium">Date</Label>
<DatePicker
date={item.date}
onDateChange={(date) =>
handleItemChange("date", date ?? new Date())
}
size="sm"
className="w-full"
/>
</div>
{/* Hours and Rate */}
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1">
<Label className="text-xs font-medium">Hours</Label>
<NumberInput
value={item.hours}
onChange={(value) => handleItemChange("hours", value)}
min={0}
step={0.25}
placeholder="0"
width="full"
/>
</div>
<div className="space-y-1">
<Label className="text-xs font-medium">Rate</Label>
<NumberInput
value={item.rate}
onChange={(value) => handleItemChange("rate", value)}
min={0}
step={0.01}
placeholder="0.00"
prefix="$"
width="full"
/>
</div>
</div>
{/* Amount */}
<div className="bg-muted/20 rounded-md border p-3">
<div className="flex items-center justify-between">
<span className="text-muted-foreground text-sm">Total Amount:</span>
<span className="font-mono text-lg font-bold text-emerald-600">
${item.amount.toFixed(2)}
</span>
</div>
</div>
</div> </div>
</div> </div>
); );
@@ -244,6 +341,20 @@ export function EditableInvoiceItems({
onItemsChange(newItems); onItemsChange(newItems);
}; };
const handleMoveUp = (index: number) => {
if (index > 0) {
const newItems = arrayMove(items, index, index - 1);
onItemsChange(newItems);
}
};
const handleMoveDown = (index: number) => {
if (index < items.length - 1) {
const newItems = arrayMove(items, index, index + 1);
onItemsChange(newItems);
}
};
// Show skeleton loading on server-side // Show skeleton loading on server-side
if (!isClient) { if (!isClient) {
return ( return (
@@ -251,28 +362,42 @@ export function EditableInvoiceItems({
{items.map((item, _index) => ( {items.map((item, _index) => (
<div <div
key={item.id} key={item.id}
className="grid animate-pulse grid-cols-12 items-center gap-2 rounded-lg border border-gray-200 p-4" className="card-secondary animate-pulse rounded-lg p-4"
> >
<div className="col-span-1 flex h-10 items-center justify-center"> {/* Desktop Skeleton */}
<div className="h-4 w-4 rounded bg-gray-300"></div> <div className="hidden grid-cols-12 gap-3 md:grid">
<div className="col-span-1">
<div className="bg-muted h-4 w-4 rounded"></div>
</div>
<div className="col-span-2">
<div className="bg-muted h-9 rounded"></div>
</div>
<div className="col-span-4">
<div className="bg-muted h-9 rounded"></div>
</div>
<div className="col-span-1">
<div className="bg-muted h-9 rounded"></div>
</div>
<div className="col-span-2">
<div className="bg-muted h-9 rounded"></div>
</div>
<div className="col-span-1">
<div className="bg-muted h-9 rounded"></div>
</div>
<div className="col-span-1">
<div className="bg-muted h-9 w-9 rounded"></div>
</div>
</div> </div>
<div className="col-span-2"> {/* Mobile Skeleton */}
<div className="h-10 rounded bg-gray-300"></div> <div className="space-y-3 md:hidden">
</div> <div className="bg-muted h-4 w-20 rounded"></div>
<div className="col-span-4"> <div className="bg-muted h-16 rounded"></div>
<div className="h-10 rounded bg-gray-300"></div> <div className="bg-muted h-9 rounded"></div>
</div> <div className="grid grid-cols-2 gap-3">
<div className="col-span-1"> <div className="bg-muted h-9 rounded"></div>
<div className="h-10 rounded bg-gray-300"></div> <div className="bg-muted h-9 rounded"></div>
</div> </div>
<div className="col-span-2"> <div className="bg-muted h-12 rounded"></div>
<div className="h-10 rounded bg-gray-300"></div>
</div>
<div className="col-span-1">
<div className="h-10 rounded bg-gray-300"></div>
</div>
<div className="col-span-1">
<div className="h-10 w-10 rounded bg-gray-300"></div>
</div> </div>
</div> </div>
))} ))}
@@ -281,27 +406,44 @@ export function EditableInvoiceItems({
} }
return ( return (
<DndContext <>
sensors={sensors} {/* Desktop Header Labels - Hidden on Mobile */}
collisionDetection={closestCenter} <div className="text-muted-foreground hidden items-center gap-3 px-4 pb-2 text-xs font-medium md:grid md:grid-cols-12">
onDragEnd={handleDragEnd} <div className="col-span-1"></div>
> <div className="col-span-2">Date</div>
<SortableContext <div className="col-span-4">Description</div>
items={items.map((item) => item.id)} <div className="col-span-1">Hours</div>
strategy={verticalListSortingStrategy} <div className="col-span-2">Rate</div>
<div className="col-span-1">Amount</div>
<div className="col-span-1"></div>
</div>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
> >
<div className="space-y-3"> <SortableContext
{items.map((item, index) => ( items={items.map((item) => item.id)}
<SortableItem strategy={verticalListSortingStrategy}
key={item.id} >
item={item} <div className="space-y-3">
index={index} {items.map((item, index) => (
onItemChange={handleItemChange} <SortableItem
onRemove={onRemoveItem} key={item.id}
/> item={item}
))} index={index}
</div> onItemChange={handleItemChange}
</SortableContext> onRemove={onRemoveItem}
</DndContext> onMoveUp={handleMoveUp}
onMoveDown={handleMoveDown}
canMoveUp={index > 0}
canMoveDown={index < items.length - 1}
/>
))}
</div>
</SortableContext>
</DndContext>
</>
); );
} }

View File

@@ -172,7 +172,7 @@ export function InvoiceList() {
</CardTitle> </CardTitle>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<StatusBadge status={invoice.status as StatusType} /> <StatusBadge status={invoice.status as StatusType} />
<span className="text-lg font-bold text-green-600"> <span className="text-icon-green text-lg font-bold">
{formatCurrency(invoice.totalAmount)} {formatCurrency(invoice.totalAmount)}
</span> </span>
</div> </div>

View File

@@ -136,11 +136,11 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
if (!invoice) { if (!invoice) {
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="text-muted mx-auto mb-4 h-12 w-12" />
<h3 className="mb-2 text-lg font-medium text-gray-900 dark:text-white"> <h3 className="text-accent mb-2 text-lg font-medium">
Invoice not found Invoice not found
</h3> </h3>
<p className="mb-4 text-gray-500 dark:text-gray-400"> <p className="text-muted mb-4">
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>
@@ -160,7 +160,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
{isOverdue && ( {isOverdue && (
<Card className="border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-900/20"> <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 dark:text-red-400"> <div className="text-error flex items-center gap-2">
<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>
@@ -172,7 +172,7 @@ 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 dark:bg-gray-800/80"> <Card className="card-primary">
<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">
@@ -244,7 +244,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
</Card> </Card>
{/* Client Information */} {/* Client Information */}
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm dark:bg-gray-800/80"> <Card className="card-primary">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2 text-emerald-700 dark:text-emerald-400"> <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" />
@@ -307,7 +307,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
</Card> </Card>
{/* Invoice Items */} {/* Invoice Items */}
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm dark:bg-gray-800/80"> <Card className="card-primary">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2 text-emerald-700 dark:text-emerald-400"> <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" />
@@ -367,7 +367,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
{/* Notes */} {/* Notes */}
{invoice.notes && ( {invoice.notes && (
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm dark:bg-gray-800/80"> <Card className="card-primary">
<CardHeader> <CardHeader>
<CardTitle className="text-emerald-700 dark:text-emerald-400"> <CardTitle className="text-emerald-700 dark:text-emerald-400">
Notes Notes
@@ -385,7 +385,7 @@ 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 dark:bg-gray-800/80"> <Card className="card-primary">
<CardHeader> <CardHeader>
<CardTitle className="text-emerald-700 dark:text-emerald-400"> <CardTitle className="text-emerald-700 dark:text-emerald-400">
Status Actions Status Actions
@@ -437,7 +437,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
</Card> </Card>
{/* Invoice Summary */} {/* Invoice Summary */}
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm dark:bg-gray-800/80"> <Card className="card-primary">
<CardHeader> <CardHeader>
<CardTitle className="text-emerald-700 dark:text-emerald-400"> <CardTitle className="text-emerald-700 dark:text-emerald-400">
Summary Summary
@@ -476,7 +476,7 @@ 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 dark:border-red-800 dark:bg-gray-800/80"> <Card className="card-primary border-red-200 dark:border-red-800">
<CardHeader> <CardHeader>
<CardTitle className="text-red-700 dark:text-red-400"> <CardTitle className="text-red-700 dark:text-red-400">
Danger Zone Danger Zone
@@ -498,7 +498,7 @@ 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 dark:bg-gray-800/95"> <DialogContent className="card-primary">
<DialogHeader> <DialogHeader>
<DialogTitle className="text-xl font-bold text-gray-800 dark:text-white"> <DialogTitle className="text-xl font-bold text-gray-800 dark:text-white">
Delete Invoice Delete Invoice

View File

@@ -94,7 +94,7 @@ export function StatsCard({
export function StatsCardSkeleton() { export function StatsCardSkeleton() {
return ( return (
<Card className="border-0 shadow-md"> <Card className="card-primary">
<CardContent className="p-6"> <CardContent className="p-6">
<div className="animate-pulse"> <div className="animate-pulse">
<div className="bg-muted mb-2 h-4 w-1/2 rounded"></div> <div className="bg-muted mb-2 h-4 w-1/2 rounded"></div>

View File

@@ -1,6 +1,6 @@
import * as React from "react"; import * as React from "react";
import { Badge, type badgeVariants } from "~/components/ui/badge"; import { Badge } from "~/components/ui/badge";
import { type VariantProps } from "class-variance-authority"; import { cn } from "~/lib/utils";
type StatusType = type StatusType =
| "draft" | "draft"
@@ -18,18 +18,15 @@ interface StatusBadgeProps
children?: React.ReactNode; children?: React.ReactNode;
} }
const statusVariantMap: Record< const statusClassMap: Record<StatusType, string> = {
StatusType, draft: "status-badge-draft",
VariantProps<typeof badgeVariants>["variant"] sent: "status-badge-sent",
> = { paid: "status-badge-paid",
draft: "outline-draft", overdue: "status-badge-overdue",
sent: "outline-sent", success: "badge-success",
paid: "outline-paid", warning: "badge-warning",
overdue: "outline-overdue", error: "badge-error",
success: "success", info: "badge-features",
warning: "warning",
error: "error",
info: "info",
}; };
const statusLabelMap: Record<StatusType, string> = { const statusLabelMap: Record<StatusType, string> = {
@@ -43,12 +40,17 @@ const statusLabelMap: Record<StatusType, string> = {
info: "Info", info: "Info",
}; };
export function StatusBadge({ status, children, ...props }: StatusBadgeProps) { export function StatusBadge({
const variant = statusVariantMap[status]; status,
children,
className,
...props
}: StatusBadgeProps) {
const statusClass = statusClassMap[status];
const label = children || statusLabelMap[status]; const label = children || statusLabelMap[status];
return ( return (
<Badge variant={variant} {...props}> <Badge className={cn(statusClass, className)} {...props}>
{label} {label}
</Badge> </Badge>
); );

View File

@@ -255,11 +255,11 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
{/* Main Form Container - styled like data table */} {/* Main Form Container - styled like data table */}
<div className="space-y-4"> <div className="space-y-4">
{/* Basic Information */} {/* Basic Information */}
<Card> <Card className="card-primary">
<CardHeader> <CardHeader>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-r from-emerald-600/10 to-teal-600/10"> <div className="bg-brand-muted flex h-10 w-10 items-center justify-center rounded-lg">
<Building className="h-5 w-5 text-emerald-700 dark:text-emerald-400" /> <Building className="text-brand-light h-5 w-5" />
</div> </div>
<div> <div>
<CardTitle>Basic Information</CardTitle> <CardTitle>Basic Information</CardTitle>
@@ -376,12 +376,12 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
</Card> </Card>
{/* Address */} {/* Address */}
<Card> <Card className="card-primary">
<CardHeader> <CardHeader>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-r from-emerald-600/10 to-teal-600/10"> <div className="bg-brand-muted flex h-10 w-10 items-center justify-center rounded-lg">
<svg <svg
className="h-5 w-5 text-emerald-700 dark:text-emerald-400" className="text-brand-light h-5 w-5"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
@@ -424,11 +424,11 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
</Card> </Card>
{/* Settings */} {/* Settings */}
<Card> <Card className="card-primary">
<CardHeader> <CardHeader>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-r from-emerald-600/10 to-teal-600/10"> <div className="bg-brand-muted flex h-10 w-10 items-center justify-center rounded-lg">
<Star className="h-5 w-5 text-emerald-700 dark:text-emerald-400" /> <Star className="text-brand-light h-5 w-5" />
</div> </div>
<div> <div>
<CardTitle>Settings</CardTitle> <CardTitle>Settings</CardTitle>
@@ -439,7 +439,7 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="border-border/40 flex items-center justify-between rounded-xl border bg-gradient-to-r from-emerald-600/5 to-teal-600/5 p-4"> <div className="bg-brand-muted border-border/40 flex items-center justify-between rounded-xl border p-4">
<div className="space-y-0.5"> <div className="space-y-0.5">
<Label htmlFor="isDefault" className="text-base font-medium"> <Label htmlFor="isDefault" className="text-base font-medium">
Default Business Default Business
@@ -485,7 +485,7 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
<Button <Button
type="submit" type="submit"
disabled={isSubmitting || !isDirty} disabled={isSubmitting || !isDirty}
className="bg-gradient-to-r from-emerald-600 to-teal-600 shadow-md transition-all duration-200 hover:from-emerald-700 hover:to-teal-700 hover:shadow-lg" className="btn-brand-primary shadow-md"
> >
{isSubmitting ? ( {isSubmitting ? (
<> <>
@@ -525,18 +525,22 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
disabled={isSubmitting || !isDirty} disabled={isSubmitting || !isDirty}
className="bg-gradient-to-r from-emerald-600 to-teal-600 shadow-md transition-all duration-200 hover:from-emerald-700 hover:to-teal-700 hover:shadow-lg" className="btn-brand-primary shadow-md"
size="sm" size="sm"
> >
{isSubmitting ? ( {isSubmitting ? (
<> <>
<Loader2 className="h-4 w-4 animate-spin sm:mr-2" /> <Loader2 className="h-4 w-4 animate-spin sm:mr-2" />
<span className="hidden sm:inline">{mode === "create" ? "Creating..." : "Saving..."}</span> <span className="hidden sm:inline">
{mode === "create" ? "Creating..." : "Saving..."}
</span>
</> </>
) : ( ) : (
<> <>
<Save className="h-4 w-4 sm:mr-2" /> <Save className="h-4 w-4 sm:mr-2" />
<span className="hidden sm:inline">{mode === "create" ? "Create Business" : "Save Changes"}</span> <span className="hidden sm:inline">
{mode === "create" ? "Create Business" : "Save Changes"}
</span>
</> </>
)} )}
</Button> </Button>

View File

@@ -221,7 +221,7 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
{/* Main Form Container - styled like data table */} {/* Main Form Container - styled like data table */}
<div className="space-y-4"> <div className="space-y-4">
{/* Basic Information */} {/* Basic Information */}
<Card> <Card className="card-primary">
<CardHeader> <CardHeader>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-r from-emerald-600/10 to-teal-600/10"> <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-r from-emerald-600/10 to-teal-600/10">
@@ -300,7 +300,7 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
</Card> </Card>
{/* Address */} {/* Address */}
<Card> <Card className="card-primary">
<CardHeader> <CardHeader>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-r from-emerald-600/10 to-teal-600/10"> <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-r from-emerald-600/10 to-teal-600/10">
@@ -348,7 +348,7 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
</Card> </Card>
{/* Billing Information */} {/* Billing Information */}
<Card> <Card className="card-primary">
<CardHeader> <CardHeader>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-r from-emerald-600/10 to-teal-600/10"> <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-r from-emerald-600/10 to-teal-600/10">

View File

@@ -59,7 +59,7 @@ function InvoiceFormSkeleton() {
</div> </div>
{/* Invoice Details Card */} {/* Invoice Details Card */}
<Card className="shadow-sm"> <Card className="card-primary">
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="bg-muted/30 h-5 w-5 animate-pulse rounded"></div> <div className="bg-muted/30 h-5 w-5 animate-pulse rounded"></div>
@@ -118,7 +118,7 @@ function InvoiceFormSkeleton() {
</Card> </Card>
{/* Invoice Items Card */} {/* Invoice Items Card */}
<Card className="shadow-sm"> <Card className="card-primary">
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="bg-muted/30 h-5 w-5 animate-pulse rounded"></div> <div className="bg-muted/30 h-5 w-5 animate-pulse rounded"></div>
@@ -172,7 +172,7 @@ function InvoiceFormSkeleton() {
{/* Right Column - Summary */} {/* Right Column - Summary */}
<div className="space-y-6"> <div className="space-y-6">
<Card className="sticky top-6 shadow-sm"> <Card className="card-primary sticky top-6">
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="bg-muted/30 h-5 w-5 animate-pulse rounded"></div> <div className="bg-muted/30 h-5 w-5 animate-pulse rounded"></div>
@@ -493,7 +493,7 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
</TabsList> </TabsList>
<TabsContent value="invoice-details"> <TabsContent value="invoice-details">
{/* Invoice Details */} {/* Invoice Details */}
<Card className="shadow-sm"> <Card className="card-primary">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<FileText className="h-5 w-5" /> <FileText className="h-5 w-5" />
@@ -554,6 +554,7 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
issueDate: date ?? new Date(), issueDate: date ?? new Date(),
})) }))
} }
className="w-full"
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
@@ -566,6 +567,7 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
dueDate: date ?? new Date(), dueDate: date ?? new Date(),
})) }))
} }
className="w-full"
/> />
</div> </div>
</div> </div>
@@ -681,7 +683,7 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
</Card> </Card>
</TabsContent> </TabsContent>
<TabsContent value="invoice-items"> <TabsContent value="invoice-items">
<Card className="shadow-sm"> <Card className="card-primary">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<DollarSign className="h-5 w-5" /> <DollarSign className="h-5 w-5" />
@@ -705,7 +707,7 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
{/* Right Column - Summary (Always Visible) */} {/* Right Column - Summary (Always Visible) */}
<div className="space-y-6"> <div className="space-y-6">
<Card className="sticky top-6 shadow-sm"> <Card className="card-primary sticky top-6">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<Check className="h-5 w-5" /> <Check className="h-5 w-5" />

View File

@@ -2,6 +2,7 @@
import * as React from "react"; import * as React from "react";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { Card, CardContent } from "~/components/ui/card";
import { Input } from "~/components/ui/input"; import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label"; import { Label } from "~/components/ui/label";
import { DatePicker } from "~/components/ui/date-picker"; import { DatePicker } from "~/components/ui/date-picker";
@@ -62,28 +63,27 @@ function LineItemRow({
onUpdate, onUpdate,
}: LineItemRowProps) { }: LineItemRowProps) {
return ( return (
<> <div className="card-secondary hidden rounded-lg p-4 md:block">
{/* Desktop Layout - Table Row */} <div className="flex items-start gap-3">
<tr className="group hover:bg-muted/20 hidden transition-colors lg:table-row">
{/* Drag Handle */} {/* Drag Handle */}
<td className="w-6 p-2 text-center align-top"> <div className="mt-1 flex items-center justify-center">
<GripVertical className="text-muted-foreground mt-1 h-4 w-4 cursor-grab" /> <GripVertical className="text-muted-foreground h-4 w-4 cursor-grab" />
</td> </div>
{/* Main Content */} {/* Main Content */}
<td className="p-2" colSpan={5}> <div className="flex-1 space-y-3">
{/* Description */} {/* Description */}
<div className="mb-3"> <div>
<Input <Input
value={item.description} value={item.description}
onChange={(e) => onUpdate(index, "description", e.target.value)} onChange={(e) => onUpdate(index, "description", e.target.value)}
placeholder="Describe the work performed..." placeholder="Describe the work performed..."
className="w-full border-0 bg-transparent py-0 pr-0 pl-2 text-sm font-medium focus-visible:ring-0" className="w-full text-sm font-medium"
/> />
</div> </div>
{/* Controls Row */} {/* Controls Row */}
<div className="flex items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
{/* Date */} {/* Date */}
<DatePicker <DatePicker
date={item.date} date={item.date}
@@ -91,7 +91,7 @@ function LineItemRow({
onUpdate(index, "date", date ?? new Date()) onUpdate(index, "date", date ?? new Date())
} }
size="sm" size="sm"
className="h-9 w-28" className="h-9 w-36"
/> />
{/* Hours */} {/* Hours */}
@@ -101,7 +101,7 @@ function LineItemRow({
min={0} min={0}
step={0.25} step={0.25}
width="auto" width="auto"
className="h-9 w-28" className="h-9 w-32"
/> />
{/* Rate */} {/* Rate */}
@@ -112,7 +112,7 @@ function LineItemRow({
step={1} step={1}
prefix="$" prefix="$"
width="auto" width="auto"
className="h-9 w-28" className="h-9 w-32"
/> />
{/* Amount */} {/* Amount */}
@@ -138,80 +138,9 @@ function LineItemRow({
<Trash2 className="h-4 w-4" /> <Trash2 className="h-4 w-4" />
</Button> </Button>
</div> </div>
</td> </div>
</tr> </div>
</div>
{/* Tablet Layout - Condensed Row */}
<tr className="group hover:bg-muted/20 hidden transition-colors md:table-row lg:hidden">
{/* Drag Handle */}
<td className="w-6 p-2 text-center align-top">
<GripVertical className="text-muted-foreground mt-1 h-4 w-4 cursor-grab" />
</td>
{/* Main Content - Description on top, inputs below */}
<td className="p-3" colSpan={6}>
{/* Description */}
<div className="mb-3">
<Input
value={item.description}
onChange={(e) => onUpdate(index, "description", e.target.value)}
placeholder="Describe the work performed..."
className="w-full pl-3 text-sm font-medium"
/>
</div>
{/* Controls Row - Date/Hours/Rate break to separate rows on smaller screens */}
<div className="flex flex-wrap items-center gap-2 sm:gap-3">
<DatePicker
date={item.date}
onDateChange={(date) =>
onUpdate(index, "date", date ?? new Date())
}
size="sm"
className="h-9 w-full sm:w-28"
/>
<NumberInput
value={item.hours}
onChange={(value) => onUpdate(index, "hours", value)}
min={0}
step={0.25}
width="full"
className="h-9 w-1/2 sm:w-28"
/>
<NumberInput
value={item.rate}
onChange={(value) => onUpdate(index, "rate", value)}
min={0}
step={1}
prefix="$"
width="full"
className="h-9 w-1/2 sm:w-28"
/>
{/* Amount and Actions - inline with controls on larger screens */}
<div className="mt-3 flex items-center justify-between sm:mt-0 sm:ml-auto sm:gap-3">
<span className="text-primary font-semibold">
${(item.hours * item.rate).toFixed(2)}
</span>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => onRemove(index)}
className={cn(
"text-muted-foreground h-8 w-8 p-0 transition-colors hover:text-red-500",
!canRemove && "cursor-not-allowed opacity-50",
)}
disabled={!canRemove}
aria-label="Remove item"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</td>
</tr>
</>
); );
} }
@@ -227,55 +156,57 @@ function MobileLineItem({
isLast, isLast,
}: LineItemRowProps) { }: LineItemRowProps) {
return ( return (
<div className="bg-card space-y-3 rounded-lg border p-4 md:hidden"> <div className="card-secondary space-y-3 rounded-lg md:hidden">
{/* Description */} <div className="space-y-3 p-4">
<div className="space-y-1"> {/* Description */}
<Label className="text-muted-foreground text-xs">Description</Label>
<Input
value={item.description}
onChange={(e) => onUpdate(index, "description", e.target.value)}
placeholder="Describe the work performed..."
className="pl-3 text-sm"
/>
</div>
{/* Date */}
<div className="space-y-1">
<Label className="text-muted-foreground text-xs">Date</Label>
<DatePicker
date={item.date}
onDateChange={(date) => onUpdate(index, "date", date ?? new Date())}
size="sm"
/>
</div>
{/* Hours and Rate in a row */}
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1"> <div className="space-y-1">
<Label className="text-muted-foreground text-xs">Hours</Label> <Label className="text-muted-foreground text-xs">Description</Label>
<NumberInput <Input
value={item.hours} value={item.description}
onChange={(value) => onUpdate(index, "hours", value)} onChange={(e) => onUpdate(index, "description", e.target.value)}
min={0} placeholder="Describe the work performed..."
step={0.25} className="pl-3 text-sm"
width="full"
/> />
</div> </div>
{/* Date */}
<div className="space-y-1"> <div className="space-y-1">
<Label className="text-muted-foreground text-xs">Rate</Label> <Label className="text-muted-foreground text-xs">Date</Label>
<NumberInput <DatePicker
value={item.rate} date={item.date}
onChange={(value) => onUpdate(index, "rate", value)} onDateChange={(date) => onUpdate(index, "date", date ?? new Date())}
min={0} size="sm"
step={1}
prefix="$"
width="full"
/> />
</div> </div>
{/* Hours and Rate in a row */}
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1">
<Label className="text-muted-foreground text-xs">Hours</Label>
<NumberInput
value={item.hours}
onChange={(value) => onUpdate(index, "hours", value)}
min={0}
step={0.25}
width="full"
/>
</div>
<div className="space-y-1">
<Label className="text-muted-foreground text-xs">Rate</Label>
<NumberInput
value={item.rate}
onChange={(value) => onUpdate(index, "rate", value)}
min={0}
step={1}
prefix="$"
width="full"
/>
</div>
</div>
</div> </div>
{/* Bottom section with controls, item name, and total */} {/* Bottom section with controls, item name, and total */}
<div className="flex items-center justify-between border-t pt-2"> <div className="flex items-center justify-between rounded-b-lg border-t border-slate-400/60 bg-slate-200/30 px-4 py-2 dark:border-slate-500/60 dark:bg-slate-700/30">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button <Button
type="button" type="button"
@@ -355,71 +286,36 @@ export function InvoiceLineItems({
return ( return (
<div className={cn("space-y-2", className)}> <div className={cn("space-y-2", className)}>
{/* Desktop and Tablet Table */} {/* Desktop and Mobile Cards */}
<div className="hidden md:block"> <div className="space-y-2">
<div className="overflow-hidden rounded-lg border">
<table className="w-full">
{/* Desktop Header */}
<thead className="bg-muted/30 hidden lg:table-header-group">
<tr>
<th className="w-6 p-2"></th>
<th
className="text-muted-foreground p-2 text-left text-xs font-medium"
colSpan={5}
>
Invoice Items
</th>
</tr>
</thead>
{/* Tablet Header */}
<thead className="bg-muted/30 md:table-header-group lg:hidden">
<tr>
<th className="w-6 p-2"></th>
<th
className="text-muted-foreground p-2 text-left text-xs font-medium"
colSpan={6}
>
Invoice Items
</th>
</tr>
</thead>
<tbody className="divide-y">
{items.map((item, index) => (
<LineItemRow
key={item.id}
item={item}
index={index}
canRemove={canRemoveItems}
onRemove={onRemoveItem}
onUpdate={onUpdateItem}
onMoveUp={onMoveUp}
onMoveDown={onMoveDown}
isFirst={index === 0}
isLast={index === items.length - 1}
/>
))}
</tbody>
</table>
</div>
</div>
{/* Mobile Cards */}
<div className="space-y-2 md:hidden">
{items.map((item, index) => ( {items.map((item, index) => (
<MobileLineItem <React.Fragment key={item.id}>
key={item.id} {/* Desktop/Tablet Card */}
item={item} <LineItemRow
index={index} item={item}
canRemove={canRemoveItems} index={index}
onRemove={onRemoveItem} canRemove={canRemoveItems}
onUpdate={onUpdateItem} onRemove={onRemoveItem}
onMoveUp={onMoveUp} onUpdate={onUpdateItem}
onMoveDown={onMoveDown} onMoveUp={onMoveUp}
isFirst={index === 0} onMoveDown={onMoveDown}
isLast={index === items.length - 1} isFirst={index === 0}
/> isLast={index === items.length - 1}
/>
{/* Mobile Card */}
<MobileLineItem
item={item}
index={index}
canRemove={canRemoveItems}
onRemove={onRemoveItem}
onUpdate={onUpdateItem}
onMoveUp={onMoveUp}
onMoveDown={onMoveDown}
isFirst={index === 0}
isLast={index === items.length - 1}
/>
</React.Fragment>
))} ))}
</div> </div>

View File

@@ -97,19 +97,11 @@ export function FloatingActionBar({
if (!isVisible) return null; if (!isVisible) return null;
return ( return (
<div <div ref={floatingRef} className={cn("floating-action-bar", className)}>
ref={floatingRef} <div className="floating-action-bar-content">
className={cn( {leftContent || <p className="floating-action-bar-title">{title}</p>}
"border-border/40 bg-background/60 animate-in slide-in-from-bottom-4 sticky bottom-4 z-20 flex items-center justify-between rounded-2xl border p-4 shadow-lg backdrop-blur-xl backdrop-saturate-150 duration-300",
className,
)}
>
<div className="flex-1">
{leftContent || (
<p className="text-muted-foreground text-sm">{title}</p>
)}
</div> </div>
<div className="flex items-center gap-2 sm:gap-3">{children}</div> <div className="floating-action-bar-actions">{children}</div>
</div> </div>
); );
} }

View File

@@ -22,11 +22,11 @@ export function PageHeader({
switch (variant) { switch (variant) {
case "gradient": case "gradient":
return `${baseClasses} text-3xl bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-transparent`; return `${baseClasses} text-3xl text-brand-gradient`;
case "large": case "large":
return `${baseClasses} text-4xl text-foreground`; return `${baseClasses} text-4xl text-foreground`;
case "large-gradient": case "large-gradient":
return `${baseClasses} text-4xl bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-transparent`; return `${baseClasses} text-4xl text-brand-gradient`;
default: default:
return `${baseClasses} text-3xl text-foreground`; return `${baseClasses} text-3xl text-foreground`;
} }

View File

@@ -99,7 +99,7 @@ export function QuickActionCard({
export function QuickActionCardSkeleton() { export function QuickActionCardSkeleton() {
return ( return (
<Card className="border-0 shadow-md"> <Card className="card-primary">
<CardContent className="p-6"> <CardContent className="p-6">
<div className="animate-pulse"> <div className="animate-pulse">
<div className="bg-muted mx-auto mb-3 h-12 w-12 rounded-full"></div> <div className="bg-muted mx-auto mb-3 h-12 w-12 rounded-full"></div>

View File

@@ -10,22 +10,26 @@ const badgeVariants = cva(
variants: { variants: {
variant: { variant: {
default: default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", "border-slate-300 bg-slate-200 text-slate-800 shadow-sm dark:border-slate-600 dark:bg-slate-700 dark:text-slate-200",
secondary: secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", "border-slate-300 bg-slate-200/80 text-slate-700 shadow-sm dark:border-slate-600 dark:bg-slate-700/80 dark:text-slate-300",
destructive: destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline: outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", "border-2 border-slate-300 bg-transparent text-slate-700 dark:border-slate-600 dark:text-slate-300",
success: "border-transparent bg-status-success [a&]:hover:opacity-90", success: "border-transparent bg-status-success [a&]:hover:opacity-90",
warning: "border-transparent bg-status-warning [a&]:hover:opacity-90", warning: "border-transparent bg-status-warning [a&]:hover:opacity-90",
error: "border-transparent bg-status-error [a&]:hover:opacity-90", error: "border-transparent bg-status-error [a&]:hover:opacity-90",
info: "border-transparent bg-status-info [a&]:hover:opacity-90", info: "border-transparent bg-status-info [a&]:hover:opacity-90",
// Outlined variants for status badges // Outlined variants for status badges
"outline-draft": "border-gray-400 text-gray-600 dark:border-gray-500 dark:text-gray-300 bg-transparent", "outline-draft":
"outline-sent": "border-blue-400 text-blue-600 dark:border-blue-500 dark:text-blue-300 bg-transparent", "border-gray-400 text-gray-600 dark:border-gray-500 dark:text-gray-300 bg-transparent",
"outline-paid": "border-green-400 text-green-600 dark:border-green-500 dark:text-green-300 bg-transparent", "outline-sent":
"outline-overdue": "border-red-400 text-red-600 dark:border-red-500 dark:text-red-300 bg-transparent", "border-blue-400 text-blue-600 dark:border-blue-500 dark:text-blue-300 bg-transparent",
"outline-paid":
"border-green-400 text-green-600 dark:border-green-500 dark:text-green-300 bg-transparent",
"outline-overdue":
"border-red-400 text-red-600 dark:border-red-500 dark:text-red-300 bg-transparent",
}, },
}, },
defaultVariants: { defaultVariants: {

View File

@@ -1,15 +1,22 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import { import {
ChevronDownIcon, ChevronDownIcon,
ChevronLeftIcon, ChevronLeftIcon,
ChevronRightIcon, ChevronRightIcon,
} from "lucide-react" } from "lucide-react";
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker" import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker";
import { cn } from "~/lib/utils" import { cn } from "~/lib/utils";
import { Button, buttonVariants } from "~/components/ui/button" import { Button, buttonVariants } from "~/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "~/components/ui/select";
function Calendar({ function Calendar({
className, className,
@@ -19,11 +26,33 @@ function Calendar({
buttonVariant = "ghost", buttonVariant = "ghost",
formatters, formatters,
components, components,
month,
onMonthChange,
...props ...props
}: React.ComponentProps<typeof DayPicker> & { }: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"] buttonVariant?: React.ComponentProps<typeof Button>["variant"];
}) { }) {
const defaultClassNames = getDefaultClassNames() const defaultClassNames = getDefaultClassNames();
const months = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
];
const currentYear = month?.getFullYear() || new Date().getFullYear();
const currentMonth = month?.getMonth() || new Date().getMonth();
const years = Array.from({ length: 11 }, (_, i) => currentYear - 5 + i);
return ( return (
<DayPicker <DayPicker
@@ -32,94 +61,82 @@ function Calendar({
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent", "bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`, String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className className,
)} )}
captionLayout={captionLayout} captionLayout={captionLayout}
formatters={{ month={month}
formatMonthDropdown: (date) => onMonthChange={onMonthChange}
date.toLocaleString("default", { month: "short" }),
...formatters,
}}
classNames={{ classNames={{
root: cn("w-fit", defaultClassNames.root), root: cn("w-fit", defaultClassNames.root),
months: cn( months: cn(
"flex gap-4 flex-col md:flex-row relative", "flex gap-4 flex-col md:flex-row relative",
defaultClassNames.months defaultClassNames.months,
), ),
month: cn("flex flex-col w-full gap-4", defaultClassNames.month), month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
nav: cn( nav: cn(
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between", "flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
defaultClassNames.nav defaultClassNames.nav,
), ),
button_previous: cn( button_previous: cn(
buttonVariants({ variant: buttonVariant }), buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
defaultClassNames.button_previous defaultClassNames.button_previous,
), ),
button_next: cn( button_next: cn(
buttonVariants({ variant: buttonVariant }), buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
defaultClassNames.button_next defaultClassNames.button_next,
), ),
month_caption: cn( month_caption: cn(
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)", "flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
defaultClassNames.month_caption defaultClassNames.month_caption,
), ),
dropdowns: cn( dropdowns: cn(
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5", "w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
defaultClassNames.dropdowns defaultClassNames.dropdowns,
), ),
dropdown_root: cn( dropdown_root: cn(
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md", "relative has-focus:border-ring border border-input shadow-sm has-focus:ring-ring/50 has-focus:ring-2 rounded-md h-8",
defaultClassNames.dropdown_root defaultClassNames.dropdown_root,
), ),
dropdown: cn( dropdown: cn(
"absolute bg-popover inset-0 opacity-0", "absolute bg-transparent inset-0 w-full h-full opacity-0 cursor-pointer",
defaultClassNames.dropdown defaultClassNames.dropdown,
), ),
caption_label: cn( caption_label: cn(
"select-none font-medium", "select-none font-medium text-sm hidden",
captionLayout === "label" defaultClassNames.caption_label,
? "text-sm"
: "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
defaultClassNames.caption_label
), ),
table: "w-full border-collapse", table: "w-full border-collapse",
weekdays: cn("flex", defaultClassNames.weekdays), weekdays: cn("flex", defaultClassNames.weekdays),
weekday: cn( weekday: cn(
"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none", "text-muted-foreground flex-1 font-normal text-[0.8rem] select-none",
defaultClassNames.weekday defaultClassNames.weekday,
), ),
week: cn("flex w-full mt-2", defaultClassNames.week), week: cn("flex w-full mt-2", defaultClassNames.week),
week_number_header: cn( week_number_header: cn(
"select-none w-(--cell-size)", "select-none w-(--cell-size)",
defaultClassNames.week_number_header defaultClassNames.week_number_header,
), ),
week_number: cn( week_number: cn(
"text-[0.8rem] select-none text-muted-foreground", "text-[0.8rem] select-none text-muted-foreground",
defaultClassNames.week_number defaultClassNames.week_number,
), ),
day: cn( day: cn(
"relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none", "relative w-full h-full p-0 text-center group/day aspect-square select-none",
defaultClassNames.day defaultClassNames.day,
),
range_start: cn(
"rounded-l-md bg-accent",
defaultClassNames.range_start
),
range_middle: cn("rounded-none", defaultClassNames.range_middle),
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
today: cn(
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
defaultClassNames.today
), ),
range_start: cn("", defaultClassNames.range_start),
range_middle: cn("", defaultClassNames.range_middle),
range_end: cn("", defaultClassNames.range_end),
today: cn("font-semibold", defaultClassNames.today),
outside: cn( outside: cn(
"text-muted-foreground aria-selected:text-muted-foreground", "text-muted-foreground aria-selected:text-muted-foreground",
defaultClassNames.outside defaultClassNames.outside,
), ),
disabled: cn( disabled: cn(
"text-muted-foreground opacity-50", "text-muted-foreground opacity-50",
defaultClassNames.disabled defaultClassNames.disabled,
), ),
hidden: cn("invisible", defaultClassNames.hidden), hidden: cn("invisible", defaultClassNames.hidden),
...classNames, ...classNames,
@@ -133,13 +150,13 @@ function Calendar({
className={cn(className)} className={cn(className)}
{...props} {...props}
/> />
) );
}, },
Chevron: ({ className, orientation, ...props }) => { Chevron: ({ className, orientation, ...props }) => {
if (orientation === "left") { if (orientation === "left") {
return ( return (
<ChevronLeftIcon className={cn("size-4", className)} {...props} /> <ChevronLeftIcon className={cn("size-4", className)} {...props} />
) );
} }
if (orientation === "right") { if (orientation === "right") {
@@ -148,14 +165,67 @@ function Calendar({
className={cn("size-4", className)} className={cn("size-4", className)}
{...props} {...props}
/> />
) );
} }
return ( return (
<ChevronDownIcon className={cn("size-4", className)} {...props} /> <ChevronDownIcon className={cn("size-4", className)} {...props} />
) );
}, },
DayButton: CalendarDayButton, DayButton: CalendarDayButton,
MonthCaption: ({ calendarMonth }) => {
if (captionLayout !== "dropdown") {
return null;
}
return (
<div className="calendar-custom-header flex items-center justify-center gap-2 py-2">
<Select
value={currentMonth.toString()}
onValueChange={(value) => {
const newDate = new Date(currentYear, parseInt(value), 1);
onMonthChange?.(newDate);
}}
>
<SelectTrigger
size="sm"
className="w-auto px-2 text-sm font-semibold"
>
<SelectValue />
</SelectTrigger>
<SelectContent>
{months.map((monthName, index) => (
<SelectItem key={index} value={index.toString()}>
{monthName}
</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={currentYear.toString()}
onValueChange={(value) => {
const newDate = new Date(parseInt(value), currentMonth, 1);
onMonthChange?.(newDate);
}}
>
<SelectTrigger
size="sm"
className="w-auto px-2 text-sm font-semibold"
>
<SelectValue />
</SelectTrigger>
<SelectContent>
{years.map((year) => (
<SelectItem key={year} value={year.toString()}>
{year}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
},
WeekNumber: ({ children, ...props }) => { WeekNumber: ({ children, ...props }) => {
return ( return (
<td {...props}> <td {...props}>
@@ -163,13 +233,13 @@ function Calendar({
{children} {children}
</div> </div>
</td> </td>
) );
}, },
...components, ...components,
}} }}
{...props} {...props}
/> />
) );
} }
function CalendarDayButton({ function CalendarDayButton({
@@ -178,12 +248,12 @@ function CalendarDayButton({
modifiers, modifiers,
...props ...props
}: React.ComponentProps<typeof DayButton>) { }: React.ComponentProps<typeof DayButton>) {
const defaultClassNames = getDefaultClassNames() const defaultClassNames = getDefaultClassNames();
const ref = React.useRef<HTMLButtonElement>(null) const ref = React.useRef<HTMLButtonElement>(null);
React.useEffect(() => { React.useEffect(() => {
if (modifiers.focused) ref.current?.focus() if (modifiers.focused) ref.current?.focus();
}, [modifiers.focused]) }, [modifiers.focused]);
return ( return (
<Button <Button
@@ -201,13 +271,14 @@ function CalendarDayButton({
data-range-end={modifiers.range_end} data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle} data-range-middle={modifiers.range_middle}
className={cn( className={cn(
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70", "hover:bg-accent hover:text-accent-foreground flex aspect-square size-auto h-8 w-full min-w-8 items-center justify-center rounded-md border-0 text-sm leading-none font-normal shadow-none",
defaultClassNames.day, modifiers.selected && "bg-primary text-primary-foreground",
className modifiers.today && !modifiers.selected && "bg-accent font-semibold",
className,
)} )}
{...props} {...props}
/> />
) );
} }
export { Calendar, CalendarDayButton } export { Calendar, CalendarDayButton };

View File

@@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
<div <div
data-slot="card" data-slot="card"
className={cn( className={cn(
"bg-background/60 text-card-foreground border-border/40 flex flex-col gap-6 rounded-2xl border py-6 shadow-lg backdrop-blur-xl backdrop-saturate-150", "bg-background/60 text-card-foreground border-border/40 flex flex-col gap-2 rounded-2xl border py-2 shadow-lg backdrop-blur-xl backdrop-saturate-150",
className, className,
)} )}
{...props} {...props}
@@ -20,7 +20,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
<div <div
data-slot="card-header" data-slot="card-header"
className={cn( className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6", "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 p-3 px-5 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className, className,
)} )}
{...props} {...props}
@@ -65,7 +65,7 @@ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="card-content" data-slot="card-content"
className={cn("px-6", className)} className={cn("px-5 pb-3", className)}
{...props} {...props}
/> />
); );
@@ -75,7 +75,7 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="card-footer" data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)} className={cn("flex items-center px-6 py-6 [.border-t]:pt-6", className)}
{...props} {...props}
/> />
); );

View File

@@ -1,11 +1,12 @@
"use client"; "use client";
import { format } from "date-fns";
import { Calendar as CalendarIcon } from "lucide-react";
import * as React from "react"; import * as React from "react";
import { parseDate } from "chrono-node";
import { CalendarIcon } from "lucide-react";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { Calendar } from "~/components/ui/calendar"; import { Calendar } from "~/components/ui/calendar";
import { Input } from "~/components/ui/input";
import { import {
Popover, Popover,
PopoverContent, PopoverContent,
@@ -13,6 +14,18 @@ import {
} from "~/components/ui/popover"; } from "~/components/ui/popover";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
function formatDate(date: Date | undefined) {
if (!date) {
return "";
}
return date.toLocaleDateString("en-US", {
day: "2-digit",
month: "long",
year: "numeric",
});
}
interface DatePickerProps { interface DatePickerProps {
date?: Date; date?: Date;
onDateChange: (date: Date | undefined) => void; onDateChange: (date: Date | undefined) => void;
@@ -26,13 +39,15 @@ interface DatePickerProps {
export function DatePicker({ export function DatePicker({
date, date,
onDateChange, onDateChange,
placeholder = "Select date", placeholder = "Tomorrow or next week",
className, className,
disabled = false, disabled = false,
id, id,
size = "md", size = "md",
}: DatePickerProps) { }: DatePickerProps) {
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const [value, setValue] = React.useState(formatDate(date));
const [month, setMonth] = React.useState<Date | undefined>(date);
const sizeClasses = { const sizeClasses = {
sm: "h-9 text-xs", sm: "h-9 text-xs",
@@ -40,42 +55,68 @@ export function DatePicker({
lg: "h-10 text-sm", lg: "h-10 text-sm",
}; };
const formatDate = (date: Date) => { const inputWidthClass = className?.includes("w-full")
if (size === "sm") { ? "w-full"
return format(date, "MMM dd"); : className?.includes("w-32") ||
} className?.includes("w-28") ||
return format(date, "PPP"); className?.includes("w-36")
}; ? className
: "w-full md:w-32 md:min-w-32";
React.useEffect(() => {
setValue(formatDate(date));
setMonth(date);
}, [date]);
return ( return (
<Popover open={open} onOpenChange={setOpen}> <div className={cn("relative flex gap-2", inputWidthClass, className)}>
<PopoverTrigger asChild> <Input
<Button id={id}
variant="outline" value={value}
id={id} placeholder={placeholder}
disabled={disabled} disabled={disabled}
className={cn( className={cn("bg-background pr-10", sizeClasses[size], "w-full")}
"w-full justify-between font-normal", onChange={(e) => {
sizeClasses[size], setValue(e.target.value);
!date && "text-muted-foreground", const parsedDate = parseDate(e.target.value);
className, if (parsedDate) {
)} onDateChange(parsedDate);
> setMonth(parsedDate);
{date ? formatDate(date) : placeholder} }
<CalendarIcon className="text-muted-foreground h-4 w-4" /> }}
</Button> onKeyDown={(e) => {
</PopoverTrigger> if (e.key === "ArrowDown") {
<PopoverContent className="w-auto overflow-hidden p-0" align="start"> e.preventDefault();
<Calendar setOpen(true);
mode="single" }
selected={date} }}
captionLayout="dropdown" />
onSelect={(selectedDate: Date | undefined) => { <Popover open={open} onOpenChange={setOpen}>
onDateChange(selectedDate); <PopoverTrigger asChild>
setOpen(false); <Button
}} variant="ghost"
/> disabled={disabled}
</PopoverContent> className="absolute top-1/2 right-2 size-6 -translate-y-1/2"
</Popover> >
<CalendarIcon className="size-3.5" />
<span className="sr-only">Select date</span>
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto overflow-hidden p-0" align="end">
<Calendar
mode="single"
selected={date}
captionLayout="dropdown"
month={month}
onMonthChange={setMonth}
onSelect={(selectedDate) => {
onDateChange(selectedDate);
setValue(formatDate(selectedDate));
setOpen(false);
}}
/>
</PopoverContent>
</Popover>
</div>
); );
} }

View File

@@ -71,12 +71,12 @@ export function NumberInput({
onChange(Math.max(min, (value || 0) - step)); onChange(Math.max(min, (value || 0) - step));
}; };
const widthClass = width === "full" ? "w-full" : "w-24"; const widthClass = width === "full" ? "w-full" : "w-24 min-w-24";
return ( return (
<div <div
className={cn( className={cn(
"border-input bg-background ring-offset-background flex h-9 items-center justify-center rounded-md border px-2 text-sm", "bg-background flex h-9 items-center justify-center rounded-md text-sm shadow-none",
widthClass, widthClass,
disabled && "cursor-not-allowed opacity-50", disabled && "cursor-not-allowed opacity-50",
className, className,
@@ -103,7 +103,7 @@ export function NumberInput({
onBlur={handleBlur} onBlur={handleBlur}
placeholder={placeholder} placeholder={placeholder}
disabled={disabled} disabled={disabled}
className="w-16 border-0 bg-transparent text-center outline-none focus-visible:ring-0" className="number-input-field w-full border-0 bg-transparent text-center ring-0 outline-none focus:border-transparent focus:ring-0 focus:outline-none focus-visible:ring-0"
/> />
{suffix && ( {suffix && (
<span className="text-muted-foreground text-xs">{suffix}</span> <span className="text-muted-foreground text-xs">{suffix}</span>

File diff suppressed because it is too large Load Diff