Refactor clients section to use client components

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

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

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

View File

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

View File

@@ -1,25 +1,12 @@
import Link from "next/link";
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" />;
}

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>

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" />;
}

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>

View File

@@ -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>
))}

View File

@@ -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&apos;ve already transformed
their business with BeenVoice. Start your journey
today&mdash;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
todaycompletely 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">
&copy; 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">
&copy; 2025 Sean O'Connor.
</p>
</div>
</div>

View File

@@ -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) =>

View File

@@ -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&apos;s primary details
</p>
</div>
</div>
<div>
<CardTitle>Basic Information</CardTitle>
<p className="text-muted-foreground mt-1 text-sm">
Enter the client&apos;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&apos;s physical location
</p>
</div>
</div>
</CardHeader>
<CardContent>
<AddressForm
addressLine1={formData.addressLine1}
addressLine2={formData.addressLine2}
city={formData.city}
state={formData.state}
postalCode={formData.postalCode}
country={formData.country}
onChange={handleInputChange}
errors={errors}
required={false}
/>
{errors.name && (
<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&apos;s physical location
</p>
</div>
</div>
</CardHeader>
<CardContent>
<AddressForm
addressLine1={formData.addressLine1}
addressLine2={formData.addressLine2}
city={formData.city}
state={formData.state}
postalCode={formData.postalCode}
country={formData.country}
onChange={handleInputChange}
errors={errors}
required={false}
/>
</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>
</>
);
}

View File

@@ -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}

View File

@@ -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 {