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 513 additions and 464 deletions
+8 -21
View File
@@ -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 function EditClientPage() {
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 (
<div>
<PageHeader
title="Edit Client"
description="Update client information below."
variant="gradient"
/>
<HydrateClient>
<ClientForm mode="edit" clientId={id} />
</HydrateClient>
</div>
);
return <ClientForm clientId={clientId} mode="edit" />;
}
+174 -144
View File
@@ -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>
+5 -17
View File
@@ -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" />;
}
+1 -1
View File
@@ -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>