mirror of
https://github.com/soconnor0919/beenvoice.git
synced 2026-02-05 00:06:36 -05:00
Theme overhaul
This commit is contained in:
@@ -1,20 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useState, Suspense } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Card, CardContent } from "~/components/ui/card";
|
||||
import { Input } from "~/components/ui/input";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Label } from "~/components/ui/label";
|
||||
import { toast } from "sonner";
|
||||
import { Logo } from "~/components/branding/logo";
|
||||
import { User, Mail, Lock, ArrowRight } from "lucide-react";
|
||||
import { LegalModal } from "~/components/ui/legal-modal";
|
||||
import { Mail, Lock, ArrowRight, User, Clock, Rocket, Zap } from "lucide-react";
|
||||
|
||||
function RegisterForm() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const callbackUrl = searchParams.get("callbackUrl") ?? "/dashboard";
|
||||
|
||||
const [firstName, setFirstName] = useState("");
|
||||
const [lastName, setLastName] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
@@ -24,177 +23,238 @@ function RegisterForm() {
|
||||
async function handleRegister(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
|
||||
const res = await fetch("/api/auth/register", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
firstName,
|
||||
lastName,
|
||||
name: `${firstName} ${lastName}`,
|
||||
email,
|
||||
password,
|
||||
}),
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
|
||||
if (res.ok) {
|
||||
toast.success("Account created successfully! Please sign in.");
|
||||
const signInUrl =
|
||||
callbackUrl !== "/dashboard"
|
||||
? `/auth/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`
|
||||
: "/auth/signin";
|
||||
router.push(signInUrl);
|
||||
router.push("/auth/signin");
|
||||
} else {
|
||||
const error = await res.text();
|
||||
toast.error(error || "Failed to create account");
|
||||
const data = (await res.json()) as { error?: string };
|
||||
toast.error(data.error ?? "Registration failed");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="floating-orbs flex min-h-screen items-center justify-center p-4">
|
||||
<div className="w-full max-w-md space-y-8">
|
||||
{/* Logo and Welcome */}
|
||||
<div className="space-y-4 text-center">
|
||||
<Logo size="lg" className="mx-auto" />
|
||||
<div>
|
||||
<h1 className="text-foreground text-2xl font-bold">
|
||||
Join beenvoice
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Create your account to get started
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-background flex min-h-screen items-center justify-center">
|
||||
<Card className="mx-auto h-screen w-full overflow-hidden border-0 shadow-none md:h-auto md:max-w-6xl md:border md:shadow-lg">
|
||||
<CardContent className="grid h-full p-0 md:grid-cols-2">
|
||||
{/* Hero Section - Hidden on mobile */}
|
||||
<div className="bg-muted relative hidden md:flex md:flex-col md:justify-center md:p-12">
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Logo size="xl" />
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<h1 className="text-3xl font-bold lg:text-4xl">
|
||||
Start your
|
||||
<span className="text-primary"> invoicing journey</span>
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-lg">
|
||||
Join thousands of freelancers and small businesses who trust
|
||||
beenvoice to manage their invoicing and get paid faster.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Registration Form */}
|
||||
<Card className="card-primary">
|
||||
<CardHeader className="space-y-1">
|
||||
<CardTitle className="text-center text-xl">
|
||||
Create Account
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleRegister} className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="firstName">First Name</Label>
|
||||
<div className="relative">
|
||||
<User className="form-icon-left" />
|
||||
<Input
|
||||
id="firstName"
|
||||
type="text"
|
||||
value={firstName}
|
||||
onChange={(e) => setFirstName(e.target.value)}
|
||||
required
|
||||
autoFocus
|
||||
className="form-input-with-icon"
|
||||
placeholder="First name"
|
||||
/>
|
||||
<div className="grid gap-4">
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="bg-primary/10 rounded-lg p-2">
|
||||
<Rocket className="text-primary h-5 w-5" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h3 className="font-semibold">Quick Setup</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Get started in minutes with our intuitive setup wizard
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="lastName">Last Name</Label>
|
||||
<div className="relative">
|
||||
<User className="form-icon-left" />
|
||||
<Input
|
||||
id="lastName"
|
||||
type="text"
|
||||
value={lastName}
|
||||
onChange={(e) => setLastName(e.target.value)}
|
||||
required
|
||||
className="form-input-with-icon"
|
||||
placeholder="Last name"
|
||||
/>
|
||||
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="bg-primary/10 rounded-lg p-2">
|
||||
<Zap className="text-primary h-5 w-5" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h3 className="font-semibold">Fast Payments</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Professional invoices that get you paid 3x faster
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="bg-primary/10 rounded-lg p-2">
|
||||
<Clock className="text-primary h-5 w-5" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h3 className="font-semibold">Time Tracking</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Track time and convert it to accurate invoices instantly
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<div className="relative">
|
||||
<Mail className="form-icon-left" />
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
className="form-input-with-icon"
|
||||
placeholder="Enter your email"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sign Up Form */}
|
||||
<div className="flex flex-col justify-center p-6 md:p-12">
|
||||
<div className="mx-auto w-full max-w-sm space-y-6">
|
||||
{/* Mobile Logo */}
|
||||
<div className="flex justify-center md:hidden">
|
||||
<Logo size="lg" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<div className="relative">
|
||||
<Lock className="form-icon-left" />
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
minLength={6}
|
||||
className="form-input-with-icon"
|
||||
placeholder="Create a password"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Must be at least 6 characters
|
||||
|
||||
<div className="space-y-2 text-center md:text-left">
|
||||
<h1 className="text-2xl font-bold">Create your account</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Supercharge your invoicing today
|
||||
</p>
|
||||
</div>
|
||||
<Button type="submit" className="w-full" disabled={loading}>
|
||||
{loading ? (
|
||||
"Creating account..."
|
||||
) : (
|
||||
<>
|
||||
Create Account
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
<div className="mt-6 text-center text-sm">
|
||||
<span className="text-muted-foreground">
|
||||
Already have an account?{" "}
|
||||
</span>
|
||||
<Link href="/auth/signin" className="nav-link-brand">
|
||||
Sign in here
|
||||
</Link>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Features */}
|
||||
<div className="space-y-4 text-center">
|
||||
<p className="welcome-description">Start invoicing like a pro</p>
|
||||
<div className="welcome-feature-list">
|
||||
<span>✓ Free to start</span>
|
||||
<span>✓ No credit card</span>
|
||||
<span>✓ Cancel anytime</span>
|
||||
<form onSubmit={handleRegister} className="space-y-4">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="firstName">First Name</Label>
|
||||
<div className="relative">
|
||||
<User className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 z-10 h-4 w-4 -translate-y-1/2" />
|
||||
<Input
|
||||
id="firstName"
|
||||
type="text"
|
||||
value={firstName}
|
||||
onChange={(e) => setFirstName(e.target.value)}
|
||||
required
|
||||
autoFocus
|
||||
className="h-11 pl-10"
|
||||
placeholder="John"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="lastName">Last Name</Label>
|
||||
<div className="relative">
|
||||
<User className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 z-10 h-4 w-4 -translate-y-1/2" />
|
||||
<Input
|
||||
id="lastName"
|
||||
type="text"
|
||||
value={lastName}
|
||||
onChange={(e) => setLastName(e.target.value)}
|
||||
required
|
||||
className="h-11 pl-10"
|
||||
placeholder="Doe"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email Address</Label>
|
||||
<div className="relative">
|
||||
<Mail className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 z-10 h-4 w-4 -translate-y-1/2" />
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
className="h-11 pl-10"
|
||||
placeholder="john@example.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<div className="relative">
|
||||
<Lock className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 z-10 h-4 w-4 -translate-y-1/2" />
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
className="h-11 pl-10"
|
||||
placeholder="Create a strong password"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Must be at least 8 characters long
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="h-11 w-full"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="border-primary-foreground/30 border-t-primary-foreground h-4 w-4 animate-spin rounded-full border-2"></div>
|
||||
<span>Creating account...</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center space-x-2">
|
||||
<span>Create Account</span>
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="text-center text-sm">
|
||||
Already have an account?{" "}
|
||||
<a
|
||||
href="/auth/signin"
|
||||
className="text-primary font-medium hover:underline"
|
||||
>
|
||||
Sign in
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="text-muted-foreground text-center text-xs leading-relaxed">
|
||||
By creating an account, you agree to our{" "}
|
||||
<LegalModal
|
||||
type="terms"
|
||||
trigger={
|
||||
<span className="text-primary inline cursor-pointer hover:underline">
|
||||
Terms of Service
|
||||
</span>
|
||||
}
|
||||
/>{" "}
|
||||
and{" "}
|
||||
<LegalModal
|
||||
type="privacy"
|
||||
trigger={
|
||||
<span className="text-primary inline cursor-pointer hover:underline">
|
||||
Privacy Policy
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RegisterPage() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="floating-orbs flex min-h-screen items-center justify-center p-4">
|
||||
<div className="w-full max-w-md space-y-8">
|
||||
<div className="space-y-4 text-center">
|
||||
<Logo size="lg" className="mx-auto" />
|
||||
<div>
|
||||
<h1 className="text-foreground text-2xl font-bold">
|
||||
Join beenvoice
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-2">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<RegisterForm />
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useState, Suspense } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { signIn } from "next-auth/react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||
import { Card, CardContent } from "~/components/ui/card";
|
||||
import { Input } from "~/components/ui/input";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Label } from "~/components/ui/label";
|
||||
import { toast } from "sonner";
|
||||
import { Logo } from "~/components/branding/logo";
|
||||
import { Mail, Lock, ArrowRight } from "lucide-react";
|
||||
import { LegalModal } from "~/components/ui/legal-modal";
|
||||
import {
|
||||
Mail,
|
||||
Lock,
|
||||
ArrowRight,
|
||||
Users,
|
||||
FileText,
|
||||
TrendingUp,
|
||||
} from "lucide-react";
|
||||
|
||||
function SignInForm() {
|
||||
const router = useRouter();
|
||||
@@ -42,114 +49,184 @@ function SignInForm() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="floating-orbs flex min-h-screen items-center justify-center p-4">
|
||||
<div className="w-full max-w-md space-y-8">
|
||||
{/* Logo and Welcome */}
|
||||
<div className="space-y-4 text-center">
|
||||
<Logo size="lg" className="mx-auto" />
|
||||
<div>
|
||||
<h1 className="text-foreground text-2xl font-bold">Welcome back</h1>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Sign in to your beenvoice account
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-background flex min-h-screen items-center justify-center">
|
||||
<Card className="mx-auto h-screen w-full overflow-hidden border-0 shadow-none md:h-auto md:max-w-6xl md:border md:shadow-lg">
|
||||
<CardContent className="grid h-full p-0 md:grid-cols-2">
|
||||
{/* Hero Section - Hidden on mobile */}
|
||||
<div className="bg-muted relative hidden md:flex md:flex-col md:justify-center md:p-12">
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-4">
|
||||
<Logo size="xl" />
|
||||
<div className="space-y-3">
|
||||
<h1 className="text-3xl font-bold lg:text-4xl">
|
||||
Welcome back to your
|
||||
<span className="text-primary"> invoicing workspace</span>
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-lg">
|
||||
Continue managing your clients and creating professional
|
||||
invoices that get you paid faster.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sign In Form */}
|
||||
<Card className="card-primary">
|
||||
<CardHeader className="space-y-1">
|
||||
<CardTitle className="text-center text-xl">Sign In</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSignIn} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<div className="relative">
|
||||
<Mail className="form-icon-left" />
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
autoFocus
|
||||
className="form-input-with-icon"
|
||||
placeholder="Enter your email"
|
||||
/>
|
||||
<div className="grid gap-4">
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="bg-primary/10 rounded-lg p-2">
|
||||
<Users className="text-primary h-5 w-5" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h3 className="font-semibold">Client Management</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Organize and track all your clients in one place
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="bg-primary/10 rounded-lg p-2">
|
||||
<FileText className="text-primary h-5 w-5" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h3 className="font-semibold">Professional Invoices</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Beautiful templates that get you paid faster
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="bg-primary/10 rounded-lg p-2">
|
||||
<TrendingUp className="text-primary h-5 w-5" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h3 className="font-semibold">Payment Tracking</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Monitor your income with real-time insights
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<div className="relative">
|
||||
<Lock className="form-icon-left" />
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
className="form-input-with-icon"
|
||||
placeholder="Enter your password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="submit" className="w-full" disabled={loading}>
|
||||
{loading ? (
|
||||
"Signing in..."
|
||||
) : (
|
||||
<>
|
||||
Sign In
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
<div className="mt-6 text-center text-sm">
|
||||
<span className="text-muted-foreground">
|
||||
Don't have an account?{" "}
|
||||
</span>
|
||||
<Link href="/auth/register" className="nav-link-brand">
|
||||
Create one now
|
||||
</Link>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Features */}
|
||||
<div className="space-y-4 text-center">
|
||||
<p className="welcome-description">
|
||||
Simple invoicing for freelancers and small businesses
|
||||
</p>
|
||||
<div className="welcome-feature-list">
|
||||
<span>✓ Easy client management</span>
|
||||
<span>✓ Professional invoices</span>
|
||||
<span>✓ Payment tracking</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sign In Form */}
|
||||
<div className="flex flex-col justify-center p-6 md:p-12">
|
||||
<div className="mx-auto w-full max-w-sm space-y-6">
|
||||
{/* Mobile Logo */}
|
||||
<div className="flex justify-center md:hidden">
|
||||
<Logo size="lg" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-center md:text-left">
|
||||
<h1 className="text-2xl font-bold">Sign In</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Enter your credentials to access your account
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSignIn} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email Address</Label>
|
||||
<div className="relative">
|
||||
<Mail className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 z-10 h-4 w-4 -translate-y-1/2" />
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
autoFocus
|
||||
className="h-11 pl-10"
|
||||
placeholder="m@example.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<a
|
||||
href="/auth/forgot-password"
|
||||
className="text-primary text-sm hover:underline"
|
||||
>
|
||||
Forgot password?
|
||||
</a>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Lock className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 z-10 h-4 w-4 -translate-y-1/2" />
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
className="h-11 pl-10"
|
||||
placeholder="Enter your password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="h-11 w-full"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="border-primary-foreground/30 border-t-primary-foreground h-4 w-4 animate-spin rounded-full border-2"></div>
|
||||
<span>Signing in...</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center space-x-2">
|
||||
<span>Sign In</span>
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="text-center text-sm">
|
||||
Don't have an account?{" "}
|
||||
<a
|
||||
href="/auth/register"
|
||||
className="text-primary font-medium hover:underline"
|
||||
>
|
||||
Sign up
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="text-muted-foreground text-center text-xs leading-relaxed">
|
||||
By signing in, you agree to our{" "}
|
||||
<LegalModal
|
||||
type="terms"
|
||||
trigger={
|
||||
<span className="text-primary inline cursor-pointer hover:underline">
|
||||
Terms of Service
|
||||
</span>
|
||||
}
|
||||
/>{" "}
|
||||
and{" "}
|
||||
<LegalModal
|
||||
type="privacy"
|
||||
trigger={
|
||||
<span className="text-primary inline cursor-pointer hover:underline">
|
||||
Privacy Policy
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SignInPage() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="floating-orbs flex min-h-screen items-center justify-center p-4">
|
||||
<div className="w-full max-w-md space-y-8">
|
||||
<div className="space-y-4 text-center">
|
||||
<Logo size="lg" className="mx-auto" />
|
||||
<div>
|
||||
<h1 className="text-foreground text-2xl font-bold">
|
||||
Welcome back
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-2">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<SignInForm />
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
@@ -229,7 +229,7 @@ export function StatusManager({
|
||||
|
||||
{/* Overdue Warning */}
|
||||
{isOverdue && (
|
||||
<div className="flex items-center gap-2 rounded-lg bg-red-50 p-3 text-red-800 dark:bg-red-900/20 dark:text-red-300">
|
||||
<div className="bg-destructive/10 text-destructive flex items-center gap-2 p-3">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<span className="text-sm font-medium">
|
||||
{daysPastDue} day{daysPastDue !== 1 ? "s" : ""} overdue
|
||||
@@ -325,7 +325,7 @@ export function StatusManager({
|
||||
|
||||
{/* No Email Warning */}
|
||||
{!clientEmail && effectiveStatus !== "paid" && (
|
||||
<div className="rounded-lg bg-amber-50 p-3 text-amber-800 dark:bg-amber-900/20 dark:text-amber-300">
|
||||
<div className="bg-muted text-muted-foreground p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<span className="text-sm font-medium">
|
||||
|
||||
@@ -55,7 +55,7 @@ export default async function BusinessDetailPage({
|
||||
<span>Back to Businesses</span>
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild className="btn-brand-primary shadow-md">
|
||||
<Button asChild variant="default" className="shadow-md">
|
||||
<Link href={`/dashboard/businesses/${business.id}/edit`}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
<span>Edit Business</span>
|
||||
@@ -66,11 +66,11 @@ export default async function BusinessDetailPage({
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||
{/* Business Information Card */}
|
||||
<div className="lg:col-span-2">
|
||||
<Card className="card-primary">
|
||||
<Card className="bg-card border-border border">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<div className="bg-blue-subtle rounded-lg p-2">
|
||||
<Building className="text-icon-blue h-5 w-5" />
|
||||
<div className="bg-primary/10 p-2">
|
||||
<Building className="text-primary h-5 w-5" />
|
||||
</div>
|
||||
<span>Business Information</span>
|
||||
</CardTitle>
|
||||
@@ -84,8 +84,8 @@ export default async function BusinessDetailPage({
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
{business.email && (
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="bg-green-subtle rounded-lg p-2">
|
||||
<Mail className="text-icon-green h-4 w-4" />
|
||||
<div className="bg-primary/10 p-2">
|
||||
<Mail className="text-primary h-4 w-4" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground text-sm font-medium">
|
||||
@@ -100,8 +100,8 @@ export default async function BusinessDetailPage({
|
||||
|
||||
{business.phone && (
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="bg-green-subtle rounded-lg p-2">
|
||||
<Phone className="text-icon-green h-4 w-4" />
|
||||
<div className="bg-primary/10 p-2">
|
||||
<Phone className="text-primary h-4 w-4" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground text-sm font-medium">
|
||||
@@ -116,8 +116,8 @@ export default async function BusinessDetailPage({
|
||||
|
||||
{business.website && (
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="bg-green-subtle rounded-lg p-2">
|
||||
<Globe className="text-icon-green h-4 w-4" />
|
||||
<div className="bg-primary/10 p-2">
|
||||
<Globe className="text-primary h-4 w-4" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground text-sm font-medium">
|
||||
@@ -137,8 +137,8 @@ export default async function BusinessDetailPage({
|
||||
|
||||
{business.taxId && (
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="bg-green-subtle rounded-lg p-2">
|
||||
<Hash className="text-icon-green h-4 w-4" />
|
||||
<div className="bg-primary/10 p-2">
|
||||
<Hash className="text-primary h-4 w-4" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground text-sm font-medium">
|
||||
@@ -162,8 +162,8 @@ export default async function BusinessDetailPage({
|
||||
Business Address
|
||||
</h3>
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="bg-green-subtle rounded-lg p-2">
|
||||
<MapPin className="text-icon-green h-4 w-4" />
|
||||
<div className="bg-primary/10 p-2">
|
||||
<MapPin className="text-primary h-4 w-4" />
|
||||
</div>
|
||||
<div className="space-y-1 text-sm">
|
||||
{business.addressLine1 && (
|
||||
@@ -205,8 +205,8 @@ export default async function BusinessDetailPage({
|
||||
<h3 className="mb-4 text-lg font-semibold">Business Details</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="bg-green-subtle rounded-lg p-2">
|
||||
<Calendar className="text-icon-green h-4 w-4" />
|
||||
<div className="bg-primary/10 p-2">
|
||||
<Calendar className="text-primary h-4 w-4" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground text-sm font-medium">
|
||||
@@ -221,8 +221,8 @@ export default async function BusinessDetailPage({
|
||||
{/* Default Business Badge */}
|
||||
{business.isDefault && (
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="bg-green-subtle rounded-lg p-2">
|
||||
<Building className="text-icon-green h-4 w-4" />
|
||||
<div className="bg-primary/10 p-2">
|
||||
<Building className="text-primary h-4 w-4" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground text-sm font-medium">
|
||||
@@ -230,7 +230,7 @@ export default async function BusinessDetailPage({
|
||||
</p>
|
||||
<Badge
|
||||
variant="default"
|
||||
className="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
|
||||
className="bg-primary/10 text-primary"
|
||||
>
|
||||
Default Business
|
||||
</Badge>
|
||||
@@ -245,11 +245,11 @@ export default async function BusinessDetailPage({
|
||||
|
||||
{/* Settings & Actions Card */}
|
||||
<div className="space-y-6">
|
||||
<Card className="card-primary">
|
||||
<Card className="bg-card border-border border">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<div className="bg-blue-subtle rounded-lg p-2">
|
||||
<Building className="text-icon-blue h-5 w-5" />
|
||||
<div className="bg-primary/10 p-2">
|
||||
<Building className="text-primary h-5 w-5" />
|
||||
</div>
|
||||
<span>Quick Actions</span>
|
||||
</CardTitle>
|
||||
@@ -281,7 +281,7 @@ export default async function BusinessDetailPage({
|
||||
</Card>
|
||||
|
||||
{/* Information Card */}
|
||||
<Card className="card-primary">
|
||||
<Card className="bg-card border-border border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">About This Business</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -292,7 +292,7 @@ export default async function BusinessDetailPage({
|
||||
represents your company information to clients.
|
||||
</p>
|
||||
{business.isDefault && (
|
||||
<p className="text-green-600 dark:text-green-400">
|
||||
<p className="text-primary">
|
||||
This is your default business and will be automatically
|
||||
selected when creating new invoices.
|
||||
</p>
|
||||
|
||||
@@ -91,8 +91,8 @@ export function BusinessesDataTable({ businesses }: BusinessesDataTableProps) {
|
||||
const business = row.original;
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-blue-subtle hidden rounded-lg p-2 sm:flex">
|
||||
<Building className="text-icon-blue h-4 w-4" />
|
||||
<div className="bg-primary/10 hidden p-2 sm:flex">
|
||||
<Building className="text-primary h-4 w-4" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate font-medium">{business.name}</p>
|
||||
|
||||
@@ -22,7 +22,7 @@ export default async function BusinessesPage() {
|
||||
description="Manage your businesses and their information"
|
||||
variant="gradient"
|
||||
>
|
||||
<Button asChild className="btn-brand-primary shadow-md">
|
||||
<Button asChild variant="default" className="shadow-md">
|
||||
<Link href="/dashboard/businesses/new">
|
||||
<Plus className="mr-2 h-5 w-5" />
|
||||
<span>Add Business</span>
|
||||
|
||||
@@ -69,7 +69,7 @@ export default async function ClientDetailPage({
|
||||
<span>Back to Clients</span>
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild className="btn-brand-primary shadow-md">
|
||||
<Button asChild variant="default" className="shadow-md">
|
||||
<Link href={`/dashboard/clients/${client.id}/edit`}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
<span>Edit Client</span>
|
||||
@@ -80,11 +80,11 @@ export default async function ClientDetailPage({
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||
{/* Client Information Card */}
|
||||
<div className="lg:col-span-2">
|
||||
<Card className="card-primary">
|
||||
<Card className="bg-card border-border border">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<div className="bg-blue-subtle rounded-lg p-2">
|
||||
<Building className="text-icon-blue h-5 w-5" />
|
||||
<div className="bg-primary/10 p-2">
|
||||
<Building className="text-primary h-5 w-5" />
|
||||
</div>
|
||||
<span>Contact Information</span>
|
||||
</CardTitle>
|
||||
@@ -94,8 +94,8 @@ export default async function ClientDetailPage({
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
{client.email && (
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="bg-green-subtle rounded-lg p-2">
|
||||
<Mail className="text-icon-green h-4 w-4" />
|
||||
<div className="bg-primary/10 p-2">
|
||||
<Mail className="text-primary h-4 w-4" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground text-sm font-medium">
|
||||
@@ -108,8 +108,8 @@ export default async function ClientDetailPage({
|
||||
|
||||
{client.phone && (
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="bg-green-subtle rounded-lg p-2">
|
||||
<Phone className="text-icon-green h-4 w-4" />
|
||||
<div className="bg-primary/10 p-2">
|
||||
<Phone className="text-primary h-4 w-4" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground text-sm font-medium">
|
||||
@@ -126,8 +126,8 @@ export default async function ClientDetailPage({
|
||||
<div>
|
||||
<h3 className="mb-4 text-lg font-semibold">Client Address</h3>
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="bg-green-subtle rounded-lg p-2">
|
||||
<MapPin className="text-icon-green h-4 w-4" />
|
||||
<div className="bg-primary/10 p-2">
|
||||
<MapPin className="text-primary h-4 w-4" />
|
||||
</div>
|
||||
<div className="space-y-1 text-sm">
|
||||
{client.addressLine1 && (
|
||||
@@ -155,8 +155,8 @@ export default async function ClientDetailPage({
|
||||
<div>
|
||||
<h3 className="mb-4 text-lg font-semibold">Client Details</h3>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="bg-green-subtle rounded-lg p-2">
|
||||
<Calendar className="text-icon-green h-4 w-4" />
|
||||
<div className="bg-primary/10 p-2">
|
||||
<Calendar className="text-primary h-4 w-4" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground text-sm font-medium">
|
||||
@@ -174,11 +174,11 @@ export default async function ClientDetailPage({
|
||||
|
||||
{/* Stats Card */}
|
||||
<div className="space-y-6">
|
||||
<Card className="card-primary">
|
||||
<Card className="bg-card border-border border">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<div className="bg-blue-subtle rounded-lg p-2">
|
||||
<DollarSign className="text-icon-blue h-5 w-5" />
|
||||
<div className="bg-primary/10 p-2">
|
||||
<DollarSign className="text-primary h-5 w-5" />
|
||||
</div>
|
||||
<span>Invoice Summary</span>
|
||||
</CardTitle>
|
||||
@@ -213,8 +213,8 @@ export default async function ClientDetailPage({
|
||||
<Card className="">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<div className="bg-blue-subtle rounded-lg p-2">
|
||||
<DollarSign className="text-icon-blue h-5 w-5" />
|
||||
<div className="bg-primary/10 p-2">
|
||||
<DollarSign className="text-primary h-5 w-5" />
|
||||
</div>
|
||||
<span>Recent Invoices</span>
|
||||
</CardTitle>
|
||||
@@ -224,7 +224,7 @@ export default async function ClientDetailPage({
|
||||
{client.invoices.slice(0, 3).map((invoice) => (
|
||||
<div
|
||||
key={invoice.id}
|
||||
className="card-secondary flex items-center justify-between rounded-lg border p-3 transition-colors hover:bg-gray-200/70 dark:hover:bg-gray-700/60"
|
||||
className="card-secondary hover:bg-muted/50 flex items-center justify-between border p-3 transition-colors"
|
||||
>
|
||||
<div>
|
||||
<p className="text-foreground font-medium">
|
||||
|
||||
@@ -90,7 +90,7 @@ export function ClientsDataTable({
|
||||
const client = row.original;
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-status-info-muted hidden rounded-lg p-2 sm:flex">
|
||||
<div className="bg-status-info-muted hidden p-2 sm:flex">
|
||||
<UserPlus className="text-status-info h-4 w-4" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
|
||||
@@ -13,7 +13,7 @@ export default async function ClientsPage() {
|
||||
description="Manage your clients and their information."
|
||||
variant="gradient"
|
||||
>
|
||||
<Button asChild className="btn-brand-primary shadow-md">
|
||||
<Button asChild variant="default" className="shadow-md">
|
||||
<Link href="/dashboard/clients/new">
|
||||
<Plus className="mr-2 h-5 w-5" />
|
||||
<span>Add Client</span>
|
||||
|
||||
@@ -22,7 +22,7 @@ export function InvoiceDetailsSkeleton() {
|
||||
{/* Left Column */}
|
||||
<div className="space-y-6 lg:col-span-2">
|
||||
{/* Invoice Header Skeleton */}
|
||||
<Card className="card-primary">
|
||||
<Card className="bg-card border-border border">
|
||||
<CardContent className="p-4 sm:p-6">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start justify-between gap-6">
|
||||
@@ -48,7 +48,7 @@ export function InvoiceDetailsSkeleton() {
|
||||
{/* Client & Business Info */}
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
{Array.from({ length: 2 }).map((_, i) => (
|
||||
<Card key={i} className="card-primary">
|
||||
<Card key={i} className="bg-card border-border border">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="bg-muted/30 h-4 w-4 sm:h-5 sm:w-5" />
|
||||
@@ -60,7 +60,7 @@ export function InvoiceDetailsSkeleton() {
|
||||
<div className="space-y-3">
|
||||
{Array.from({ length: 3 }).map((_, j) => (
|
||||
<div key={j} className="flex items-center gap-3">
|
||||
<Skeleton className="bg-muted/30 h-8 w-8 rounded-lg" />
|
||||
<Skeleton className="bg-muted/30 h-8 w-8 " />
|
||||
<Skeleton className="bg-muted/30 h-4 w-28" />
|
||||
</div>
|
||||
))}
|
||||
@@ -71,7 +71,7 @@ export function InvoiceDetailsSkeleton() {
|
||||
</div>
|
||||
|
||||
{/* Invoice Items Skeleton */}
|
||||
<Card className="card-primary">
|
||||
<Card className="bg-card border-border border">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="bg-muted/30 h-4 w-4 sm:h-5 sm:w-5" />
|
||||
@@ -80,7 +80,7 @@ export function InvoiceDetailsSkeleton() {
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<div key={i} className="space-y-3 rounded-lg border p-4">
|
||||
<div key={i} className="space-y-3 border p-4">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="min-w-0 flex-1">
|
||||
<Skeleton className="bg-muted/30 mb-2 h-4 w-full sm:h-5 sm:w-3/4" />
|
||||
@@ -98,7 +98,7 @@ export function InvoiceDetailsSkeleton() {
|
||||
))}
|
||||
|
||||
{/* Totals */}
|
||||
<div className="bg-muted/30 rounded-lg p-4">
|
||||
<div className="bg-muted/30 p-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between">
|
||||
<Skeleton className="bg-muted/30 h-4 w-16" />
|
||||
@@ -119,7 +119,7 @@ export function InvoiceDetailsSkeleton() {
|
||||
</Card>
|
||||
|
||||
{/* Notes */}
|
||||
<Card className="card-primary">
|
||||
<Card className="bg-card border-border border">
|
||||
<CardHeader>
|
||||
<Skeleton className="bg-muted/30 h-6 w-16" />
|
||||
</CardHeader>
|
||||
@@ -135,7 +135,7 @@ export function InvoiceDetailsSkeleton() {
|
||||
|
||||
{/* Right Column - Actions */}
|
||||
<div className="space-y-6">
|
||||
<Card className="card-primary sticky top-6">
|
||||
<Card className="bg-card border-border border sticky top-6">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="bg-muted/30 h-5 w-5" />
|
||||
|
||||
@@ -66,7 +66,7 @@ const columns: ColumnDef<InvoiceItem>[] = [
|
||||
accessorKey: "amount",
|
||||
header: "Amount",
|
||||
cell: ({ row }) => (
|
||||
<div className="text-icon-emerald text-right font-medium">
|
||||
<div className="text-primary text-right font-medium">
|
||||
{formatCurrency(row.getValue("amount"))}
|
||||
</div>
|
||||
),
|
||||
|
||||
@@ -1,12 +1,511 @@
|
||||
"use client";
|
||||
|
||||
import { useParams } from "next/navigation";
|
||||
import InvoiceForm from "~/components/forms/invoice-form";
|
||||
import { DollarSign, Edit, Loader2, Trash2 } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { notFound, useParams, useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { StatusBadge, type StatusType } from "~/components/data/status-badge";
|
||||
import { PageHeader } from "~/components/layout/page-header";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "~/components/ui/dialog";
|
||||
import { Separator } from "~/components/ui/separator";
|
||||
import {
|
||||
getEffectiveInvoiceStatus,
|
||||
isInvoiceOverdue,
|
||||
} from "~/lib/invoice-status";
|
||||
import { api } from "~/trpc/react";
|
||||
import type { StoredInvoiceStatus } from "~/types/invoice";
|
||||
import { InvoiceDetailsSkeleton } from "./_components/invoice-details-skeleton";
|
||||
import { PDFDownloadButton } from "./_components/pdf-download-button";
|
||||
import { EnhancedSendInvoiceButton } from "~/components/forms/enhanced-send-invoice-button";
|
||||
|
||||
export default function InvoiceFormPage() {
|
||||
import {
|
||||
AlertTriangle,
|
||||
Building,
|
||||
Check,
|
||||
FileText,
|
||||
Mail,
|
||||
MapPin,
|
||||
Phone,
|
||||
User,
|
||||
} from "lucide-react";
|
||||
|
||||
function InvoiceViewContent({ invoiceId }: { invoiceId: string }) {
|
||||
const router = useRouter();
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
|
||||
const { data: invoice, isLoading } = api.invoices.getById.useQuery({
|
||||
id: invoiceId,
|
||||
});
|
||||
const utils = api.useUtils();
|
||||
|
||||
const deleteInvoice = api.invoices.delete.useMutation({
|
||||
onSuccess: () => {
|
||||
toast.success("Invoice deleted successfully");
|
||||
router.push("/dashboard/invoices");
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message ?? "Failed to delete invoice");
|
||||
},
|
||||
});
|
||||
|
||||
const updateStatus = api.invoices.updateStatus.useMutation({
|
||||
onSuccess: (data) => {
|
||||
toast.success(data.message);
|
||||
void utils.invoices.getById.invalidate({ id: invoiceId });
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message ?? "Failed to update invoice status");
|
||||
},
|
||||
});
|
||||
|
||||
const handleDelete = () => {
|
||||
setDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleMarkAsPaid = () => {
|
||||
updateStatus.mutate({
|
||||
id: invoiceId,
|
||||
status: "paid" as StoredInvoiceStatus,
|
||||
});
|
||||
};
|
||||
|
||||
const confirmDelete = () => {
|
||||
deleteInvoice.mutate({ id: invoiceId });
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <InvoiceDetailsSkeleton />;
|
||||
}
|
||||
|
||||
if (!invoice) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
return new Intl.DateTimeFormat("en-US", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
}).format(new Date(date));
|
||||
};
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
const subtotal = invoice.items.reduce((sum, item) => sum + item.amount, 0);
|
||||
const taxAmount = (subtotal * invoice.taxRate) / 100;
|
||||
const total = subtotal + taxAmount;
|
||||
const effectiveStatus = getEffectiveInvoiceStatus(
|
||||
invoice.status as StoredInvoiceStatus,
|
||||
invoice.dueDate,
|
||||
);
|
||||
const isOverdue = isInvoiceOverdue(
|
||||
invoice.status as StoredInvoiceStatus,
|
||||
invoice.dueDate,
|
||||
);
|
||||
|
||||
const getStatusType = (): StatusType => {
|
||||
return effectiveStatus as StatusType;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6 pb-24">
|
||||
<PageHeader
|
||||
title="Invoice Details"
|
||||
description="View and manage invoice information"
|
||||
variant="gradient"
|
||||
>
|
||||
<PDFDownloadButton invoiceId={invoice.id} variant="outline" />
|
||||
<Button asChild variant="default">
|
||||
<Link href={`/dashboard/invoices/${invoice.id}`}>
|
||||
<Edit className="h-5 w-5" />
|
||||
<span>Edit</span>
|
||||
</Link>
|
||||
</Button>
|
||||
</PageHeader>
|
||||
|
||||
{/* Content */}
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||
{/* Left Column */}
|
||||
<div className="space-y-6 lg:col-span-2">
|
||||
{/* Invoice Header */}
|
||||
<Card>
|
||||
<CardContent className="p-4 sm:p-6">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start justify-between gap-6">
|
||||
<div className="min-w-0 flex-1 space-y-2">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
|
||||
<h2 className="text-foreground truncate text-2xl font-bold">
|
||||
{invoice.invoiceNumber}
|
||||
</h2>
|
||||
<StatusBadge status={getStatusType()} />
|
||||
</div>
|
||||
<div className="text-muted-foreground space-y-1 text-sm sm:space-y-0">
|
||||
<div className="sm:inline">
|
||||
Issued {formatDate(invoice.issueDate)}
|
||||
</div>
|
||||
<div className="sm:inline sm:before:content-['_•_']">
|
||||
Due {formatDate(invoice.dueDate)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0 text-right">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Total Amount
|
||||
</p>
|
||||
<p className="text-primary text-3xl font-bold">
|
||||
{formatCurrency(total)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Overdue Alert */}
|
||||
{isOverdue && (
|
||||
<Card className="border-destructive/20 bg-destructive/5">
|
||||
<CardContent className="p-4">
|
||||
<div className="text-destructive flex items-center gap-3">
|
||||
<AlertTriangle className="h-5 w-5 flex-shrink-0" />
|
||||
<div>
|
||||
<p className="font-medium">Invoice Overdue</p>
|
||||
<p className="text-sm">
|
||||
{Math.ceil(
|
||||
(new Date().getTime() -
|
||||
new Date(invoice.dueDate).getTime()) /
|
||||
(1000 * 60 * 60 * 24),
|
||||
)}{" "}
|
||||
days past due date
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Client & Business Info */}
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
{/* Client Information */}
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<User className="h-5 w-5" />
|
||||
Bill To
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-foreground text-xl font-semibold">
|
||||
{invoice.client.name}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{invoice.client.email && (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-primary/10 p-2">
|
||||
<Mail className="text-primary h-4 w-4" />
|
||||
</div>
|
||||
<span className="text-sm break-all">
|
||||
{invoice.client.email}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{invoice.client.phone && (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-primary/10 p-2">
|
||||
<Phone className="text-primary h-4 w-4" />
|
||||
</div>
|
||||
<span className="text-sm">{invoice.client.phone}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(invoice.client.addressLine1 ?? invoice.client.city) && (
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="bg-primary/10 p-2">
|
||||
<MapPin className="text-primary h-4 w-4" />
|
||||
</div>
|
||||
<div className="space-y-1 text-sm">
|
||||
{invoice.client.addressLine1 && (
|
||||
<div>{invoice.client.addressLine1}</div>
|
||||
)}
|
||||
{invoice.client.addressLine2 && (
|
||||
<div>{invoice.client.addressLine2}</div>
|
||||
)}
|
||||
{(invoice.client.city ??
|
||||
invoice.client.state ??
|
||||
invoice.client.postalCode) && (
|
||||
<div>
|
||||
{[
|
||||
invoice.client.city,
|
||||
invoice.client.state,
|
||||
invoice.client.postalCode,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(", ")}
|
||||
</div>
|
||||
)}
|
||||
{invoice.client.country && (
|
||||
<div>{invoice.client.country}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Business Information */}
|
||||
{invoice.business && (
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Building className="h-5 w-5" />
|
||||
From
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-foreground text-xl font-semibold">
|
||||
{invoice.business.name}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{invoice.business.email && (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-primary/10 p-2">
|
||||
<Mail className="text-primary h-4 w-4" />
|
||||
</div>
|
||||
<span className="text-sm break-all">
|
||||
{invoice.business.email}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{invoice.business.phone && (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-primary/10 p-2">
|
||||
<Phone className="text-primary h-4 w-4" />
|
||||
</div>
|
||||
<span className="text-sm">
|
||||
{invoice.business.phone}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Invoice Items */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<FileText className="h-5 w-5" />
|
||||
Invoice Items
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{invoice.items.map((item) => (
|
||||
<Card key={item.id} className="card-secondary">
|
||||
<CardContent className="py-2">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-foreground mb-2 text-base font-medium">
|
||||
{item.description}
|
||||
</p>
|
||||
<div className="text-muted-foreground text-sm">
|
||||
<span className="inline whitespace-nowrap">
|
||||
{formatDate(item.date).replace(/ /g, "\u00A0")}
|
||||
</span>
|
||||
<span className="inline whitespace-nowrap before:mx-2 before:content-['_|_']">
|
||||
{item.hours.toString().replace(/ /g, "\u00A0")}
|
||||
hours
|
||||
</span>
|
||||
<span className="inline whitespace-nowrap before:mx-2 before:content-['_|_']">
|
||||
@ ${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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
{/* Totals */}
|
||||
<div className="bg-muted/30 p-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Subtotal:</span>
|
||||
<span className="font-medium">
|
||||
{formatCurrency(subtotal)}
|
||||
</span>
|
||||
</div>
|
||||
{invoice.taxRate > 0 && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">
|
||||
Tax ({invoice.taxRate}%):
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
{formatCurrency(taxAmount)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<Separator />
|
||||
<div className="flex justify-between text-lg font-bold">
|
||||
<span>Total:</span>
|
||||
<span className="text-primary">
|
||||
{formatCurrency(total)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Notes */}
|
||||
{invoice.notes && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Notes</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-foreground whitespace-pre-wrap">
|
||||
{invoice.notes}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right Column - Actions */}
|
||||
<div className="space-y-6">
|
||||
<Card className="sticky top-6">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Check className="h-5 w-5" />
|
||||
Actions
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<Button asChild variant="outline" className="w-full">
|
||||
<Link href={`/dashboard/invoices/${invoice.id}/edit`}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Edit Invoice
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
{invoice.items && invoice.client && (
|
||||
<PDFDownloadButton invoiceId={invoice.id} className="w-full" />
|
||||
)}
|
||||
|
||||
{/* Send Invoice Button - Show for draft, sent, and overdue */}
|
||||
{effectiveStatus === "draft" && (
|
||||
<EnhancedSendInvoiceButton
|
||||
invoiceId={invoice.id}
|
||||
className="w-full"
|
||||
/>
|
||||
)}
|
||||
|
||||
{(effectiveStatus === "sent" ||
|
||||
effectiveStatus === "overdue") && (
|
||||
<EnhancedSendInvoiceButton
|
||||
invoiceId={invoice.id}
|
||||
className="w-full"
|
||||
showResend={true}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Manual Status Updates */}
|
||||
{(effectiveStatus === "sent" ||
|
||||
effectiveStatus === "overdue") && (
|
||||
<Button
|
||||
onClick={handleMarkAsPaid}
|
||||
disabled={updateStatus.isPending}
|
||||
className="bg-primary text-primary-foreground hover:bg-primary/90 w-full"
|
||||
>
|
||||
{updateStatus.isPending ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<DollarSign className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
Mark as Paid
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleDelete}
|
||||
disabled={deleteInvoice.isPending}
|
||||
className="text-destructive hover:bg-destructive/10 w-full"
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete Invoice
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete Invoice</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to delete invoice{" "}
|
||||
<strong>{invoice.invoiceNumber}</strong>? This action cannot be
|
||||
undone and will permanently remove the invoice and all its data.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setDeleteDialogOpen(false)}
|
||||
disabled={deleteInvoice.isPending}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={confirmDelete}
|
||||
disabled={deleteInvoice.isPending}
|
||||
>
|
||||
{deleteInvoice.isPending ? "Deleting..." : "Delete Invoice"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function InvoiceViewPage() {
|
||||
const params = useParams();
|
||||
const id = params.id as string;
|
||||
|
||||
// Pass the actual id, let the form component handle the logic
|
||||
return <InvoiceForm invoiceId={id} />;
|
||||
return <InvoiceViewContent invoiceId={id} />;
|
||||
}
|
||||
|
||||
@@ -44,10 +44,10 @@ function SendEmailPageSkeleton() {
|
||||
/>
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||
<div className="space-y-6 lg:col-span-2">
|
||||
<div className="bg-muted h-96 animate-pulse rounded-lg" />
|
||||
<div className="bg-muted h-96 animate-pulse" />
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
<div className="bg-muted h-64 animate-pulse rounded-lg" />
|
||||
<div className="bg-muted h-64 animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -91,7 +91,7 @@ export default function SendEmailPage() {
|
||||
});
|
||||
|
||||
// Navigate back to invoice view
|
||||
router.push(`/dashboard/invoices/${invoiceId}/view`);
|
||||
router.push(`/dashboard/invoices/${invoiceId}`);
|
||||
|
||||
// Refresh invoice data
|
||||
void utils.invoices.getById.invalidate({ id: invoiceId });
|
||||
@@ -275,7 +275,7 @@ export default function SendEmailPage() {
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => router.push(`/dashboard/invoices/${invoiceId}/view`)}
|
||||
onClick={() => router.push(`/dashboard/invoices/${invoiceId}`)}
|
||||
>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back to Invoice
|
||||
@@ -334,9 +334,9 @@ export default function SendEmailPage() {
|
||||
onBccEmailChange={setBccEmail}
|
||||
/>
|
||||
) : (
|
||||
<div className="bg-muted flex h-[400px] items-center justify-center rounded-md border">
|
||||
<div className="bg-muted flex h-[400px] items-center justify-center border">
|
||||
<div className="text-center">
|
||||
<div className="border-primary mx-auto mb-2 h-4 w-4 animate-spin rounded-full border-2 border-t-transparent"></div>
|
||||
<div className="border-primary mx-auto mb-2 h-4 w-4 animate-spin border-2 border-t-transparent"></div>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Initializing email content...
|
||||
</p>
|
||||
@@ -382,7 +382,7 @@ export default function SendEmailPage() {
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg">
|
||||
<FileText className="h-5 w-5 text-green-600" />
|
||||
<FileText className="text-primary h-5 w-5" />
|
||||
Invoice #{invoice.invoiceNumber}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -506,14 +506,12 @@ export default function SendEmailPage() {
|
||||
<FloatingActionBar
|
||||
leftContent={
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="rounded-lg bg-green-100 p-2 dark:bg-green-900/30">
|
||||
<Send className="h-5 w-5 text-green-600 dark:text-green-400" />
|
||||
<div className="bg-primary/10 p-2">
|
||||
<Send className="text-primary h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 dark:text-gray-100">
|
||||
Send Invoice
|
||||
</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
<p className="text-foreground font-medium">Send Invoice</p>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Email invoice to {invoice.client?.name ?? "client"}
|
||||
</p>
|
||||
</div>
|
||||
@@ -523,7 +521,7 @@ export default function SendEmailPage() {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => router.push(`/dashboard/invoices/${invoiceId}/view`)}
|
||||
onClick={() => router.push(`/dashboard/invoices/${invoiceId}`)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
@@ -531,7 +529,7 @@ export default function SendEmailPage() {
|
||||
<Button
|
||||
onClick={handleSendEmail}
|
||||
disabled={!canSend || isSending}
|
||||
className="bg-gradient-to-r from-emerald-600 to-teal-600 shadow-md transition-colors duration-200 hover:from-emerald-700 hover:to-teal-700"
|
||||
variant="default"
|
||||
size="sm"
|
||||
>
|
||||
{isSending ? (
|
||||
@@ -570,7 +568,7 @@ export default function SendEmailPage() {
|
||||
)}
|
||||
.
|
||||
{retryCount > 0 && (
|
||||
<div className="mt-2 text-sm text-yellow-600">
|
||||
<div className="text-muted-foreground mt-2 text-sm">
|
||||
Retry attempt {retryCount} of 2
|
||||
</div>
|
||||
)}
|
||||
@@ -583,10 +581,7 @@ export default function SendEmailPage() {
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={confirmSendEmail}
|
||||
className="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700"
|
||||
>
|
||||
<Button onClick={confirmSendEmail} variant="default">
|
||||
<Send className="mr-2 h-4 w-4" />
|
||||
Send Email
|
||||
</Button>
|
||||
|
||||
@@ -1,511 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { DollarSign, Edit, Loader2, Trash2 } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { notFound, useParams, useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { StatusBadge, type StatusType } from "~/components/data/status-badge";
|
||||
import { PageHeader } from "~/components/layout/page-header";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "~/components/ui/dialog";
|
||||
import { Separator } from "~/components/ui/separator";
|
||||
import {
|
||||
getEffectiveInvoiceStatus,
|
||||
isInvoiceOverdue,
|
||||
} from "~/lib/invoice-status";
|
||||
import { api } from "~/trpc/react";
|
||||
import type { StoredInvoiceStatus } from "~/types/invoice";
|
||||
import { InvoiceDetailsSkeleton } from "../_components/invoice-details-skeleton";
|
||||
import { PDFDownloadButton } from "../_components/pdf-download-button";
|
||||
import { EnhancedSendInvoiceButton } from "~/components/forms/enhanced-send-invoice-button";
|
||||
|
||||
import {
|
||||
AlertTriangle,
|
||||
Building,
|
||||
Check,
|
||||
FileText,
|
||||
Mail,
|
||||
MapPin,
|
||||
Phone,
|
||||
User,
|
||||
} from "lucide-react";
|
||||
|
||||
function InvoiceViewContent({ invoiceId }: { invoiceId: string }) {
|
||||
const router = useRouter();
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
|
||||
const { data: invoice, isLoading } = api.invoices.getById.useQuery({
|
||||
id: invoiceId,
|
||||
});
|
||||
const utils = api.useUtils();
|
||||
|
||||
const deleteInvoice = api.invoices.delete.useMutation({
|
||||
onSuccess: () => {
|
||||
toast.success("Invoice deleted successfully");
|
||||
router.push("/dashboard/invoices");
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message ?? "Failed to delete invoice");
|
||||
},
|
||||
});
|
||||
|
||||
const updateStatus = api.invoices.updateStatus.useMutation({
|
||||
onSuccess: (data) => {
|
||||
toast.success(data.message);
|
||||
void utils.invoices.getById.invalidate({ id: invoiceId });
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message ?? "Failed to update invoice status");
|
||||
},
|
||||
});
|
||||
|
||||
const handleDelete = () => {
|
||||
setDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleMarkAsPaid = () => {
|
||||
updateStatus.mutate({
|
||||
id: invoiceId,
|
||||
status: "paid" as StoredInvoiceStatus,
|
||||
});
|
||||
};
|
||||
|
||||
const confirmDelete = () => {
|
||||
deleteInvoice.mutate({ id: invoiceId });
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <InvoiceDetailsSkeleton />;
|
||||
}
|
||||
|
||||
if (!invoice) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
return new Intl.DateTimeFormat("en-US", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
}).format(new Date(date));
|
||||
};
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
const subtotal = invoice.items.reduce((sum, item) => sum + item.amount, 0);
|
||||
const taxAmount = (subtotal * invoice.taxRate) / 100;
|
||||
const total = subtotal + taxAmount;
|
||||
const effectiveStatus = getEffectiveInvoiceStatus(
|
||||
invoice.status as StoredInvoiceStatus,
|
||||
invoice.dueDate,
|
||||
);
|
||||
const isOverdue = isInvoiceOverdue(
|
||||
invoice.status as StoredInvoiceStatus,
|
||||
invoice.dueDate,
|
||||
);
|
||||
|
||||
const getStatusType = (): StatusType => {
|
||||
return effectiveStatus as StatusType;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6 pb-24">
|
||||
<PageHeader
|
||||
title="Invoice Details"
|
||||
description="View and manage invoice information"
|
||||
variant="gradient"
|
||||
>
|
||||
<PDFDownloadButton invoiceId={invoice.id} variant="outline" />
|
||||
<Button asChild variant="default">
|
||||
<Link href={`/dashboard/invoices/${invoice.id}`}>
|
||||
<Edit className="h-5 w-5" />
|
||||
<span>Edit</span>
|
||||
</Link>
|
||||
</Button>
|
||||
</PageHeader>
|
||||
|
||||
{/* Content */}
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||
{/* Left Column */}
|
||||
<div className="space-y-6 lg:col-span-2">
|
||||
{/* Invoice Header */}
|
||||
<Card className="card-primary">
|
||||
<CardContent className="p-4 sm:p-6">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start justify-between gap-6">
|
||||
<div className="min-w-0 flex-1 space-y-2">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
|
||||
<h2 className="text-foreground truncate text-2xl font-bold">
|
||||
{invoice.invoiceNumber}
|
||||
</h2>
|
||||
<StatusBadge status={getStatusType()} />
|
||||
</div>
|
||||
<div className="text-muted-foreground space-y-1 text-sm sm:space-y-0">
|
||||
<div className="sm:inline">
|
||||
Issued {formatDate(invoice.issueDate)}
|
||||
</div>
|
||||
<div className="sm:inline sm:before:content-['_•_']">
|
||||
Due {formatDate(invoice.dueDate)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0 text-right">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Total Amount
|
||||
</p>
|
||||
<p className="text-primary text-3xl font-bold">
|
||||
{formatCurrency(total)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Overdue Alert */}
|
||||
{isOverdue && (
|
||||
<Card className="border-destructive/20 bg-destructive/5 card-secondary">
|
||||
<CardContent className="p-4">
|
||||
<div className="text-destructive flex items-center gap-3">
|
||||
<AlertTriangle className="h-5 w-5 flex-shrink-0" />
|
||||
<div>
|
||||
<p className="font-medium">Invoice Overdue</p>
|
||||
<p className="text-sm">
|
||||
{Math.ceil(
|
||||
(new Date().getTime() -
|
||||
new Date(invoice.dueDate).getTime()) /
|
||||
(1000 * 60 * 60 * 24),
|
||||
)}{" "}
|
||||
days past due date
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Client & Business Info */}
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
{/* Client Information */}
|
||||
<Card className="card-primary">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<User className="h-5 w-5" />
|
||||
Bill To
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-foreground text-xl font-semibold">
|
||||
{invoice.client.name}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{invoice.client.email && (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-primary/10 rounded-lg p-2">
|
||||
<Mail className="text-primary h-4 w-4" />
|
||||
</div>
|
||||
<span className="text-sm break-all">
|
||||
{invoice.client.email}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{invoice.client.phone && (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-primary/10 rounded-lg p-2">
|
||||
<Phone className="text-primary h-4 w-4" />
|
||||
</div>
|
||||
<span className="text-sm">{invoice.client.phone}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(invoice.client.addressLine1 ?? invoice.client.city) && (
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="bg-primary/10 rounded-lg p-2">
|
||||
<MapPin className="text-primary h-4 w-4" />
|
||||
</div>
|
||||
<div className="space-y-1 text-sm">
|
||||
{invoice.client.addressLine1 && (
|
||||
<div>{invoice.client.addressLine1}</div>
|
||||
)}
|
||||
{invoice.client.addressLine2 && (
|
||||
<div>{invoice.client.addressLine2}</div>
|
||||
)}
|
||||
{(invoice.client.city ??
|
||||
invoice.client.state ??
|
||||
invoice.client.postalCode) && (
|
||||
<div>
|
||||
{[
|
||||
invoice.client.city,
|
||||
invoice.client.state,
|
||||
invoice.client.postalCode,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(", ")}
|
||||
</div>
|
||||
)}
|
||||
{invoice.client.country && (
|
||||
<div>{invoice.client.country}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Business Information */}
|
||||
{invoice.business && (
|
||||
<Card className="card-primary">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Building className="h-5 w-5" />
|
||||
From
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-foreground text-xl font-semibold">
|
||||
{invoice.business.name}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{invoice.business.email && (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-primary/10 rounded-lg p-2">
|
||||
<Mail className="text-primary h-4 w-4" />
|
||||
</div>
|
||||
<span className="text-sm break-all">
|
||||
{invoice.business.email}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{invoice.business.phone && (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-primary/10 rounded-lg p-2">
|
||||
<Phone className="text-primary h-4 w-4" />
|
||||
</div>
|
||||
<span className="text-sm">
|
||||
{invoice.business.phone}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Invoice Items */}
|
||||
<Card className="card-primary">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<FileText className="h-5 w-5" />
|
||||
Invoice Items
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{invoice.items.map((item) => (
|
||||
<Card key={item.id} className="card-secondary">
|
||||
<CardContent className="py-2">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-foreground mb-2 text-base font-medium">
|
||||
{item.description}
|
||||
</p>
|
||||
<div className="text-muted-foreground text-sm">
|
||||
<span className="inline whitespace-nowrap">
|
||||
{formatDate(item.date).replace(/ /g, "\u00A0")}
|
||||
</span>
|
||||
<span className="inline whitespace-nowrap before:mx-2 before:content-['_|_']">
|
||||
{item.hours.toString().replace(/ /g, "\u00A0")}
|
||||
hours
|
||||
</span>
|
||||
<span className="inline whitespace-nowrap before:mx-2 before:content-['_|_']">
|
||||
@ ${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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
{/* Totals */}
|
||||
<div className="bg-muted/30 rounded-lg p-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Subtotal:</span>
|
||||
<span className="font-medium">
|
||||
{formatCurrency(subtotal)}
|
||||
</span>
|
||||
</div>
|
||||
{invoice.taxRate > 0 && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">
|
||||
Tax ({invoice.taxRate}%):
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
{formatCurrency(taxAmount)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<Separator />
|
||||
<div className="flex justify-between text-lg font-bold">
|
||||
<span>Total:</span>
|
||||
<span className="text-primary">
|
||||
{formatCurrency(total)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Notes */}
|
||||
{invoice.notes && (
|
||||
<Card className="card-primary">
|
||||
<CardHeader>
|
||||
<CardTitle>Notes</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-foreground whitespace-pre-wrap">
|
||||
{invoice.notes}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right Column - Actions */}
|
||||
<div className="space-y-6">
|
||||
<Card className="card-primary sticky top-6">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Check className="h-5 w-5" />
|
||||
Actions
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<Button asChild variant="outline" className="w-full">
|
||||
<Link href={`/dashboard/invoices/${invoice.id}`}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Edit Invoice
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
{invoice.items && invoice.client && (
|
||||
<PDFDownloadButton invoiceId={invoice.id} className="w-full" />
|
||||
)}
|
||||
|
||||
{/* Send Invoice Button - Show for draft, sent, and overdue */}
|
||||
{effectiveStatus === "draft" && (
|
||||
<EnhancedSendInvoiceButton
|
||||
invoiceId={invoice.id}
|
||||
className="w-full"
|
||||
/>
|
||||
)}
|
||||
|
||||
{(effectiveStatus === "sent" ||
|
||||
effectiveStatus === "overdue") && (
|
||||
<EnhancedSendInvoiceButton
|
||||
invoiceId={invoice.id}
|
||||
className="w-full"
|
||||
showResend={true}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Manual Status Updates */}
|
||||
{(effectiveStatus === "sent" ||
|
||||
effectiveStatus === "overdue") && (
|
||||
<Button
|
||||
onClick={handleMarkAsPaid}
|
||||
disabled={updateStatus.isPending}
|
||||
className="w-full bg-green-600 text-white hover:bg-green-700"
|
||||
>
|
||||
{updateStatus.isPending ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<DollarSign className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
Mark as Paid
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleDelete}
|
||||
disabled={deleteInvoice.isPending}
|
||||
className="w-full text-red-700 hover:bg-red-50"
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete Invoice
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete Invoice</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to delete invoice{" "}
|
||||
<strong>{invoice.invoiceNumber}</strong>? This action cannot be
|
||||
undone and will permanently remove the invoice and all its data.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setDeleteDialogOpen(false)}
|
||||
disabled={deleteInvoice.isPending}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={confirmDelete}
|
||||
disabled={deleteInvoice.isPending}
|
||||
>
|
||||
{deleteInvoice.isPending ? "Deleting..." : "Delete Invoice"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function InvoiceViewPage() {
|
||||
const params = useParams();
|
||||
const id = params.id as string;
|
||||
|
||||
return <InvoiceViewContent invoiceId={id} />;
|
||||
}
|
||||
@@ -107,7 +107,7 @@ export function InvoicesDataTable({ invoices }: InvoicesDataTableProps) {
|
||||
});
|
||||
|
||||
const handleRowClick = (invoice: Invoice) => {
|
||||
router.push(`/dashboard/invoices/${invoice.id}/view`);
|
||||
router.push(`/dashboard/invoices/${invoice.id}`);
|
||||
};
|
||||
|
||||
const handleDelete = (invoice: Invoice) => {
|
||||
@@ -206,7 +206,7 @@ export function InvoicesDataTable({ invoices }: InvoicesDataTableProps) {
|
||||
const invoice = row.original;
|
||||
return (
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<Link href={`/dashboard/invoices/${invoice.id}/view`}>
|
||||
<Link href={`/dashboard/invoices/${invoice.id}`}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -216,7 +216,7 @@ export function InvoicesDataTable({ invoices }: InvoicesDataTableProps) {
|
||||
<Eye className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href={`/dashboard/invoices/${invoice.id}`}>
|
||||
<Link href={`/dashboard/invoices/${invoice.id}/edit`}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -229,7 +229,7 @@ export function InvoicesDataTable({ invoices }: InvoicesDataTableProps) {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0 text-red-600 hover:text-red-700"
|
||||
className="text-destructive hover:text-destructive/80 h-8 w-8 p-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDelete(invoice);
|
||||
|
||||
@@ -21,16 +21,16 @@ function FormatInstructions() {
|
||||
return (
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
{/* Required Format */}
|
||||
<Card className="card-primary">
|
||||
<Card className="bg-card border-border border">
|
||||
<CardHeader>
|
||||
<CardTitle className="card-title-info">
|
||||
<FileText className="text-icon-blue h-5 w-5" />
|
||||
<CardTitle className="text-foreground flex items-center gap-2">
|
||||
<FileText className="text-primary h-5 w-5" />
|
||||
Required CSV Format
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="bg-muted-subtle rounded-lg p-4">
|
||||
<p className="text-secondary font-mono text-sm">
|
||||
<div className="bg-muted/50 p-4">
|
||||
<p className="text-muted-foreground font-mono text-sm">
|
||||
DATE,DESCRIPTION,HOURS,RATE,AMOUNT
|
||||
</p>
|
||||
</div>
|
||||
@@ -49,7 +49,7 @@ function FormatInstructions() {
|
||||
},
|
||||
].map((col) => (
|
||||
<div key={col.field} className="flex items-start gap-3">
|
||||
<Badge className="badge-outline text-xs">{col.field}</Badge>
|
||||
<Badge className="border text-xs">{col.field}</Badge>
|
||||
<span className="text-muted-foreground text-sm">
|
||||
{col.desc}
|
||||
</span>
|
||||
@@ -72,10 +72,10 @@ function FormatInstructions() {
|
||||
</Card>
|
||||
|
||||
{/* Sample Data & Download */}
|
||||
<Card className="card-primary">
|
||||
<Card className="bg-card border-border border">
|
||||
<CardHeader>
|
||||
<CardTitle className="card-title-secondary">
|
||||
<Download className="text-icon-green h-5 w-5" />
|
||||
<CardTitle className="text-foreground flex items-center gap-2">
|
||||
<Download className="text-primary h-5 w-5" />
|
||||
Sample Template
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -85,9 +85,9 @@ function FormatInstructions() {
|
||||
for importing time entries.
|
||||
</p>
|
||||
|
||||
<div className="bg-green-subtle rounded-lg p-4">
|
||||
<div className="bg-primary/10 p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<Info className="text-icon-green mt-0.5 h-5 w-5" />
|
||||
<Info className="text-primary mt-0.5 h-5 w-5" />
|
||||
<div>
|
||||
<p className="text-success text-sm font-medium">Pro Tip</p>
|
||||
<p className="text-success text-sm">
|
||||
@@ -100,7 +100,7 @@ function FormatInstructions() {
|
||||
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-semibold">Sample Row:</h4>
|
||||
<div className="bg-muted-subtle rounded-lg p-3">
|
||||
<div className="bg-muted/50 p-3">
|
||||
<p className="text-muted font-mono text-xs break-all">
|
||||
1/15/24,"Web development work",8,75.00,600.00
|
||||
</p>
|
||||
@@ -109,7 +109,7 @@ function FormatInstructions() {
|
||||
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-semibold">Sample Filename:</h4>
|
||||
<div className="bg-muted-subtle rounded-lg p-3">
|
||||
<div className="bg-muted/50 p-3">
|
||||
<p className="text-muted font-mono text-xs">2024-01-15.csv</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -122,10 +122,10 @@ function FormatInstructions() {
|
||||
// Important Notes Section
|
||||
function ImportantNotes() {
|
||||
return (
|
||||
<Card className="card-primary border-l-4 border-l-amber-500">
|
||||
<Card className="bg-card border-border border border-l-4 border-l-amber-500">
|
||||
<CardHeader>
|
||||
<CardTitle className="card-title-warning">
|
||||
<AlertCircle className="text-icon-amber h-5 w-5" />
|
||||
<CardTitle className="text-destructive flex items-center gap-2">
|
||||
<AlertCircle className="text-primary h-5 w-5" />
|
||||
Important Notes
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -158,18 +158,18 @@ function ImportantNotes() {
|
||||
// File Format Help Section
|
||||
function FileFormatHelp() {
|
||||
return (
|
||||
<Card className="card-primary">
|
||||
<Card className="bg-card border-border border">
|
||||
<CardHeader>
|
||||
<CardTitle className="card-title-info">
|
||||
<FileSpreadsheet className="text-icon-blue h-5 w-5" />
|
||||
<CardTitle className="text-foreground flex items-center gap-2">
|
||||
<FileSpreadsheet className="text-primary h-5 w-5" />
|
||||
Supported File Formats
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<div className="grid gap-6 md:grid-cols-3">
|
||||
<div className="space-y-2 text-center">
|
||||
<div className="mx-auto w-fit rounded-full bg-blue-50 p-3 dark:bg-blue-900/20">
|
||||
<FileSpreadsheet className="h-6 w-6 text-blue-600" />
|
||||
<div className="bg-accent mx-auto w-fit p-3">
|
||||
<FileSpreadsheet className="text-foreground-foreground h-6 w-6" />
|
||||
</div>
|
||||
<h4 className="font-semibold">CSV Files</h4>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
@@ -178,8 +178,8 @@ function FileFormatHelp() {
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2 text-center">
|
||||
<div className="mx-auto w-fit rounded-full bg-green-50 p-3 dark:bg-green-900/20">
|
||||
<Upload className="h-6 w-6 text-green-600" />
|
||||
<div className="bg-primary/10 mx-auto w-fit p-3">
|
||||
<Upload className="text-primary h-6 w-6" />
|
||||
</div>
|
||||
<h4 className="font-semibold">Max Size</h4>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
@@ -187,8 +187,8 @@ function FileFormatHelp() {
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2 text-center">
|
||||
<div className="mx-auto w-fit rounded-full bg-purple-50 p-3 dark:bg-purple-900/20">
|
||||
<CheckCircle className="h-6 w-6 text-purple-600" />
|
||||
<div className="bg-secondary mx-auto w-fit p-3">
|
||||
<CheckCircle className="text-muted-foreground-foreground h-6 w-6" />
|
||||
</div>
|
||||
<h4 className="font-semibold">Validation</h4>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
|
||||
@@ -28,7 +28,7 @@ export default async function InvoicesPage() {
|
||||
<span>Import CSV</span>
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild className="btn-brand-primary shadow-md">
|
||||
<Button asChild variant="default" className="shadow-md">
|
||||
<Link href="/dashboard/invoices/new">
|
||||
<Plus className="mr-2 h-5 w-5" />
|
||||
<span>Create Invoice</span>
|
||||
|
||||
@@ -8,19 +8,19 @@ export default function DashboardLayout({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="floating-orbs relative min-h-screen">
|
||||
<div className="bg-dashboard relative min-h-screen">
|
||||
<Navbar />
|
||||
<Sidebar />
|
||||
{/* Mobile layout - no left margin */}
|
||||
<main className="relative z-10 min-h-screen pt-20 md:hidden">
|
||||
<div className="px-4 pt-4 pb-6 sm:px-6">
|
||||
<main className="relative z-10 min-h-screen pt-16 md:hidden">
|
||||
<div className="bg-background px-4 pt-4 pb-6 sm:px-6">
|
||||
<DashboardBreadcrumbs />
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
{/* Desktop layout - with sidebar margin */}
|
||||
<main className="relative z-10 hidden min-h-screen pt-20 md:ml-[276px] md:block">
|
||||
<div className="px-6 pt-6 pb-6">
|
||||
<main className="relative z-10 hidden min-h-screen pt-16 md:ml-64 md:block">
|
||||
<div className="bg-background px-6 pt-6 pb-6">
|
||||
<DashboardBreadcrumbs />
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
Eye,
|
||||
FileText,
|
||||
Plus,
|
||||
TrendingDown,
|
||||
TrendingUp,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
@@ -21,23 +23,23 @@ import { getEffectiveInvoiceStatus } from "~/lib/invoice-status";
|
||||
import { auth } from "~/server/auth";
|
||||
import { HydrateClient, api } from "~/trpc/server";
|
||||
import type { StoredInvoiceStatus } from "~/types/invoice";
|
||||
import { RevenueChart } from "~/app/dashboard/_components/revenue-chart";
|
||||
import { InvoiceStatusChart } from "~/app/dashboard/_components/invoice-status-chart";
|
||||
import { MonthlyMetricsChart } from "~/app/dashboard/_components/monthly-metrics-chart";
|
||||
|
||||
// Modern gradient background component
|
||||
// Hero section with clean mono design
|
||||
function DashboardHero({ firstName }: { firstName: string }) {
|
||||
return (
|
||||
<Card className="relative mb-8 overflow-hidden border-0 p-8 shadow-sm transition-shadow hover:shadow-md">
|
||||
<div className="absolute inset-0" />
|
||||
<div className="relative z-10">
|
||||
<h1 className="mb-2 text-3xl font-bold">Welcome back, {firstName}!</h1>
|
||||
<p className="text-lg">Ready to manage your invoicing business</p>
|
||||
</div>
|
||||
<div className="absolute -top-8 -right-8 h-32 w-32 rounded-full bg-white/10" />
|
||||
<div className="absolute -right-4 -bottom-4 h-24 w-24 rounded-full bg-white/5" />
|
||||
</Card>
|
||||
<div className="mb-8">
|
||||
<h1 className="mb-2 text-3xl font-bold">Welcome back, {firstName}!</h1>
|
||||
<p className="text-muted-foreground text-lg">
|
||||
Here's what's happening with your business today
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Enhanced stats cards with better visual hierarchy
|
||||
// Enhanced stats cards with better visuals
|
||||
async function DashboardStats() {
|
||||
const [clients, invoices] = await Promise.all([
|
||||
api.clients.getAll(),
|
||||
@@ -45,8 +47,48 @@ async function DashboardStats() {
|
||||
]);
|
||||
|
||||
const totalClients = clients.length;
|
||||
const totalInvoices = invoices.length;
|
||||
const totalRevenue = invoices
|
||||
const paidInvoices = invoices.filter(
|
||||
(invoice) =>
|
||||
getEffectiveInvoiceStatus(
|
||||
invoice.status as StoredInvoiceStatus,
|
||||
invoice.dueDate,
|
||||
) === "paid",
|
||||
);
|
||||
const totalRevenue = paidInvoices.reduce(
|
||||
(sum, invoice) => sum + invoice.totalAmount,
|
||||
0,
|
||||
);
|
||||
|
||||
const pendingInvoices = invoices.filter((invoice) => {
|
||||
const effectiveStatus = getEffectiveInvoiceStatus(
|
||||
invoice.status as StoredInvoiceStatus,
|
||||
invoice.dueDate,
|
||||
);
|
||||
return effectiveStatus === "sent" || effectiveStatus === "overdue";
|
||||
});
|
||||
const pendingAmount = pendingInvoices.reduce(
|
||||
(sum, invoice) => sum + invoice.totalAmount,
|
||||
0,
|
||||
);
|
||||
|
||||
const overdueInvoices = invoices.filter(
|
||||
(invoice) =>
|
||||
getEffectiveInvoiceStatus(
|
||||
invoice.status as StoredInvoiceStatus,
|
||||
invoice.dueDate,
|
||||
) === "overdue",
|
||||
);
|
||||
|
||||
// Calculate month-over-month trends
|
||||
const now = new Date();
|
||||
const currentMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
||||
|
||||
// Current month data
|
||||
const currentMonthInvoices = invoices.filter(
|
||||
(invoice) => new Date(invoice.issueDate) >= currentMonth,
|
||||
);
|
||||
const currentMonthRevenue = currentMonthInvoices
|
||||
.filter(
|
||||
(invoice) =>
|
||||
getEffectiveInvoiceStatus(
|
||||
@@ -55,79 +97,134 @@ async function DashboardStats() {
|
||||
) === "paid",
|
||||
)
|
||||
.reduce((sum, invoice) => sum + invoice.totalAmount, 0);
|
||||
const pendingAmount = invoices
|
||||
.filter((invoice) => {
|
||||
const effectiveStatus = getEffectiveInvoiceStatus(
|
||||
|
||||
// Last month data
|
||||
const lastMonthInvoices = invoices.filter((invoice) => {
|
||||
const date = new Date(invoice.issueDate);
|
||||
return date >= lastMonth && date < currentMonth;
|
||||
});
|
||||
const lastMonthRevenue = lastMonthInvoices
|
||||
.filter(
|
||||
(invoice) =>
|
||||
getEffectiveInvoiceStatus(
|
||||
invoice.status as StoredInvoiceStatus,
|
||||
invoice.dueDate,
|
||||
) === "paid",
|
||||
)
|
||||
.reduce((sum, invoice) => sum + invoice.totalAmount, 0);
|
||||
|
||||
// Previous month data for clients
|
||||
const prevMonthClients = clients.filter(
|
||||
(client) => new Date(client.createdAt) < currentMonth,
|
||||
).length;
|
||||
|
||||
// Calculate trends
|
||||
const revenueChange =
|
||||
lastMonthRevenue > 0
|
||||
? ((currentMonthRevenue - lastMonthRevenue) / lastMonthRevenue) * 100
|
||||
: currentMonthRevenue > 0
|
||||
? 100
|
||||
: 0;
|
||||
|
||||
const pendingChange =
|
||||
lastMonthInvoices.length > 0
|
||||
? ((pendingInvoices.length -
|
||||
lastMonthInvoices.filter((invoice) => {
|
||||
const status = getEffectiveInvoiceStatus(
|
||||
invoice.status as StoredInvoiceStatus,
|
||||
invoice.dueDate,
|
||||
);
|
||||
return status === "sent" || status === "overdue";
|
||||
}).length) /
|
||||
lastMonthInvoices.length) *
|
||||
100
|
||||
: pendingInvoices.length > 0
|
||||
? 100
|
||||
: 0;
|
||||
|
||||
const clientChange = totalClients - prevMonthClients;
|
||||
|
||||
const lastMonthOverdue = lastMonthInvoices.filter(
|
||||
(invoice) =>
|
||||
getEffectiveInvoiceStatus(
|
||||
invoice.status as StoredInvoiceStatus,
|
||||
invoice.dueDate,
|
||||
);
|
||||
return effectiveStatus === "sent" || effectiveStatus === "overdue";
|
||||
})
|
||||
.reduce((sum, invoice) => sum + invoice.totalAmount, 0);
|
||||
) === "overdue",
|
||||
).length;
|
||||
const overdueChange = overdueInvoices.length - lastMonthOverdue;
|
||||
|
||||
const formatTrend = (value: number, isCount = false) => {
|
||||
if (isCount) {
|
||||
return value > 0 ? `+${value}` : value.toString();
|
||||
}
|
||||
return value > 0 ? `+${value.toFixed(1)}%` : `${value.toFixed(1)}%`;
|
||||
};
|
||||
|
||||
const stats = [
|
||||
{
|
||||
title: "Total Revenue",
|
||||
value: `$${totalRevenue.toLocaleString("en-US", { minimumFractionDigits: 2 })}`,
|
||||
change: "+12.5%",
|
||||
change: formatTrend(revenueChange),
|
||||
trend: revenueChange >= 0 ? ("up" as const) : ("down" as const),
|
||||
icon: DollarSign,
|
||||
color: "",
|
||||
bgColor: "bg-green-50",
|
||||
changeColor: "",
|
||||
description: `From ${paidInvoices.length} paid invoices`,
|
||||
},
|
||||
{
|
||||
title: "Pending Amount",
|
||||
value: `$${pendingAmount.toLocaleString("en-US", { minimumFractionDigits: 2 })}`,
|
||||
change: "+8.2%",
|
||||
change: formatTrend(pendingChange),
|
||||
trend: pendingChange >= 0 ? ("up" as const) : ("down" as const),
|
||||
icon: Clock,
|
||||
color: "",
|
||||
bgColor: "bg-amber-50",
|
||||
changeColor: "",
|
||||
description: `${pendingInvoices.length} invoices awaiting payment`,
|
||||
},
|
||||
{
|
||||
title: "Active Clients",
|
||||
value: totalClients.toString(),
|
||||
change: "+3",
|
||||
change: formatTrend(clientChange, true),
|
||||
trend: clientChange >= 0 ? ("up" as const) : ("down" as const),
|
||||
icon: Users,
|
||||
color: "",
|
||||
bgColor: "bg-blue-50",
|
||||
changeColor: "",
|
||||
description: "Total registered clients",
|
||||
},
|
||||
{
|
||||
title: "Total Invoices",
|
||||
value: totalInvoices.toString(),
|
||||
change: "+15",
|
||||
icon: FileText,
|
||||
color: "",
|
||||
bgColor: "bg-purple-50",
|
||||
changeColor: "",
|
||||
title: "Overdue Invoices",
|
||||
value: overdueInvoices.length.toString(),
|
||||
change: formatTrend(overdueChange, true),
|
||||
trend: overdueChange <= 0 ? ("up" as const) : ("down" as const),
|
||||
icon: TrendingDown,
|
||||
description: "Invoices past due date",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="mb-8 grid grid-cols-2 gap-3 sm:gap-6 lg:grid-cols-4">
|
||||
<div className="mb-8 grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{stats.map((stat) => {
|
||||
const Icon = stat.icon;
|
||||
const TrendIcon = stat.trend === "up" ? TrendingUp : TrendingDown;
|
||||
const isPositive = stat.trend === "up";
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={stat.title}
|
||||
className="border-0 shadow-sm transition-shadow hover:shadow-md"
|
||||
>
|
||||
<CardContent className="p-3 sm:p-4 lg:p-6">
|
||||
<div className="mb-2 flex items-center justify-between sm:mb-3 lg:mb-4">
|
||||
<div className={`rounded-lg p-1.5 sm:p-2 ${stat.bgColor}`}>
|
||||
<Icon className="h-3 w-3 text-gray-700 sm:h-4 sm:w-4 lg:h-5 lg:w-5 dark:text-gray-800" />
|
||||
<Card key={stat.title}>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between space-y-0 pb-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Icon className="text-muted-foreground h-5 w-5" />
|
||||
<p className="text-muted-foreground text-sm font-medium">
|
||||
{stat.title}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className={`flex items-center space-x-1 text-xs ${
|
||||
isPositive ? "text-green-600" : "text-red-600"
|
||||
}`}
|
||||
>
|
||||
<TrendIcon className="h-3 w-3" />
|
||||
<span>{stat.change}</span>
|
||||
</div>
|
||||
<span className="text-xs font-medium text-teal-600 dark:text-teal-400">
|
||||
{stat.change}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="mb-1 text-base font-bold text-gray-900 sm:text-xl lg:text-2xl dark:text-gray-100">
|
||||
{stat.value}
|
||||
</p>
|
||||
<p className="text-xs text-gray-600 lg:text-sm dark:text-gray-300">
|
||||
{stat.title}
|
||||
<div className="space-y-1">
|
||||
<p className="text-2xl font-bold">{stat.value}</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{stat.description}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -138,64 +235,111 @@ async function DashboardStats() {
|
||||
);
|
||||
}
|
||||
|
||||
// Quick Actions with better visual design
|
||||
// Charts section
|
||||
async function ChartsSection() {
|
||||
const invoices = await api.invoices.getAll();
|
||||
|
||||
return (
|
||||
<div className="mb-8 grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
{/* Revenue Trend Chart */}
|
||||
<Card className="lg:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<BarChart3 className="h-5 w-5" />
|
||||
Revenue Over Time
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<RevenueChart invoices={invoices} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Invoice Status Breakdown */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Activity className="h-5 w-5" />
|
||||
Invoice Status
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<InvoiceStatusChart invoices={invoices} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Monthly Metrics */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Calendar className="h-5 w-5" />
|
||||
Monthly Metrics
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<MonthlyMetricsChart invoices={invoices} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Enhanced Quick Actions
|
||||
function QuickActions() {
|
||||
const actions = [
|
||||
{
|
||||
title: "Create Invoice",
|
||||
description: "Start a new invoice",
|
||||
description: "Start a new invoice for a client",
|
||||
href: "/dashboard/invoices/new",
|
||||
icon: FileText,
|
||||
primary: true,
|
||||
featured: true,
|
||||
},
|
||||
{
|
||||
title: "Add Client",
|
||||
description: "Add a new client",
|
||||
description: "Register a new client",
|
||||
href: "/dashboard/clients/new",
|
||||
icon: Users,
|
||||
primary: false,
|
||||
featured: false,
|
||||
},
|
||||
{
|
||||
title: "View Reports",
|
||||
description: "Business analytics",
|
||||
href: "/dashboard/reports",
|
||||
title: "View All Invoices",
|
||||
description: "Manage your invoice pipeline",
|
||||
href: "/dashboard/invoices",
|
||||
icon: BarChart3,
|
||||
primary: false,
|
||||
featured: false,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Card className="border-0 shadow-sm">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center gap-2 text-lg">
|
||||
<Plus className="h-5 w-5 text-teal-600 dark:text-teal-400" />
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Plus className="h-5 w-5" />
|
||||
Quick Actions
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<CardContent className="space-y-3">
|
||||
{actions.map((action) => {
|
||||
const Icon = action.icon;
|
||||
return (
|
||||
<Button
|
||||
key={action.title}
|
||||
asChild
|
||||
variant={action.primary ? "default" : "outline"}
|
||||
className={`h-12 w-full justify-start px-3 ${
|
||||
action.primary
|
||||
? "bg-teal-600 text-white hover:bg-teal-700"
|
||||
: "border-gray-200 hover:bg-gray-50 dark:border-gray-600 dark:hover:bg-gray-800"
|
||||
variant="outline"
|
||||
className={`h-auto w-full justify-start p-4 ${
|
||||
action.featured
|
||||
? "border-foreground/20 bg-muted/50 hover:bg-muted"
|
||||
: "hover:bg-muted/50"
|
||||
}`}
|
||||
>
|
||||
<Link href={action.href}>
|
||||
<div className="flex items-center gap-3">
|
||||
<Icon
|
||||
className={`h-4 w-4 ${action.primary ? "text-white" : "text-gray-600 dark:text-gray-300"}`}
|
||||
/>
|
||||
<span
|
||||
className={`font-medium ${action.primary ? "text-white" : "text-gray-900 dark:text-gray-100"}`}
|
||||
>
|
||||
{action.title}
|
||||
</span>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Icon className="h-5 w-5 flex-shrink-0" />
|
||||
<div className="text-left">
|
||||
<p className="font-semibold">{action.title}</p>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{action.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</Button>
|
||||
@@ -206,7 +350,7 @@ function QuickActions() {
|
||||
);
|
||||
}
|
||||
|
||||
// Current work in progress
|
||||
// Current work section with enhanced design
|
||||
async function CurrentWork() {
|
||||
const invoices = await api.invoices.getAll();
|
||||
const draftInvoices = invoices.filter(
|
||||
@@ -220,20 +364,21 @@ async function CurrentWork() {
|
||||
|
||||
if (!currentInvoice) {
|
||||
return (
|
||||
<Card className="border-0 shadow-sm">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg">
|
||||
<Activity className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Activity className="h-5 w-5" />
|
||||
Current Work
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="py-8 text-center">
|
||||
<FileText className="mx-auto mb-4 h-12 w-12 text-gray-300 dark:text-gray-600" />
|
||||
<p className="mb-4 text-gray-600 dark:text-gray-300">
|
||||
No draft invoices found
|
||||
<FileText className="text-muted-foreground mx-auto mb-4 h-12 w-12" />
|
||||
<h3 className="mb-2 text-lg font-semibold">No active drafts</h3>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Create a new invoice to get started
|
||||
</p>
|
||||
<Button asChild className="bg-teal-600 hover:bg-teal-700">
|
||||
<Button asChild variant="outline" className="border-foreground/20">
|
||||
<Link href="/dashboard/invoices/new">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Create Invoice
|
||||
@@ -249,49 +394,41 @@ async function CurrentWork() {
|
||||
currentInvoice.items?.reduce((sum, item) => sum + item.hours, 0) ?? 0;
|
||||
|
||||
return (
|
||||
<Card className="border-0 shadow-sm">
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle className="flex items-center gap-2 text-lg">
|
||||
<Activity className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Activity className="h-5 w-5" />
|
||||
Current Work
|
||||
</CardTitle>
|
||||
<Badge variant="secondary">In Progress</Badge>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-lg font-semibold">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold">
|
||||
#{currentInvoice.invoiceNumber}
|
||||
</p>
|
||||
<p className="text-gray-600 dark:text-gray-300">
|
||||
{currentInvoice.client?.name}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-2xl font-bold text-teal-600 dark:text-teal-400">
|
||||
</h3>
|
||||
<span className="text-primary text-2xl font-bold">
|
||||
${currentInvoice.totalAmount.toFixed(2)}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
{totalHours.toFixed(1)} hours
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-muted-foreground flex items-center justify-between text-sm">
|
||||
<span>{currentInvoice.client?.name}</span>
|
||||
<span>{totalHours.toFixed(1)} hours logged</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button asChild variant="outline" size="sm" className="flex-1">
|
||||
<Link href={`/dashboard/invoices/${currentInvoice.id}`}>
|
||||
<Eye className="mr-2 h-3 w-3" />
|
||||
<Eye className="mr-2 h-4 w-4" />
|
||||
View
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
size="sm"
|
||||
className="flex-1 bg-teal-600 hover:bg-teal-700"
|
||||
>
|
||||
<Link href={`/dashboard/invoices/${currentInvoice.id}`}>
|
||||
<Edit className="mr-2 h-3 w-3" />
|
||||
<Button asChild size="sm" className="flex-1">
|
||||
<Link href={`/dashboard/invoices/${currentInvoice.id}/edit`}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Continue
|
||||
</Link>
|
||||
</Button>
|
||||
@@ -302,7 +439,7 @@ async function CurrentWork() {
|
||||
);
|
||||
}
|
||||
|
||||
// Recent activity with enhanced design
|
||||
// Enhanced recent activity
|
||||
async function RecentActivity() {
|
||||
const invoices = await api.invoices.getAll();
|
||||
const recentInvoices = invoices
|
||||
@@ -315,21 +452,21 @@ async function RecentActivity() {
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case "paid":
|
||||
return "bg-green-50 border-green-200";
|
||||
return "bg-green-50 border-green-200 text-green-700";
|
||||
case "sent":
|
||||
return "bg-blue-50 border-blue-200";
|
||||
return "bg-blue-50 border-blue-200 text-blue-700";
|
||||
case "overdue":
|
||||
return "bg-red-50 border-red-200";
|
||||
return "bg-red-50 border-red-200 text-red-700";
|
||||
default:
|
||||
return "bg-gray-50 border-gray-200";
|
||||
return "bg-gray-50 border-gray-200 text-gray-700";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="border-0 shadow-sm">
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle className="flex items-center gap-2 text-lg">
|
||||
<Calendar className="h-5 w-5 text-purple-600 dark:text-purple-400" />
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Calendar className="h-5 w-5" />
|
||||
Recent Activity
|
||||
</CardTitle>
|
||||
<Button variant="ghost" size="sm" asChild>
|
||||
@@ -342,11 +479,12 @@ async function RecentActivity() {
|
||||
<CardContent>
|
||||
{recentInvoices.length === 0 ? (
|
||||
<div className="py-8 text-center">
|
||||
<FileText className="mx-auto mb-4 h-12 w-12 text-gray-300 dark:text-gray-600" />
|
||||
<p className="mb-4 text-gray-600 dark:text-gray-300">
|
||||
No invoices yet
|
||||
<FileText className="text-muted-foreground mx-auto mb-4 h-12 w-12" />
|
||||
<h3 className="mb-2 text-lg font-semibold">No invoices yet</h3>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Create your first invoice to get started
|
||||
</p>
|
||||
<Button asChild className="bg-teal-600 hover:bg-teal-700">
|
||||
<Button asChild variant="outline" className="border-foreground/20">
|
||||
<Link href="/dashboard/invoices/new">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Create Your First Invoice
|
||||
@@ -361,39 +499,28 @@ async function RecentActivity() {
|
||||
href={`/dashboard/invoices/${invoice.id}`}
|
||||
className="block"
|
||||
>
|
||||
<Card className="card-secondary transition-colors hover:bg-gray-200/70 dark:hover:bg-gray-700/60">
|
||||
<CardContent className="p-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="rounded-lg bg-gray-100 p-2 dark:bg-gray-700">
|
||||
<FileText className="h-4 w-4 text-gray-600 dark:text-gray-300" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="font-medium text-gray-900 dark:text-gray-100">
|
||||
#{invoice.invoiceNumber}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
{invoice.client?.name} •{" "}
|
||||
{new Date(invoice.issueDate).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg p-1 transition-colors hover:bg-gray-300/50 dark:hover:bg-gray-600/50">
|
||||
<Eye className="h-4 w-4 text-gray-600 dark:text-gray-300" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Badge
|
||||
className={`border ${getStatusColor(invoice.status)}`}
|
||||
>
|
||||
{invoice.status}
|
||||
</Badge>
|
||||
<p className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
${invoice.totalAmount.toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="hover:bg-muted/50 flex items-center justify-between rounded-lg border p-3 transition-colors">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="bg-muted rounded-lg p-2">
|
||||
<FileText className="text-muted-foreground h-4 w-4" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="font-medium">#{invoice.invoiceNumber}</p>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{invoice.client?.name} •{" "}
|
||||
{new Date(invoice.issueDate).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Badge className={getStatusColor(invoice.status)}>
|
||||
{invoice.status}
|
||||
</Badge>
|
||||
<span className="font-semibold">
|
||||
${invoice.totalAmount.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
@@ -406,16 +533,16 @@ async function RecentActivity() {
|
||||
// Loading skeletons
|
||||
function StatsSkeleton() {
|
||||
return (
|
||||
<div className="mb-8 grid grid-cols-2 gap-3 sm:gap-6 lg:grid-cols-4">
|
||||
<div className="mb-8 grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<Card key={i} className="border-0 shadow-sm">
|
||||
<CardContent className="p-3 sm:p-4 lg:p-6">
|
||||
<div className="mb-2 flex items-center justify-between sm:mb-3 lg:mb-4">
|
||||
<Skeleton className="h-6 w-6 rounded-lg sm:h-8 sm:w-8 lg:h-9 lg:w-9" />
|
||||
<Skeleton className="h-3 w-8 sm:h-4 sm:w-12" />
|
||||
<Card key={i}>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between space-y-0 pb-2">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-4 w-12" />
|
||||
</div>
|
||||
<Skeleton className="mb-1 h-5 w-16 sm:mb-2 sm:h-6 sm:w-20 lg:h-8" />
|
||||
<Skeleton className="h-3 w-20 sm:h-4 sm:w-24" />
|
||||
<Skeleton className="mb-2 h-8 w-20" />
|
||||
<Skeleton className="h-3 w-32" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
@@ -423,9 +550,40 @@ function StatsSkeleton() {
|
||||
);
|
||||
}
|
||||
|
||||
function ChartsSkeleton() {
|
||||
return (
|
||||
<div className="mb-8 grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
<Card className="lg:col-span-2">
|
||||
<CardHeader>
|
||||
<Skeleton className="h-6 w-40" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Skeleton className="h-64 w-full" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-6 w-32" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Skeleton className="h-64 w-full" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-6 w-36" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Skeleton className="h-64 w-full" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CardSkeleton() {
|
||||
return (
|
||||
<Card className="border-0 shadow-sm">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-6 w-32" />
|
||||
</CardHeader>
|
||||
@@ -454,21 +612,28 @@ export default async function DashboardPage() {
|
||||
</Suspense>
|
||||
</HydrateClient>
|
||||
|
||||
<div className="grid grid-cols-1 gap-8 lg:grid-cols-2">
|
||||
<HydrateClient>
|
||||
<Suspense fallback={<CardSkeleton />}>
|
||||
<CurrentWork />
|
||||
</Suspense>
|
||||
</HydrateClient>
|
||||
|
||||
<QuickActions />
|
||||
</div>
|
||||
|
||||
<HydrateClient>
|
||||
<Suspense fallback={<CardSkeleton />}>
|
||||
<RecentActivity />
|
||||
<Suspense fallback={<ChartsSkeleton />}>
|
||||
<ChartsSection />
|
||||
</Suspense>
|
||||
</HydrateClient>
|
||||
|
||||
<div className="grid grid-cols-1 gap-8 lg:grid-cols-2">
|
||||
<div className="space-y-8">
|
||||
<HydrateClient>
|
||||
<Suspense fallback={<CardSkeleton />}>
|
||||
<CurrentWork />
|
||||
</Suspense>
|
||||
</HydrateClient>
|
||||
<QuickActions />
|
||||
</div>
|
||||
|
||||
<HydrateClient>
|
||||
<Suspense fallback={<CardSkeleton />}>
|
||||
<RecentActivity />
|
||||
</Suspense>
|
||||
</HydrateClient>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -267,22 +267,22 @@ export function SettingsContent() {
|
||||
label: "Clients",
|
||||
value: dataStats?.clients ?? 0,
|
||||
icon: Users,
|
||||
color: "text-blue-600",
|
||||
bgColor: "bg-blue-50 dark:bg-blue-900/20",
|
||||
color: "text-primary",
|
||||
bgColor: "bg-primary/10",
|
||||
},
|
||||
{
|
||||
label: "Businesses",
|
||||
value: dataStats?.businesses ?? 0,
|
||||
icon: Building,
|
||||
color: "text-purple-600",
|
||||
bgColor: "bg-purple-50 dark:bg-purple-900/20",
|
||||
color: "text-muted-foreground",
|
||||
bgColor: "bg-muted",
|
||||
},
|
||||
{
|
||||
label: "Invoices",
|
||||
value: dataStats?.invoices ?? 0,
|
||||
icon: FileText,
|
||||
color: "text-emerald-600",
|
||||
bgColor: "bg-emerald-50 dark:bg-emerald-900/20",
|
||||
color: "text-primary",
|
||||
bgColor: "bg-accent",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -291,10 +291,10 @@ export function SettingsContent() {
|
||||
{/* Profile & Account Overview */}
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
{/* Profile Section */}
|
||||
<Card className="card-primary">
|
||||
<Card className="bg-card border-border border">
|
||||
<CardHeader>
|
||||
<CardTitle className="card-title-secondary">
|
||||
<User className="text-icon-blue h-5 w-5" />
|
||||
<CardTitle className="text-foreground flex items-center gap-2">
|
||||
<User className="text-primary h-5 w-5" />
|
||||
Profile Information
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
@@ -327,7 +327,7 @@ export function SettingsContent() {
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={updateProfileMutation.isPending}
|
||||
className="btn-brand-primary"
|
||||
variant="default"
|
||||
>
|
||||
{updateProfileMutation.isPending
|
||||
? "Updating..."
|
||||
@@ -338,10 +338,10 @@ export function SettingsContent() {
|
||||
</Card>
|
||||
|
||||
{/* Data Overview */}
|
||||
<Card className="card-primary">
|
||||
<Card className="bg-card border-border border">
|
||||
<CardHeader>
|
||||
<CardTitle className="card-title-info">
|
||||
<Database className="text-icon-blue h-5 w-5" />
|
||||
<CardTitle className="text-foreground flex items-center gap-2">
|
||||
<Database className="text-primary h-5 w-5" />
|
||||
Account Data
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
@@ -355,10 +355,10 @@ export function SettingsContent() {
|
||||
return (
|
||||
<div
|
||||
key={item.label}
|
||||
className="bg-card flex items-center justify-between rounded-lg border p-4 transition-shadow hover:shadow-sm"
|
||||
className="bg-card flex items-center justify-between border p-4 transition-shadow hover:shadow-sm"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`rounded-lg p-2 ${item.bgColor}`}>
|
||||
<div className={` p-2 ${item.bgColor}`}>
|
||||
<Icon className={`h-4 w-4 ${item.color}`} />
|
||||
</div>
|
||||
<span className="font-medium">{item.label}</span>
|
||||
@@ -378,10 +378,10 @@ export function SettingsContent() {
|
||||
</div>
|
||||
|
||||
{/* Security Settings */}
|
||||
<Card className="card-primary">
|
||||
<Card className="bg-card border-border border">
|
||||
<CardHeader>
|
||||
<CardTitle className="card-title-secondary">
|
||||
<Key className="text-icon-amber h-5 w-5" />
|
||||
<CardTitle className="text-foreground flex items-center gap-2">
|
||||
<Key className="text-primary h-5 w-5" />
|
||||
Security Settings
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
@@ -474,7 +474,7 @@ export function SettingsContent() {
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={changePasswordMutation.isPending}
|
||||
className="btn-brand-primary"
|
||||
variant="default"
|
||||
>
|
||||
{changePasswordMutation.isPending
|
||||
? "Changing Password..."
|
||||
@@ -485,9 +485,9 @@ export function SettingsContent() {
|
||||
</Card>
|
||||
|
||||
{/* Data Management */}
|
||||
<Card className="card-primary">
|
||||
<Card className="bg-card border-border border">
|
||||
<CardHeader>
|
||||
<CardTitle className="card-title-secondary">
|
||||
<CardTitle className="text-foreground flex items-center gap-2">
|
||||
<Shield className="text-icon-indigo h-5 w-5" />
|
||||
Data Management
|
||||
</CardTitle>
|
||||
@@ -604,7 +604,7 @@ export function SettingsContent() {
|
||||
disabled={
|
||||
!importData.trim() || importDataMutation.isPending
|
||||
}
|
||||
className="btn-brand-primary"
|
||||
variant="default"
|
||||
>
|
||||
{importDataMutation.isPending
|
||||
? "Importing..."
|
||||
@@ -617,7 +617,7 @@ export function SettingsContent() {
|
||||
</div>
|
||||
|
||||
{/* Backup Information */}
|
||||
<div className="border-border bg-muted/20 rounded-lg border p-4">
|
||||
<div className="border-border bg-muted/20 border p-4">
|
||||
<h4 className="font-medium">Backup Information</h4>
|
||||
<ul className="text-muted-foreground mt-2 space-y-1 text-sm">
|
||||
<li>• Regular backups protect your important business data</li>
|
||||
@@ -634,9 +634,9 @@ export function SettingsContent() {
|
||||
</Card>
|
||||
|
||||
{/* Danger Zone */}
|
||||
<Card className="card-primary border-l-4 border-l-red-500">
|
||||
<Card className="bg-card border-border border border-l-4 border-l-red-500">
|
||||
<CardHeader>
|
||||
<CardTitle className="card-title-warning">
|
||||
<CardTitle className="text-destructive flex items-center gap-2">
|
||||
<AlertTriangle className="text-icon-red h-5 w-5" />
|
||||
Danger Zone
|
||||
</CardTitle>
|
||||
@@ -646,8 +646,8 @@ export function SettingsContent() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-lg border border-red-200 bg-red-50 p-4 dark:border-red-800 dark:bg-red-900/20">
|
||||
<h4 className="font-medium text-red-600 dark:text-red-400">
|
||||
<div className="bg-destructive/10 border-destructive/20 border p-4">
|
||||
<h4 className="text-destructive font-medium">
|
||||
Delete All Account Data
|
||||
</h4>
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
@@ -672,7 +672,7 @@ export function SettingsContent() {
|
||||
This action cannot be undone. This will permanently delete
|
||||
all your:
|
||||
</div>
|
||||
<ul className="border-border bg-muted/50 list-inside list-disc space-y-1 rounded-lg border p-3 text-sm">
|
||||
<ul className="border-border bg-muted/50 list-inside list-disc space-y-1 border p-3 text-sm">
|
||||
<li>Client information and contact details</li>
|
||||
<li>Business profiles and settings</li>
|
||||
<li>Invoices and invoice line items</li>
|
||||
@@ -703,7 +703,7 @@ export function SettingsContent() {
|
||||
deleteConfirmText !== "delete all my data" ||
|
||||
deleteDataMutation.isPending
|
||||
}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
className="bg-destructive hover:bg-destructive/90"
|
||||
>
|
||||
{deleteDataMutation.isPending
|
||||
? "Deleting..."
|
||||
|
||||
@@ -2,10 +2,10 @@ import "~/styles/globals.css";
|
||||
|
||||
import { Analytics } from "@vercel/analytics/next";
|
||||
import { type Metadata } from "next";
|
||||
import { Geist, Azeret_Mono } from "next/font/google";
|
||||
import { Geist_Mono } from "next/font/google";
|
||||
|
||||
import { TRPCReactProvider } from "~/trpc/react";
|
||||
import { Toaster } from "~/components/ui/toaster";
|
||||
import { Toaster } from "~/components/ui/sonner";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "beenvoice - Invoicing Made Simple",
|
||||
@@ -14,14 +14,9 @@ export const metadata: Metadata = {
|
||||
icons: [{ rel: "icon", url: "/favicon.ico" }],
|
||||
};
|
||||
|
||||
const geist = Geist({
|
||||
const geistMono = Geist_Mono({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-geist-sans",
|
||||
});
|
||||
|
||||
const azeretMono = Azeret_Mono({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-azeret-mono",
|
||||
variable: "--font-geist-mono",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
@@ -29,9 +24,9 @@ export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{ children: React.ReactNode }>) {
|
||||
return (
|
||||
<html lang="en" className={`${geist.variable} ${azeretMono.variable}`}>
|
||||
<html lang="en" className={geistMono.variable}>
|
||||
<Analytics />
|
||||
<body className="relative min-h-screen overflow-x-hidden font-sans antialiased">
|
||||
<body className="bg-background text-foreground relative min-h-screen overflow-x-hidden font-sans antialiased">
|
||||
<TRPCReactProvider>{children}</TRPCReactProvider>
|
||||
<Toaster />
|
||||
</body>
|
||||
|
||||
441
src/app/page.tsx
441
src/app/page.tsx
@@ -9,30 +9,32 @@ import {
|
||||
Check,
|
||||
Zap,
|
||||
Shield,
|
||||
Sparkles,
|
||||
BarChart3,
|
||||
Clock,
|
||||
Rocket,
|
||||
Heart,
|
||||
ChevronRight,
|
||||
Stars,
|
||||
} from "lucide-react";
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<div className="bg-page-gradient min-h-screen">
|
||||
<div className="bg-background min-h-screen">
|
||||
<AuthRedirect />
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="nav-sticky">
|
||||
<nav className="bg-background border-border sticky top-0 z-50 border-b">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="flex h-14 items-center justify-between sm:h-16">
|
||||
<Logo />
|
||||
<div className="hidden items-center space-x-6 md:flex">
|
||||
<a href="#features" className="nav-link">
|
||||
<a
|
||||
href="#features"
|
||||
className="text-muted-foreground hover:text-foreground text-sm font-medium transition-colors"
|
||||
>
|
||||
Features
|
||||
</a>
|
||||
<a href="#pricing" className="nav-link">
|
||||
<a
|
||||
href="#pricing"
|
||||
className="text-muted-foreground hover:text-foreground text-sm font-medium transition-colors"
|
||||
>
|
||||
Pricing
|
||||
</a>
|
||||
</div>
|
||||
@@ -41,14 +43,14 @@ export default function HomePage() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-slate-700 hover:text-slate-900 dark:text-slate-200 dark:hover:text-white"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<span className="hidden sm:inline">Sign In</span>
|
||||
<span className="sm:hidden">Sign In</span>
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/auth/register">
|
||||
<Button size="sm" className="btn-brand-primary">
|
||||
<Button size="sm" variant="default">
|
||||
<span className="hidden sm:inline">Get Started</span>
|
||||
<span className="sm:hidden">Start</span>
|
||||
</Button>
|
||||
@@ -59,96 +61,59 @@ export default function HomePage() {
|
||||
</nav>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="bg-hero-gradient relative overflow-hidden px-4 pt-12 pb-16 sm:pt-20">
|
||||
{/* Background decoration */}
|
||||
<div className="hero-overlay"></div>
|
||||
<div className="hero-orb-1 animate-glow-pulse animate-pulse"></div>
|
||||
<div className="hero-orb-2 animate-rainbow animate-bounce"></div>
|
||||
<div className="hero-orb-3 animate-glow-pulse animate-pulse"></div>
|
||||
|
||||
{/* Particle Effects */}
|
||||
<div className="particles-container">
|
||||
<div className="particle"></div>
|
||||
<div className="particle"></div>
|
||||
<div className="particle"></div>
|
||||
<div className="particle"></div>
|
||||
<div className="particle"></div>
|
||||
<div className="particle"></div>
|
||||
<div className="particle"></div>
|
||||
<div className="particle"></div>
|
||||
<div className="particle"></div>
|
||||
</div>
|
||||
|
||||
{/* Floating icons */}
|
||||
<div className="animate-float-slow hover:animate-wiggle absolute top-20 left-10">
|
||||
<Stars className="h-6 w-6 cursor-pointer text-white/20 transition-colors hover:text-white/40" />
|
||||
</div>
|
||||
<div className="animate-float-delayed hover:animate-wiggle absolute top-32 right-16">
|
||||
<Zap className="h-8 w-8 cursor-pointer text-emerald-300/30 transition-colors hover:text-emerald-300/60" />
|
||||
</div>
|
||||
<div className="animate-float hover:animate-wiggle absolute bottom-32 left-20">
|
||||
<Heart className="h-5 w-5 cursor-pointer text-pink-300/25 transition-colors hover:text-pink-300/50" />
|
||||
</div>
|
||||
<section className="bg-background relative overflow-hidden px-4 pt-12 pb-16 sm:pt-20">
|
||||
<div className="relative container mx-auto text-center">
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<Badge className="badge-brand hover:animate-wiggle mb-4 animate-pulse cursor-pointer sm:mb-6">
|
||||
<Sparkles className="hover:animate-rainbow mr-1 h-3 w-3 animate-spin" />
|
||||
<Badge className="bg-primary/10 text-primary border-primary/20 mb-4 border sm:mb-6">
|
||||
<Zap className="mr-1 h-3 w-3" />
|
||||
Free Forever
|
||||
</Badge>
|
||||
|
||||
<h1 className="animate-fade-in-up animate-glitch mb-4 text-4xl font-bold tracking-tight text-white sm:mb-6 sm:text-6xl lg:text-7xl">
|
||||
<h1 className="text-foreground mb-4 text-4xl font-bold tracking-tight sm:mb-6 sm:text-6xl lg:text-7xl">
|
||||
Simple Invoicing for
|
||||
<span className="animate-text-shimmer neon-glow block bg-gradient-to-r from-emerald-50 via-white to-emerald-50 bg-[length:200%_100%] bg-clip-text text-transparent">
|
||||
Freelancers
|
||||
</span>
|
||||
<span className="text-primary block">Freelancers</span>
|
||||
</h1>
|
||||
|
||||
<p className="animate-fade-in-up animation-delay-300 mx-auto mb-6 max-w-2xl text-lg leading-relaxed text-emerald-50/90 sm:mb-8 sm:text-xl">
|
||||
<p className="text-muted-foreground mx-auto mb-6 max-w-2xl text-lg leading-relaxed sm:mb-8 sm:text-xl">
|
||||
Create professional invoices, manage clients, and track payments.
|
||||
Built for freelancers and small businesses—
|
||||
<span className="animate-pulse font-semibold text-white">
|
||||
<span className="text-foreground font-semibold">
|
||||
completely free
|
||||
</span>
|
||||
.
|
||||
</p>
|
||||
|
||||
<div className="btn-group animate-fade-in-up animation-delay-500">
|
||||
<div className="flex flex-col items-center gap-4 sm:flex-row sm:justify-center">
|
||||
<Link href="/auth/register">
|
||||
<Button
|
||||
size="lg"
|
||||
className="btn-brand-secondary btn-flashy group w-full transform px-6 py-3 text-base font-semibold shadow-xl transition-all duration-300 hover:scale-105 hover:shadow-2xl sm:w-auto sm:px-8 sm:py-4 sm:text-lg"
|
||||
variant="default"
|
||||
className="group w-full px-6 py-3 text-base font-semibold sm:w-auto sm:px-8 sm:py-4 sm:text-lg"
|
||||
>
|
||||
Get Started
|
||||
<ArrowRight className="ml-2 h-4 w-4 transition-all group-hover:translate-x-1 group-hover:scale-110 group-hover:rotate-12 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" />
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="#features">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
className="btn-flashy group w-full transform border-white/30 px-6 py-3 text-base text-white transition-all duration-300 hover:scale-105 hover:bg-white/10 hover:shadow-xl sm:w-auto sm:px-8 sm:py-4 sm:text-lg"
|
||||
className="group w-full px-6 py-3 text-base sm:w-auto sm:px-8 sm:py-4 sm:text-lg"
|
||||
>
|
||||
Learn More
|
||||
<ChevronRight className="ml-2 h-4 w-4 transition-all group-hover:translate-x-1 group-hover:scale-110 group-hover:rotate-12 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" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="animate-fade-in-up animation-delay-700 mt-8 flex flex-col items-center justify-center gap-2 text-sm text-emerald-50/80 sm:mt-12 sm:flex-row sm:gap-6">
|
||||
<div className="text-muted-foreground mt-8 flex flex-col items-center justify-center gap-2 text-sm sm:mt-12 sm:flex-row sm:gap-6">
|
||||
{[
|
||||
"No credit card required",
|
||||
"Setup in 2 minutes",
|
||||
"Free forever",
|
||||
].map((text, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="animate-fade-in-up flex items-center gap-2"
|
||||
style={{ animationDelay: `${800 + i * 100}ms` }}
|
||||
>
|
||||
<Check
|
||||
className="h-4 w-4 animate-bounce text-emerald-100"
|
||||
style={{ animationDelay: `${1000 + i * 150}ms` }}
|
||||
/>
|
||||
<div key={i} className="flex items-center gap-2">
|
||||
<Check className="text-primary h-4 w-4" />
|
||||
<span className="text-center">{text}</span>
|
||||
</div>
|
||||
))}
|
||||
@@ -160,110 +125,125 @@ export default function HomePage() {
|
||||
{/* Features Section */}
|
||||
<section
|
||||
id="features"
|
||||
className="bg-features-gradient relative overflow-hidden py-16 sm:py-24"
|
||||
className="bg-muted/20 relative overflow-hidden py-16 sm:py-24"
|
||||
>
|
||||
{/* 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">
|
||||
<Badge className="badge-features mb-4">
|
||||
<Badge className="bg-primary/10 text-primary border-primary/20 mb-4 border">
|
||||
<Zap className="mr-1 h-3 w-3" />
|
||||
Features
|
||||
</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="text-foreground mb-4 text-3xl font-bold tracking-tight sm:text-4xl lg:text-5xl">
|
||||
Everything you need to
|
||||
<span className="text-brand-gradient block">get paid</span>
|
||||
<span className="text-primary block">get paid</span>
|
||||
</h2>
|
||||
<p className="mx-auto max-w-2xl text-lg text-slate-600 sm:text-xl dark:text-slate-300">
|
||||
<p className="text-muted-foreground mx-auto max-w-2xl text-lg sm:text-xl">
|
||||
Simple, powerful features for freelancers and small businesses.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 sm:gap-8 md:grid-cols-2 lg:grid-cols-3">
|
||||
{/* Feature 1 */}
|
||||
<Card className="card-floating interactive-card group animate-fade-in-up animate-glow-pulse transform transition-all duration-500 hover:scale-105 hover:shadow-2xl">
|
||||
<Card className="bg-card border-border hover:border-primary/20 border transition-all">
|
||||
<CardContent className="p-6 sm:p-8">
|
||||
<div className="icon-bg-brand hover:animate-wiggle mb-4 animate-bounce">
|
||||
<Rocket className="h-6 w-6 transition-all group-hover:scale-125 group-hover:rotate-12" />
|
||||
<div className="bg-primary/10 text-primary mb-4 inline-flex p-3">
|
||||
<Rocket className="h-6 w-6" />
|
||||
</div>
|
||||
<h3 className="mb-3 text-xl font-bold text-slate-900 dark:text-slate-100">
|
||||
<h3 className="text-foreground mb-3 text-xl font-bold">
|
||||
Quick Setup
|
||||
</h3>
|
||||
<p className="mb-4 text-slate-600 dark:text-slate-300">
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Start creating invoices immediately. No complicated setup
|
||||
required.
|
||||
</p>
|
||||
<ul className="feature-list">
|
||||
<li className="feature-item">
|
||||
<Check className="feature-check" />
|
||||
Simple client management
|
||||
<ul className="space-y-2">
|
||||
<li className="flex items-center gap-2">
|
||||
<Check className="text-primary h-4 w-4" />
|
||||
<span className="text-muted-foreground text-sm">
|
||||
Simple client management
|
||||
</span>
|
||||
</li>
|
||||
<li className="feature-item">
|
||||
<Check className="feature-check" />
|
||||
Professional templates
|
||||
<li className="flex items-center gap-2">
|
||||
<Check className="text-primary h-4 w-4" />
|
||||
<span className="text-muted-foreground text-sm">
|
||||
Professional templates
|
||||
</span>
|
||||
</li>
|
||||
<li className="feature-item">
|
||||
<Check className="feature-check" />
|
||||
Easy invoice sending
|
||||
<li className="flex items-center gap-2">
|
||||
<Check className="text-primary h-4 w-4" />
|
||||
<span className="text-muted-foreground text-sm">
|
||||
Easy invoice sending
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Feature 2 */}
|
||||
<Card className="card-floating interactive-card group animate-fade-in-up animation-delay-300 transform transition-all duration-500 hover:scale-105 hover:shadow-2xl">
|
||||
<Card className="bg-card border-border hover:border-primary/20 border transition-all">
|
||||
<CardContent className="p-6 sm:p-8">
|
||||
<div className="icon-bg-blue mb-4 animate-pulse">
|
||||
<BarChart3 className="h-6 w-6 transition-transform group-hover:scale-110" />
|
||||
<div className="bg-primary/10 text-primary mb-4 inline-flex p-3">
|
||||
<BarChart3 className="h-6 w-6" />
|
||||
</div>
|
||||
<h3 className="mb-3 text-xl font-bold text-slate-900 dark:text-slate-100">
|
||||
<h3 className="text-foreground mb-3 text-xl font-bold">
|
||||
Payment Tracking
|
||||
</h3>
|
||||
<p className="mb-4 text-slate-600 dark:text-slate-300">
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Keep track of invoice status and monitor payments.
|
||||
</p>
|
||||
<ul className="feature-list">
|
||||
<li className="feature-item">
|
||||
<Check className="feature-check" />
|
||||
Invoice status tracking
|
||||
<ul className="space-y-2">
|
||||
<li className="flex items-center gap-2">
|
||||
<Check className="text-primary h-4 w-4" />
|
||||
<span className="text-muted-foreground text-sm">
|
||||
Invoice status tracking
|
||||
</span>
|
||||
</li>
|
||||
<li className="feature-item">
|
||||
<Check className="feature-check" />
|
||||
Payment history
|
||||
<li className="flex items-center gap-2">
|
||||
<Check className="text-primary h-4 w-4" />
|
||||
<span className="text-muted-foreground text-sm">
|
||||
Payment history
|
||||
</span>
|
||||
</li>
|
||||
<li className="feature-item">
|
||||
<Check className="feature-check" />
|
||||
Overdue notifications
|
||||
<li className="flex items-center gap-2">
|
||||
<Check className="text-primary h-4 w-4" />
|
||||
<span className="text-muted-foreground text-sm">
|
||||
Overdue notifications
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Feature 3 */}
|
||||
<Card className="card-floating interactive-card group animate-fade-in-up animation-delay-500 transform transition-all duration-500 hover:scale-105 hover:shadow-2xl">
|
||||
<Card className="bg-card border-border hover:border-primary/20 border transition-all">
|
||||
<CardContent className="p-6 sm:p-8">
|
||||
<div className="icon-bg-purple animate-float mb-4">
|
||||
<Shield className="h-6 w-6 transition-all group-hover:scale-110 group-hover:rotate-12" />
|
||||
<div className="bg-primary/10 text-primary mb-4 inline-flex p-3">
|
||||
<Shield className="h-6 w-6" />
|
||||
</div>
|
||||
<h3 className="mb-3 text-xl font-bold text-slate-900 dark:text-slate-100">
|
||||
<h3 className="text-foreground mb-3 text-xl font-bold">
|
||||
Professional Features
|
||||
</h3>
|
||||
<p className="mb-4 text-slate-600 dark:text-slate-300">
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Professional features to help you get paid on time.
|
||||
</p>
|
||||
<ul className="feature-list">
|
||||
<li className="feature-item">
|
||||
<Check className="feature-check" />
|
||||
PDF generation
|
||||
<ul className="space-y-2">
|
||||
<li className="flex items-center gap-2">
|
||||
<Check className="text-primary h-4 w-4" />
|
||||
<span className="text-muted-foreground text-sm">
|
||||
PDF generation
|
||||
</span>
|
||||
</li>
|
||||
<li className="feature-item">
|
||||
<Check className="feature-check" />
|
||||
Custom tax rates
|
||||
<li className="flex items-center gap-2">
|
||||
<Check className="text-primary h-4 w-4" />
|
||||
<span className="text-muted-foreground text-sm">
|
||||
Custom tax rates
|
||||
</span>
|
||||
</li>
|
||||
<li className="feature-item">
|
||||
<Check className="feature-check" />
|
||||
Professional numbering
|
||||
<li className="flex items-center gap-2">
|
||||
<Check className="text-primary h-4 w-4" />
|
||||
<span className="text-muted-foreground text-sm">
|
||||
Professional numbering
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
@@ -275,223 +255,110 @@ export default function HomePage() {
|
||||
{/* Pricing Section */}
|
||||
<section
|
||||
id="pricing"
|
||||
className="bg-features-gradient relative overflow-hidden py-16 sm:py-24"
|
||||
className="bg-background relative overflow-hidden py-16 sm:py-24"
|
||||
>
|
||||
{/* 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">
|
||||
<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="text-foreground mb-4 text-3xl font-bold tracking-tight sm:text-4xl lg:text-5xl">
|
||||
Simple pricing
|
||||
</h2>
|
||||
<p className="mx-auto max-w-2xl text-lg text-slate-600 sm:text-xl dark:text-slate-300">
|
||||
<p className="text-muted-foreground mx-auto max-w-2xl text-lg sm:text-xl">
|
||||
Start free, stay free. No hidden fees or limits.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto max-w-md">
|
||||
<Card className="animate-fade-in-up animate-glow-pulse interactive-card hover:shadow-3xl relative transform border-2 border-emerald-500 bg-white/90 shadow-2xl backdrop-blur-sm transition-all duration-500 hover:scale-105 dark:border-emerald-400 dark:bg-slate-800/90">
|
||||
<div className="absolute -top-4 left-1/2 -translate-x-1/2">
|
||||
<Badge className="badge-success animate-pulse px-6 py-1">
|
||||
Forever Free
|
||||
</Badge>
|
||||
<Card className="bg-card border-primary border-2">
|
||||
<div className="bg-primary/10 text-primary border-primary/20 mx-auto -mt-3 w-fit border px-6 py-1 text-sm font-medium">
|
||||
Forever Free
|
||||
</div>
|
||||
<CardContent className="p-6 text-center sm:p-8">
|
||||
<div className="mb-6">
|
||||
<div className="animate-text-shimmer mb-2 bg-gradient-to-r from-emerald-500 via-teal-500 to-emerald-500 bg-[length:200%_100%] bg-clip-text text-5xl font-bold text-transparent sm:text-6xl">
|
||||
<CardContent className="p-6 sm:p-8">
|
||||
<div className="mb-6 text-center">
|
||||
<div className="text-foreground mb-2 text-4xl font-bold sm:text-5xl">
|
||||
$0
|
||||
</div>
|
||||
<div className="text-slate-600 dark:text-slate-400">
|
||||
per month, forever
|
||||
</div>
|
||||
<p className="text-muted-foreground">
|
||||
Forever. No credit card required.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 space-y-3 text-left sm:mb-8 sm:space-y-4">
|
||||
<ul className="mb-8 space-y-3">
|
||||
{[
|
||||
"Unlimited invoices",
|
||||
"Unlimited clients",
|
||||
"Professional templates",
|
||||
"PDF export",
|
||||
"Client management",
|
||||
"PDF generation",
|
||||
"Payment tracking",
|
||||
"Multi-business support",
|
||||
"Line item details",
|
||||
"Free forever",
|
||||
"Professional templates",
|
||||
"Custom tax rates",
|
||||
"Email support",
|
||||
].map((feature, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="animate-fade-in-up flex items-center gap-3"
|
||||
style={{ animationDelay: `${i * 100}ms` }}
|
||||
>
|
||||
<Check
|
||||
className="h-5 w-5 flex-shrink-0 animate-bounce text-emerald-500"
|
||||
style={{ animationDelay: `${200 + i * 100}ms` }}
|
||||
/>
|
||||
<span className="text-slate-700 dark:text-slate-300">
|
||||
{feature}
|
||||
</span>
|
||||
</div>
|
||||
<li key={i} className="flex items-center gap-3">
|
||||
<Check className="text-primary h-5 w-5" />
|
||||
<span className="text-foreground">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</div>
|
||||
</ul>
|
||||
|
||||
<Link href="/auth/register">
|
||||
<Button
|
||||
variant="brand"
|
||||
className="btn-flashy animate-magnetic w-full transform py-3 text-base font-semibold transition-all duration-300 hover:scale-105 hover:shadow-lg sm:text-lg"
|
||||
>
|
||||
Get Started ✨
|
||||
<Link href="/auth/register" className="block">
|
||||
<Button className="w-full" size="lg">
|
||||
Get Started Free
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<p className="mt-4 text-sm text-slate-600 dark:text-slate-400">
|
||||
No credit card required
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Why Choose */}
|
||||
<section className="bg-features-gradient relative overflow-hidden py-16 sm:py-24">
|
||||
{/* 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">
|
||||
<h2 className="mb-4 text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl lg:text-5xl dark:text-slate-100">
|
||||
Why choose
|
||||
<span className="animate-text-shimmer neon-glow animate-glitch block bg-gradient-to-r from-teal-500 via-emerald-600 to-teal-500 bg-[length:200%_100%] bg-clip-text text-transparent">
|
||||
BeenVoice
|
||||
</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 sm:gap-8 md:grid-cols-3">
|
||||
<div className="text-center">
|
||||
<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" />
|
||||
</div>
|
||||
<h3 className="mb-3 text-xl font-bold text-slate-900 dark:text-slate-100">
|
||||
Quick & Simple
|
||||
</h3>
|
||||
<p className="text-slate-600 dark:text-slate-300">
|
||||
No learning curve. Start creating invoices in minutes.
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<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" />
|
||||
</div>
|
||||
<h3 className="mb-3 text-xl font-bold text-slate-900 dark:text-slate-100">
|
||||
Always Free
|
||||
</h3>
|
||||
<p className="text-slate-600 dark:text-slate-300">
|
||||
No hidden fees, no premium tiers. All features are free.
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="icon-bg-purple mb-4">
|
||||
<Clock className="h-6 w-6" />
|
||||
</div>
|
||||
<h3 className="mb-3 text-xl font-bold text-slate-900 dark:text-slate-100">
|
||||
Save Time
|
||||
</h3>
|
||||
<p className="text-slate-600 dark:text-slate-300">
|
||||
Focus on your work, not paperwork. Automated calculations and
|
||||
formatting.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="bg-hero-gradient relative overflow-hidden py-16 sm:py-24">
|
||||
<div className="hero-overlay"></div>
|
||||
<div className="hero-orb-1"></div>
|
||||
<div className="hero-orb-2"></div>
|
||||
<div className="hero-orb-3"></div>
|
||||
|
||||
<section className="bg-primary relative overflow-hidden py-16 sm:py-24">
|
||||
<div className="relative container mx-auto px-4 text-center">
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<h2 className="mb-4 text-3xl font-bold text-white sm:mb-6 sm:text-4xl lg:text-5xl">
|
||||
Ready to get started?
|
||||
</h2>
|
||||
<p className="mb-6 text-lg text-emerald-50/90 sm:mb-8 sm:text-xl">
|
||||
Join thousands of freelancers already using BeenVoice. Start
|
||||
today—completely free.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col items-center gap-4 sm:flex-row sm:justify-center">
|
||||
<Link href="/auth/register">
|
||||
<Button
|
||||
size="lg"
|
||||
variant="secondary"
|
||||
className="btn-brand-secondary btn-flashy group animate-glow-pulse w-full transform px-6 py-3 text-base font-semibold shadow-xl transition-all duration-300 hover:scale-105 hover:shadow-2xl sm:w-auto sm:px-8 sm:py-4 sm:text-lg"
|
||||
>
|
||||
Start Free Today
|
||||
<Rocket className="ml-2 h-4 w-4 animate-bounce transition-all group-hover:translate-x-1 group-hover:scale-125 group-hover:rotate-45 sm:h-5 sm:w-5" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex flex-col items-center justify-center gap-3 text-emerald-50/80 sm:mt-8 sm:flex-row sm:gap-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Heart className="h-4 w-4" />
|
||||
Free forever
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield className="h-4 w-4" />
|
||||
Secure & private
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="h-4 w-4" />
|
||||
2-minute setup
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="text-primary-foreground mb-4 text-3xl font-bold tracking-tight sm:text-4xl lg:text-5xl">
|
||||
Ready to get started?
|
||||
</h2>
|
||||
<p className="text-primary-foreground/80 mx-auto mb-8 max-w-2xl text-lg sm:text-xl">
|
||||
Join thousands of freelancers who trust beenvoice for their
|
||||
invoicing needs.
|
||||
</p>
|
||||
<div className="flex flex-col items-center gap-4 sm:flex-row sm:justify-center">
|
||||
<Link href="/auth/register">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
className="group w-full px-6 py-3 text-base font-semibold sm:w-auto sm:px-8 sm:py-4 sm:text-lg"
|
||||
>
|
||||
Start Free Today
|
||||
<ArrowRight className="ml-2 h-4 w-4 transition-transform group-hover:translate-x-1 sm:h-5 sm:w-5" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="bg-features-gradient border-t py-8 sm:py-12 dark:border-slate-700">
|
||||
<footer className="bg-muted border-border border-t py-8">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="text-center">
|
||||
<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">
|
||||
Simple invoicing for freelancers. Free, forever.
|
||||
</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="flex flex-col items-center justify-between gap-4 sm:flex-row">
|
||||
<div className="flex items-center gap-2">
|
||||
<Logo size="sm" />
|
||||
<span className="text-muted-foreground text-sm">
|
||||
© 2024 beenvoice. Built for freelancers.
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-6">
|
||||
<Link
|
||||
href="/auth/signin"
|
||||
className="transition-colors hover:text-emerald-600 dark:hover:text-emerald-400"
|
||||
className="text-muted-foreground hover:text-foreground text-sm transition-colors"
|
||||
>
|
||||
Sign In
|
||||
</Link>
|
||||
<Link
|
||||
href="/auth/register"
|
||||
className="transition-colors hover:text-emerald-600 dark:hover:text-emerald-400"
|
||||
className="text-muted-foreground hover:text-foreground text-sm transition-colors"
|
||||
>
|
||||
Register
|
||||
Get Started
|
||||
</Link>
|
||||
<a
|
||||
href="#features"
|
||||
className="transition-colors hover:text-emerald-600 dark:hover:text-emerald-400"
|
||||
>
|
||||
Features
|
||||
</a>
|
||||
<a
|
||||
href="#pricing"
|
||||
className="transition-colors hover:text-emerald-600 dark:hover:text-emerald-400"
|
||||
>
|
||||
Pricing
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-6 border-t border-slate-200 pt-6 sm:mt-8 sm:pt-8 dark:border-slate-700">
|
||||
<p className="text-sm text-slate-600 sm:text-base dark:text-slate-400">
|
||||
© 2025 Sean O'Connor.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -64,7 +64,7 @@ export function AddressAutocomplete({
|
||||
onBlur={() => setTimeout(() => setShowSuggestions(false), 150)}
|
||||
/>
|
||||
{showSuggestions && suggestions.length > 0 && (
|
||||
<Card className="card-primary absolute z-10 mt-1 max-h-60 w-full overflow-auto">
|
||||
<Card className="bg-card border-border border absolute z-10 mt-1 max-h-60 w-full overflow-auto">
|
||||
<ul>
|
||||
{suggestions.map((s) => (
|
||||
<li
|
||||
|
||||
@@ -1,26 +1,72 @@
|
||||
import Image from "next/image";
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
interface LogoProps {
|
||||
className?: string;
|
||||
size?: "sm" | "md" | "lg";
|
||||
size?: "sm" | "md" | "lg" | "xl";
|
||||
animated?: boolean;
|
||||
}
|
||||
|
||||
export function Logo({ className, size = "md" }: LogoProps) {
|
||||
export function Logo({ className, size = "md", animated = true }: LogoProps) {
|
||||
const sizeClasses = {
|
||||
sm: { width: 120, height: 32 },
|
||||
md: { width: 160, height: 42 },
|
||||
lg: { width: 240, height: 64 },
|
||||
sm: "text-sm",
|
||||
md: "text-lg",
|
||||
lg: "text-2xl",
|
||||
xl: "text-4xl",
|
||||
};
|
||||
const { width, height } = sizeClasses[size];
|
||||
|
||||
const LogoContent = () => (
|
||||
<div className={cn("flex items-center", sizeClasses[size], className)}>
|
||||
<span className="text-primary font-bold tracking-tight">$</span>
|
||||
<span className="inline-block w-2"></span>
|
||||
<span className="text-foreground font-bold tracking-tight">been</span>
|
||||
<span className="text-foreground/70 font-bold tracking-tight">voice</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!animated) {
|
||||
return <LogoContent />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Image
|
||||
src="/beenvoice-logo.svg"
|
||||
alt="beenvoice logo"
|
||||
width={width}
|
||||
height={height}
|
||||
className={className}
|
||||
priority
|
||||
/>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.1, ease: "easeOut" }}
|
||||
className={cn("flex items-center", sizeClasses[size], className)}
|
||||
>
|
||||
<motion.span
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.02, duration: 0.05, ease: "easeOut" }}
|
||||
className="text-primary font-bold tracking-tight"
|
||||
>
|
||||
$
|
||||
</motion.span>
|
||||
<motion.span
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.03, duration: 0.05, ease: "easeOut" }}
|
||||
className="inline-block w-2"
|
||||
></motion.span>
|
||||
<motion.span
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.04, duration: 0.05, ease: "easeOut" }}
|
||||
className="text-foreground font-bold tracking-tight"
|
||||
>
|
||||
been
|
||||
</motion.span>
|
||||
<motion.span
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.06, duration: 0.05, ease: "easeOut" }}
|
||||
className="text-foreground/70 font-bold tracking-tight"
|
||||
>
|
||||
voice
|
||||
</motion.span>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -438,10 +438,10 @@ export function CSVImportPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Global Client Selection */}
|
||||
<Card className="card-primary">
|
||||
<Card className="bg-card border-border border">
|
||||
<CardHeader>
|
||||
<CardTitle className="card-title-secondary">
|
||||
<Users className="text-icon-blue h-5 w-5" />
|
||||
<CardTitle className="text-foreground flex items-center gap-2">
|
||||
<Users className="text-primary h-5 w-5" />
|
||||
Default Client
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -460,7 +460,7 @@ export function CSVImportPage() {
|
||||
applyGlobalClient(newClientId);
|
||||
}
|
||||
}}
|
||||
className="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-12 w-full rounded-md border px-3 py-2 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus:ring-1 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||
className="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-12 w-full border px-3 py-2 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus:ring-1 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||
disabled={loadingClients}
|
||||
>
|
||||
<option value="">No default client (select individually)</option>
|
||||
@@ -479,10 +479,10 @@ export function CSVImportPage() {
|
||||
</Card>
|
||||
|
||||
{/* File Upload Area */}
|
||||
<Card className="card-primary">
|
||||
<Card className="bg-card border-border border">
|
||||
<CardHeader>
|
||||
<CardTitle className="card-title-secondary">
|
||||
<Upload className="text-icon-emerald h-5 w-5" />
|
||||
<CardTitle className="text-foreground flex items-center gap-2">
|
||||
<Upload className="text-primary h-5 w-5" />
|
||||
Upload CSV Files
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -498,49 +498,45 @@ export function CSVImportPage() {
|
||||
|
||||
{/* Summary Card */}
|
||||
{totalFiles > 0 && (
|
||||
<Card className="card-primary">
|
||||
<Card className="bg-card border-border border">
|
||||
<CardHeader>
|
||||
<CardTitle className="card-title-secondary">
|
||||
<FileText className="text-icon-emerald h-5 w-5" />
|
||||
<CardTitle className="text-foreground flex items-center gap-2">
|
||||
<FileText className="text-primary h-5 w-5" />
|
||||
Import Summary
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-4 rounded-lg bg-green-50/50 p-4 md:grid-cols-4 dark:bg-green-900/10">
|
||||
<div className="bg-primary/10 grid grid-cols-2 gap-4 p-4 md:grid-cols-4">
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
<div className="text-primary text-2xl font-bold">
|
||||
{totalFiles}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Files
|
||||
</div>
|
||||
<div className="text-muted-foreground text-sm">Files</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
<div className="text-primary text-2xl font-bold">
|
||||
{totalItems}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
<div className="text-muted-foreground text-sm">
|
||||
Total Items
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
<div className="text-primary text-2xl font-bold">
|
||||
{totalAmount.toLocaleString("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
})}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
<div className="text-muted-foreground text-sm">
|
||||
Total Amount
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
<div className="text-primary text-2xl font-bold">
|
||||
{readyFiles}/{totalFiles}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Ready
|
||||
</div>
|
||||
<div className="text-muted-foreground text-sm">Ready</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -551,9 +547,9 @@ export function CSVImportPage() {
|
||||
|
||||
{/* File List */}
|
||||
{files.length > 0 && (
|
||||
<Card className="card-primary">
|
||||
<Card className="bg-card border-border border">
|
||||
<CardHeader>
|
||||
<CardTitle className="card-title-secondary">
|
||||
<CardTitle className="text-foreground flex items-center gap-2">
|
||||
Uploaded Files
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -562,11 +558,11 @@ export function CSVImportPage() {
|
||||
{files.map((fileData, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="border-border bg-card rounded-lg border p-4"
|
||||
className="border-border bg-card border p-4"
|
||||
>
|
||||
<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">
|
||||
<FileText className="text-icon-emerald h-5 w-5" />
|
||||
<FileText className="text-primary h-5 w-5" />
|
||||
<div>
|
||||
<h3 className="text-foreground truncate font-medium">
|
||||
{fileData.file.name}
|
||||
@@ -593,7 +589,7 @@ export function CSVImportPage() {
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => removeFile(index)}
|
||||
className="text-red-600 hover:text-red-700"
|
||||
className="text-destructive hover:text-destructive/80"
|
||||
>
|
||||
<Trash2 className="mr-1 h-4 w-4" />
|
||||
Remove
|
||||
@@ -623,7 +619,7 @@ export function CSVImportPage() {
|
||||
onChange={(e) =>
|
||||
updateFileData(index, { clientId: e.target.value })
|
||||
}
|
||||
className="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-9 w-full rounded-md border px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus:ring-1 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||
className="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-9 w-full border px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus:ring-1 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||
disabled={loadingClients}
|
||||
>
|
||||
<option value="">Select Client</option>
|
||||
@@ -666,20 +662,20 @@ export function CSVImportPage() {
|
||||
|
||||
{/* Error Display */}
|
||||
{fileData.errors.length > 0 && (
|
||||
<div className="mt-4 rounded-lg border border-red-200 bg-red-50 p-3 dark:border-red-800 dark:bg-red-900/20">
|
||||
<div className="border-destructive/20 bg-destructive/10 mt-4 border p-3">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<AlertCircle className="h-4 w-4 text-red-600" />
|
||||
<span className="text-sm font-medium text-red-800 dark:text-red-200">
|
||||
<AlertCircle className="text-destructive h-4 w-4" />
|
||||
<span className="text-destructive text-sm font-medium">
|
||||
Issues Found
|
||||
</span>
|
||||
</div>
|
||||
<ul className="space-y-1 text-sm text-red-700 dark:text-red-300">
|
||||
<ul className="text-destructive space-y-1 text-sm">
|
||||
{fileData.errors.map((error, errorIndex) => (
|
||||
<li
|
||||
key={errorIndex}
|
||||
className="flex items-start gap-2"
|
||||
>
|
||||
<span className="text-red-600">•</span>
|
||||
<span className="text-destructive">•</span>
|
||||
<span>{error}</span>
|
||||
</li>
|
||||
))}
|
||||
@@ -735,10 +731,10 @@ export function CSVImportPage() {
|
||||
|
||||
{/* Batch Actions */}
|
||||
{files.length > 0 && (
|
||||
<Card className="card-primary">
|
||||
<Card className="bg-card border-border border">
|
||||
<CardHeader>
|
||||
<CardTitle className="card-title-secondary">
|
||||
<DollarSign className="text-icon-green h-5 w-5" />
|
||||
<CardTitle className="text-foreground flex items-center gap-2">
|
||||
<DollarSign className="text-primary h-5 w-5" />
|
||||
Create Invoices
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -762,7 +758,7 @@ export function CSVImportPage() {
|
||||
<Button
|
||||
onClick={processBatch}
|
||||
disabled={readyFiles === 0 || isProcessing}
|
||||
className="btn-brand-primary"
|
||||
variant="default"
|
||||
>
|
||||
{isProcessing
|
||||
? "Processing..."
|
||||
@@ -776,10 +772,10 @@ export function CSVImportPage() {
|
||||
|
||||
{/* Preview Modal */}
|
||||
<Dialog open={previewModalOpen} onOpenChange={setPreviewModalOpen}>
|
||||
<DialogContent className="card-primary flex max-h-[90vh] max-w-4xl flex-col">
|
||||
<DialogContent className="bg-card border-border border flex max-h-[90vh] max-w-4xl flex-col">
|
||||
<DialogHeader className="flex-shrink-0">
|
||||
<DialogTitle className="flex items-center gap-2 text-xl font-bold text-gray-800">
|
||||
<FileText className="h-5 w-5 text-emerald-600" />
|
||||
<DialogTitle className="text-foreground flex items-center gap-2 text-xl font-bold">
|
||||
<FileText className="text-primary h-5 w-5" />
|
||||
{selectedFileIndex !== null &&
|
||||
files[selectedFileIndex]?.file.name}
|
||||
</DialogTitle>
|
||||
@@ -792,14 +788,14 @@ export function CSVImportPage() {
|
||||
<div className="flex min-h-0 flex-1 flex-col space-y-4">
|
||||
<div className="grid flex-shrink-0 grid-cols-1 gap-4 md:grid-cols-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText className="h-4 w-4 text-emerald-600" />
|
||||
<span className="text-sm text-gray-600">
|
||||
<FileText className="text-primary h-4 w-4" />
|
||||
<span className="text-muted-foreground text-sm">
|
||||
{files[selectedFileIndex].parsedItems.length} items
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="h-4 w-4 text-emerald-600" />
|
||||
<span className="text-sm text-gray-600">
|
||||
<Clock className="text-primary h-4 w-4" />
|
||||
<span className="text-muted-foreground text-sm">
|
||||
{files[selectedFileIndex].parsedItems
|
||||
.reduce((sum, item) => sum + item.hours, 0)
|
||||
.toFixed(1)}{" "}
|
||||
@@ -807,8 +803,8 @@ export function CSVImportPage() {
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<DollarSign className="h-4 w-4 text-emerald-600" />
|
||||
<span className="text-sm font-medium text-gray-600">
|
||||
<DollarSign className="text-primary h-4 w-4" />
|
||||
<span className="text-muted-foreground text-sm font-medium">
|
||||
{files[selectedFileIndex].parsedItems
|
||||
.reduce((sum, item) => sum + item.amount, 0)
|
||||
.toLocaleString("en-US", {
|
||||
|
||||
@@ -73,7 +73,7 @@ export function ClientList() {
|
||||
return (
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{Array.from({ length: 3 }, (_, i: number) => (
|
||||
<Card key={i} className="card-primary">
|
||||
<Card key={i} className="bg-card border-border border">
|
||||
<CardHeader>
|
||||
<div className="h-4 animate-pulse rounded bg-gray-200" />
|
||||
</CardHeader>
|
||||
@@ -91,9 +91,9 @@ export function ClientList() {
|
||||
|
||||
if (!clients || clients.length === 0) {
|
||||
return (
|
||||
<Card className="card-primary">
|
||||
<Card className="bg-card border-border border">
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle className="text-brand-gradient text-2xl font-bold">
|
||||
<CardTitle className="text-primary text-2xl font-bold">
|
||||
No Clients Yet
|
||||
</CardTitle>
|
||||
<CardDescription className="text-lg">
|
||||
@@ -142,20 +142,20 @@ export function ClientList() {
|
||||
{filteredClients.map((client) => (
|
||||
<Card
|
||||
key={client.id}
|
||||
className="group card-primary transition-all duration-300 hover:shadow-lg"
|
||||
className="group bg-card border-border border transition-all duration-300 hover:shadow-lg"
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between text-lg">
|
||||
<span className="text-accent group-hover:text-icon-emerald font-semibold transition-colors">
|
||||
<span className="text-foreground group-hover:text-primary font-semibold transition-colors">
|
||||
{client.name}
|
||||
</span>
|
||||
<div className="flex space-x-1 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
<Link href={`/clients/${client.id}`}>
|
||||
<Link href={`/dashboard/clients/${client.id}`}>
|
||||
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href={`/clients/${client.id}/edit`}>
|
||||
<Link href={`/dashboard/clients/${client.id}/edit`}>
|
||||
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -173,25 +173,25 @@ export function ClientList() {
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{client.email && (
|
||||
<div className="text-secondary flex items-center text-sm">
|
||||
<div className="bg-brand-muted mr-3 rounded p-1.5">
|
||||
<Mail className="text-icon-emerald h-3 w-3" />
|
||||
<div className="text-muted-foreground flex items-center text-sm">
|
||||
<div className="bg-muted mr-3 rounded p-1.5">
|
||||
<Mail className="text-muted-foreground h-3 w-3" />
|
||||
</div>
|
||||
{client.email}
|
||||
</div>
|
||||
)}
|
||||
{client.phone && (
|
||||
<div className="text-secondary flex items-center text-sm">
|
||||
<div className="bg-brand-muted-blue mr-3 rounded p-1.5">
|
||||
<Phone className="text-icon-blue h-3 w-3" />
|
||||
<div className="text-muted-foreground flex items-center text-sm">
|
||||
<div className="bg-muted mr-3 rounded p-1.5">
|
||||
<Phone className="text-muted-foreground h-3 w-3" />
|
||||
</div>
|
||||
{client.phone}
|
||||
</div>
|
||||
)}
|
||||
{(client.addressLine1 ?? client.city ?? client.state) && (
|
||||
<div className="text-secondary flex items-start text-sm">
|
||||
<div className="bg-brand-muted-teal mt-0.5 mr-3 flex-shrink-0 rounded p-1.5">
|
||||
<MapPin className="text-icon-teal h-3 w-3" />
|
||||
<div className="text-muted-foreground flex items-start text-sm">
|
||||
<div className="bg-muted mt-0.5 mr-3 flex-shrink-0 rounded p-1.5">
|
||||
<MapPin className="text-muted-foreground h-3 w-3" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
{client.addressLine1 && <div>{client.addressLine1}</div>}
|
||||
@@ -213,12 +213,12 @@ export function ClientList() {
|
||||
</div>
|
||||
|
||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<DialogContent className="card-primary">
|
||||
<DialogContent className="bg-card border-border border">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-accent text-xl font-bold">
|
||||
<DialogTitle className="text-foreground text-xl font-bold">
|
||||
Delete Client
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-secondary">
|
||||
<DialogDescription className="text-muted-foreground">
|
||||
Are you sure you want to delete this client? This action cannot be
|
||||
undone.
|
||||
</DialogDescription>
|
||||
@@ -227,14 +227,14 @@ export function ClientList() {
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setDeleteDialogOpen(false)}
|
||||
className="text-secondary"
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={confirmDelete}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
className="bg-destructive hover:bg-destructive/90"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
|
||||
@@ -28,10 +28,10 @@ export function CurrentOpenInvoiceCard() {
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card className="card-primary">
|
||||
<Card className="bg-card border-border border">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="card-title-secondary">
|
||||
<FileText className="text-icon-emerald h-5 w-5" />
|
||||
<CardTitle className="text-foreground flex items-center gap-2">
|
||||
<FileText className="text-primary h-5 w-5" />
|
||||
Current Open Invoice
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -49,10 +49,10 @@ export function CurrentOpenInvoiceCard() {
|
||||
|
||||
if (!currentInvoice) {
|
||||
return (
|
||||
<Card className="card-primary">
|
||||
<Card className="bg-card border-border border">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="card-title-secondary">
|
||||
<FileText className="text-icon-emerald h-5 w-5" />
|
||||
<CardTitle className="text-foreground flex items-center gap-2">
|
||||
<FileText className="text-primary h-5 w-5" />
|
||||
Current Open Invoice
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -80,10 +80,10 @@ export function CurrentOpenInvoiceCard() {
|
||||
const totalAmount = currentInvoice.totalAmount;
|
||||
|
||||
return (
|
||||
<Card className="card-primary">
|
||||
<Card className="bg-card border-border border">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="card-title-secondary">
|
||||
<FileText className="text-icon-emerald h-5 w-5" />
|
||||
<CardTitle className="text-foreground flex items-center gap-2">
|
||||
<FileText className="text-primary h-5 w-5" />
|
||||
Current Open Invoice
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -91,13 +91,13 @@ export function CurrentOpenInvoiceCard() {
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge className="badge-secondary text-xs">
|
||||
<Badge className="bg-secondary text-secondary-foreground text-xs">
|
||||
{currentInvoice.invoiceNumber}
|
||||
</Badge>
|
||||
<Badge className="badge-outline text-xs">Draft</Badge>
|
||||
<Badge className="border text-xs">Draft</Badge>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-icon-emerald text-sm font-medium">
|
||||
<p className="text-primary text-sm font-medium">
|
||||
{formatCurrency(totalAmount)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -215,12 +215,12 @@ export function DataTable<TData, TValue>({
|
||||
|
||||
{/* Filter Bar Card */}
|
||||
{(showSearch || filterableColumns.length > 0 || showColumnVisibility) && (
|
||||
<Card className="card-primary py-2">
|
||||
<Card className="bg-card border-border border py-2">
|
||||
<CardContent className="px-3 py-0">
|
||||
<div className="flex items-center gap-2">
|
||||
{showSearch && (
|
||||
<div className="relative min-w-0 flex-1">
|
||||
<Search className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
|
||||
<Search className="text-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
|
||||
<Input
|
||||
placeholder={searchPlaceholder}
|
||||
value={globalFilter ?? ""}
|
||||
@@ -244,7 +244,7 @@ export function DataTable<TData, TValue>({
|
||||
>
|
||||
<SelectTrigger className="h-9 w-9 p-0 sm:w-[180px] sm:px-3 [&>svg]:hidden sm:[&>svg]:inline-flex">
|
||||
<div className="flex w-full items-center justify-center">
|
||||
<Filter className="h-4 w-4 sm:hidden" />
|
||||
<Filter className="text-foreground h-4 w-4 sm:hidden" />
|
||||
<span className="hidden sm:inline">
|
||||
<SelectValue placeholder={column.title} />
|
||||
</span>
|
||||
@@ -272,7 +272,7 @@ export function DataTable<TData, TValue>({
|
||||
>
|
||||
<X className="h-4 w-4 sm:hidden" />
|
||||
<span className="hidden sm:flex sm:items-center">
|
||||
<Filter className="mr-2 h-3.5 w-3.5" />
|
||||
<Filter className="text-foreground mr-2 h-3.5 w-3.5" />
|
||||
Clear filters
|
||||
</span>
|
||||
</Button>
|
||||
@@ -315,7 +315,7 @@ export function DataTable<TData, TValue>({
|
||||
)}
|
||||
|
||||
{/* Table Content Card */}
|
||||
<Card className="card-primary overflow-hidden p-0">
|
||||
<Card className="bg-card border-border overflow-hidden border p-0">
|
||||
<div className="w-full overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
@@ -400,7 +400,7 @@ export function DataTable<TData, TValue>({
|
||||
|
||||
{/* Pagination Bar Card */}
|
||||
{showPagination && (
|
||||
<Card className="card-primary py-2">
|
||||
<Card className="bg-card border-border border py-2">
|
||||
<CardContent className="px-3 py-0">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -561,17 +561,17 @@ export function DataTableSkeleton({
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Filter bar skeleton */}
|
||||
<Card className="card-primary py-2">
|
||||
<Card className="bg-card border-border border py-2">
|
||||
<CardContent className="px-3 py-0">
|
||||
<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-24 animate-pulse rounded-md"></div>
|
||||
<div className="bg-muted/30 h-9 w-full flex-1 animate-pulse sm:max-w-sm"></div>
|
||||
<div className="bg-muted/30 h-9 w-24 animate-pulse"></div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Table skeleton */}
|
||||
<Card className="card-primary overflow-hidden p-0">
|
||||
<Card className="bg-card border-border overflow-hidden border p-0">
|
||||
<div className="w-full overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
@@ -632,7 +632,7 @@ export function DataTableSkeleton({
|
||||
</Card>
|
||||
|
||||
{/* Pagination skeleton */}
|
||||
<Card className="card-primary py-2">
|
||||
<Card className="bg-card border-border border py-2">
|
||||
<CardContent className="px-3 py-0">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -87,7 +87,7 @@ function SortableItem({
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className={`card-secondary rounded-lg transition-colors ${
|
||||
className={`card-secondary transition-colors ${
|
||||
isDragging ? "opacity-50 shadow-lg" : ""
|
||||
}`}
|
||||
>
|
||||
@@ -154,7 +154,7 @@ function SortableItem({
|
||||
|
||||
{/* Amount */}
|
||||
<div className="col-span-1">
|
||||
<div className="bg-muted/30 flex h-9 items-center rounded-md border px-3 font-medium text-emerald-600">
|
||||
<div className="bg-muted/30 text-primary flex h-9 items-center border px-3 font-medium">
|
||||
${item.amount.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -166,7 +166,7 @@ function SortableItem({
|
||||
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"
|
||||
className="text-destructive hover:bg-destructive/10 hover:text-destructive/80 h-9 w-9 p-0"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -206,7 +206,7 @@ function SortableItem({
|
||||
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"
|
||||
className="text-destructive hover:bg-destructive/10 hover:text-destructive/80 h-6 w-6 p-0"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
@@ -266,10 +266,10 @@ function SortableItem({
|
||||
</div>
|
||||
|
||||
{/* Amount */}
|
||||
<div className="bg-muted/20 rounded-md border p-3">
|
||||
<div className="bg-muted/20 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">
|
||||
<span className="text-primary font-mono text-lg font-bold">
|
||||
${item.amount.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -362,7 +362,7 @@ export function EditableInvoiceItems({
|
||||
{items.map((item, _index) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="card-secondary animate-pulse rounded-lg p-4"
|
||||
className="card-secondary animate-pulse p-4"
|
||||
>
|
||||
{/* Desktop Skeleton */}
|
||||
<div className="hidden grid-cols-12 gap-3 md:grid">
|
||||
|
||||
@@ -150,12 +150,12 @@ export function InvoiceList() {
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span className="truncate">{invoice.invoiceNumber}</span>
|
||||
<div className="flex space-x-1">
|
||||
<Link href={`/dashboard/invoices/${invoice.id}/view`}>
|
||||
<Link href={`/dashboard/invoices/${invoice.id}`}>
|
||||
<Button variant="ghost" size="sm">
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href={`/dashboard/invoices/${invoice.id}`}>
|
||||
<Link href={`/dashboard/invoices/${invoice.id}/edit`}>
|
||||
<Button variant="ghost" size="sm">
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -171,7 +171,7 @@ export function InvoiceList() {
|
||||
</CardTitle>
|
||||
<div className="flex items-center justify-between">
|
||||
<StatusBadge status={invoice.status as StatusType} />
|
||||
<span className="text-icon-green text-lg font-bold">
|
||||
<span className="text-primary text-lg font-bold">
|
||||
{formatCurrency(invoice.totalAmount)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -148,7 +148,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
return (
|
||||
<div className="py-12 text-center">
|
||||
<FileText className="text-muted mx-auto mb-4 h-12 w-12" />
|
||||
<h3 className="text-accent mb-2 text-lg font-medium">
|
||||
<h3 className="text-foreground mb-2 text-lg font-medium">
|
||||
Invoice not found
|
||||
</h3>
|
||||
<p className="text-muted mb-4">
|
||||
@@ -169,9 +169,9 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
<div className="space-y-6">
|
||||
{/* Status Alert */}
|
||||
{isOverdue && (
|
||||
<Card className="border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-900/20">
|
||||
<Card className="border-destructive/20 bg-destructive/10">
|
||||
<CardContent className="p-4">
|
||||
<div className="text-error flex items-center gap-2">
|
||||
<div className="text-destructive flex items-center gap-2">
|
||||
<AlertCircle className="h-5 w-5" />
|
||||
<span className="font-medium">This invoice is overdue</span>
|
||||
</div>
|
||||
@@ -183,13 +183,13 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
{/* Main Content */}
|
||||
<div className="space-y-6 lg:col-span-2">
|
||||
{/* Invoice Header Card */}
|
||||
<Card className="card-primary">
|
||||
<Card className="bg-card border-border border">
|
||||
<CardContent>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="rounded-lg bg-emerald-100 p-2 dark:bg-emerald-900/30">
|
||||
<FileText className="h-6 w-6 text-emerald-600 dark:text-emerald-400" />
|
||||
<div className="bg-primary/10 p-2">
|
||||
<FileText className="text-primary h-6 w-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
@@ -228,7 +228,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
>
|
||||
<StatusIcon className="mr-1 h-3 w-3" />
|
||||
</StatusBadge>
|
||||
<div className="text-3xl font-bold text-emerald-600 dark:text-emerald-400">
|
||||
<div className="text-primary text-3xl font-bold">
|
||||
{formatCurrency(invoice.totalAmount)}
|
||||
</div>
|
||||
<Button
|
||||
@@ -239,7 +239,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
>
|
||||
{isExportingPDF ? (
|
||||
<>
|
||||
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" />
|
||||
<div className="mr-2 h-4 w-4 animate-spin border-2 border-white border-t-transparent" />
|
||||
Generating PDF...
|
||||
</>
|
||||
) : (
|
||||
@@ -255,9 +255,9 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
</Card>
|
||||
|
||||
{/* Client Information */}
|
||||
<Card className="card-primary">
|
||||
<Card className="bg-card border-border border">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-emerald-700 dark:text-emerald-400">
|
||||
<CardTitle className="text-primary flex items-center gap-2">
|
||||
<User className="h-5 w-5" />
|
||||
Bill To
|
||||
</CardTitle>
|
||||
@@ -318,31 +318,31 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
</Card>
|
||||
|
||||
{/* Invoice Items */}
|
||||
<Card className="card-primary">
|
||||
<Card className="bg-card border-border border">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-emerald-700 dark:text-emerald-400">
|
||||
<CardTitle className="text-primary flex items-center gap-2">
|
||||
<Clock className="h-5 w-5" />
|
||||
Invoice Items
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<div className="border-border overflow-hidden border">
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-50 dark:bg-gray-700">
|
||||
<thead className="bg-muted">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||
<th className="text-muted-foreground px-4 py-3 text-left text-sm font-semibold">
|
||||
Date
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||
<th className="text-muted-foreground px-4 py-3 text-left text-sm font-semibold">
|
||||
Description
|
||||
</th>
|
||||
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||
<th className="text-muted-foreground px-4 py-3 text-right text-sm font-semibold">
|
||||
Hours
|
||||
</th>
|
||||
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||
<th className="text-muted-foreground px-4 py-3 text-right text-sm font-semibold">
|
||||
Rate
|
||||
</th>
|
||||
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||
<th className="text-muted-foreground px-4 py-3 text-right text-sm font-semibold">
|
||||
Amount
|
||||
</th>
|
||||
</tr>
|
||||
@@ -351,21 +351,21 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
{invoice.items?.map((item, index) => (
|
||||
<tr
|
||||
key={item.id || index}
|
||||
className="border-t border-gray-100 hover:bg-gray-50 dark:border-gray-600 dark:hover:bg-gray-700"
|
||||
className="border-border hover:bg-muted/50 border-t"
|
||||
>
|
||||
<td className="px-4 py-3 text-sm text-gray-900 dark:text-gray-300">
|
||||
<td className="text-foreground px-4 py-3 text-sm">
|
||||
{formatDate(item.date)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-900 dark:text-gray-300">
|
||||
<td className="text-foreground px-4 py-3 text-sm">
|
||||
{item.description}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-sm text-gray-900 dark:text-gray-300">
|
||||
<td className="text-foreground px-4 py-3 text-right text-sm">
|
||||
{item.hours}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-sm text-gray-900 dark:text-gray-300">
|
||||
<td className="text-foreground px-4 py-3 text-right text-sm">
|
||||
{formatCurrency(item.rate)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-sm font-medium text-gray-900 dark:text-gray-300">
|
||||
<td className="text-foreground px-4 py-3 text-right text-sm font-medium">
|
||||
{formatCurrency(item.amount)}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -378,11 +378,9 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
|
||||
{/* Notes */}
|
||||
{invoice.notes && (
|
||||
<Card className="card-primary">
|
||||
<Card className="bg-card border-border border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-emerald-700 dark:text-emerald-400">
|
||||
Notes
|
||||
</CardTitle>
|
||||
<CardTitle className="text-primary">Notes</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="whitespace-pre-wrap text-gray-700 dark:text-gray-300">
|
||||
@@ -396,18 +394,16 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
{/* Sidebar */}
|
||||
<div className="space-y-6">
|
||||
{/* Status Actions */}
|
||||
<Card className="card-primary">
|
||||
<Card className="bg-card border-border border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-emerald-700 dark:text-emerald-400">
|
||||
Status Actions
|
||||
</CardTitle>
|
||||
<CardTitle className="text-primary">Status Actions</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{invoice.status === "draft" && (
|
||||
<Button
|
||||
onClick={() => handleStatusUpdate("sent")}
|
||||
disabled={updateStatus.isPending}
|
||||
className="w-full bg-blue-600 text-white hover:bg-blue-700"
|
||||
className="bg-primary text-primary-foreground hover:bg-primary/90 w-full"
|
||||
>
|
||||
<Send className="mr-2 h-4 w-4" />
|
||||
Mark as Sent
|
||||
@@ -418,7 +414,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
<Button
|
||||
onClick={() => handleStatusUpdate("paid")}
|
||||
disabled={updateStatus.isPending}
|
||||
className="w-full bg-green-600 text-white hover:bg-green-700"
|
||||
className="bg-primary text-primary-foreground hover:bg-primary/90 w-full"
|
||||
>
|
||||
<DollarSign className="mr-2 h-4 w-4" />
|
||||
Mark as Paid
|
||||
@@ -429,7 +425,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
<Button
|
||||
onClick={() => handleStatusUpdate("paid")}
|
||||
disabled={updateStatus.isPending}
|
||||
className="w-full bg-green-600 text-white hover:bg-green-700"
|
||||
className="bg-primary text-primary-foreground hover:bg-primary/90 w-full"
|
||||
>
|
||||
<DollarSign className="mr-2 h-4 w-4" />
|
||||
Mark as Paid
|
||||
@@ -438,21 +434,17 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
|
||||
{invoice.status === "paid" && (
|
||||
<div className="py-4 text-center">
|
||||
<DollarSign className="mx-auto mb-2 h-8 w-8 text-green-600 dark:text-green-400" />
|
||||
<p className="font-medium text-green-600 dark:text-green-400">
|
||||
Invoice Paid
|
||||
</p>
|
||||
<DollarSign className="text-primary mx-auto mb-2 h-8 w-8" />
|
||||
<p className="text-primary font-medium">Invoice Paid</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Invoice Summary */}
|
||||
<Card className="card-primary">
|
||||
<Card className="bg-card border-border border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-emerald-700 dark:text-emerald-400">
|
||||
Summary
|
||||
</CardTitle>
|
||||
<CardTitle className="text-primary">Summary</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
@@ -471,14 +463,14 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
<Separator />
|
||||
<div className="flex justify-between text-lg font-bold">
|
||||
<span className="dark:text-white">Total</span>
|
||||
<span className="text-emerald-600 dark:text-emerald-400">
|
||||
<span className="text-primary">
|
||||
{formatCurrency(invoice.totalAmount)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-200 pt-4 text-center dark:border-gray-700">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
<div className="border-border border-t pt-4 text-center">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{invoice.items?.length ?? 0} item
|
||||
{invoice.items?.length !== 1 ? "s" : ""}
|
||||
</p>
|
||||
@@ -487,17 +479,15 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
</Card>
|
||||
|
||||
{/* Danger Zone */}
|
||||
<Card className="card-primary border-red-200 dark:border-red-800">
|
||||
<Card className="bg-card border-border border border-destructive/20">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-red-700 dark:text-red-400">
|
||||
Danger Zone
|
||||
</CardTitle>
|
||||
<CardTitle className="text-destructive">Danger Zone</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button
|
||||
onClick={handleDelete}
|
||||
variant="outline"
|
||||
className="w-full border-red-200 text-red-700 hover:bg-red-50 dark:border-red-800 dark:text-red-400 dark:hover:bg-red-900/20"
|
||||
className="border-destructive/20 text-destructive hover:bg-destructive/10 w-full"
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete Invoice
|
||||
@@ -509,7 +499,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<DialogContent className="card-primary">
|
||||
<DialogContent className="bg-card border-border border">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl font-bold text-gray-800 dark:text-white">
|
||||
Delete Invoice
|
||||
@@ -524,7 +514,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setDeleteDialogOpen(false)}
|
||||
className="border-gray-300 text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
className="border-border text-muted-foreground hover:bg-muted"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
@@ -532,7 +522,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
variant="destructive"
|
||||
onClick={confirmDelete}
|
||||
disabled={deleteInvoice.isPending}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
className="bg-destructive hover:bg-destructive/90"
|
||||
>
|
||||
{deleteInvoice.isPending ? "Deleting..." : "Delete Invoice"}
|
||||
</Button>
|
||||
|
||||
@@ -22,8 +22,8 @@ const variantStyles = {
|
||||
background: "bg-muted/50",
|
||||
},
|
||||
success: {
|
||||
icon: "text-status-success",
|
||||
background: "bg-status-success-muted",
|
||||
icon: "text-primary",
|
||||
background: "bg-primary/10",
|
||||
},
|
||||
warning: {
|
||||
icon: "text-status-warning",
|
||||
@@ -67,9 +67,7 @@ export function StatsCard({
|
||||
<span
|
||||
className={cn(
|
||||
"text-sm font-medium",
|
||||
trend.isPositive
|
||||
? "text-status-success"
|
||||
: "text-status-error",
|
||||
trend.isPositive ? "text-primary" : "text-destructive",
|
||||
)}
|
||||
>
|
||||
{trend.isPositive ? "+" : ""}
|
||||
@@ -82,7 +80,7 @@ export function StatsCard({
|
||||
)}
|
||||
</div>
|
||||
{Icon && (
|
||||
<div className={cn("rounded-full p-3", styles.background)}>
|
||||
<div className={cn(" p-3", styles.background)}>
|
||||
<Icon className={cn("h-6 w-6", styles.icon)} />
|
||||
</div>
|
||||
)}
|
||||
@@ -94,7 +92,7 @@ export function StatsCard({
|
||||
|
||||
export function StatsCardSkeleton() {
|
||||
return (
|
||||
<Card className="card-primary">
|
||||
<Card className="bg-card border-border border">
|
||||
<CardContent className="p-6">
|
||||
<div className="animate-pulse">
|
||||
<div className="bg-muted mb-2 h-4 w-1/2 rounded"></div>
|
||||
|
||||
@@ -19,19 +19,15 @@ interface StatusBadgeProps
|
||||
}
|
||||
|
||||
const statusClassMap: Record<StatusType, string> = {
|
||||
draft:
|
||||
"border-slate-400 bg-slate-100/90 text-slate-800 shadow-md dark:border-slate-600 dark:bg-slate-700/90 dark:text-slate-200",
|
||||
sent: "border-blue-400 bg-blue-100/90 text-blue-800 shadow-md dark:border-blue-600 dark:bg-blue-700/90 dark:text-blue-200",
|
||||
paid: "border-green-400 bg-green-100/90 text-green-800 shadow-md dark:border-green-600 dark:bg-green-700/90 dark:text-green-200",
|
||||
overdue:
|
||||
"border-red-400 bg-red-100/90 text-red-800 shadow-md dark:border-red-600 dark:bg-red-700/90 dark:text-red-200",
|
||||
success:
|
||||
"border-green-400 bg-green-100/90 text-green-800 shadow-md dark:border-green-600 dark:bg-green-700/90 dark:text-green-200",
|
||||
draft: "border-muted-foreground/40 bg-muted text-muted-foreground shadow-sm",
|
||||
sent: "border-primary/40 bg-primary/10 text-primary shadow-sm",
|
||||
paid: "border-primary/40 bg-primary/10 text-primary shadow-sm",
|
||||
overdue: "border-destructive/40 bg-destructive/10 text-destructive shadow-sm",
|
||||
success: "border-primary/40 bg-primary/10 text-primary shadow-sm",
|
||||
warning:
|
||||
"border-yellow-400 bg-yellow-100/90 text-yellow-800 shadow-md dark:border-yellow-600 dark:bg-yellow-700/90 dark:text-yellow-200",
|
||||
error:
|
||||
"border-red-400 bg-red-100/90 text-red-800 shadow-md dark:border-red-600 dark:bg-red-700/90 dark:text-red-200",
|
||||
info: "border-blue-400 bg-blue-100/90 text-blue-800 shadow-md dark:border-blue-600 dark:bg-blue-700/90 dark:text-blue-200",
|
||||
"border-muted-foreground/40 bg-muted text-muted-foreground shadow-sm",
|
||||
error: "border-destructive/40 bg-destructive/10 text-destructive shadow-sm",
|
||||
info: "border-primary/40 bg-primary/10 text-primary shadow-sm",
|
||||
};
|
||||
|
||||
const statusLabelMap: Record<StatusType, string> = {
|
||||
|
||||
@@ -414,7 +414,8 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
||||
type="submit"
|
||||
form="business-form"
|
||||
disabled={isSubmitting}
|
||||
className="btn-brand-primary shadow-md"
|
||||
variant="default"
|
||||
className="shadow-md"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
@@ -438,11 +439,11 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
||||
{/* Main Form Container - styled like data table */}
|
||||
<div className="space-y-4">
|
||||
{/* Basic Information */}
|
||||
<Card className="card-primary">
|
||||
<Card className="bg-card border-border border">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-brand-muted flex h-10 w-10 items-center justify-center rounded-lg">
|
||||
<Building className="text-brand-light h-5 w-5" />
|
||||
<div className="bg-muted flex h-10 w-10 items-center justify-center ">
|
||||
<Building className="text-muted-foreground h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>Basic Information</CardTitle>
|
||||
@@ -565,12 +566,12 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
||||
</Card>
|
||||
|
||||
{/* Address */}
|
||||
<Card className="card-primary">
|
||||
<Card className="bg-card border-border border">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-brand-muted flex h-10 w-10 items-center justify-center rounded-lg">
|
||||
<div className="bg-muted flex h-10 w-10 items-center justify-center ">
|
||||
<svg
|
||||
className="text-brand-light h-5 w-5"
|
||||
className="text-muted-foreground h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -613,11 +614,11 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
||||
</Card>
|
||||
|
||||
{/* Email Configuration */}
|
||||
<Card className="card-primary">
|
||||
<Card className="bg-card border-border border">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-brand-muted flex h-10 w-10 items-center justify-center rounded-lg">
|
||||
<Mail className="text-brand-light h-5 w-5" />
|
||||
<div className="bg-muted flex h-10 w-10 items-center justify-center ">
|
||||
<Mail className="text-muted-foreground h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>Email Configuration</CardTitle>
|
||||
@@ -631,7 +632,7 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
||||
<CardContent className="space-y-6">
|
||||
{/* Current Status */}
|
||||
{mode === "edit" && (
|
||||
<div className="flex items-center justify-between rounded-lg bg-gray-50 p-4">
|
||||
<div className="flex items-center justify-between bg-gray-50 p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium">
|
||||
Current Status:
|
||||
@@ -639,7 +640,7 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
||||
{emailConfig?.hasApiKey && emailConfig?.resendDomain ? (
|
||||
<Badge
|
||||
variant="default"
|
||||
className="bg-green-100 text-green-800"
|
||||
className="bg-primary/10 text-primary"
|
||||
>
|
||||
<Key className="mr-1 h-3 w-3" />
|
||||
Custom Configuration Active
|
||||
@@ -670,7 +671,7 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
||||
href="https://resend.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:underline"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
resend.com
|
||||
</a>
|
||||
@@ -709,7 +710,9 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
||||
? "Enter new API key to update"
|
||||
: "re_..."
|
||||
}
|
||||
className={errors.resendApiKey ? "border-red-500" : ""}
|
||||
className={
|
||||
errors.resendApiKey ? "border-destructive" : ""
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -726,7 +729,7 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
||||
</Button>
|
||||
</div>
|
||||
{errors.resendApiKey && (
|
||||
<p className="text-sm text-red-600">
|
||||
<p className="text-destructive text-sm">
|
||||
{errors.resendApiKey}
|
||||
</p>
|
||||
)}
|
||||
@@ -749,10 +752,12 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
||||
handleInputChange("resendDomain", e.target.value)
|
||||
}
|
||||
placeholder="yourdomain.com"
|
||||
className={errors.resendDomain ? "border-red-500" : ""}
|
||||
className={
|
||||
errors.resendDomain ? "border-destructive" : ""
|
||||
}
|
||||
/>
|
||||
{errors.resendDomain && (
|
||||
<p className="text-sm text-red-600">
|
||||
<p className="text-destructive text-sm">
|
||||
{errors.resendDomain}
|
||||
</p>
|
||||
)}
|
||||
@@ -779,10 +784,12 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
||||
handleInputChange("emailFromName", e.target.value)
|
||||
}
|
||||
placeholder={formData.name || "Your Business Name"}
|
||||
className={errors.emailFromName ? "border-red-500" : ""}
|
||||
className={
|
||||
errors.emailFromName ? "border-destructive" : ""
|
||||
}
|
||||
/>
|
||||
{errors.emailFromName && (
|
||||
<p className="text-sm text-red-600">
|
||||
<p className="text-destructive text-sm">
|
||||
{errors.emailFromName}
|
||||
</p>
|
||||
)}
|
||||
@@ -796,11 +803,11 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
||||
</Card>
|
||||
|
||||
{/* Settings */}
|
||||
<Card className="card-primary">
|
||||
<Card className="bg-card border-border border">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-brand-muted flex h-10 w-10 items-center justify-center rounded-lg">
|
||||
<Star className="text-brand-light h-5 w-5" />
|
||||
<div className="bg-muted flex h-10 w-10 items-center justify-center ">
|
||||
<Star className="text-muted-foreground h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>Settings</CardTitle>
|
||||
@@ -811,7 +818,7 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="bg-brand-muted border-border/40 flex items-center justify-between rounded-xl border p-4">
|
||||
<div className="bg-muted border-border/40 flex items-center justify-between border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<Label
|
||||
htmlFor="isDefault"
|
||||
@@ -841,8 +848,8 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
||||
<FloatingActionBar
|
||||
leftContent={
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="rounded-lg bg-blue-100 p-2 dark:bg-blue-900/30">
|
||||
<FileText className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
||||
<div className="bg-primary/10 p-2">
|
||||
<FileText className="text-primary h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 dark:text-gray-100">
|
||||
@@ -873,7 +880,8 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={isSubmitting || !isDirty}
|
||||
className="btn-brand-primary shadow-md"
|
||||
variant="default"
|
||||
className="shadow-md"
|
||||
size="sm"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
|
||||
@@ -262,7 +262,8 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
|
||||
type="submit"
|
||||
form="client-form"
|
||||
disabled={isSubmitting}
|
||||
className="btn-brand-primary shadow-md"
|
||||
variant="default"
|
||||
className="shadow-md"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
@@ -286,11 +287,11 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
|
||||
{/* Main Form Container - styled like data table */}
|
||||
<div className="space-y-4">
|
||||
{/* Basic Information */}
|
||||
<Card className="card-primary">
|
||||
<Card className="bg-card border-border border">
|
||||
<CardHeader>
|
||||
<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">
|
||||
<UserPlus className="h-5 w-5 text-emerald-700 dark:text-emerald-400" />
|
||||
<div className="bg-primary/10 flex h-10 w-10 items-center justify-center ">
|
||||
<UserPlus className="text-primary h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>Basic Information</CardTitle>
|
||||
@@ -367,12 +368,12 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
|
||||
</Card>
|
||||
|
||||
{/* Address */}
|
||||
<Card className="card-primary">
|
||||
<Card className="bg-card border-border border">
|
||||
<CardHeader>
|
||||
<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-primary/10 flex h-10 w-10 items-center justify-center ">
|
||||
<svg
|
||||
className="h-5 w-5 text-emerald-700 dark:text-emerald-400"
|
||||
className="text-primary h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -415,11 +416,11 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
|
||||
</Card>
|
||||
|
||||
{/* Billing Information */}
|
||||
<Card className="card-primary">
|
||||
<Card className="bg-card border-border border">
|
||||
<CardHeader>
|
||||
<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">
|
||||
<DollarSign className="h-5 w-5 text-emerald-700 dark:text-emerald-400" />
|
||||
<div className="bg-primary/10 flex h-10 w-10 items-center justify-center ">
|
||||
<DollarSign className="text-primary h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>Billing Information</CardTitle>
|
||||
@@ -463,8 +464,8 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
|
||||
<FloatingActionBar
|
||||
leftContent={
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="rounded-lg bg-blue-100 p-2 dark:bg-blue-900/30">
|
||||
<FileText className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
||||
<div className="bg-primary/10 p-2">
|
||||
<FileText className="text-primary h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 dark:text-gray-100">
|
||||
@@ -495,7 +496,7 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
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"
|
||||
variant="default"
|
||||
size="sm"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
|
||||
@@ -101,7 +101,7 @@ export function EmailComposer({
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class:
|
||||
"prose prose-sm sm:prose lg:prose-lg xl:prose-2xl mx-auto focus:outline-none min-h-[120px] p-4 border rounded-md bg-background",
|
||||
"prose prose-sm sm:prose lg:prose-lg xl:prose-2xl mx-auto focus:outline-none min-h-[120px] p-4 border bg-background",
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -133,9 +133,9 @@ export function EmailComposer({
|
||||
|
||||
if (!editor) {
|
||||
return (
|
||||
<div className="bg-muted flex h-[200px] items-center justify-center rounded-md border">
|
||||
<div className="bg-muted flex h-[200px] items-center justify-center border">
|
||||
<div className="text-center">
|
||||
<div className="border-primary mx-auto mb-2 h-4 w-4 animate-spin rounded-full border-2 border-t-transparent"></div>
|
||||
<div className="border-primary mx-auto mb-2 h-4 w-4 animate-spin border-2 border-t-transparent"></div>
|
||||
<p className="text-muted-foreground text-sm">Loading editor...</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -145,7 +145,7 @@ export function EmailComposer({
|
||||
return (
|
||||
<div className={className}>
|
||||
{/* Email Headers */}
|
||||
<div className="bg-muted/20 space-y-4 rounded-lg border p-4">
|
||||
<div className="bg-muted/20 space-y-4 border p-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="from-email" className="text-sm font-medium">
|
||||
@@ -231,7 +231,7 @@ export function EmailComposer({
|
||||
</div>
|
||||
|
||||
{/* Editor Toolbar */}
|
||||
<div className="bg-muted/20 flex flex-wrap items-center gap-1 rounded-lg border p-2">
|
||||
<div className="bg-muted/20 flex flex-wrap items-center gap-1 border p-2">
|
||||
<MenuButton
|
||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||
isActive={editor.isActive("bold")}
|
||||
|
||||
@@ -95,7 +95,7 @@ export function EmailPreview({
|
||||
return (
|
||||
<div className={className}>
|
||||
{/* Email Headers */}
|
||||
<div className="bg-muted/20 mb-4 space-y-3 rounded-lg p-4">
|
||||
<div className="bg-muted/20 mb-4 space-y-3 p-4">
|
||||
<div className="grid grid-cols-1 gap-3 text-sm md:grid-cols-3">
|
||||
<div>
|
||||
<span className="text-muted-foreground block text-xs font-medium">
|
||||
@@ -142,7 +142,7 @@ export function EmailPreview({
|
||||
|
||||
{/* Email Content */}
|
||||
{emailTemplate ? (
|
||||
<div className="rounded-lg border bg-gray-50 p-1 shadow-sm">
|
||||
<div className=" border bg-gray-50 p-1 shadow-sm">
|
||||
<iframe
|
||||
srcDoc={emailTemplate.html}
|
||||
className="h-[700px] w-full rounded border-0"
|
||||
|
||||
@@ -34,9 +34,9 @@ function FilePreview({
|
||||
const getStatusIcon = () => {
|
||||
switch (status) {
|
||||
case "success":
|
||||
return <CheckCircle className="h-4 w-4 text-green-600" />;
|
||||
return <CheckCircle className="text-primary h-4 w-4" />;
|
||||
case "error":
|
||||
return <AlertCircle className="h-4 w-4 text-red-600" />;
|
||||
return <AlertCircle className="text-destructive h-4 w-4" />;
|
||||
default:
|
||||
return <FileText className="h-4 w-4 text-gray-400" />;
|
||||
}
|
||||
@@ -45,9 +45,9 @@ function FilePreview({
|
||||
const getStatusColor = () => {
|
||||
switch (status) {
|
||||
case "success":
|
||||
return "border-green-200 bg-green-50";
|
||||
return "border-primary/20 bg-primary/10";
|
||||
case "error":
|
||||
return "border-red-200 bg-red-50";
|
||||
return "border-destructive/20 bg-destructive/10";
|
||||
default:
|
||||
return "border-gray-200 bg-gray-50";
|
||||
}
|
||||
@@ -56,20 +56,20 @@ function FilePreview({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-between rounded-lg border p-3",
|
||||
"flex items-center justify-between border p-3",
|
||||
getStatusColor(),
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
{getStatusIcon()}
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-medium text-gray-900">
|
||||
<p className="text-foreground truncate text-sm font-medium">
|
||||
{file.name}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{(file.size / 1024 / 1024).toFixed(2)} MB
|
||||
</p>
|
||||
{error && <p className="mt-1 text-xs text-red-600">{error}</p>}
|
||||
{error && <p className="text-destructive mt-1 text-xs">{error}</p>}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
@@ -152,28 +152,28 @@ export function FileUpload({
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className={cn(
|
||||
"cursor-pointer rounded-lg border-2 border-dashed p-8 text-center transition-colors",
|
||||
"hover:border-emerald-400 hover:bg-emerald-50/50",
|
||||
isDragActive && "border-emerald-400 bg-emerald-50/50",
|
||||
isDragReject && "border-red-400 bg-red-50/50",
|
||||
"cursor-pointer border-2 border-dashed p-8 text-center transition-colors",
|
||||
"hover:border-primary/40 hover:bg-primary/10",
|
||||
isDragActive && "border-primary/40 bg-primary/10",
|
||||
isDragReject && "border-destructive/40 bg-destructive/10",
|
||||
disabled && "cursor-not-allowed opacity-50",
|
||||
"bg-white/80 backdrop-blur-sm",
|
||||
"bg-background/80 backdrop-blur-sm",
|
||||
)}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-full p-3 transition-colors",
|
||||
isDragActive ? "bg-emerald-100" : "bg-gray-100",
|
||||
isDragReject && "bg-red-100",
|
||||
" p-3 transition-colors",
|
||||
isDragActive ? "bg-primary/10" : "bg-muted",
|
||||
isDragReject && "bg-destructive/10",
|
||||
)}
|
||||
>
|
||||
<Upload
|
||||
className={cn(
|
||||
"h-6 w-6 transition-colors",
|
||||
isDragActive ? "text-emerald-600" : "text-gray-400",
|
||||
isDragReject && "text-red-600",
|
||||
isDragActive ? "text-primary" : "text-muted-foreground",
|
||||
isDragReject && "text-destructive",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -181,8 +181,8 @@ export function FileUpload({
|
||||
<p
|
||||
className={cn(
|
||||
"text-lg font-medium transition-colors",
|
||||
isDragActive ? "text-emerald-600" : "text-gray-900",
|
||||
isDragReject && "text-red-600",
|
||||
isDragActive ? "text-primary" : "text-foreground",
|
||||
isDragReject && "text-destructive",
|
||||
)}
|
||||
>
|
||||
{isDragActive
|
||||
@@ -222,17 +222,17 @@ export function FileUpload({
|
||||
|
||||
{/* Error Summary */}
|
||||
{Object.keys(errors).length > 0 && (
|
||||
<div className="rounded-lg border border-red-200 bg-red-50 p-3">
|
||||
<div className="border-destructive/20 bg-destructive/10 border p-3">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<AlertCircle className="h-4 w-4 text-red-600" />
|
||||
<span className="text-sm font-medium text-red-800">
|
||||
<AlertCircle className="text-destructive h-4 w-4" />
|
||||
<span className="text-destructive text-sm font-medium">
|
||||
Upload Errors
|
||||
</span>
|
||||
</div>
|
||||
<ul className="space-y-1 text-sm text-red-700">
|
||||
<ul className="text-destructive space-y-1 text-sm">
|
||||
{Object.entries(errors).map(([fileName, error]) => (
|
||||
<li key={fileName} className="flex items-start gap-2">
|
||||
<span className="text-red-600">•</span>
|
||||
<span className="text-destructive">•</span>
|
||||
<span>
|
||||
<strong>{fileName}:</strong> {error}
|
||||
</span>
|
||||
|
||||
@@ -76,10 +76,10 @@ function InvoiceFormSkeleton() {
|
||||
/>
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||
<div className="space-y-6 lg:col-span-2">
|
||||
<div className="bg-muted h-96 animate-pulse rounded-lg" />
|
||||
<div className="bg-muted h-96 animate-pulse" />
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
<div className="bg-muted h-64 animate-pulse rounded-lg" />
|
||||
<div className="bg-muted h-64 animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -306,7 +306,7 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
onSuccess: (invoice) => {
|
||||
toast.success("Invoice created successfully");
|
||||
void utils.invoices.getAll.invalidate();
|
||||
router.push(`/dashboard/invoices/${invoice.id}/view`);
|
||||
router.push(`/dashboard/invoices/${invoice.id}`);
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message || "Failed to create invoice");
|
||||
@@ -319,7 +319,7 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
await utils.invoices.getAll.invalidate();
|
||||
// The update mutation returns { success: true }, so we use the current invoiceId
|
||||
if (invoiceId && invoiceId !== "new") {
|
||||
router.push(`/dashboard/invoices/${invoiceId}/view`);
|
||||
router.push(`/dashboard/invoices/${invoiceId}`);
|
||||
} else {
|
||||
router.push("/dashboard/invoices");
|
||||
}
|
||||
@@ -462,17 +462,13 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
variant="outline"
|
||||
onClick={handleDelete}
|
||||
disabled={loading || deleteInvoice.isPending}
|
||||
className="text-red-700 shadow-sm hover:bg-red-50"
|
||||
className="text-destructive hover:bg-destructive/10 shadow-sm"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 sm:mr-2" />
|
||||
<span className="hidden sm:inline">Delete Invoice</span>
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={loading}
|
||||
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"
|
||||
>
|
||||
<Button onClick={handleSubmit} disabled={loading} variant="default">
|
||||
{loading ? (
|
||||
<>
|
||||
<Clock className="h-4 w-4 animate-spin sm:mr-2" />
|
||||
@@ -499,7 +495,7 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="invoice-details">
|
||||
<Card className="card-primary">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<FileText className="h-5 w-5" />
|
||||
@@ -657,14 +653,14 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="invoice-items">
|
||||
<Card className="card-primary">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<DollarSign className="h-5 w-5" />
|
||||
Invoice Items
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="px-3 py-0">
|
||||
<CardContent className="px-3 pb-4">
|
||||
<InvoiceLineItems
|
||||
items={formData.items}
|
||||
onAddItem={addItem}
|
||||
@@ -681,7 +677,7 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<Card className="card-primary sticky top-6">
|
||||
<Card className="sticky top-6">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Check className="h-5 w-5" />
|
||||
@@ -747,8 +743,8 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
<FloatingActionBar
|
||||
leftContent={
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="rounded-lg bg-green-100 p-2 dark:bg-green-900/30">
|
||||
<FileText className="h-5 w-5 text-green-600 dark:text-green-400" />
|
||||
<div className="bg-primary/10 p-2">
|
||||
<FileText className="text-primary h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 dark:text-gray-100">
|
||||
@@ -769,7 +765,7 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
size="sm"
|
||||
onClick={handleDelete}
|
||||
disabled={loading || deleteInvoice.isPending}
|
||||
className="text-red-700 hover:bg-red-50"
|
||||
className="text-destructive hover:bg-destructive/10"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 sm:mr-2" />
|
||||
<span className="hidden sm:inline">Delete</span>
|
||||
@@ -778,7 +774,7 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={loading}
|
||||
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"
|
||||
variant="default"
|
||||
size="sm"
|
||||
>
|
||||
{loading ? (
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { DatePicker } from "~/components/ui/date-picker";
|
||||
import { Input } from "~/components/ui/input";
|
||||
@@ -114,11 +115,16 @@ function SortableLineItem({
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
<motion.div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
layout
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.2, ease: "easeOut" }}
|
||||
className={cn(
|
||||
"card-secondary hidden rounded-lg p-4 md:block",
|
||||
"bg-card border-border hidden border p-4 md:block",
|
||||
isDragging && "opacity-50",
|
||||
)}
|
||||
>
|
||||
@@ -227,7 +233,7 @@ function SortableLineItem({
|
||||
size="sm"
|
||||
onClick={() => onRemove(index)}
|
||||
className={cn(
|
||||
"text-muted-foreground h-8 w-8 p-0 transition-colors hover:text-red-500",
|
||||
"text-muted-foreground hover:text-destructive h-8 w-8 p-0 transition-colors",
|
||||
!canRemove && "cursor-not-allowed opacity-50",
|
||||
)}
|
||||
disabled={!canRemove}
|
||||
@@ -238,7 +244,7 @@ function SortableLineItem({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -254,8 +260,15 @@ function MobileLineItem({
|
||||
isLast,
|
||||
}: LineItemRowProps) {
|
||||
return (
|
||||
<div className="card-secondary space-y-3 rounded-lg md:hidden">
|
||||
<div className="space-y-3 p-4">
|
||||
<motion.div
|
||||
layout
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.2, ease: "easeOut" }}
|
||||
className="bg-card border-border space-y-3 border md:hidden"
|
||||
>
|
||||
<div className="bg-secondary space-y-3 p-4">
|
||||
{/* Description */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-muted-foreground text-xs">Description</Label>
|
||||
@@ -344,7 +357,7 @@ function MobileLineItem({
|
||||
size="sm"
|
||||
onClick={() => onRemove(index)}
|
||||
className={cn(
|
||||
"text-muted-foreground h-8 w-8 p-0 transition-colors hover:text-red-500",
|
||||
"text-muted-foreground hover:text-destructive h-8 w-8 p-0 transition-colors",
|
||||
!canRemove && "cursor-not-allowed opacity-50",
|
||||
)}
|
||||
disabled={!canRemove}
|
||||
@@ -367,7 +380,7 @@ function MobileLineItem({
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -414,51 +427,57 @@ export function InvoiceLineItems({
|
||||
items={items.map((item) => item.id)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
{items.map((item, index) => (
|
||||
<React.Fragment key={item.id}>
|
||||
{/* Desktop/Tablet Card with Drag and Drop */}
|
||||
<SortableLineItem
|
||||
item={item}
|
||||
index={index}
|
||||
canRemove={canRemoveItems}
|
||||
onRemove={onRemoveItem}
|
||||
onUpdate={onUpdateItem}
|
||||
onMoveUp={onMoveUp}
|
||||
onMoveDown={onMoveDown}
|
||||
isFirst={index === 0}
|
||||
isLast={index === items.length - 1}
|
||||
/>
|
||||
<AnimatePresence>
|
||||
<div className="space-y-2">
|
||||
{items.map((item, index) => (
|
||||
<React.Fragment key={item.id}>
|
||||
{/* Desktop/Tablet Card with Drag and Drop */}
|
||||
<SortableLineItem
|
||||
item={item}
|
||||
index={index}
|
||||
canRemove={canRemoveItems}
|
||||
onRemove={onRemoveItem}
|
||||
onUpdate={onUpdateItem}
|
||||
onMoveUp={onMoveUp}
|
||||
onMoveDown={onMoveDown}
|
||||
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>
|
||||
{/* 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>
|
||||
</AnimatePresence>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
|
||||
{/* Add Item Button */}
|
||||
<div className="px-3 pt-3">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={onAddItem}
|
||||
className="w-full"
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add Another Item
|
||||
</Button>
|
||||
<div className="border-t pt-6">
|
||||
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={onAddItem}
|
||||
className="w-full"
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add Line Item
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -242,7 +242,7 @@ export function SendEmailDialog({
|
||||
<DialogContent className="flex max-h-[90vh] max-w-4xl flex-col overflow-hidden">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Mail className="h-5 w-5 text-green-600" />
|
||||
<Mail className="text-primary h-5 w-5" />
|
||||
Send Invoice Email
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
@@ -419,7 +419,7 @@ export function SendEmailDialog({
|
||||
<Button
|
||||
onClick={handleSendEmail}
|
||||
disabled={!canSend || isSending}
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
className="bg-primary hover:bg-primary/90"
|
||||
>
|
||||
{isSending ? (
|
||||
<>
|
||||
|
||||
@@ -50,7 +50,7 @@ export function FloatingActionBar({
|
||||
// Base positioning - always at bottom
|
||||
"fixed right-0 left-0 z-50",
|
||||
// Safe area and sidebar adjustments
|
||||
"pb-safe-area-inset-bottom md:left-[276px]",
|
||||
"pb-safe-area-inset-bottom md:left-64",
|
||||
// Conditional centering based on dock state
|
||||
isDocked ? "flex justify-center" : "",
|
||||
// Dynamic bottom positioning
|
||||
@@ -65,7 +65,7 @@ export function FloatingActionBar({
|
||||
isDocked ? "mx-auto mb-0 px-4" : "mb-4 px-4",
|
||||
)}
|
||||
>
|
||||
<Card className="card-primary">
|
||||
<Card className="bg-card border-border border">
|
||||
<CardContent className="flex items-center justify-between p-4">
|
||||
{/* Left content */}
|
||||
{leftContent && (
|
||||
|
||||
@@ -15,60 +15,59 @@ export function Navbar() {
|
||||
// const { data: currentInvoice } = api.invoices.getCurrentOpen.useQuery();
|
||||
|
||||
return (
|
||||
<header className="fixed top-2 right-2 left-2 z-30 md:top-3 md:right-3 md:left-3">
|
||||
<div className="bg-background/60 border-border/40 relative rounded-2xl border shadow-lg backdrop-blur-xl backdrop-saturate-150">
|
||||
<div className="flex h-14 items-center justify-between px-4 md:h-16 md:px-8">
|
||||
<div className="flex items-center gap-4 md:gap-6">
|
||||
<SidebarTrigger
|
||||
isOpen={isMobileNavOpen}
|
||||
onToggle={() => setIsMobileNavOpen(!isMobileNavOpen)}
|
||||
/>
|
||||
<Link href="/dashboard" className="flex items-center gap-2">
|
||||
<Logo size="md" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 md:gap-4">
|
||||
{status === "loading" ? (
|
||||
<>
|
||||
<Skeleton className="bg-muted/20 hidden h-5 w-20 sm:inline" />
|
||||
<Skeleton className="bg-muted/20 h-8 w-16" />
|
||||
</>
|
||||
) : session?.user ? (
|
||||
<>
|
||||
<span className="text-muted-foreground hidden text-xs font-medium sm:inline md:text-sm">
|
||||
{session.user.name ?? session.user.email}
|
||||
</span>
|
||||
<header className="bg-navbar border-navbar-border text-navbar-foreground fixed top-0 right-0 left-0 z-30 border-b">
|
||||
<div className="flex h-14 items-center justify-between px-4 md:h-16 md:px-8">
|
||||
<div className="flex items-center gap-4 md:gap-6">
|
||||
<SidebarTrigger
|
||||
isOpen={isMobileNavOpen}
|
||||
onToggle={() => setIsMobileNavOpen(!isMobileNavOpen)}
|
||||
/>
|
||||
<Link href="/dashboard" className="flex items-center gap-2">
|
||||
<Logo size="md" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 md:gap-4">
|
||||
{status === "loading" ? (
|
||||
<>
|
||||
<Skeleton className="bg-muted/20 hidden h-5 w-20 sm:inline" />
|
||||
<Skeleton className="bg-muted/20 h-8 w-16" />
|
||||
</>
|
||||
) : session?.user ? (
|
||||
<>
|
||||
<span className="text-muted-foreground hidden text-xs font-medium sm:inline md:text-sm">
|
||||
{session.user.name ?? session.user.email}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => signOut({ callbackUrl: "/" })}
|
||||
className="text-xs md:text-sm"
|
||||
>
|
||||
Sign Out
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Link href="/auth/signin">
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => signOut({ callbackUrl: "/" })}
|
||||
className="border-border/40 hover:bg-accent/50 text-xs md:text-sm"
|
||||
className="text-xs md:text-sm"
|
||||
>
|
||||
Sign Out
|
||||
Sign In
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Link href="/auth/signin">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="hover:bg-accent/50 text-xs md:text-sm"
|
||||
>
|
||||
Sign In
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/auth/register">
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-gradient-to-r from-emerald-600 to-teal-600 text-xs font-medium text-white shadow-md transition-all duration-200 hover:from-emerald-700 hover:to-teal-700 hover:shadow-lg md:text-sm"
|
||||
>
|
||||
Register
|
||||
</Button>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
<Link href="/auth/register">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="default"
|
||||
className="text-xs font-medium md:text-sm"
|
||||
>
|
||||
Register
|
||||
</Button>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -132,7 +132,7 @@ export function EmptyState({
|
||||
return (
|
||||
<div className={cn("py-12 text-center", className)}>
|
||||
{icon && (
|
||||
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-muted/50">
|
||||
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center bg-muted/50">
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -35,10 +35,9 @@ const variantStyles = {
|
||||
hoverBackground: "group-hover:bg-status-warning-muted/70",
|
||||
},
|
||||
purple: {
|
||||
icon: "text-purple-600",
|
||||
background: "bg-purple-100 dark:bg-purple-900/30",
|
||||
hoverBackground:
|
||||
"group-hover:bg-purple-200 dark:group-hover:bg-purple-900/50",
|
||||
icon: "text-primary",
|
||||
background: "bg-secondary",
|
||||
hoverBackground: "group-hover:bg-secondary/80",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -57,7 +56,7 @@ export function QuickActionCard({
|
||||
<CardContent className="p-6 text-center">
|
||||
<div
|
||||
className={cn(
|
||||
"mx-auto mb-3 flex h-12 w-12 items-center justify-center rounded-full transition-colors",
|
||||
"mx-auto mb-3 flex h-12 w-12 items-center justify-center transition-colors",
|
||||
styles.background,
|
||||
styles.hoverBackground,
|
||||
)}
|
||||
@@ -99,10 +98,10 @@ export function QuickActionCard({
|
||||
|
||||
export function QuickActionCardSkeleton() {
|
||||
return (
|
||||
<Card className="card-primary">
|
||||
<Card className="bg-card border-border border">
|
||||
<CardContent className="p-6">
|
||||
<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 "></div>
|
||||
<div className="bg-muted mx-auto mb-2 h-4 w-2/3 rounded"></div>
|
||||
<div className="bg-muted mx-auto h-3 w-1/2 rounded"></div>
|
||||
</div>
|
||||
|
||||
@@ -2,23 +2,25 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useSession, signOut } from "next-auth/react";
|
||||
import { Skeleton } from "~/components/ui/skeleton";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { LogOut, User } from "lucide-react";
|
||||
import { navigationConfig } from "~/lib/navigation";
|
||||
|
||||
export function Sidebar() {
|
||||
const pathname = usePathname();
|
||||
const { status } = useSession();
|
||||
const { data: session, status } = useSession();
|
||||
|
||||
return (
|
||||
<aside className="border-border/40 bg-background/60 fixed top-[5.75rem] bottom-3 left-3 z-20 hidden w-64 flex-col justify-between rounded-2xl border p-6 shadow-lg backdrop-blur-xl backdrop-saturate-150 md:flex">
|
||||
<aside className="bg-sidebar border-sidebar-border text-sidebar-foreground fixed top-[4rem] bottom-0 left-0 z-20 hidden w-64 flex-col justify-between border-r p-6 md:flex">
|
||||
<nav className="flex flex-col">
|
||||
{navigationConfig.map((section, sectionIndex) => (
|
||||
<div key={section.title} className={sectionIndex > 0 ? "mt-6" : ""}>
|
||||
{sectionIndex > 0 && (
|
||||
<div className="border-border/40 my-4 border-t" />
|
||||
)}
|
||||
<div className="text-muted-foreground mb-3 text-xs font-semibold tracking-wider uppercase">
|
||||
<div className="text-sidebar-foreground/60 mb-3 text-xs font-semibold tracking-wider uppercase">
|
||||
{section.title}
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
@@ -27,10 +29,10 @@ export function Sidebar() {
|
||||
{Array.from({ length: section.links.length }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-center gap-3 rounded-lg px-3 py-2.5"
|
||||
className="flex items-center gap-3 px-3 py-2.5"
|
||||
>
|
||||
<Skeleton className="bg-muted/20 h-4 w-4" />
|
||||
<Skeleton className="bg-muted/20 h-4 w-20" />
|
||||
<Skeleton className="bg-sidebar-accent/20 h-4 w-4" />
|
||||
<Skeleton className="bg-sidebar-accent/20 h-4 w-20" />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
@@ -42,10 +44,10 @@ export function Sidebar() {
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
aria-current={pathname === link.href ? "page" : undefined}
|
||||
className={`flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-200 ${
|
||||
className={`flex items-center gap-3 px-3 py-2.5 text-sm font-medium transition-colors ${
|
||||
pathname === link.href
|
||||
? "bg-gradient-to-r from-emerald-600/10 to-teal-600/10 text-emerald-700 shadow-sm dark:from-emerald-500/20 dark:to-teal-500/20 dark:text-emerald-400"
|
||||
: "text-foreground hover:bg-accent/50 hover:text-accent-foreground"
|
||||
? "bg-sidebar-primary text-sidebar-primary-foreground"
|
||||
: "text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
||||
}`}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
@@ -58,6 +60,48 @@ export function Sidebar() {
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* User Section */}
|
||||
<div className="border-sidebar-border border-t pt-4">
|
||||
{status === "loading" ? (
|
||||
<div className="space-y-3">
|
||||
<Skeleton className="bg-sidebar-accent/20 h-8 w-full" />
|
||||
<Skeleton className="bg-sidebar-accent/20 h-8 w-full" />
|
||||
<div className="flex items-center gap-3 p-3">
|
||||
<Skeleton className="bg-sidebar-accent/20 h-8 w-8 rounded-full" />
|
||||
<div className="flex-1">
|
||||
<Skeleton className="bg-sidebar-accent/20 mb-1 h-4 w-24" />
|
||||
<Skeleton className="bg-sidebar-accent/20 h-3 w-32" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : session?.user ? (
|
||||
<div className="space-y-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => signOut()}
|
||||
className="text-sidebar-foreground/60 hover:text-sidebar-foreground hover:bg-sidebar-accent w-full justify-start px-3"
|
||||
>
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
Sign Out
|
||||
</Button>
|
||||
<div className="flex items-center gap-3 px-3 pt-2">
|
||||
<div className="bg-sidebar-accent flex h-8 w-8 items-center justify-center rounded-full">
|
||||
<User className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sidebar-foreground truncate text-sm font-medium">
|
||||
{session.user.name ?? "User"}
|
||||
</p>
|
||||
<p className="text-sidebar-foreground/60 truncate text-xs">
|
||||
{session.user.email}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import { cn } from "~/lib/utils";
|
||||
|
||||
const navigation = [
|
||||
{ name: "Dashboard", href: "/dashboard" },
|
||||
{ name: "Clients", href: "/clients" },
|
||||
{ name: "Invoices", href: "/invoices" },
|
||||
{ name: "Clients", href: "/dashboard/clients" },
|
||||
{ name: "Invoices", href: "/dashboard/invoices" },
|
||||
];
|
||||
|
||||
export function Navigation() {
|
||||
@@ -24,7 +24,7 @@ export function Navigation() {
|
||||
"transition-colors",
|
||||
pathname === item.href
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "hover:bg-muted"
|
||||
: "hover:bg-muted",
|
||||
)}
|
||||
>
|
||||
{item.name}
|
||||
@@ -33,4 +33,4 @@ export function Navigation() {
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,14 +24,14 @@ export function SidebarTrigger({ isOpen, onToggle }: SidebarTriggerProps) {
|
||||
size="icon"
|
||||
aria-label="Toggle navigation"
|
||||
onClick={onToggle}
|
||||
className="bg-card/80 h-8 w-8 shadow-lg backdrop-blur-sm md:hidden"
|
||||
className="h-8 w-8 md:hidden"
|
||||
>
|
||||
{isOpen ? <X className="h-4 w-4" /> : <MenuIcon className="h-4 w-4" />}
|
||||
</Button>
|
||||
|
||||
{/* Mobile dropdown navigation */}
|
||||
{isOpen && (
|
||||
<div className="bg-background/95 border-border/40 absolute top-full right-0 left-0 z-40 mt-2 rounded-2xl border shadow-2xl backdrop-blur-xl md:hidden">
|
||||
<div className="bg-background border-border absolute top-full right-0 left-0 z-40 mt-1 border-t">
|
||||
{/* Navigation content */}
|
||||
<nav className="flex flex-col p-4">
|
||||
{navigationConfig.map((section, sectionIndex) => (
|
||||
@@ -52,7 +52,7 @@ export function SidebarTrigger({ isOpen, onToggle }: SidebarTriggerProps) {
|
||||
(_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-center gap-3 rounded-lg px-3 py-2.5"
|
||||
className="flex items-center gap-3 px-3 py-2.5"
|
||||
>
|
||||
<Skeleton className="bg-muted/20 h-4 w-4" />
|
||||
<Skeleton className="bg-muted/20 h-4 w-20" />
|
||||
@@ -70,10 +70,10 @@ export function SidebarTrigger({ isOpen, onToggle }: SidebarTriggerProps) {
|
||||
aria-current={
|
||||
pathname === link.href ? "page" : undefined
|
||||
}
|
||||
className={`flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-200 ${
|
||||
className={`flex items-center gap-3 px-3 py-2.5 text-sm font-medium transition-colors ${
|
||||
pathname === link.href
|
||||
? "bg-gradient-to-r from-emerald-600/10 to-teal-600/10 text-emerald-700 shadow-sm dark:from-emerald-500/20 dark:to-teal-500/20 dark:text-emerald-400"
|
||||
: "text-foreground hover:bg-accent/50 hover:text-accent-foreground"
|
||||
? "bg-primary/10 text-primary"
|
||||
: "text-foreground hover:bg-muted"
|
||||
}`}
|
||||
onClick={onToggle}
|
||||
>
|
||||
|
||||
@@ -54,7 +54,7 @@ function AlertDialogContent({
|
||||
<AlertDialogPrimitive.Content
|
||||
data-slot="alert-dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { cn } from "~/lib/utils"
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
||||
"relative w-full border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
"inline-flex items-center justify-center border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
@@ -23,13 +23,11 @@ const badgeVariants = cva(
|
||||
info: "border-transparent bg-status-info [a&]:hover:opacity-90",
|
||||
// Outlined variants for status badges
|
||||
"outline-draft":
|
||||
"border-gray-400 text-gray-600 dark:border-gray-500 dark:text-gray-300 bg-transparent",
|
||||
"outline-sent":
|
||||
"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",
|
||||
"border-muted-foreground/40 text-muted-foreground bg-transparent",
|
||||
"outline-sent": "border-primary/40 text-primary bg-transparent",
|
||||
"outline-paid": "border-primary/40 text-primary bg-transparent",
|
||||
"outline-overdue":
|
||||
"border-red-400 text-red-600 dark:border-red-500 dark:text-red-300 bg-transparent",
|
||||
"border-destructive/40 text-destructive bg-transparent",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -5,28 +5,28 @@ import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors duration-150 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium transition-colors duration-150 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
brand:
|
||||
"bg-brand-gradient text-white shadow-lg hover:bg-brand-gradient hover:shadow-xl font-medium",
|
||||
"bg-primary text-primary-foreground shadow-lg hover:bg-primary/90 hover:shadow-xl font-medium",
|
||||
destructive:
|
||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border border-border/40 bg-background/60 shadow-sm backdrop-blur-sm hover:bg-accent/50 hover:text-accent-foreground hover:border-border/60 transition-colors duration-150",
|
||||
"border border-border/40 bg-background/60 shadow-sm backdrop-blur-sm hover:bg-accent/50 hover:text-foreground-foreground hover:border-border/60 transition-colors duration-150",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
"bg-secondary text-muted-foreground-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
"hover:bg-accent hover:text-foreground-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
sm: "h-8 gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -100,7 +100,7 @@ function Calendar({
|
||||
defaultClassNames.dropdowns,
|
||||
),
|
||||
dropdown_root: cn(
|
||||
"relative has-focus:border-ring border border-input shadow-sm has-focus:ring-ring/50 has-focus:ring-2 rounded-md h-8",
|
||||
"relative has-focus:border-ring border border-input shadow-sm has-focus:ring-ring/50 has-focus:ring-2 h-8",
|
||||
defaultClassNames.dropdown_root,
|
||||
),
|
||||
dropdown: cn(
|
||||
@@ -275,7 +275,7 @@ function CalendarDayButton({
|
||||
data-range-end={modifiers.range_end}
|
||||
data-range-middle={modifiers.range_middle}
|
||||
className={cn(
|
||||
"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",
|
||||
"hover:bg-accent hover:text-foreground-foreground flex aspect-square size-auto h-8 w-full min-w-8 items-center justify-center border-0 text-sm leading-none font-normal shadow-none",
|
||||
modifiers.selected && "bg-primary text-primary-foreground",
|
||||
modifiers.today && !modifiers.selected && "bg-accent font-semibold",
|
||||
className,
|
||||
|
||||
@@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"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",
|
||||
"bg-card text-card-foreground border-border/40 flex flex-col border shadow-lg backdrop-blur-xl backdrop-saturate-150",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -20,7 +20,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@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",
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-5 pt-4 pb-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -65,7 +65,7 @@ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-5 pb-3", className)}
|
||||
className={cn("px-5 pb-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -60,7 +60,7 @@ function DialogContent({
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -42,7 +42,7 @@ function DropdownMenuContent({
|
||||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border-0 shadow-md",
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto border-0 shadow-md",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -74,7 +74,7 @@ function DropdownMenuItem({
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"focus:bg-accent focus:text-foreground-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -92,7 +92,7 @@ function DropdownMenuCheckboxItem({
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"focus:bg-accent focus:text-foreground-foreground relative flex cursor-default items-center gap-2 py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
@@ -128,7 +128,7 @@ function DropdownMenuRadioItem({
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"focus:bg-accent focus:text-foreground-foreground relative flex cursor-default items-center gap-2 py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -211,7 +211,7 @@ function DropdownMenuSubTrigger({
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
|
||||
"focus:bg-accent focus:text-foreground-foreground data-[state=open]:bg-accent data-[state=open]:text-foreground-foreground flex cursor-default items-center px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -230,7 +230,7 @@ function DropdownMenuSubContent({
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border-0 shadow-lg",
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden border-0 shadow-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground bg-background/50 text-foreground border-border/40 flex h-10 w-full min-w-0 rounded-md border px-3 py-2 text-sm shadow-sm backdrop-blur-sm transition-all duration-200 outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground bg-background/50 text-foreground border-border/40 flex h-10 w-full min-w-0 border px-3 py-2 text-sm shadow-sm backdrop-blur-sm transition-all duration-200 outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"focus-visible:border-ring focus-visible:bg-background/80 focus-visible:ring-ring/20 focus-visible:ring-[3px]",
|
||||
"hover:border-border/60 hover:bg-background/60",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
|
||||
@@ -59,7 +59,7 @@ function NavigationMenuItem({
|
||||
}
|
||||
|
||||
const navigationMenuTriggerStyle = cva(
|
||||
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1"
|
||||
"group inline-flex h-9 w-max items-center justify-center bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-foreground-foreground focus:bg-accent focus:text-foreground-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-foreground-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1"
|
||||
)
|
||||
|
||||
function NavigationMenuTrigger({
|
||||
@@ -91,7 +91,7 @@ function NavigationMenuContent({
|
||||
data-slot="navigation-menu-content"
|
||||
className={cn(
|
||||
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
|
||||
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
|
||||
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu: group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -112,7 +112,7 @@ function NavigationMenuViewport({
|
||||
<NavigationMenuPrimitive.Viewport
|
||||
data-slot="navigation-menu-viewport"
|
||||
className={cn(
|
||||
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
|
||||
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -129,7 +129,7 @@ function NavigationMenuLink({
|
||||
<NavigationMenuPrimitive.Link
|
||||
data-slot="navigation-menu-link"
|
||||
className={cn(
|
||||
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
|
||||
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-foreground-foreground hover:bg-accent hover:text-foreground-foreground focus:bg-accent focus:text-foreground-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -76,7 +76,7 @@ export function NumberInput({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"bg-background flex h-9 items-center justify-center rounded-md text-sm shadow-none",
|
||||
"bg-background flex h-9 items-center justify-center text-sm shadow-none",
|
||||
widthClass,
|
||||
disabled && "cursor-not-allowed opacity-50",
|
||||
className,
|
||||
|
||||
@@ -30,7 +30,7 @@ function PopoverContent({
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) border p-4 shadow-md outline-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -14,7 +14,7 @@ function Progress({
|
||||
<ProgressPrimitive.Root
|
||||
data-slot="progress"
|
||||
className={cn(
|
||||
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
|
||||
"bg-primary/20 relative h-2 w-full overflow-hidden ",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -42,7 +42,7 @@ function SelectTrigger({
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"data-[placeholder]:text-muted-foreground border-input bg-background text-foreground focus-visible:border-ring focus-visible:ring-ring/50 flex h-10 w-full items-center justify-between gap-2 rounded-md border px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"data-[placeholder]:text-muted-foreground border-input bg-background text-foreground focus-visible:border-ring focus-visible:ring-ring/50 flex h-10 w-full items-center justify-between gap-2 border px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -66,7 +66,7 @@ function SelectContent({
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border-0 shadow-md",
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto border-0 shadow-md",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className,
|
||||
@@ -112,7 +112,7 @@ function SelectItem({
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
"focus:bg-accent focus:text-foreground-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -210,7 +210,7 @@ function SelectContentWithSearch({
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-96 min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-hidden rounded-md border-0 shadow-md",
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-96 min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-hidden border-0 shadow-md",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className,
|
||||
@@ -235,7 +235,7 @@ function SelectContentWithSearch({
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
className="placeholder:text-muted-foreground text-foreground flex h-8 w-full rounded-md border-0 bg-transparent py-2 text-sm outline-none focus:ring-0 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||
className="placeholder:text-muted-foreground text-foreground flex h-8 w-full border-0 bg-transparent py-2 text-sm outline-none focus:ring-0 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||
placeholder={searchPlaceholder}
|
||||
value={searchValue}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
|
||||
@@ -6,7 +6,7 @@ function Skeleton({
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn("bg-muted animate-pulse rounded-md", className)}
|
||||
className={cn("bg-muted animate-pulse ", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -19,10 +19,10 @@ export function DashboardStatsSkeleton() {
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="rounded-lg border border-gray-100 bg-white p-6 shadow-sm"
|
||||
className=" border border-gray-100 bg-white p-6 shadow-sm"
|
||||
>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<Skeleton className="h-9 w-9 rounded-lg" />
|
||||
<Skeleton className="h-9 w-9 " />
|
||||
<Skeleton className="h-4 w-12" />
|
||||
</div>
|
||||
<div>
|
||||
@@ -41,7 +41,7 @@ export function DashboardCardsSkeleton() {
|
||||
{Array.from({ length: 2 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="rounded-lg border border-gray-100 bg-white p-6 shadow-sm"
|
||||
className=" border border-gray-100 bg-white p-6 shadow-sm"
|
||||
>
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -69,7 +69,7 @@ export function DashboardCardsSkeleton() {
|
||||
|
||||
export function DashboardActivitySkeleton() {
|
||||
return (
|
||||
<div className="rounded-lg border border-gray-100 bg-white p-6 shadow-sm">
|
||||
<div className=" border border-gray-100 bg-white p-6 shadow-sm">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="h-5 w-5 rounded" />
|
||||
@@ -81,17 +81,17 @@ export function DashboardActivitySkeleton() {
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-center justify-between rounded-lg border border-gray-100 p-4"
|
||||
className="flex items-center justify-between border border-gray-100 p-4"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Skeleton className="h-8 w-8 rounded-lg" />
|
||||
<Skeleton className="h-8 w-8 " />
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-3 w-32" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Skeleton className="h-6 w-16 rounded-full" />
|
||||
<Skeleton className="h-6 w-16 " />
|
||||
<Skeleton className="h-4 w-16" />
|
||||
<Skeleton className="h-8 w-8 rounded" />
|
||||
</div>
|
||||
@@ -115,14 +115,14 @@ export function DashboardHeroSkeleton() {
|
||||
|
||||
export function QuickActionsSkeleton() {
|
||||
return (
|
||||
<div className="rounded-lg border border-gray-100 bg-white p-6 shadow-sm">
|
||||
<div className=" border border-gray-100 bg-white p-6 shadow-sm">
|
||||
<div className="mb-4 flex items-center gap-2">
|
||||
<Skeleton className="h-5 w-5 rounded" />
|
||||
<Skeleton className="h-6 w-32" />
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<div key={i} className="rounded-lg border border-gray-200 p-4">
|
||||
<div key={i} className=" border border-gray-200 p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Skeleton className="h-5 w-5" />
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -13,7 +13,7 @@ function Switch({
|
||||
<SwitchPrimitive.Root
|
||||
data-slot="switch"
|
||||
className={cn(
|
||||
"peer data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 inline-flex h-5 w-9 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-emerald-600",
|
||||
"peer data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 data-[state=checked]:bg-primary inline-flex h-5 w-9 shrink-0 items-center border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -21,7 +21,7 @@ function Switch({
|
||||
<SwitchPrimitive.Thumb
|
||||
data-slot="switch-thumb"
|
||||
className={cn(
|
||||
"bg-background pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=checked]:bg-white data-[state=unchecked]:translate-x-0",
|
||||
"bg-background pointer-events-none block size-4 ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=checked]:bg-white data-[state=unchecked]:translate-x-0",
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitive.Root>
|
||||
|
||||
@@ -26,7 +26,7 @@ function TabsList({
|
||||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
className={cn(
|
||||
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
||||
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center p-[3px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -42,7 +42,7 @@ function TabsTrigger({
|
||||
<TabsPrimitive.Trigger
|
||||
data-slot="tabs-trigger"
|
||||
className={cn(
|
||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive bg-background text-foreground flex field-sizing-content min-h-16 w-full resize-y rounded-md border px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive bg-background text-foreground flex field-sizing-content min-h-16 w-full resize-y border px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
// Copied from shadcn/ui documentation
|
||||
|
||||
import * as React from "react"
|
||||
import { Toaster as Sonner } from "sonner"
|
||||
|
||||
export function Toaster() {
|
||||
return <Sonner richColors position="top-center" />
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
export { generateInvoiceEmailTemplate } from "./invoice-email";
|
||||
export { generatePasswordResetEmailTemplate } from "./password-reset-email";
|
||||
|
||||
@@ -81,17 +81,17 @@ export const statusConfig = {
|
||||
},
|
||||
sent: {
|
||||
label: "Sent",
|
||||
color: "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300",
|
||||
color: "bg-primary/10 text-primary",
|
||||
description: "Invoice sent to client",
|
||||
},
|
||||
paid: {
|
||||
label: "Paid",
|
||||
color: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300",
|
||||
color: "bg-primary/10 text-primary",
|
||||
description: "Payment received",
|
||||
},
|
||||
overdue: {
|
||||
label: "Overdue",
|
||||
color: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300",
|
||||
color: "bg-destructive/10 text-destructive",
|
||||
description: "Payment is overdue",
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -22,6 +22,8 @@ export const users = createTable("user", (d) => ({
|
||||
password: d.varchar({ length: 255 }),
|
||||
emailVerified: d.timestamp().default(sql`CURRENT_TIMESTAMP`),
|
||||
image: d.varchar({ length: 255 }),
|
||||
resetToken: d.varchar({ length: 255 }),
|
||||
resetTokenExpiry: d.timestamp(),
|
||||
}));
|
||||
|
||||
export const usersRelations = relations(users, ({ many }) => ({
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user