Refactor clients section to use client components

The commit makes several updates to the client-related pages in the
dashboard:

- Convert client edit/new pages to client components
- Remove server-side rendering wrappers
- Update client detail page styling and layout
- Add back button to client detail page
- Fix ID param handling in edit page
- Adjust visual styles and spacing

I focused on capturing the key changes while staying within the 50
character limit for the subject line and using the imperative mood. The
subject line alone adequately describes the core change without needing
a message body.
This commit is contained in:
2025-07-16 14:19:50 -04:00
parent 4976c13f32
commit b5784061eb
11 changed files with 514 additions and 465 deletions

View File

@@ -259,4 +259,4 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
--- ---
Built with ❤️ for freelancers and small businesses who deserve better invoicing tools. Built for freelancers and small businesses who deserve better invoicing tools.

View File

@@ -1,25 +1,12 @@
import Link from "next/link"; "use client";
import { HydrateClient } from "~/trpc/server";
import { useParams } from "next/navigation";
import { ClientForm } from "~/components/forms/client-form"; import { ClientForm } from "~/components/forms/client-form";
import { PageHeader } from "~/components/layout/page-header";
interface EditClientPageProps { export default function EditClientPage() {
params: Promise<{ id: string }>; const params = useParams();
} const clientId = Array.isArray(params?.id) ? params.id[0] : params?.id;
if (!clientId) return null;
export default async function EditClientPage({ params }: EditClientPageProps) {
const { id } = await params; return <ClientForm clientId={clientId} mode="edit" />;
return (
<div>
<PageHeader
title="Edit Client"
description="Update client information below."
variant="gradient"
/>
<HydrateClient>
<ClientForm mode="edit" clientId={id} />
</HydrateClient>
</div>
);
} }

View File

@@ -13,6 +13,7 @@ import {
Building, Building,
Calendar, Calendar,
DollarSign, DollarSign,
ArrowLeft,
} from "lucide-react"; } from "lucide-react";
interface ClientDetailPageProps { interface ClientDetailPageProps {
@@ -54,177 +55,206 @@ export default async function ClientDetailPage({
client.invoices?.filter((invoice) => invoice.status === "sent").length || 0; client.invoices?.filter((invoice) => invoice.status === "sent").length || 0;
return ( return (
<div> <div className="space-y-6 pb-32">
<div className="mx-auto max-w-4xl space-y-6"> <PageHeader
<PageHeader title={client.name}
title={client.name} description="View client details and information"
description="Client Details" variant="gradient"
variant="gradient" >
> <Button asChild variant="outline" className="shadow-sm">
<Link href={`/dashboard/clients/${client.id}/edit`}> <Link href="/dashboard/clients">
<Button variant="brand"> <ArrowLeft className="mr-2 h-4 w-4" />
<Edit className="mr-2 h-4 w-4" /> <span>Back to Clients</span>
Edit Client
</Button>
</Link> </Link>
</PageHeader> </Button>
<Button asChild className="btn-brand-primary shadow-md">
<Link href={`/dashboard/clients/${client.id}/edit`}>
<Edit className="mr-2 h-4 w-4" />
<span>Edit Client</span>
</Link>
</Button>
</PageHeader>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3"> <div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
{/* Client Information Card */} {/* Client Information Card */}
<div className="lg:col-span-2"> <div className="lg:col-span-2">
<Card className="card-primary"> <Card className="card-primary">
<CardHeader> <CardHeader>
<CardTitle className="client-section-title"> <CardTitle className="flex items-center gap-2">
<Building className="h-5 w-5" /> <div className="bg-blue-subtle rounded-lg p-2">
<span>Contact Information</span> <Building className="text-icon-blue h-5 w-5" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Basic Info */}
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
{client.email && (
<div className="client-info-item">
<div className="client-info-icon">
<Mail className="client-info-icon-emerald" />
</div>
<div>
<p className="client-info-label">Email</p>
<p className="client-info-value">{client.email}</p>
</div>
</div>
)}
{client.phone && (
<div className="client-info-item">
<div className="client-info-icon">
<Phone className="client-info-icon-emerald" />
</div>
<div>
<p className="client-info-label">Phone</p>
<p className="client-info-value">{client.phone}</p>
</div>
</div>
)}
</div> </div>
<span>Contact Information</span>
{/* Address */} </CardTitle>
{(client.addressLine1 ?? client.city ?? client.state) && ( </CardHeader>
<div className="space-y-4"> <CardContent className="space-y-6">
<div className="client-info-item"> {/* Basic Info */}
<div className="client-info-icon"> <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<MapPin className="client-info-icon-emerald" /> {client.email && (
</div> <div className="flex items-center space-x-3">
<div> <div className="bg-green-subtle rounded-lg p-2">
<p className="client-info-label">Address</p> <Mail className="text-icon-green h-4 w-4" />
</div>
</div> </div>
<div className="client-address-content"> <div>
{client.addressLine1 && <p>{client.addressLine1}</p>} <p className="text-muted-foreground text-sm font-medium">
{client.addressLine2 && <p>{client.addressLine2}</p>} Email
</p>
<p className="text-foreground text-sm">{client.email}</p>
</div>
</div>
)}
{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>
<div>
<p className="text-muted-foreground text-sm font-medium">
Phone
</p>
<p className="text-foreground text-sm">{client.phone}</p>
</div>
</div>
)}
</div>
{/* Address */}
{(client.addressLine1 ?? client.city ?? client.state) && (
<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>
<div className="space-y-1 text-sm">
{client.addressLine1 && (
<p className="text-foreground">{client.addressLine1}</p>
)}
{client.addressLine2 && (
<p className="text-foreground">{client.addressLine2}</p>
)}
{(client.city ?? client.state ?? client.postalCode) && ( {(client.city ?? client.state ?? client.postalCode) && (
<p> <p className="text-foreground">
{[client.city, client.state, client.postalCode] {[client.city, client.state, client.postalCode]
.filter(Boolean) .filter(Boolean)
.join(", ")} .join(", ")}
</p> </p>
)} )}
{client.country && <p>{client.country}</p>} {client.country && (
<p className="text-foreground">{client.country}</p>
)}
</div> </div>
</div> </div>
)} </div>
)}
{/* Client Since */} {/* Client Since */}
<div className="client-info-item"> <div>
<div className="client-info-icon"> <h3 className="mb-4 text-lg font-semibold">Client Details</h3>
<Calendar className="client-info-icon-emerald" /> <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> </div>
<div> <div>
<p className="client-info-label">Client Since</p> <p className="text-muted-foreground text-sm font-medium">
<p className="client-info-value"> Client Since
</p>
<p className="text-foreground text-sm">
{formatDate(client.createdAt)} {formatDate(client.createdAt)}
</p> </p>
</div> </div>
</div> </div>
</CardContent> </div>
</Card> </CardContent>
</div> </Card>
</div>
{/* Stats Card */} {/* Stats Card */}
<div className="space-y-6"> <div className="space-y-6">
<Card className="card-primary">
<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>
<span>Invoice Summary</span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="text-center">
<p className="text-primary text-3xl font-bold">
{formatCurrency(totalInvoiced)}
</p>
<p className="text-muted-foreground text-sm">Total Invoiced</p>
</div>
<div className="grid grid-cols-2 gap-4 text-center">
<div>
<p className="text-foreground text-xl font-semibold">
{paidInvoices}
</p>
<p className="text-muted-foreground text-sm">Paid</p>
</div>
<div>
<p className="text-foreground text-xl font-semibold">
{pendingInvoices}
</p>
<p className="text-muted-foreground text-sm">Pending</p>
</div>
</div>
</CardContent>
</Card>
{/* Recent Invoices */}
{client.invoices && client.invoices.length > 0 && (
<Card className="card-primary"> <Card className="card-primary">
<CardHeader> <CardHeader>
<CardTitle className="client-stats-title"> <CardTitle className="flex items-center gap-2">
<DollarSign className="h-5 w-5" /> <div className="bg-blue-subtle rounded-lg p-2">
<span>Invoice Summary</span> <DollarSign className="text-icon-blue h-5 w-5" />
</div>
<span>Recent Invoices</span>
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent>
<div className="text-center"> <div className="space-y-3">
<p className="client-total-amount"> {client.invoices.slice(0, 3).map((invoice) => (
{formatCurrency(totalInvoiced)} <div
</p> key={invoice.id}
<p className="client-total-label">Total Invoiced</p> className="flex items-center justify-between rounded-lg border p-3"
</div> >
<div>
<div className="client-stats-grid"> <p className="text-foreground font-medium">
<div className="text-center"> {invoice.invoiceNumber}
<p className="client-stat-value-paid">{paidInvoices}</p> </p>
<p className="client-stat-label">Paid</p> <p className="text-muted-foreground text-sm">
</div> {formatDate(invoice.issueDate)}
<div className="text-center"> </p>
<p className="client-stat-value-pending"> </div>
{pendingInvoices} <div className="text-right">
</p> <p className="text-foreground font-semibold">
<p className="client-stat-label">Pending</p> {formatCurrency(invoice.totalAmount)}
</div> </p>
<Badge
variant={
invoice.status === "paid"
? "default"
: invoice.status === "sent"
? "secondary"
: "outline"
}
className="text-xs"
>
{invoice.status}
</Badge>
</div>
</div>
))}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
)}
{/* Recent Invoices */}
{client.invoices && client.invoices.length > 0 && (
<Card className="card-primary">
<CardHeader>
<CardTitle className="text-lg dark:text-white">
Recent Invoices
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{client.invoices.slice(0, 3).map((invoice) => (
<div key={invoice.id} className="invoice-item">
<div>
<p className="invoice-item-title">
{invoice.invoiceNumber}
</p>
<p className="invoice-item-date">
{formatDate(invoice.issueDate)}
</p>
</div>
<div className="text-right">
<p className="invoice-item-amount">
{formatCurrency(invoice.totalAmount)}
</p>
<Badge
variant={
invoice.status === "paid"
? "default"
: invoice.status === "sent"
? "secondary"
: "outline"
}
className="text-xs"
>
{invoice.status}
</Badge>
</div>
</div>
))}
</div>
</CardContent>
</Card>
)}
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,19 +1,7 @@
import Link from "next/link"; "use client";
import { HydrateClient } from "~/trpc/server";
import { ClientForm } from "~/components/forms/client-form";
import { PageHeader } from "~/components/layout/page-header";
export default async function NewClientPage() { import { ClientForm } from "~/components/forms/client-form";
return (
<div> export default function NewClientPage() {
<PageHeader return <ClientForm mode="create" />;
title="Add Client"
description="Enter client details below to add a new client."
variant="gradient"
/>
<HydrateClient>
<ClientForm mode="create" />
</HydrateClient>
</div>
);
} }

View File

@@ -14,7 +14,7 @@ export default async function ClientsPage() {
description="Manage your clients and their information." description="Manage your clients and their information."
variant="gradient" variant="gradient"
> >
<Button asChild variant="brand"> <Button asChild className="btn-brand-primary shadow-md">
<Link href="/dashboard/clients/new"> <Link href="/dashboard/clients/new">
<Plus className="mr-2 h-5 w-5" /> <Plus className="mr-2 h-5 w-5" />
<span>Add Client</span> <span>Add Client</span>

View File

@@ -24,11 +24,11 @@ import {
// Modern gradient background component // Modern gradient background component
function DashboardHero({ firstName }: { firstName: string }) { function DashboardHero({ firstName }: { firstName: string }) {
return ( return (
<div className="relative mb-8 overflow-hidden rounded-3xl bg-gradient-to-br from-green-500 via-green-600 to-green-700 p-8 text-white"> <div className="relative mb-8 overflow-hidden rounded-3xl bg-gradient-to-br from-teal-600 via-blue-600 to-blue-700 p-8 text-white">
<div className="absolute inset-0 bg-black/10" /> <div className="absolute inset-0 bg-black/10" />
<div className="relative z-10"> <div className="relative z-10">
<h1 className="mb-2 text-3xl font-bold">Welcome back, {firstName}!</h1> <h1 className="mb-2 text-3xl font-bold">Welcome back, {firstName}!</h1>
<p className="text-lg text-green-100"> <p className="text-lg text-blue-100">
Ready to manage your invoicing business Ready to manage your invoicing business
</p> </p>
</div> </div>
@@ -94,7 +94,7 @@ async function DashboardStats() {
]; ];
return ( return (
<div className="mb-8 grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4"> <div className="mb-8 grid grid-cols-2 gap-3 sm:gap-6 lg:grid-cols-4">
{stats.map((stat) => { {stats.map((stat) => {
const Icon = stat.icon; const Icon = stat.icon;
return ( return (
@@ -102,17 +102,17 @@ async function DashboardStats() {
key={stat.title} key={stat.title}
className="border-0 shadow-sm transition-shadow hover:shadow-md" className="border-0 shadow-sm transition-shadow hover:shadow-md"
> >
<CardContent className="p-4 lg:p-6"> <CardContent className="p-3 sm:p-4 lg:p-6">
<div className="mb-3 flex items-center justify-between lg:mb-4"> <div className="mb-2 flex items-center justify-between sm:mb-3 lg:mb-4">
<div className={`rounded-lg p-2 ${stat.bgColor}`}> <div className={`rounded-lg p-1.5 sm:p-2 ${stat.bgColor}`}>
<Icon className="h-4 w-4 text-gray-700 lg:h-5 lg:w-5 dark:text-gray-800" /> <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" />
</div> </div>
<span className="text-xs font-medium text-green-600 lg:text-sm dark:text-green-400"> <span className="text-xs font-medium text-teal-600 dark:text-teal-400">
{stat.change} {stat.change}
</span> </span>
</div> </div>
<div> <div>
<p className="mb-1 text-xl font-bold text-gray-900 lg:text-2xl dark:text-gray-100"> <p className="mb-1 text-base font-bold text-gray-900 sm:text-xl lg:text-2xl dark:text-gray-100">
{stat.value} {stat.value}
</p> </p>
<p className="text-xs text-gray-600 lg:text-sm dark:text-gray-300"> <p className="text-xs text-gray-600 lg:text-sm dark:text-gray-300">
@@ -157,7 +157,7 @@ function QuickActions() {
<Card className="border-0 shadow-sm"> <Card className="border-0 shadow-sm">
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-lg"> <CardTitle className="flex items-center gap-2 text-lg">
<Plus className="h-5 w-5 text-green-600 dark:text-green-400" /> <Plus className="h-5 w-5 text-teal-600 dark:text-teal-400" />
Quick Actions Quick Actions
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@@ -171,7 +171,7 @@ function QuickActions() {
variant={action.primary ? "default" : "outline"} variant={action.primary ? "default" : "outline"}
className={`h-12 w-full justify-start px-3 ${ className={`h-12 w-full justify-start px-3 ${
action.primary action.primary
? "bg-green-600 text-white hover:bg-green-700" ? "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" : "border-gray-200 hover:bg-gray-50 dark:border-gray-600 dark:hover:bg-gray-800"
}`} }`}
> >
@@ -218,7 +218,7 @@ async function CurrentWork() {
<p className="mb-4 text-gray-600 dark:text-gray-300"> <p className="mb-4 text-gray-600 dark:text-gray-300">
No draft invoices found No draft invoices found
</p> </p>
<Button asChild className="bg-green-600 hover:bg-green-700"> <Button asChild className="bg-teal-600 hover:bg-teal-700">
<Link href="/dashboard/invoices/new"> <Link href="/dashboard/invoices/new">
<Plus className="mr-2 h-4 w-4" /> <Plus className="mr-2 h-4 w-4" />
Create Invoice Create Invoice
@@ -254,7 +254,7 @@ async function CurrentWork() {
</p> </p>
</div> </div>
<div className="text-right"> <div className="text-right">
<p className="text-2xl font-bold text-green-600 dark:text-green-400"> <p className="text-2xl font-bold text-teal-600 dark:text-teal-400">
${currentInvoice.totalAmount.toFixed(2)} ${currentInvoice.totalAmount.toFixed(2)}
</p> </p>
<p className="text-sm text-gray-600 dark:text-gray-300"> <p className="text-sm text-gray-600 dark:text-gray-300">
@@ -273,7 +273,7 @@ async function CurrentWork() {
<Button <Button
asChild asChild
size="sm" size="sm"
className="flex-1 bg-green-600 hover:bg-green-700" className="flex-1 bg-teal-600 hover:bg-teal-700"
> >
<Link href={`/dashboard/invoices/${currentInvoice.id}/edit`}> <Link href={`/dashboard/invoices/${currentInvoice.id}/edit`}>
<Edit className="mr-2 h-3 w-3" /> <Edit className="mr-2 h-3 w-3" />
@@ -331,7 +331,7 @@ async function RecentActivity() {
<p className="mb-4 text-gray-600 dark:text-gray-300"> <p className="mb-4 text-gray-600 dark:text-gray-300">
No invoices yet No invoices yet
</p> </p>
<Button asChild className="bg-green-600 hover:bg-green-700"> <Button asChild className="bg-teal-600 hover:bg-teal-700">
<Link href="/dashboard/invoices/new"> <Link href="/dashboard/invoices/new">
<Plus className="mr-2 h-4 w-4" /> <Plus className="mr-2 h-4 w-4" />
Create Your First Invoice Create Your First Invoice
@@ -348,12 +348,12 @@ async function RecentActivity() {
> >
<Card className="card-secondary transition-colors hover:bg-gray-200/70 dark:hover:bg-gray-700/60"> <Card className="card-secondary transition-colors hover:bg-gray-200/70 dark:hover:bg-gray-700/60">
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex items-center justify-between"> <div className="space-y-3">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="rounded-lg bg-gray-100 p-2 dark:bg-gray-700"> <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" /> <FileText className="h-4 w-4 text-gray-600 dark:text-gray-300" />
</div> </div>
<div> <div className="min-w-0 flex-1">
<p className="font-medium text-gray-900 dark:text-gray-100"> <p className="font-medium text-gray-900 dark:text-gray-100">
#{invoice.invoiceNumber} #{invoice.invoiceNumber}
</p> </p>
@@ -362,8 +362,11 @@ async function RecentActivity() {
{new Date(invoice.issueDate).toLocaleDateString()} {new Date(invoice.issueDate).toLocaleDateString()}
</p> </p>
</div> </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>
<div className="flex items-center gap-3"> <div className="flex items-center justify-between">
<Badge <Badge
className={`border ${getStatusColor(invoice.status)}`} className={`border ${getStatusColor(invoice.status)}`}
> >
@@ -372,9 +375,6 @@ async function RecentActivity() {
<p className="font-semibold text-gray-900 dark:text-gray-100"> <p className="font-semibold text-gray-900 dark:text-gray-100">
${invoice.totalAmount.toFixed(2)} ${invoice.totalAmount.toFixed(2)}
</p> </p>
<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>
</div> </div>
</CardContent> </CardContent>
@@ -391,16 +391,16 @@ async function RecentActivity() {
// Loading skeletons // Loading skeletons
function StatsSkeleton() { function StatsSkeleton() {
return ( return (
<div className="mb-8 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4"> <div className="mb-8 grid grid-cols-2 gap-3 sm:gap-6 lg:grid-cols-4">
{Array.from({ length: 4 }).map((_, i) => ( {Array.from({ length: 4 }).map((_, i) => (
<Card key={i} className="border-0 shadow-sm"> <Card key={i} className="border-0 shadow-sm">
<CardContent className="p-6"> <CardContent className="p-3 sm:p-4 lg:p-6">
<div className="mb-4 flex items-center justify-between"> <div className="mb-2 flex items-center justify-between sm:mb-3 lg:mb-4">
<Skeleton className="h-9 w-9 rounded-lg" /> <Skeleton className="h-6 w-6 rounded-lg sm:h-8 sm:w-8 lg:h-9 lg:w-9" />
<Skeleton className="h-4 w-12" /> <Skeleton className="h-3 w-8 sm:h-4 sm:w-12" />
</div> </div>
<Skeleton className="mb-2 h-8 w-20" /> <Skeleton className="mb-1 h-5 w-16 sm:mb-2 sm:h-6 sm:w-20 lg:h-8" />
<Skeleton className="h-4 w-24" /> <Skeleton className="h-3 w-20 sm:h-4 sm:w-24" />
</CardContent> </CardContent>
</Card> </Card>
))} ))}

View File

@@ -41,15 +41,16 @@ export default function HomePage() {
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="hidden sm:inline-flex" className="text-slate-700 hover:text-slate-900 dark:text-slate-200 dark:hover:text-white"
> >
Sign In <span className="hidden sm:inline">Sign In</span>
<span className="sm:hidden">Sign In</span>
</Button> </Button>
</Link> </Link>
<Link href="/auth/register"> <Link href="/auth/register">
<Button size="sm" className="btn-brand-primary"> <Button size="sm" className="btn-brand-primary">
<span className="hidden sm:inline">Get Started Free</span> <span className="hidden sm:inline">Get Started</span>
<span className="sm:hidden">Start Free</span> <span className="sm:hidden">Start</span>
</Button> </Button>
</Link> </Link>
</div> </div>
@@ -68,17 +69,17 @@ export default function HomePage() {
<div className="mx-auto max-w-4xl"> <div className="mx-auto max-w-4xl">
<Badge className="badge-brand mb-4 sm:mb-6"> <Badge className="badge-brand mb-4 sm:mb-6">
<Sparkles className="mr-1 h-3 w-3" /> <Sparkles className="mr-1 h-3 w-3" />
100% Free Forever Free Forever
</Badge> </Badge>
<h1 className="mb-4 text-4xl font-bold tracking-tight text-white sm:mb-6 sm:text-6xl lg:text-7xl"> <h1 className="mb-4 text-4xl font-bold tracking-tight text-white sm:mb-6 sm:text-6xl lg:text-7xl">
Simple Invoicing for Simple Invoicing for
<span className="block text-emerald-100">Freelancers</span> <span className="block text-emerald-50">Freelancers</span>
</h1> </h1>
<p className="mx-auto mb-6 max-w-2xl text-lg leading-relaxed text-emerald-100 sm:mb-8 sm:text-xl"> <p className="mx-auto mb-6 max-w-2xl text-lg leading-relaxed text-emerald-50/90 sm:mb-8 sm:text-xl">
Create professional invoices, manage clients, and track payments. Create professional invoices, manage clients, and track payments.
Built specifically for freelancers and small businesses Built for freelancers and small businesses
<span className="font-semibold text-white">completely free</span>. <span className="font-semibold text-white">completely free</span>.
</p> </p>
@@ -86,9 +87,9 @@ export default function HomePage() {
<Link href="/auth/register"> <Link href="/auth/register">
<Button <Button
size="lg" size="lg"
className="btn-brand-secondary group w-full px-6 py-3 text-base font-semibold text-emerald-700 shadow-xl transition-all duration-300 sm:w-auto sm:px-8 sm:py-4 sm:text-lg" className="btn-brand-secondary group w-full px-6 py-3 text-base font-semibold shadow-xl transition-all duration-300 sm:w-auto sm:px-8 sm:py-4 sm:text-lg"
> >
Start Free Get Started
<ArrowRight className="ml-2 h-4 w-4 transition-transform group-hover:translate-x-1 sm:h-5 sm:w-5" /> <ArrowRight className="ml-2 h-4 w-4 transition-transform group-hover:translate-x-1 sm:h-5 sm:w-5" />
</Button> </Button>
</Link> </Link>
@@ -96,22 +97,22 @@ export default function HomePage() {
<Button <Button
variant="outline" variant="outline"
size="lg" size="lg"
className="btn-brand-secondary group w-full px-6 py-3 text-base sm:w-auto sm:px-8 sm:py-4 sm:text-lg" className="group w-full border-white/30 px-6 py-3 text-base text-white hover:bg-white/10 sm:w-auto sm:px-8 sm:py-4 sm:text-lg"
> >
See Features Learn More
<ChevronRight className="ml-2 h-4 w-4 transition-transform group-hover:translate-x-1 sm:h-5 sm:w-5" /> <ChevronRight className="ml-2 h-4 w-4 transition-transform group-hover:translate-x-1 sm:h-5 sm:w-5" />
</Button> </Button>
</Link> </Link>
</div> </div>
<div className="mt-8 flex flex-col items-center justify-center gap-2 text-sm text-emerald-200 sm:mt-12 sm:flex-row sm:gap-6"> <div className="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">
{[ {[
"No credit card required", "No credit card required",
"Setup in 2 minutes", "Setup in 2 minutes",
"Cancel anytime", "Free forever",
].map((text, i) => ( ].map((text, i) => (
<div key={i} className="flex items-center gap-2"> <div key={i} className="flex items-center gap-2">
<Check className="h-4 w-4 text-emerald-300" /> <Check className="h-4 w-4 text-emerald-100" />
<span className="text-center">{text}</span> <span className="text-center">{text}</span>
</div> </div>
))} ))}
@@ -132,17 +133,14 @@ export default function HomePage() {
<div className="mb-12 text-center sm:mb-16"> <div className="mb-12 text-center sm:mb-16">
<Badge className="badge-features mb-4"> <Badge className="badge-features mb-4">
<Zap className="mr-1 h-3 w-3" /> <Zap className="mr-1 h-3 w-3" />
Supercharged Features Features
</Badge> </Badge>
<h2 className="mb-4 text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl lg:text-5xl dark:text-slate-100"> <h2 className="mb-4 text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl lg:text-5xl dark:text-slate-100">
Everything you need to Everything you need to
<span className="text-brand-gradient block"> <span className="text-brand-gradient block">get paid</span>
invoice professionally
</span>
</h2> </h2>
<p className="mx-auto max-w-2xl text-lg text-slate-600 sm:text-xl dark:text-slate-300"> <p className="mx-auto max-w-2xl text-lg text-slate-600 sm:text-xl dark:text-slate-300">
Simple, powerful features designed specifically for freelancers Simple, powerful features for freelancers and small businesses.
and small businesses.
</p> </p>
</div> </div>
@@ -157,8 +155,8 @@ export default function HomePage() {
Quick Setup Quick Setup
</h3> </h3>
<p className="mb-4 text-slate-600 dark:text-slate-300"> <p className="mb-4 text-slate-600 dark:text-slate-300">
Start creating invoices immediately. No complicated setup or Start creating invoices immediately. No complicated setup
configuration required. required.
</p> </p>
<ul className="feature-list"> <ul className="feature-list">
<li className="feature-item"> <li className="feature-item">
@@ -187,8 +185,7 @@ export default function HomePage() {
Payment Tracking Payment Tracking
</h3> </h3>
<p className="mb-4 text-slate-600 dark:text-slate-300"> <p className="mb-4 text-slate-600 dark:text-slate-300">
Keep track of invoice status and monitor which clients have Keep track of invoice status and monitor payments.
paid.
</p> </p>
<ul className="feature-list"> <ul className="feature-list">
<li className="feature-item"> <li className="feature-item">
@@ -217,7 +214,7 @@ export default function HomePage() {
Professional Features Professional Features
</h3> </h3>
<p className="mb-4 text-slate-600 dark:text-slate-300"> <p className="mb-4 text-slate-600 dark:text-slate-300">
Everything you need to look professional and get paid on time. Professional features to help you get paid on time.
</p> </p>
<ul className="feature-list"> <ul className="feature-list">
<li className="feature-item"> <li className="feature-item">
@@ -250,11 +247,10 @@ export default function HomePage() {
<div className="relative container mx-auto px-4"> <div className="relative container mx-auto px-4">
<div className="mb-12 text-center sm:mb-16"> <div className="mb-12 text-center sm:mb-16">
<h2 className="mb-4 text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl lg:text-5xl dark:text-slate-100"> <h2 className="mb-4 text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl lg:text-5xl dark:text-slate-100">
Simple, transparent pricing Simple pricing
</h2> </h2>
<p className="mx-auto max-w-2xl text-lg text-slate-600 sm:text-xl dark:text-slate-300"> <p className="mx-auto max-w-2xl text-lg text-slate-600 sm:text-xl dark:text-slate-300">
Start free, stay free. No hidden fees, no gotchas, no limits on Start free, stay free. No hidden fees or limits.
your success.
</p> </p>
</div> </div>
@@ -298,7 +294,7 @@ export default function HomePage() {
variant="brand" variant="brand"
className="w-full py-3 text-base font-semibold sm:text-lg" className="w-full py-3 text-base font-semibold sm:text-lg"
> >
Get Started Now Get Started
</Button> </Button>
</Link> </Link>
@@ -319,10 +315,8 @@ export default function HomePage() {
<div className="relative container mx-auto px-4"> <div className="relative container mx-auto px-4">
<div className="mb-12 text-center sm:mb-16"> <div className="mb-12 text-center sm:mb-16">
<h2 className="mb-4 text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl lg:text-5xl dark:text-slate-100"> <h2 className="mb-4 text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl lg:text-5xl dark:text-slate-100">
Why freelancers Why choose
<span className="text-brand-gradient block"> <span className="text-brand-gradient block">BeenVoice</span>
choose BeenVoice
</span>
</h2> </h2>
</div> </div>
@@ -335,8 +329,7 @@ export default function HomePage() {
Quick & Simple Quick & Simple
</h3> </h3>
<p className="text-slate-600 dark:text-slate-300"> <p className="text-slate-600 dark:text-slate-300">
No learning curve. Start creating professional invoices in No learning curve. Start creating invoices in minutes.
minutes, not hours.
</p> </p>
</div> </div>
<div className="text-center"> <div className="text-center">
@@ -347,8 +340,7 @@ export default function HomePage() {
Always Free Always Free
</h3> </h3>
<p className="text-slate-600 dark:text-slate-300"> <p className="text-slate-600 dark:text-slate-300">
No hidden fees, no premium tiers. All features are free for as No hidden fees, no premium tiers. All features are free.
long as you need them.
</p> </p>
</div> </div>
<div className="text-center"> <div className="text-center">
@@ -360,7 +352,7 @@ export default function HomePage() {
</h3> </h3>
<p className="text-slate-600 dark:text-slate-300"> <p className="text-slate-600 dark:text-slate-300">
Focus on your work, not paperwork. Automated calculations and Focus on your work, not paperwork. Automated calculations and
professional formatting. formatting.
</p> </p>
</div> </div>
</div> </div>
@@ -377,13 +369,11 @@ export default function HomePage() {
<div className="relative container mx-auto px-4 text-center"> <div className="relative container mx-auto px-4 text-center">
<div className="mx-auto max-w-3xl"> <div className="mx-auto max-w-3xl">
<h2 className="mb-4 text-3xl font-bold text-white sm:mb-6 sm:text-4xl lg:text-5xl"> <h2 className="mb-4 text-3xl font-bold text-white sm:mb-6 sm:text-4xl lg:text-5xl">
Ready to revolutionize Ready to get started?
<span className="block">your invoicing?</span>
</h2> </h2>
<p className="mb-6 text-lg text-emerald-100 sm:mb-8 sm:text-xl"> <p className="mb-6 text-lg text-emerald-50/90 sm:mb-8 sm:text-xl">
Join thousands of entrepreneurs who&apos;ve already transformed Join thousands of freelancers already using BeenVoice. Start
their business with BeenVoice. Start your journey todaycompletely free.
today&mdash;completely free.
</p> </p>
<div className="flex flex-col items-center gap-4 sm:flex-row sm:justify-center"> <div className="flex flex-col items-center gap-4 sm:flex-row sm:justify-center">
@@ -391,15 +381,15 @@ export default function HomePage() {
<Button <Button
size="lg" size="lg"
variant="secondary" variant="secondary"
className="btn-brand-secondary group w-full px-6 py-3 text-base font-semibold text-emerald-700 shadow-xl transition-all duration-300 sm:w-auto sm:px-8 sm:py-4 sm:text-lg" className="btn-brand-secondary group w-full px-6 py-3 text-base font-semibold shadow-xl transition-all duration-300 sm:w-auto sm:px-8 sm:py-4 sm:text-lg"
> >
Start Your Success Story Start Free Today
<Rocket className="ml-2 h-4 w-4 transition-transform group-hover:translate-x-1 sm:h-5 sm:w-5" /> <Rocket className="ml-2 h-4 w-4 transition-transform group-hover:translate-x-1 sm:h-5 sm:w-5" />
</Button> </Button>
</Link> </Link>
</div> </div>
<div className="mt-6 flex flex-col items-center justify-center gap-3 text-emerald-200 sm:mt-8 sm:flex-row sm:gap-6"> <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"> <div className="flex items-center gap-2">
<Heart className="h-4 w-4" /> <Heart className="h-4 w-4" />
Free forever Free forever
@@ -422,26 +412,38 @@ export default function HomePage() {
<div className="container mx-auto px-4"> <div className="container mx-auto px-4">
<div className="text-center"> <div className="text-center">
<Logo className="mx-auto mb-4" /> <Logo className="mx-auto mb-4" />
<p className="text-muted mb-4 text-sm sm:mb-6 sm:text-base"> <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. Simple invoicing for freelancers. Free, forever.
</p> </p>
<div className="text-muted flex flex-wrap items-center justify-center gap-4 text-sm sm:gap-6"> <div className="flex flex-wrap items-center justify-center gap-4 text-sm text-slate-600 sm:gap-6 dark:text-slate-400">
<Link href="/auth/signin" className="link-primary"> <Link
href="/auth/signin"
className="transition-colors hover:text-emerald-600 dark:hover:text-emerald-400"
>
Sign In Sign In
</Link> </Link>
<Link href="/auth/register" className="link-primary"> <Link
href="/auth/register"
className="transition-colors hover:text-emerald-600 dark:hover:text-emerald-400"
>
Register Register
</Link> </Link>
<a href="#features" className="link-primary"> <a
href="#features"
className="transition-colors hover:text-emerald-600 dark:hover:text-emerald-400"
>
Features Features
</a> </a>
<a href="#pricing" className="link-primary"> <a
href="#pricing"
className="transition-colors hover:text-emerald-600 dark:hover:text-emerald-400"
>
Pricing Pricing
</a> </a>
</div> </div>
<div className="mt-6 border-t pt-6 sm:mt-8 sm:pt-8"> <div className="mt-6 border-t border-slate-200 pt-6 sm:mt-8 sm:pt-8 dark:border-slate-700">
<p className="text-muted text-sm sm:text-base"> <p className="text-sm text-slate-600 sm:text-base dark:text-slate-400">
&copy; 2024 BeenVoice. Built with for entrepreneurs. &copy; 2025 Sean O'Connor.
</p> </p>
</div> </div>
</div> </div>

View File

@@ -355,7 +355,7 @@ export function DataTable<TData, TValue>({
key={row.id} key={row.id}
data-state={row.getIsSelected() && "selected"} data-state={row.getIsSelected() && "selected"}
className={cn( className={cn(
"hover:bg-muted/20 data-[state=selected]:bg-muted/50 border-b transition-colors", "hover:bg-muted/20 data-[state=selected]:bg-muted/50 border-border/40 border-b transition-colors",
onRowClick && "cursor-pointer", onRowClick && "cursor-pointer",
)} )}
onClick={(event) => onClick={(event) =>

View File

@@ -19,6 +19,7 @@ import { Label } from "~/components/ui/label";
import { Skeleton } from "~/components/ui/skeleton"; import { Skeleton } from "~/components/ui/skeleton";
import { AddressForm } from "~/components/forms/address-form"; import { AddressForm } from "~/components/forms/address-form";
import { FloatingActionBar } from "~/components/layout/floating-action-bar"; import { FloatingActionBar } from "~/components/layout/floating-action-bar";
import { PageHeader } from "~/components/layout/page-header";
import { NumberInput } from "~/components/ui/number-input"; import { NumberInput } from "~/components/ui/number-input";
import { api } from "~/trpc/react"; import { api } from "~/trpc/react";
import { import {
@@ -246,181 +247,218 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
} }
return ( return (
<div className="mx-auto max-w-6xl pb-32"> <>
<form onSubmit={handleSubmit} className="space-y-6"> <div className="space-y-6 pb-32">
{/* Main Form Container - styled like data table */} <PageHeader
<div className="space-y-4"> title={mode === "edit" ? "Edit Client" : "Add Client"}
{/* Basic Information */} description={
<Card className="card-primary"> mode === "edit"
<CardHeader> ? "Update client information below"
<div className="flex items-center gap-3"> : "Enter client details below to add a new client."
<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" /> variant="gradient"
>
<Button
type="submit"
form="client-form"
disabled={isSubmitting}
className="btn-brand-primary shadow-md"
>
{isSubmitting ? (
<>
<Loader2 className="h-4 w-4 animate-spin sm:mr-2" />
<span className="hidden sm:inline">
{mode === "create" ? "Creating..." : "Saving..."}
</span>
</>
) : (
<>
<Save className="h-4 w-4 sm:mr-2" />
<span className="hidden sm:inline">
{mode === "create" ? "Create Client" : "Save Changes"}
</span>
</>
)}
</Button>
</PageHeader>
<form id="client-form" onSubmit={handleSubmit} className="space-y-6">
{/* Main Form Container - styled like data table */}
<div className="space-y-4">
{/* Basic Information */}
<Card className="card-primary">
<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>
<div>
<CardTitle>Basic Information</CardTitle>
<p className="text-muted-foreground mt-1 text-sm">
Enter the client&apos;s primary details
</p>
</div>
</div> </div>
<div> </CardHeader>
<CardTitle>Basic Information</CardTitle> <CardContent className="space-y-4">
<p className="text-muted-foreground mt-1 text-sm"> <div className="space-y-2">
Enter the client&apos;s primary details <Label htmlFor="name" className="text-sm font-medium">
</p> Client Name<span className="text-destructive ml-1">*</span>
</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => handleInputChange("name", e.target.value)}
placeholder={PLACEHOLDERS.name}
className={`${errors.name ? "border-destructive" : ""}`}
disabled={isSubmitting}
/>
{errors.name && (
<p className="text-destructive text-sm">{errors.name}</p>
)}
</div> </div>
</div>
</CardHeader> <div className="grid gap-4 sm:grid-cols-2">
<CardContent className="space-y-4"> <div className="space-y-2">
<div className="space-y-2"> <Label htmlFor="email" className="text-sm font-medium">
<Label htmlFor="name" className="text-sm font-medium"> Email
Client Name<span className="text-destructive ml-1">*</span> <span className="text-muted-foreground ml-1 text-xs font-normal">
</Label> (Optional)
<Input </span>
id="name" </Label>
value={formData.name} <Input
onChange={(e) => handleInputChange("name", e.target.value)} id="email"
placeholder={PLACEHOLDERS.name} type="email"
className={`${errors.name ? "border-destructive" : ""}`} value={formData.email}
disabled={isSubmitting} onChange={(e) =>
handleInputChange("email", e.target.value)
}
placeholder={PLACEHOLDERS.email}
className={`${errors.email ? "border-destructive" : ""}`}
disabled={isSubmitting}
/>
{errors.email && (
<p className="text-destructive text-sm">{errors.email}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="phone" className="text-sm font-medium">
Phone
<span className="text-muted-foreground ml-1 text-xs font-normal">
(Optional)
</span>
</Label>
<Input
id="phone"
type="tel"
value={formData.phone}
onChange={(e) => handlePhoneChange(e.target.value)}
placeholder={PLACEHOLDERS.phone}
className={`${errors.phone ? "border-destructive" : ""}`}
disabled={isSubmitting}
/>
{errors.phone && (
<p className="text-destructive text-sm">{errors.phone}</p>
)}
</div>
</div>
</CardContent>
</Card>
{/* Address */}
<Card className="card-primary">
<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">
<svg
className="h-5 w-5 text-emerald-700 dark:text-emerald-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
</div>
<div>
<CardTitle>Address</CardTitle>
<p className="text-muted-foreground mt-1 text-sm">
Client&apos;s physical location
</p>
</div>
</div>
</CardHeader>
<CardContent>
<AddressForm
addressLine1={formData.addressLine1}
addressLine2={formData.addressLine2}
city={formData.city}
state={formData.state}
postalCode={formData.postalCode}
country={formData.country}
onChange={handleInputChange}
errors={errors}
required={false}
/> />
{errors.name && ( </CardContent>
<p className="text-destructive text-sm">{errors.name}</p> </Card>
)}
</div>
<div className="grid gap-4 sm:grid-cols-2"> {/* Billing Information */}
<div className="space-y-2"> <Card className="card-primary">
<Label htmlFor="email" className="text-sm font-medium"> <CardHeader>
Email <div className="flex items-center gap-3">
<span className="text-muted-foreground ml-1 text-xs font-normal"> <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">
(Optional) <DollarSign className="h-5 w-5 text-emerald-700 dark:text-emerald-400" />
</span> </div>
</Label> <div>
<Input <CardTitle>Billing Information</CardTitle>
id="email" <p className="text-muted-foreground mt-1 text-sm">
type="email" Default billing rates for this client
value={formData.email} </p>
onChange={(e) => handleInputChange("email", e.target.value)} </div>
placeholder={PLACEHOLDERS.email}
className={`${errors.email ? "border-destructive" : ""}`}
disabled={isSubmitting}
/>
{errors.email && (
<p className="text-destructive text-sm">{errors.email}</p>
)}
</div> </div>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="phone" className="text-sm font-medium"> <Label
Phone htmlFor="defaultHourlyRate"
<span className="text-muted-foreground ml-1 text-xs font-normal"> className="text-sm font-medium"
(Optional)
</span>
</Label>
<Input
id="phone"
type="tel"
value={formData.phone}
onChange={(e) => handlePhoneChange(e.target.value)}
placeholder={PLACEHOLDERS.phone}
className={`${errors.phone ? "border-destructive" : ""}`}
disabled={isSubmitting}
/>
{errors.phone && (
<p className="text-destructive text-sm">{errors.phone}</p>
)}
</div>
</div>
</CardContent>
</Card>
{/* Address */}
<Card className="card-primary">
<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">
<svg
className="h-5 w-5 text-emerald-700 dark:text-emerald-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
> >
<path Default Hourly Rate
strokeLinecap="round" </Label>
strokeLinejoin="round" <NumberInput
strokeWidth={2} value={formData.defaultHourlyRate}
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" onChange={(value) =>
/> handleInputChange("defaultHourlyRate", value)
<path }
strokeLinecap="round" min={0}
strokeLinejoin="round" step={1}
strokeWidth={2} prefix="$"
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" width="full"
/> disabled={isSubmitting}
</svg> />
{errors.defaultHourlyRate && (
<p className="text-destructive text-sm">
{errors.defaultHourlyRate}
</p>
)}
</div> </div>
<div> </CardContent>
<CardTitle>Address</CardTitle> </Card>
<p className="text-muted-foreground mt-1 text-sm"> </div>
Client&apos;s physical location </form>
</p> </div>
</div>
</div>
</CardHeader>
<CardContent>
<AddressForm
addressLine1={formData.addressLine1}
addressLine2={formData.addressLine2}
city={formData.city}
state={formData.state}
postalCode={formData.postalCode}
country={formData.country}
onChange={handleInputChange}
errors={errors}
required={false}
/>
</CardContent>
</Card>
{/* Billing Information */}
<Card className="card-primary">
<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>
<div>
<CardTitle>Billing Information</CardTitle>
<p className="text-muted-foreground mt-1 text-sm">
Default billing rates for this client
</p>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label
htmlFor="defaultHourlyRate"
className="text-sm font-medium"
>
Default Hourly Rate
</Label>
<NumberInput
value={formData.defaultHourlyRate}
onChange={(value) =>
handleInputChange("defaultHourlyRate", value)
}
min={0}
step={1}
prefix="$"
width="full"
disabled={isSubmitting}
/>
{errors.defaultHourlyRate && (
<p className="text-destructive text-sm">
{errors.defaultHourlyRate}
</p>
)}
</div>
</CardContent>
</Card>
</div>
</form>
<FloatingActionBar <FloatingActionBar
leftContent={ leftContent={
@@ -477,6 +515,6 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
)} )}
</Button> </Button>
</FloatingActionBar> </FloatingActionBar>
</div> </>
); );
} }

View File

@@ -23,7 +23,7 @@ function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return ( return (
<thead <thead
data-slot="table-header" data-slot="table-header"
className={cn("[&_tr]:border-b", className)} className={cn("[&_tr]:border-border/60 [&_tr]:border-b", className)}
{...props} {...props}
/> />
); );
@@ -44,7 +44,7 @@ function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
<tfoot <tfoot
data-slot="table-footer" data-slot="table-footer"
className={cn( className={cn(
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0", "bg-muted/50 border-border/60 border-t font-medium [&>tr]:last:border-b-0",
className, className,
)} )}
{...props} {...props}
@@ -57,7 +57,7 @@ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
<tr <tr
data-slot="table-row" data-slot="table-row"
className={cn( className={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors", "hover:bg-muted/50 data-[state=selected]:bg-muted border-border/40 border-b transition-colors",
className, className,
)} )}
{...props} {...props}

View File

@@ -695,7 +695,7 @@
/* Additional Brand Utility Classes */ /* Additional Brand Utility Classes */
.btn-brand-primary { .btn-brand-primary {
@apply bg-gradient-to-r from-emerald-600 to-teal-600 text-white shadow-lg shadow-emerald-500/25 transition-all duration-300 hover:from-emerald-700 hover:to-teal-700 hover:shadow-xl hover:shadow-emerald-500/30; @apply bg-gradient-to-r from-teal-600 to-blue-600 text-white shadow-lg shadow-teal-500/25 transition-all duration-300 hover:from-teal-700 hover:to-blue-700 hover:shadow-xl hover:shadow-teal-500/30;
} }
.btn-brand-secondary { .btn-brand-secondary {
@@ -711,20 +711,20 @@
} }
.text-brand-gradient { .text-brand-gradient {
@apply bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-transparent; @apply bg-gradient-to-r from-teal-600 to-blue-600 bg-clip-text text-transparent;
} }
.text-brand-light { .text-brand-light {
@apply text-emerald-600 dark:text-emerald-400; @apply text-teal-700 dark:text-teal-300;
} }
.text-brand-muted { .text-brand-muted {
@apply text-emerald-700 dark:text-emerald-300; @apply text-teal-600 dark:text-teal-400;
} }
/* Background Utility Classes */ /* Background Utility Classes */
.bg-brand-muted { .bg-brand-muted {
@apply bg-emerald-100 dark:bg-emerald-900/20; @apply bg-teal-50 dark:bg-teal-900/20;
} }
.bg-brand-muted-blue { .bg-brand-muted-blue {
@@ -740,15 +740,15 @@
} }
.bg-hero-gradient { .bg-hero-gradient {
@apply bg-gradient-to-br from-emerald-500 via-teal-600 to-blue-700 dark:from-emerald-500/95 dark:via-teal-600/95 dark:to-blue-700/95; @apply bg-gradient-to-br from-emerald-600 via-teal-700 to-blue-800 dark:from-emerald-600/95 dark:via-teal-700/95 dark:to-blue-800/95;
} }
.bg-page-gradient { .bg-page-gradient {
@apply bg-gradient-to-br from-white via-emerald-50/50 to-teal-50/30 dark:from-slate-900 dark:via-emerald-900/5 dark:to-teal-900/5; @apply bg-gradient-to-br from-white via-emerald-50/50 to-teal-50/30 dark:from-slate-900 dark:via-teal-900/8 dark:to-blue-900/8;
} }
.bg-features-gradient { .bg-features-gradient {
@apply bg-gradient-to-br from-white via-emerald-50/30 to-teal-50/50 dark:from-slate-900/95 dark:via-emerald-900/10 dark:to-teal-900/10; @apply bg-gradient-to-br from-white via-emerald-50/30 to-teal-50/50 dark:from-slate-900/95 dark:via-teal-900/12 dark:to-blue-900/12;
} }
/* Card Utility Classes */ /* Card Utility Classes */
@@ -776,22 +776,26 @@
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
/* Page background - rich dark base */ /* Page background - rich dark base */
.floating-orbs { .floating-orbs {
background-color: hsl(210 11% 8%) !important; /* Rich dark background */ background-color: hsl(
206 12% 8%
) !important; /* Rich dark blue-green background */
} }
/* All cards - warm neutral with subtle transparency */ /* All cards - warm neutral with subtle transparency */
[data-slot="card"] { [data-slot="card"] {
background-color: hsl(210 9% 13% / 0.9) !important; /* Warm dark cards */ background-color: hsl(
border-color: hsl(210 9% 20%) !important; /* Subtle borders */ 206 10% 13% / 0.9
) !important; /* Blue-green dark cards */
border-color: hsl(206 10% 20%) !important; /* Subtle borders */
} }
/* Secondary cards - slightly lighter for hierarchy */ /* Secondary cards - slightly lighter for hierarchy */
[data-slot="card"].card-secondary, [data-slot="card"].card-secondary,
.card-secondary { .card-secondary {
background-color: hsl( background-color: hsl(
210 8% 16% / 0.85 206 9% 16% / 0.85
) !important; /* Lighter secondary */ ) !important; /* Lighter secondary */
border-color: hsl(210 8% 24%) !important; /* Softer borders */ border-color: hsl(206 9% 24%) !important; /* Softer borders */
} }
/* Navigation elements - cohesive with cards */ /* Navigation elements - cohesive with cards */
@@ -848,7 +852,7 @@
/* Text Color Utility Classes */ /* Text Color Utility Classes */
.text-icon-emerald { .text-icon-emerald {
@apply text-emerald-600 dark:text-emerald-400; @apply text-teal-600 dark:text-teal-400;
} }
.text-icon-blue { .text-icon-blue {
@@ -872,7 +876,7 @@
} }
.text-icon-green { .text-icon-green {
@apply text-green-600 dark:text-green-400; @apply text-teal-600 dark:text-teal-400;
} }
.text-icon-yellow { .text-icon-yellow {
@@ -984,7 +988,7 @@
} }
.bg-green-subtle { .bg-green-subtle {
@apply bg-green-50 dark:bg-green-900/20; @apply bg-teal-100 dark:bg-teal-900/20;
} }
.bg-red-subtle { .bg-red-subtle {
@@ -1004,7 +1008,7 @@
} }
.bg-emerald-subtle { .bg-emerald-subtle {
@apply bg-emerald-50/50 dark:bg-emerald-900/10; @apply bg-teal-100 dark:bg-teal-900/20;
} }
.bg-upload-zone { .bg-upload-zone {
@@ -1069,19 +1073,19 @@
/* Hero Section Utility Classes */ /* Hero Section Utility Classes */
.hero-overlay { .hero-overlay {
@apply absolute inset-0 bg-gradient-to-br from-emerald-500/95 via-teal-600/95 to-blue-700/95; @apply absolute inset-0 bg-gradient-to-br from-emerald-600/90 via-teal-700/90 to-blue-800/90;
} }
.hero-orb-1 { .hero-orb-1 {
@apply absolute top-10 left-10 h-64 w-64 rounded-full bg-gradient-to-r from-white/20 to-emerald-300/20 blur-3xl; @apply absolute top-10 left-10 h-64 w-64 rounded-full bg-gradient-to-r from-white/10 to-emerald-300/10 blur-3xl;
} }
.hero-orb-2 { .hero-orb-2 {
@apply absolute right-10 bottom-10 h-80 w-80 rounded-full bg-gradient-to-r from-teal-300/15 to-blue-300/15 blur-3xl; @apply absolute right-10 bottom-10 h-80 w-80 rounded-full bg-gradient-to-r from-teal-300/10 to-blue-300/10 blur-3xl;
} }
.hero-orb-3 { .hero-orb-3 {
@apply absolute top-1/2 left-1/2 h-96 w-96 -translate-x-1/2 -translate-y-1/2 rounded-full bg-gradient-to-r from-emerald-400/10 to-teal-400/10 blur-3xl; @apply absolute top-1/2 left-1/2 h-96 w-96 -translate-x-1/2 -translate-y-1/2 rounded-full bg-gradient-to-r from-emerald-400/5 to-teal-400/5 blur-3xl;
} }
/* Floating Decoration Utility Classes */ /* Floating Decoration Utility Classes */
@@ -1095,11 +1099,11 @@
/* Badge Utility Classes */ /* Badge Utility Classes */
.badge-brand { .badge-brand {
@apply border-emerald-400 bg-emerald-200/80 text-emerald-900 shadow-md dark:border-emerald-600 dark:bg-emerald-800/80 dark:text-emerald-100; @apply border-teal-400/60 bg-teal-100/90 text-teal-800 shadow-md dark:border-teal-500/60 dark:bg-teal-900/90 dark:text-teal-100;
} }
.badge-features { .badge-features {
@apply border-blue-400 bg-blue-200/80 text-blue-900 shadow-md dark:border-blue-600 dark:bg-blue-800/80 dark:text-blue-100; @apply border-blue-400/60 bg-blue-100/90 text-blue-800 shadow-md dark:border-blue-500/60 dark:bg-blue-900/90 dark:text-blue-100;
} }
.badge-success { .badge-success {