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:
@@ -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" />;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user