mirror of
https://github.com/soconnor0919/beenvoice.git
synced 2026-05-08 09:38:55 -04:00
Add global animation system and entrance effects to UI
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
TrendingDown,
|
||||
TrendingUp,
|
||||
DollarSign,
|
||||
Clock,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import { Card, CardContent } from "~/components/ui/card";
|
||||
|
||||
type IconName = "DollarSign" | "Clock" | "Users" | "TrendingDown";
|
||||
|
||||
interface AnimatedStatsCardProps {
|
||||
title: string;
|
||||
value: string;
|
||||
change: string;
|
||||
trend: "up" | "down";
|
||||
iconName: IconName;
|
||||
description: string;
|
||||
delay?: number;
|
||||
isCurrency?: boolean;
|
||||
numericValue?: number;
|
||||
}
|
||||
|
||||
const iconMap = {
|
||||
DollarSign,
|
||||
Clock,
|
||||
Users,
|
||||
TrendingDown,
|
||||
} as const;
|
||||
|
||||
export function AnimatedStatsCard({
|
||||
title,
|
||||
value,
|
||||
change,
|
||||
trend,
|
||||
iconName,
|
||||
description,
|
||||
delay = 0,
|
||||
isCurrency = false,
|
||||
numericValue,
|
||||
}: AnimatedStatsCardProps) {
|
||||
const Icon = iconMap[iconName];
|
||||
const TrendIcon = trend === "up" ? TrendingUp : TrendingDown;
|
||||
const isPositive = trend === "up";
|
||||
|
||||
// For now, always use the formatted value prop to ensure correct display
|
||||
// Animation can be added back once the basic display is working correctly
|
||||
const displayValue = value;
|
||||
|
||||
// Suppress unused parameter warnings for now
|
||||
void delay;
|
||||
void isCurrency;
|
||||
void numericValue;
|
||||
|
||||
return (
|
||||
<Card className="stats-card">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between space-y-0 pb-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Icon className="text-muted-foreground h-5 w-5" />
|
||||
<p className="text-muted-foreground text-sm font-medium">{title}</p>
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center space-x-1 text-xs"
|
||||
style={{
|
||||
color: isPositive
|
||||
? "oklch(var(--chart-2))"
|
||||
: "oklch(var(--chart-3))",
|
||||
}}
|
||||
>
|
||||
<TrendIcon className="h-3 w-3" />
|
||||
<span>{change}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="animate-count-up text-2xl font-bold">{displayValue}</p>
|
||||
<p className="text-muted-foreground text-xs">{description}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -16,13 +16,13 @@ async function BusinessesTable() {
|
||||
|
||||
export default async function BusinessesPage() {
|
||||
return (
|
||||
<>
|
||||
<div className="page-enter space-y-8">
|
||||
<PageHeader
|
||||
title="Businesses"
|
||||
description="Manage your businesses and their information"
|
||||
variant="gradient"
|
||||
>
|
||||
<Button asChild variant="default" className="shadow-md">
|
||||
<Button asChild variant="default" className="hover-lift shadow-md">
|
||||
<Link href="/dashboard/businesses/new">
|
||||
<Plus className="mr-2 h-5 w-5" />
|
||||
<span>Add Business</span>
|
||||
@@ -35,6 +35,6 @@ export default async function BusinessesPage() {
|
||||
<BusinessesTable />
|
||||
</Suspense>
|
||||
</HydrateClient>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,13 +7,13 @@ import { ClientsTable } from "./_components/clients-table";
|
||||
|
||||
export default async function ClientsPage() {
|
||||
return (
|
||||
<>
|
||||
<div className="page-enter space-y-8">
|
||||
<PageHeader
|
||||
title="Clients"
|
||||
description="Manage your clients and their information."
|
||||
variant="gradient"
|
||||
>
|
||||
<Button asChild variant="default" className="shadow-md">
|
||||
<Button asChild variant="default" className="hover-lift shadow-md">
|
||||
<Link href="/dashboard/clients/new">
|
||||
<Plus className="mr-2 h-5 w-5" />
|
||||
<span>Add Client</span>
|
||||
@@ -24,6 +24,6 @@ export default async function ClientsPage() {
|
||||
<HydrateClient>
|
||||
<ClientsTable />
|
||||
</HydrateClient>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -123,14 +123,18 @@ function InvoiceViewContent({ invoiceId }: { invoiceId: string }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6 pb-24">
|
||||
<div className="page-enter space-y-6 pb-24">
|
||||
<PageHeader
|
||||
title="Invoice Details"
|
||||
description="View and manage invoice information"
|
||||
variant="gradient"
|
||||
>
|
||||
<PDFDownloadButton invoiceId={invoice.id} variant="outline" />
|
||||
<Button asChild variant="default">
|
||||
<PDFDownloadButton
|
||||
invoiceId={invoice.id}
|
||||
variant="outline"
|
||||
className="hover-lift"
|
||||
/>
|
||||
<Button asChild variant="default" className="hover-lift">
|
||||
<Link href={`/dashboard/invoices/${invoice.id}/edit`}>
|
||||
<Edit className="h-5 w-5" />
|
||||
<span>Edit</span>
|
||||
@@ -324,8 +328,8 @@ function InvoiceViewContent({ invoiceId }: { invoiceId: string }) {
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{invoice.items.map((item) => (
|
||||
<Card key={item.id} className="card-secondary">
|
||||
{invoice.items.map((item, _index) => (
|
||||
<Card key={item.id} className="invoice-item card-secondary">
|
||||
<CardContent className="p-3">
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
|
||||
|
||||
@@ -165,7 +165,14 @@ export function InvoicesDataTable({ invoices }: InvoicesDataTableProps) {
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const invoice = row.original;
|
||||
return <StatusBadge status={getStatusType(invoice)} />;
|
||||
return (
|
||||
<StatusBadge
|
||||
status={getStatusType(invoice)}
|
||||
className={
|
||||
getStatusType(invoice) === "sent" ? "status-pending" : ""
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
filterFn: (row, id, value: string[]) => {
|
||||
const invoice = row.original;
|
||||
@@ -210,7 +217,7 @@ export function InvoicesDataTable({ invoices }: InvoicesDataTableProps) {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
className="hover-scale h-8 w-8 p-0"
|
||||
data-action-button="true"
|
||||
>
|
||||
<Eye className="h-3.5 w-3.5" />
|
||||
@@ -220,7 +227,7 @@ export function InvoicesDataTable({ invoices }: InvoicesDataTableProps) {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
className="hover-scale h-8 w-8 p-0"
|
||||
data-action-button="true"
|
||||
>
|
||||
<Edit className="h-3.5 w-3.5" />
|
||||
@@ -229,7 +236,7 @@ export function InvoicesDataTable({ invoices }: InvoicesDataTableProps) {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-destructive hover:text-destructive/80 h-8 w-8 p-0"
|
||||
className="hover-scale text-destructive hover:text-destructive/80 h-8 w-8 p-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDelete(invoice);
|
||||
|
||||
@@ -16,19 +16,19 @@ async function InvoicesTable() {
|
||||
|
||||
export default async function InvoicesPage() {
|
||||
return (
|
||||
<>
|
||||
<div className="page-enter space-y-8">
|
||||
<PageHeader
|
||||
title="Invoices"
|
||||
description="Manage your invoices and track payments"
|
||||
variant="gradient"
|
||||
>
|
||||
<Button asChild variant="outline" className="shadow-sm">
|
||||
<Button asChild variant="outline" className="hover-lift shadow-sm">
|
||||
<Link href="/dashboard/invoices/import">
|
||||
<Upload className="mr-2 h-5 w-5" />
|
||||
<span>Import CSV</span>
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="default" className="shadow-md">
|
||||
<Button asChild variant="default" className="hover-lift shadow-md">
|
||||
<Link href="/dashboard/invoices/new">
|
||||
<Plus className="mr-2 h-5 w-5" />
|
||||
<span>Create Invoice</span>
|
||||
@@ -41,6 +41,6 @@ export default async function InvoicesPage() {
|
||||
<InvoicesTable />
|
||||
</Suspense>
|
||||
</HydrateClient>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+52
-51
@@ -3,14 +3,10 @@ import {
|
||||
ArrowUpRight,
|
||||
BarChart3,
|
||||
Calendar,
|
||||
Clock,
|
||||
DollarSign,
|
||||
Edit,
|
||||
Eye,
|
||||
FileText,
|
||||
Plus,
|
||||
TrendingDown,
|
||||
TrendingUp,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
@@ -26,6 +22,7 @@ import type { StoredInvoiceStatus } from "~/types/invoice";
|
||||
import { RevenueChart } from "~/app/dashboard/_components/revenue-chart";
|
||||
import { InvoiceStatusChart } from "~/app/dashboard/_components/invoice-status-chart";
|
||||
import { MonthlyMetricsChart } from "~/app/dashboard/_components/monthly-metrics-chart";
|
||||
import { AnimatedStatsCard } from "~/app/dashboard/_components/animated-stats-card";
|
||||
|
||||
// Hero section with clean mono design
|
||||
function DashboardHero({ firstName }: { firstName: string }) {
|
||||
@@ -160,80 +157,79 @@ async function DashboardStats() {
|
||||
return value > 0 ? `+${value.toFixed(1)}%` : `${value.toFixed(1)}%`;
|
||||
};
|
||||
|
||||
// Debug logging to see actual values
|
||||
console.log("Dashboard Stats Debug:", {
|
||||
totalRevenue,
|
||||
pendingAmount,
|
||||
totalClients,
|
||||
overdueInvoices: overdueInvoices.length,
|
||||
revenueChange,
|
||||
pendingChange,
|
||||
clientChange,
|
||||
overdueChange,
|
||||
paidInvoicesCount: paidInvoices.length,
|
||||
pendingInvoicesCount: pendingInvoices.length,
|
||||
});
|
||||
|
||||
const stats = [
|
||||
{
|
||||
title: "Total Revenue",
|
||||
value: `$${totalRevenue.toLocaleString("en-US", { minimumFractionDigits: 2 })}`,
|
||||
numericValue: totalRevenue,
|
||||
isCurrency: true,
|
||||
change: formatTrend(revenueChange),
|
||||
trend: revenueChange >= 0 ? ("up" as const) : ("down" as const),
|
||||
icon: DollarSign,
|
||||
iconName: "DollarSign" as const,
|
||||
description: `From ${paidInvoices.length} paid invoices`,
|
||||
},
|
||||
{
|
||||
title: "Pending Amount",
|
||||
value: `$${pendingAmount.toLocaleString("en-US", { minimumFractionDigits: 2 })}`,
|
||||
numericValue: pendingAmount,
|
||||
isCurrency: true,
|
||||
change: formatTrend(pendingChange),
|
||||
trend: pendingChange >= 0 ? ("up" as const) : ("down" as const),
|
||||
icon: Clock,
|
||||
iconName: "Clock" as const,
|
||||
description: `${pendingInvoices.length} invoices awaiting payment`,
|
||||
},
|
||||
{
|
||||
title: "Active Clients",
|
||||
value: totalClients.toString(),
|
||||
numericValue: totalClients,
|
||||
isCurrency: false,
|
||||
change: formatTrend(clientChange, true),
|
||||
trend: clientChange >= 0 ? ("up" as const) : ("down" as const),
|
||||
icon: Users,
|
||||
iconName: "Users" as const,
|
||||
description: "Total registered clients",
|
||||
},
|
||||
{
|
||||
title: "Overdue Invoices",
|
||||
value: overdueInvoices.length.toString(),
|
||||
numericValue: overdueInvoices.length,
|
||||
isCurrency: false,
|
||||
change: formatTrend(overdueChange, true),
|
||||
trend: overdueChange <= 0 ? ("up" as const) : ("down" as const),
|
||||
icon: TrendingDown,
|
||||
iconName: "TrendingDown" as const,
|
||||
description: "Invoices past due date",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="mb-8 grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{stats.map((stat) => {
|
||||
const Icon = stat.icon;
|
||||
const TrendIcon = stat.trend === "up" ? TrendingUp : TrendingDown;
|
||||
const isPositive = stat.trend === "up";
|
||||
|
||||
return (
|
||||
<Card key={stat.title}>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between space-y-0 pb-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Icon className="text-muted-foreground h-5 w-5" />
|
||||
<p className="text-muted-foreground text-sm font-medium">
|
||||
{stat.title}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center space-x-1 text-xs"
|
||||
style={{
|
||||
color: isPositive
|
||||
? "oklch(var(--chart-2))"
|
||||
: "oklch(var(--chart-3))",
|
||||
}}
|
||||
>
|
||||
<TrendIcon className="h-3 w-3" />
|
||||
<span>{stat.change}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-2xl font-bold">{stat.value}</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{stat.description}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
{stats.map((stat, index) => (
|
||||
<AnimatedStatsCard
|
||||
key={stat.title}
|
||||
title={stat.title}
|
||||
value={stat.value}
|
||||
numericValue={stat.numericValue}
|
||||
isCurrency={stat.isCurrency}
|
||||
iconName={stat.iconName}
|
||||
change={stat.change}
|
||||
trend={stat.trend}
|
||||
description={stat.description}
|
||||
delay={index * 100}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -327,7 +323,7 @@ function QuickActions() {
|
||||
<Link
|
||||
key={action.title}
|
||||
href={action.href}
|
||||
className={`flex w-full items-start space-x-3 rounded-lg border p-4 transition-colors ${
|
||||
className={`hover-lift flex w-full items-start space-x-3 rounded-lg border p-4 transition-colors ${
|
||||
action.featured
|
||||
? "border-foreground/20 bg-muted/50 hover:bg-muted"
|
||||
: "border-border bg-background hover:bg-muted/50"
|
||||
@@ -420,13 +416,18 @@ async function CurrentWork() {
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button asChild variant="outline" size="sm" className="flex-1">
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="hover-lift flex-1"
|
||||
>
|
||||
<Link href={`/dashboard/invoices/${currentInvoice.id}`}>
|
||||
<Eye className="mr-2 h-4 w-4" />
|
||||
View
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild size="sm" className="flex-1">
|
||||
<Button asChild size="sm" className="hover-lift flex-1">
|
||||
<Link href={`/dashboard/invoices/${currentInvoice.id}/edit`}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Continue
|
||||
@@ -509,13 +510,13 @@ async function RecentActivity() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{recentInvoices.map((invoice) => (
|
||||
{recentInvoices.map((invoice, _index) => (
|
||||
<Link
|
||||
key={invoice.id}
|
||||
href={`/dashboard/invoices/${invoice.id}`}
|
||||
className="block"
|
||||
>
|
||||
<div className="bg-muted/50 hover:bg-muted border-foreground/20 rounded-lg border p-3 transition-colors">
|
||||
<div className="recent-activity-item bg-muted/50 hover:bg-muted border-foreground/20 rounded-lg border p-3 transition-colors">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="bg-muted flex-shrink-0 rounded-lg p-2">
|
||||
<FileText className="text-muted-foreground h-4 w-4" />
|
||||
@@ -627,7 +628,7 @@ export default async function DashboardPage() {
|
||||
const firstName = session?.user?.name?.split(" ")[0] ?? "User";
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="page-enter space-y-8">
|
||||
<DashboardHero firstName={firstName} />
|
||||
|
||||
<HydrateClient>
|
||||
|
||||
@@ -1,26 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import * as React from "react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import {
|
||||
AlertTriangle,
|
||||
Building,
|
||||
ChevronDown,
|
||||
Database,
|
||||
Download,
|
||||
Eye,
|
||||
EyeOff,
|
||||
FileText,
|
||||
FileUp,
|
||||
Info,
|
||||
Key,
|
||||
Shield,
|
||||
Upload,
|
||||
User,
|
||||
Database,
|
||||
AlertTriangle,
|
||||
Shield,
|
||||
FileText,
|
||||
Users,
|
||||
Building,
|
||||
Key,
|
||||
Eye,
|
||||
FileUp,
|
||||
ChevronDown,
|
||||
Info,
|
||||
} from "lucide-react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import * as React from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { api } from "~/trpc/react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "~/components/ui/alert-dialog";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@@ -34,22 +46,6 @@ import {
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "~/components/ui/collapsible";
|
||||
import { Input } from "~/components/ui/input";
|
||||
import { Label } from "~/components/ui/label";
|
||||
import { Textarea } from "~/components/ui/textarea";
|
||||
import { Badge } from "~/components/ui/badge";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "~/components/ui/alert-dialog";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -59,6 +55,10 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "~/components/ui/dialog";
|
||||
import { Input } from "~/components/ui/input";
|
||||
import { Label } from "~/components/ui/label";
|
||||
import { Textarea } from "~/components/ui/textarea";
|
||||
import { api } from "~/trpc/react";
|
||||
|
||||
export function SettingsContent() {
|
||||
const { data: session } = useSession();
|
||||
@@ -300,7 +300,7 @@ export function SettingsContent() {
|
||||
{/* Profile & Account Overview */}
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
{/* Profile Section */}
|
||||
<Card className="bg-card border-border border">
|
||||
<Card className="form-section bg-card border-border border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground flex items-center gap-2">
|
||||
<User className="text-primary h-5 w-5" />
|
||||
@@ -337,6 +337,7 @@ export function SettingsContent() {
|
||||
type="submit"
|
||||
disabled={updateProfileMutation.isPending}
|
||||
variant="default"
|
||||
className="hover-lift"
|
||||
>
|
||||
{updateProfileMutation.isPending
|
||||
? "Updating..."
|
||||
@@ -347,7 +348,7 @@ export function SettingsContent() {
|
||||
</Card>
|
||||
|
||||
{/* Data Overview */}
|
||||
<Card className="bg-card border-border border">
|
||||
<Card className="form-section bg-card border-border border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground flex items-center gap-2">
|
||||
<Database className="text-primary h-5 w-5" />
|
||||
@@ -359,12 +360,13 @@ export function SettingsContent() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{dataStatItems.map((item) => {
|
||||
{dataStatItems.map((item, index) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<div
|
||||
key={item.label}
|
||||
className="bg-card border p-4 transition-shadow hover:shadow-sm"
|
||||
className="hover-lift bg-card border p-4 transition-shadow hover:shadow-sm"
|
||||
style={{ animationDelay: `${index * 100}ms` }}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
|
||||
@@ -6,7 +6,7 @@ import { SettingsContent } from "./_components/settings-content";
|
||||
|
||||
export default async function SettingsPage() {
|
||||
return (
|
||||
<>
|
||||
<div className="page-enter space-y-8">
|
||||
<PageHeader
|
||||
title="Settings"
|
||||
description="Manage your account preferences and data"
|
||||
@@ -18,6 +18,6 @@ export default async function SettingsPage() {
|
||||
<SettingsContent />
|
||||
</Suspense>
|
||||
</HydrateClient>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user