From b5784061ebf4ca7aa05b5db872d6c7c5caf3926b Mon Sep 17 00:00:00 2001 From: Sean O'Connor Date: Wed, 16 Jul 2025 14:19:50 -0400 Subject: [PATCH] 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. --- README.md | 2 +- src/app/dashboard/clients/[id]/edit/page.tsx | 31 +- src/app/dashboard/clients/[id]/page.tsx | 318 +++++++++------- src/app/dashboard/clients/new/page.tsx | 22 +- src/app/dashboard/clients/page.tsx | 2 +- src/app/dashboard/page.tsx | 56 +-- src/app/page.tsx | 116 +++--- src/components/data/data-table.tsx | 2 +- src/components/forms/client-form.tsx | 374 ++++++++++--------- src/components/ui/table.tsx | 6 +- src/styles/globals.css | 50 +-- 11 files changed, 514 insertions(+), 465 deletions(-) diff --git a/README.md b/README.md index 5ceb747..35d7e53 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/src/app/dashboard/clients/[id]/edit/page.tsx b/src/app/dashboard/clients/[id]/edit/page.tsx index 6583f65..fa1818f 100644 --- a/src/app/dashboard/clients/[id]/edit/page.tsx +++ b/src/app/dashboard/clients/[id]/edit/page.tsx @@ -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 ( -
- - - - -
- ); +export default function EditClientPage() { + const params = useParams(); + const clientId = Array.isArray(params?.id) ? params.id[0] : params?.id; + if (!clientId) return null; + + return ; } diff --git a/src/app/dashboard/clients/[id]/page.tsx b/src/app/dashboard/clients/[id]/page.tsx index edd11cd..96eae51 100644 --- a/src/app/dashboard/clients/[id]/page.tsx +++ b/src/app/dashboard/clients/[id]/page.tsx @@ -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 ( -
-
- - - +
+ + + + -
- {/* Client Information Card */} -
- - - - - Contact Information - - - - {/* Basic Info */} -
- {client.email && ( -
-
- -
-
-

Email

-

{client.email}

-
-
- )} - - {client.phone && ( -
-
- -
-
-

Phone

-

{client.phone}

-
-
- )} +
+ {/* Client Information Card */} +
+ + + +
+
- - {/* Address */} - {(client.addressLine1 ?? client.city ?? client.state) && ( -
-
-
- -
-
-

Address

-
+ Contact Information + + + + {/* Basic Info */} +
+ {client.email && ( +
+
+
-
- {client.addressLine1 &&

{client.addressLine1}

} - {client.addressLine2 &&

{client.addressLine2}

} +
+

+ Email +

+

{client.email}

+
+
+ )} + + {client.phone && ( +
+
+ +
+
+

+ Phone +

+

{client.phone}

+
+
+ )} +
+ + {/* Address */} + {(client.addressLine1 ?? client.city ?? client.state) && ( +
+

Client Address

+
+
+ +
+
+ {client.addressLine1 && ( +

{client.addressLine1}

+ )} + {client.addressLine2 && ( +

{client.addressLine2}

+ )} {(client.city ?? client.state ?? client.postalCode) && ( -

+

{[client.city, client.state, client.postalCode] .filter(Boolean) .join(", ")}

)} - {client.country &&

{client.country}

} + {client.country && ( +

{client.country}

+ )}
- )} +
+ )} - {/* Client Since */} -
-
- + {/* Client Since */} +
+

Client Details

+
+
+
-

Client Since

-

+

+ Client Since +

+

{formatDate(client.createdAt)}

- - -
+
+ + +
- {/* Stats Card */} -
+ {/* Stats Card */} +
+ + + +
+ +
+ Invoice Summary +
+
+ +
+

+ {formatCurrency(totalInvoiced)} +

+

Total Invoiced

+
+ +
+
+

+ {paidInvoices} +

+

Paid

+
+
+

+ {pendingInvoices} +

+

Pending

+
+
+
+
+ + {/* Recent Invoices */} + {client.invoices && client.invoices.length > 0 && ( - - - Invoice Summary + +
+ +
+ Recent Invoices
- -
-

- {formatCurrency(totalInvoiced)} -

-

Total Invoiced

-
- -
-
-

{paidInvoices}

-

Paid

-
-
-

- {pendingInvoices} -

-

Pending

-
+ +
+ {client.invoices.slice(0, 3).map((invoice) => ( +
+
+

+ {invoice.invoiceNumber} +

+

+ {formatDate(invoice.issueDate)} +

+
+
+

+ {formatCurrency(invoice.totalAmount)} +

+ + {invoice.status} + +
+
+ ))}
- - {/* Recent Invoices */} - {client.invoices && client.invoices.length > 0 && ( - - - - Recent Invoices - - - -
- {client.invoices.slice(0, 3).map((invoice) => ( -
-
-

- {invoice.invoiceNumber} -

-

- {formatDate(invoice.issueDate)} -

-
-
-

- {formatCurrency(invoice.totalAmount)} -

- - {invoice.status} - -
-
- ))} -
-
-
- )} -
+ )}
diff --git a/src/app/dashboard/clients/new/page.tsx b/src/app/dashboard/clients/new/page.tsx index fe460d8..629ccdc 100644 --- a/src/app/dashboard/clients/new/page.tsx +++ b/src/app/dashboard/clients/new/page.tsx @@ -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 ( -
- - - - -
- ); +import { ClientForm } from "~/components/forms/client-form"; + +export default function NewClientPage() { + return ; } diff --git a/src/app/dashboard/clients/page.tsx b/src/app/dashboard/clients/page.tsx index efb305d..2ffe1c6 100644 --- a/src/app/dashboard/clients/page.tsx +++ b/src/app/dashboard/clients/page.tsx @@ -14,7 +14,7 @@ export default async function ClientsPage() { description="Manage your clients and their information." variant="gradient" > -
-

+

${currentInvoice.totalAmount.toFixed(2)}

@@ -273,7 +273,7 @@ async function CurrentWork() {

@@ -68,17 +69,17 @@ export default function HomePage() {
- 100% Free Forever + Free Forever

Simple Invoicing for - Freelancers + Freelancers

-

+

Create professional invoices, manage clients, and track payments. - Built specifically for freelancers and small businesses— + Built for freelancers and small businesses— completely free.

@@ -86,9 +87,9 @@ export default function HomePage() { @@ -96,22 +97,22 @@ export default function HomePage() {
-
+
{[ "No credit card required", "Setup in 2 minutes", - "Cancel anytime", + "Free forever", ].map((text, i) => (
- + {text}
))} @@ -132,17 +133,14 @@ export default function HomePage() {
- Supercharged Features + Features

Everything you need to - - invoice professionally - + get paid

- Simple, powerful features designed specifically for freelancers - and small businesses. + Simple, powerful features for freelancers and small businesses.

@@ -157,8 +155,8 @@ export default function HomePage() { Quick Setup

- Start creating invoices immediately. No complicated setup or - configuration required. + Start creating invoices immediately. No complicated setup + required.

  • @@ -187,8 +185,7 @@ export default function HomePage() { Payment Tracking

    - Keep track of invoice status and monitor which clients have - paid. + Keep track of invoice status and monitor payments.

    • @@ -217,7 +214,7 @@ export default function HomePage() { Professional Features

      - Everything you need to look professional and get paid on time. + Professional features to help you get paid on time.

      • @@ -250,11 +247,10 @@ export default function HomePage() {

        - Simple, transparent pricing + Simple pricing

        - Start free, stay free. No hidden fees, no gotchas, no limits on - your success. + Start free, stay free. No hidden fees or limits.

        @@ -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 @@ -319,10 +315,8 @@ export default function HomePage() {

        - Why freelancers - - choose BeenVoice - + Why choose + BeenVoice

        @@ -335,8 +329,7 @@ export default function HomePage() { Quick & Simple

        - No learning curve. Start creating professional invoices in - minutes, not hours. + No learning curve. Start creating invoices in minutes.

        @@ -347,8 +340,7 @@ export default function HomePage() { Always Free

        - 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.

        @@ -360,7 +352,7 @@ export default function HomePage() {

        Focus on your work, not paperwork. Automated calculations and - professional formatting. + formatting.

        @@ -377,13 +369,11 @@ export default function HomePage() {

        - Ready to revolutionize - your invoicing? + Ready to get started?

        -

        - Join thousands of entrepreneurs who've already transformed - their business with BeenVoice. Start your journey - today—completely free. +

        + Join thousands of freelancers already using BeenVoice. Start + today—completely free.

        @@ -391,15 +381,15 @@ export default function HomePage() {
        -
        +
        Free forever @@ -422,26 +412,38 @@ export default function HomePage() {
        -

        +

        Simple invoicing for freelancers. Free, forever.

        -
        - +
        + Sign In - + Register - + Features - + Pricing
        -
        -

        - © 2024 BeenVoice. Built with ♥ for entrepreneurs. +

        +

        + © 2025 Sean O'Connor.

        diff --git a/src/components/data/data-table.tsx b/src/components/data/data-table.tsx index 55bc842..3b09e1f 100644 --- a/src/components/data/data-table.tsx +++ b/src/components/data/data-table.tsx @@ -355,7 +355,7 @@ export function DataTable({ 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) => diff --git a/src/components/forms/client-form.tsx b/src/components/forms/client-form.tsx index f391b71..c1e54fe 100644 --- a/src/components/forms/client-form.tsx +++ b/src/components/forms/client-form.tsx @@ -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 ( -
        -
        - {/* Main Form Container - styled like data table */} -
        - {/* Basic Information */} - - -
        -
        - + <> +
        + + + + + + {/* Main Form Container - styled like data table */} +
        + {/* Basic Information */} + + +
        +
        + +
        +
        + Basic Information +

        + Enter the client's primary details +

        +
        -
        - Basic Information -

        - Enter the client's primary details -

        + + +
        + + handleInputChange("name", e.target.value)} + placeholder={PLACEHOLDERS.name} + className={`${errors.name ? "border-destructive" : ""}`} + disabled={isSubmitting} + /> + {errors.name && ( +

        {errors.name}

        + )}
        -
        -
        - -
        - - handleInputChange("name", e.target.value)} - placeholder={PLACEHOLDERS.name} - className={`${errors.name ? "border-destructive" : ""}`} - disabled={isSubmitting} + +
        +
        + + + handleInputChange("email", e.target.value) + } + placeholder={PLACEHOLDERS.email} + className={`${errors.email ? "border-destructive" : ""}`} + disabled={isSubmitting} + /> + {errors.email && ( +

        {errors.email}

        + )} +
        + +
        + + handlePhoneChange(e.target.value)} + placeholder={PLACEHOLDERS.phone} + className={`${errors.phone ? "border-destructive" : ""}`} + disabled={isSubmitting} + /> + {errors.phone && ( +

        {errors.phone}

        + )} +
        +
        + + + + {/* Address */} + + +
        +
        + + + + +
        +
        + Address +

        + Client's physical location +

        +
        +
        +
        + + - {errors.name && ( -

        {errors.name}

        - )} -
        +
        +
        -
        -
        - - handleInputChange("email", e.target.value)} - placeholder={PLACEHOLDERS.email} - className={`${errors.email ? "border-destructive" : ""}`} - disabled={isSubmitting} - /> - {errors.email && ( -

        {errors.email}

        - )} + {/* Billing Information */} + + +
        +
        + +
        +
        + Billing Information +

        + Default billing rates for this client +

        +
        - +
        +
        - - handlePhoneChange(e.target.value)} - placeholder={PLACEHOLDERS.phone} - className={`${errors.phone ? "border-destructive" : ""}`} - disabled={isSubmitting} - /> - {errors.phone && ( -

        {errors.phone}

        - )} -
        -
        - - - - {/* Address */} - - -
        -
        - - - - + Default Hourly Rate + + + handleInputChange("defaultHourlyRate", value) + } + min={0} + step={1} + prefix="$" + width="full" + disabled={isSubmitting} + /> + {errors.defaultHourlyRate && ( +

        + {errors.defaultHourlyRate} +

        + )}
        -
        - Address -

        - Client's physical location -

        -
        -
        -
        - - - -
        - - {/* Billing Information */} - - -
        -
        - -
        -
        - Billing Information -

        - Default billing rates for this client -

        -
        -
        -
        - -
        - - - handleInputChange("defaultHourlyRate", value) - } - min={0} - step={1} - prefix="$" - width="full" - disabled={isSubmitting} - /> - {errors.defaultHourlyRate && ( -

        - {errors.defaultHourlyRate} -

        - )} -
        -
        -
        -
        - + + +
        + +
        -
        + ); } diff --git a/src/components/ui/table.tsx b/src/components/ui/table.tsx index 2252c26..d5c5406 100644 --- a/src/components/ui/table.tsx +++ b/src/components/ui/table.tsx @@ -23,7 +23,7 @@ function TableHeader({ className, ...props }: React.ComponentProps<"thead">) { return ( ); @@ -44,7 +44,7 @@ function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { 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">) {