mirror of
https://github.com/soconnor0919/beenvoice.git
synced 2025-12-13 01:24:44 -05:00
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:
@@ -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.
|
||||
|
||||
@@ -1,25 +1,12 @@
|
||||
import Link from "next/link";
|
||||
import { HydrateClient } from "~/trpc/server";
|
||||
"use client";
|
||||
|
||||
import { useParams } from "next/navigation";
|
||||
import { ClientForm } from "~/components/forms/client-form";
|
||||
import { PageHeader } from "~/components/layout/page-header";
|
||||
|
||||
interface EditClientPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default async function EditClientPage({ params }: EditClientPageProps) {
|
||||
const { id } = await params;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
title="Edit Client"
|
||||
description="Update client information below."
|
||||
variant="gradient"
|
||||
/>
|
||||
<HydrateClient>
|
||||
<ClientForm mode="edit" clientId={id} />
|
||||
</HydrateClient>
|
||||
</div>
|
||||
);
|
||||
export default function EditClientPage() {
|
||||
const params = useParams();
|
||||
const clientId = Array.isArray(params?.id) ? params.id[0] : params?.id;
|
||||
if (!clientId) return null;
|
||||
|
||||
return <ClientForm clientId={clientId} mode="edit" />;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
Building,
|
||||
Calendar,
|
||||
DollarSign,
|
||||
ArrowLeft,
|
||||
} from "lucide-react";
|
||||
|
||||
interface ClientDetailPageProps {
|
||||
@@ -54,177 +55,206 @@ export default async function ClientDetailPage({
|
||||
client.invoices?.filter((invoice) => invoice.status === "sent").length || 0;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mx-auto max-w-4xl space-y-6">
|
||||
<PageHeader
|
||||
title={client.name}
|
||||
description="Client Details"
|
||||
variant="gradient"
|
||||
>
|
||||
<Link href={`/dashboard/clients/${client.id}/edit`}>
|
||||
<Button variant="brand">
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Edit Client
|
||||
</Button>
|
||||
<div className="space-y-6 pb-32">
|
||||
<PageHeader
|
||||
title={client.name}
|
||||
description="View client details and information"
|
||||
variant="gradient"
|
||||
>
|
||||
<Button asChild variant="outline" className="shadow-sm">
|
||||
<Link href="/dashboard/clients">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
<span>Back to Clients</span>
|
||||
</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">
|
||||
{/* Client Information Card */}
|
||||
<div className="lg:col-span-2">
|
||||
<Card className="card-primary">
|
||||
<CardHeader>
|
||||
<CardTitle className="client-section-title">
|
||||
<Building className="h-5 w-5" />
|
||||
<span>Contact Information</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* Basic Info */}
|
||||
<div className="grid grid-cols-1 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 className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||
{/* Client Information Card */}
|
||||
<div className="lg:col-span-2">
|
||||
<Card className="card-primary">
|
||||
<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>
|
||||
|
||||
{/* Address */}
|
||||
{(client.addressLine1 ?? client.city ?? client.state) && (
|
||||
<div className="space-y-4">
|
||||
<div className="client-info-item">
|
||||
<div className="client-info-icon">
|
||||
<MapPin className="client-info-icon-emerald" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="client-info-label">Address</p>
|
||||
</div>
|
||||
<span>Contact Information</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* Basic Info */}
|
||||
<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>
|
||||
<div className="client-address-content">
|
||||
{client.addressLine1 && <p>{client.addressLine1}</p>}
|
||||
{client.addressLine2 && <p>{client.addressLine2}</p>}
|
||||
<div>
|
||||
<p className="text-muted-foreground text-sm font-medium">
|
||||
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) && (
|
||||
<p>
|
||||
<p className="text-foreground">
|
||||
{[client.city, client.state, client.postalCode]
|
||||
.filter(Boolean)
|
||||
.join(", ")}
|
||||
</p>
|
||||
)}
|
||||
{client.country && <p>{client.country}</p>}
|
||||
{client.country && (
|
||||
<p className="text-foreground">{client.country}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Client Since */}
|
||||
<div className="client-info-item">
|
||||
<div className="client-info-icon">
|
||||
<Calendar className="client-info-icon-emerald" />
|
||||
{/* Client Since */}
|
||||
<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>
|
||||
<div>
|
||||
<p className="client-info-label">Client Since</p>
|
||||
<p className="client-info-value">
|
||||
<p className="text-muted-foreground text-sm font-medium">
|
||||
Client Since
|
||||
</p>
|
||||
<p className="text-foreground text-sm">
|
||||
{formatDate(client.createdAt)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Stats Card */}
|
||||
<div className="space-y-6">
|
||||
{/* Stats Card */}
|
||||
<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">
|
||||
<CardHeader>
|
||||
<CardTitle className="client-stats-title">
|
||||
<DollarSign className="h-5 w-5" />
|
||||
<span>Invoice Summary</span>
|
||||
<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>Recent Invoices</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="text-center">
|
||||
<p className="client-total-amount">
|
||||
{formatCurrency(totalInvoiced)}
|
||||
</p>
|
||||
<p className="client-total-label">Total Invoiced</p>
|
||||
</div>
|
||||
|
||||
<div className="client-stats-grid">
|
||||
<div className="text-center">
|
||||
<p className="client-stat-value-paid">{paidInvoices}</p>
|
||||
<p className="client-stat-label">Paid</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="client-stat-value-pending">
|
||||
{pendingInvoices}
|
||||
</p>
|
||||
<p className="client-stat-label">Pending</p>
|
||||
</div>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{client.invoices.slice(0, 3).map((invoice) => (
|
||||
<div
|
||||
key={invoice.id}
|
||||
className="flex items-center justify-between rounded-lg border p-3"
|
||||
>
|
||||
<div>
|
||||
<p className="text-foreground font-medium">
|
||||
{invoice.invoiceNumber}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{formatDate(invoice.issueDate)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-foreground font-semibold">
|
||||
{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>
|
||||
|
||||
{/* 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>
|
||||
|
||||
@@ -1,19 +1,7 @@
|
||||
import Link from "next/link";
|
||||
import { HydrateClient } from "~/trpc/server";
|
||||
import { ClientForm } from "~/components/forms/client-form";
|
||||
import { PageHeader } from "~/components/layout/page-header";
|
||||
"use client";
|
||||
|
||||
export default async function NewClientPage() {
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
title="Add Client"
|
||||
description="Enter client details below to add a new client."
|
||||
variant="gradient"
|
||||
/>
|
||||
<HydrateClient>
|
||||
<ClientForm mode="create" />
|
||||
</HydrateClient>
|
||||
</div>
|
||||
);
|
||||
import { ClientForm } from "~/components/forms/client-form";
|
||||
|
||||
export default function NewClientPage() {
|
||||
return <ClientForm mode="create" />;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export default async function ClientsPage() {
|
||||
description="Manage your clients and their information."
|
||||
variant="gradient"
|
||||
>
|
||||
<Button asChild variant="brand">
|
||||
<Button asChild className="btn-brand-primary shadow-md">
|
||||
<Link href="/dashboard/clients/new">
|
||||
<Plus className="mr-2 h-5 w-5" />
|
||||
<span>Add Client</span>
|
||||
|
||||
@@ -24,11 +24,11 @@ import {
|
||||
// Modern gradient background component
|
||||
function DashboardHero({ firstName }: { firstName: string }) {
|
||||
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="relative z-10">
|
||||
<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
|
||||
</p>
|
||||
</div>
|
||||
@@ -94,7 +94,7 @@ async function DashboardStats() {
|
||||
];
|
||||
|
||||
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) => {
|
||||
const Icon = stat.icon;
|
||||
return (
|
||||
@@ -102,17 +102,17 @@ async function DashboardStats() {
|
||||
key={stat.title}
|
||||
className="border-0 shadow-sm transition-shadow hover:shadow-md"
|
||||
>
|
||||
<CardContent className="p-4 lg:p-6">
|
||||
<div className="mb-3 flex items-center justify-between lg:mb-4">
|
||||
<div className={`rounded-lg p-2 ${stat.bgColor}`}>
|
||||
<Icon className="h-4 w-4 text-gray-700 lg:h-5 lg:w-5 dark:text-gray-800" />
|
||||
<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" />
|
||||
</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}
|
||||
</span>
|
||||
</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}
|
||||
</p>
|
||||
<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">
|
||||
<CardHeader className="pb-3">
|
||||
<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
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -171,7 +171,7 @@ function QuickActions() {
|
||||
variant={action.primary ? "default" : "outline"}
|
||||
className={`h-12 w-full justify-start px-3 ${
|
||||
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"
|
||||
}`}
|
||||
>
|
||||
@@ -218,7 +218,7 @@ async function CurrentWork() {
|
||||
<p className="mb-4 text-gray-600 dark:text-gray-300">
|
||||
No draft invoices found
|
||||
</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">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Create Invoice
|
||||
@@ -254,7 +254,7 @@ async function CurrentWork() {
|
||||
</p>
|
||||
</div>
|
||||
<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)}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
@@ -273,7 +273,7 @@ async function CurrentWork() {
|
||||
<Button
|
||||
asChild
|
||||
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`}>
|
||||
<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">
|
||||
No invoices yet
|
||||
</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">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
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">
|
||||
<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="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>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="font-medium text-gray-900 dark:text-gray-100">
|
||||
#{invoice.invoiceNumber}
|
||||
</p>
|
||||
@@ -362,8 +362,11 @@ async function RecentActivity() {
|
||||
{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 gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Badge
|
||||
className={`border ${getStatusColor(invoice.status)}`}
|
||||
>
|
||||
@@ -372,9 +375,6 @@ async function RecentActivity() {
|
||||
<p className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
${invoice.totalAmount.toFixed(2)}
|
||||
</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>
|
||||
</CardContent>
|
||||
@@ -391,16 +391,16 @@ async function RecentActivity() {
|
||||
// Loading skeletons
|
||||
function StatsSkeleton() {
|
||||
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) => (
|
||||
<Card key={i} className="border-0 shadow-sm">
|
||||
<CardContent className="p-6">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<Skeleton className="h-9 w-9 rounded-lg" />
|
||||
<Skeleton className="h-4 w-12" />
|
||||
<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" />
|
||||
</div>
|
||||
<Skeleton className="mb-2 h-8 w-20" />
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<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" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
116
src/app/page.tsx
116
src/app/page.tsx
@@ -41,15 +41,16 @@ export default function HomePage() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
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>
|
||||
</Link>
|
||||
<Link href="/auth/register">
|
||||
<Button size="sm" className="btn-brand-primary">
|
||||
<span className="hidden sm:inline">Get Started Free</span>
|
||||
<span className="sm:hidden">Start Free</span>
|
||||
<span className="hidden sm:inline">Get Started</span>
|
||||
<span className="sm:hidden">Start</span>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
@@ -68,17 +69,17 @@ export default function HomePage() {
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<Badge className="badge-brand mb-4 sm:mb-6">
|
||||
<Sparkles className="mr-1 h-3 w-3" />
|
||||
100% Free Forever
|
||||
Free Forever
|
||||
</Badge>
|
||||
|
||||
<h1 className="mb-4 text-4xl font-bold tracking-tight text-white sm:mb-6 sm:text-6xl lg:text-7xl">
|
||||
Simple Invoicing for
|
||||
<span className="block text-emerald-100">Freelancers</span>
|
||||
<span className="block text-emerald-50">Freelancers</span>
|
||||
</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.
|
||||
Built specifically for freelancers and small businesses—
|
||||
Built for freelancers and small businesses—
|
||||
<span className="font-semibold text-white">completely free</span>.
|
||||
</p>
|
||||
|
||||
@@ -86,9 +87,9 @@ export default function HomePage() {
|
||||
<Link href="/auth/register">
|
||||
<Button
|
||||
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" />
|
||||
</Button>
|
||||
</Link>
|
||||
@@ -96,22 +97,22 @@ export default function HomePage() {
|
||||
<Button
|
||||
variant="outline"
|
||||
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" />
|
||||
</Button>
|
||||
</Link>
|
||||
</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",
|
||||
"Setup in 2 minutes",
|
||||
"Cancel anytime",
|
||||
"Free forever",
|
||||
].map((text, i) => (
|
||||
<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>
|
||||
</div>
|
||||
))}
|
||||
@@ -132,17 +133,14 @@ export default function HomePage() {
|
||||
<div className="mb-12 text-center sm:mb-16">
|
||||
<Badge className="badge-features mb-4">
|
||||
<Zap className="mr-1 h-3 w-3" />
|
||||
Supercharged Features
|
||||
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">
|
||||
Everything you need to
|
||||
<span className="text-brand-gradient block">
|
||||
invoice professionally
|
||||
</span>
|
||||
<span className="text-brand-gradient block">get paid</span>
|
||||
</h2>
|
||||
<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
|
||||
and small businesses.
|
||||
Simple, powerful features for freelancers and small businesses.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -157,8 +155,8 @@ export default function HomePage() {
|
||||
Quick Setup
|
||||
</h3>
|
||||
<p className="mb-4 text-slate-600 dark:text-slate-300">
|
||||
Start creating invoices immediately. No complicated setup or
|
||||
configuration required.
|
||||
Start creating invoices immediately. No complicated setup
|
||||
required.
|
||||
</p>
|
||||
<ul className="feature-list">
|
||||
<li className="feature-item">
|
||||
@@ -187,8 +185,7 @@ export default function HomePage() {
|
||||
Payment Tracking
|
||||
</h3>
|
||||
<p className="mb-4 text-slate-600 dark:text-slate-300">
|
||||
Keep track of invoice status and monitor which clients have
|
||||
paid.
|
||||
Keep track of invoice status and monitor payments.
|
||||
</p>
|
||||
<ul className="feature-list">
|
||||
<li className="feature-item">
|
||||
@@ -217,7 +214,7 @@ export default function HomePage() {
|
||||
Professional Features
|
||||
</h3>
|
||||
<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>
|
||||
<ul className="feature-list">
|
||||
<li className="feature-item">
|
||||
@@ -250,11 +247,10 @@ export default function HomePage() {
|
||||
<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">
|
||||
Simple, transparent pricing
|
||||
Simple pricing
|
||||
</h2>
|
||||
<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
|
||||
your success.
|
||||
Start free, stay free. No hidden fees or limits.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -298,7 +294,7 @@ export default function HomePage() {
|
||||
variant="brand"
|
||||
className="w-full py-3 text-base font-semibold sm:text-lg"
|
||||
>
|
||||
Get Started Now
|
||||
Get Started
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
@@ -319,10 +315,8 @@ export default function HomePage() {
|
||||
<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 freelancers
|
||||
<span className="text-brand-gradient block">
|
||||
choose BeenVoice
|
||||
</span>
|
||||
Why choose
|
||||
<span className="text-brand-gradient block">BeenVoice</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@@ -335,8 +329,7 @@ export default function HomePage() {
|
||||
Quick & Simple
|
||||
</h3>
|
||||
<p className="text-slate-600 dark:text-slate-300">
|
||||
No learning curve. Start creating professional invoices in
|
||||
minutes, not hours.
|
||||
No learning curve. Start creating invoices in minutes.
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
@@ -347,8 +340,7 @@ export default function HomePage() {
|
||||
Always Free
|
||||
</h3>
|
||||
<p className="text-slate-600 dark:text-slate-300">
|
||||
No hidden fees, no premium tiers. All features are free for as
|
||||
long as you need them.
|
||||
No hidden fees, no premium tiers. All features are free.
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
@@ -360,7 +352,7 @@ export default function HomePage() {
|
||||
</h3>
|
||||
<p className="text-slate-600 dark:text-slate-300">
|
||||
Focus on your work, not paperwork. Automated calculations and
|
||||
professional formatting.
|
||||
formatting.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -377,13 +369,11 @@ export default function HomePage() {
|
||||
<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 revolutionize
|
||||
<span className="block">your invoicing?</span>
|
||||
Ready to get started?
|
||||
</h2>
|
||||
<p className="mb-6 text-lg text-emerald-100 sm:mb-8 sm:text-xl">
|
||||
Join thousands of entrepreneurs who've already transformed
|
||||
their business with BeenVoice. Start your journey
|
||||
today—completely free.
|
||||
<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">
|
||||
@@ -391,15 +381,15 @@ export default function HomePage() {
|
||||
<Button
|
||||
size="lg"
|
||||
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" />
|
||||
</Button>
|
||||
</Link>
|
||||
</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">
|
||||
<Heart className="h-4 w-4" />
|
||||
Free forever
|
||||
@@ -422,26 +412,38 @@ export default function HomePage() {
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="text-center">
|
||||
<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.
|
||||
</p>
|
||||
<div className="text-muted flex flex-wrap items-center justify-center gap-4 text-sm sm:gap-6">
|
||||
<Link href="/auth/signin" className="link-primary">
|
||||
<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="transition-colors hover:text-emerald-600 dark:hover:text-emerald-400"
|
||||
>
|
||||
Sign In
|
||||
</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
|
||||
</Link>
|
||||
<a href="#features" className="link-primary">
|
||||
<a
|
||||
href="#features"
|
||||
className="transition-colors hover:text-emerald-600 dark:hover:text-emerald-400"
|
||||
>
|
||||
Features
|
||||
</a>
|
||||
<a href="#pricing" className="link-primary">
|
||||
<a
|
||||
href="#pricing"
|
||||
className="transition-colors hover:text-emerald-600 dark:hover:text-emerald-400"
|
||||
>
|
||||
Pricing
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-6 border-t pt-6 sm:mt-8 sm:pt-8">
|
||||
<p className="text-muted text-sm sm:text-base">
|
||||
© 2024 BeenVoice. Built with ♥ for entrepreneurs.
|
||||
<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>
|
||||
|
||||
@@ -355,7 +355,7 @@ export function DataTable<TData, TValue>({
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
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",
|
||||
)}
|
||||
onClick={(event) =>
|
||||
|
||||
@@ -19,6 +19,7 @@ import { Label } from "~/components/ui/label";
|
||||
import { Skeleton } from "~/components/ui/skeleton";
|
||||
import { AddressForm } from "~/components/forms/address-form";
|
||||
import { FloatingActionBar } from "~/components/layout/floating-action-bar";
|
||||
import { PageHeader } from "~/components/layout/page-header";
|
||||
import { NumberInput } from "~/components/ui/number-input";
|
||||
import { api } from "~/trpc/react";
|
||||
import {
|
||||
@@ -246,181 +247,218 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-6xl pb-32">
|
||||
<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 className="space-y-6 pb-32">
|
||||
<PageHeader
|
||||
title={mode === "edit" ? "Edit Client" : "Add Client"}
|
||||
description={
|
||||
mode === "edit"
|
||||
? "Update client information below"
|
||||
: "Enter client details below to add a new client."
|
||||
}
|
||||
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's primary details
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>Basic Information</CardTitle>
|
||||
<p className="text-muted-foreground mt-1 text-sm">
|
||||
Enter the client's primary details
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name" className="text-sm font-medium">
|
||||
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>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name" className="text-sm font-medium">
|
||||
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}
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email" className="text-sm font-medium">
|
||||
Email
|
||||
<span className="text-muted-foreground ml-1 text-xs font-normal">
|
||||
(Optional)
|
||||
</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={formData.email}
|
||||
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'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 && (
|
||||
<p className="text-destructive text-sm">{errors.name}</p>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email" className="text-sm font-medium">
|
||||
Email
|
||||
<span className="text-muted-foreground ml-1 text-xs font-normal">
|
||||
(Optional)
|
||||
</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={formData.email}
|
||||
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>
|
||||
)}
|
||||
{/* 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="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"
|
||||
<Label
|
||||
htmlFor="defaultHourlyRate"
|
||||
className="text-sm font-medium"
|
||||
>
|
||||
<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>
|
||||
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>
|
||||
<div>
|
||||
<CardTitle>Address</CardTitle>
|
||||
<p className="text-muted-foreground mt-1 text-sm">
|
||||
Client'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}
|
||||
/>
|
||||
</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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<FloatingActionBar
|
||||
leftContent={
|
||||
@@ -477,6 +515,6 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
|
||||
)}
|
||||
</Button>
|
||||
</FloatingActionBar>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||
return (
|
||||
<thead
|
||||
data-slot="table-header"
|
||||
className={cn("[&_tr]:border-b", className)}
|
||||
className={cn("[&_tr]:border-border/60 [&_tr]:border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -44,7 +44,7 @@ function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||
<tfoot
|
||||
data-slot="table-footer"
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
@@ -57,7 +57,7 @@ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||
<tr
|
||||
data-slot="table-row"
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -695,7 +695,7 @@
|
||||
|
||||
/* Additional Brand Utility Classes */
|
||||
.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 {
|
||||
@@ -711,20 +711,20 @@
|
||||
}
|
||||
|
||||
.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 {
|
||||
@apply text-emerald-600 dark:text-emerald-400;
|
||||
@apply text-teal-700 dark:text-teal-300;
|
||||
}
|
||||
|
||||
.text-brand-muted {
|
||||
@apply text-emerald-700 dark:text-emerald-300;
|
||||
@apply text-teal-600 dark:text-teal-400;
|
||||
}
|
||||
|
||||
/* Background Utility Classes */
|
||||
.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 {
|
||||
@@ -740,15 +740,15 @@
|
||||
}
|
||||
|
||||
.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 {
|
||||
@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 {
|
||||
@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 */
|
||||
@@ -776,22 +776,26 @@
|
||||
@media (prefers-color-scheme: dark) {
|
||||
/* Page background - rich dark base */
|
||||
.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 */
|
||||
[data-slot="card"] {
|
||||
background-color: hsl(210 9% 13% / 0.9) !important; /* Warm dark cards */
|
||||
border-color: hsl(210 9% 20%) !important; /* Subtle borders */
|
||||
background-color: hsl(
|
||||
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 */
|
||||
[data-slot="card"].card-secondary,
|
||||
.card-secondary {
|
||||
background-color: hsl(
|
||||
210 8% 16% / 0.85
|
||||
206 9% 16% / 0.85
|
||||
) !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 */
|
||||
@@ -848,7 +852,7 @@
|
||||
|
||||
/* Text Color Utility Classes */
|
||||
.text-icon-emerald {
|
||||
@apply text-emerald-600 dark:text-emerald-400;
|
||||
@apply text-teal-600 dark:text-teal-400;
|
||||
}
|
||||
|
||||
.text-icon-blue {
|
||||
@@ -872,7 +876,7 @@
|
||||
}
|
||||
|
||||
.text-icon-green {
|
||||
@apply text-green-600 dark:text-green-400;
|
||||
@apply text-teal-600 dark:text-teal-400;
|
||||
}
|
||||
|
||||
.text-icon-yellow {
|
||||
@@ -984,7 +988,7 @@
|
||||
}
|
||||
|
||||
.bg-green-subtle {
|
||||
@apply bg-green-50 dark:bg-green-900/20;
|
||||
@apply bg-teal-100 dark:bg-teal-900/20;
|
||||
}
|
||||
|
||||
.bg-red-subtle {
|
||||
@@ -1004,7 +1008,7 @@
|
||||
}
|
||||
|
||||
.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 {
|
||||
@@ -1069,19 +1073,19 @@
|
||||
|
||||
/* Hero Section Utility Classes */
|
||||
.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 {
|
||||
@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 {
|
||||
@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 {
|
||||
@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 */
|
||||
@@ -1095,11 +1099,11 @@
|
||||
|
||||
/* Badge Utility Classes */
|
||||
.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 {
|
||||
@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 {
|
||||
|
||||
Reference in New Issue
Block a user