);
},
diff --git a/src/app/dashboard/invoices/page.tsx b/src/app/dashboard/invoices/page.tsx
index af0342e..e5e27e2 100644
--- a/src/app/dashboard/invoices/page.tsx
+++ b/src/app/dashboard/invoices/page.tsx
@@ -6,6 +6,7 @@ import { PageHeader } from "~/components/layout/page-header";
import { Plus, Upload } from "lucide-react";
import { InvoicesDataTable } from "./_components/invoices-data-table";
import { DataTableSkeleton } from "~/components/data/data-table";
+import { Card, CardContent } from "~/components/ui/card";
// Invoices Table Component
async function InvoicesTable() {
@@ -16,7 +17,7 @@ async function InvoicesTable() {
export default async function InvoicesPage() {
return (
-
+
-
-
- {/* Mobile layout - no left margin */}
-
-
-
- {children}
-
-
- {/* Desktop layout - with sidebar margin */}
-
-
-
- {children}
-
-
-
- );
+ return
{children};
}
diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx
index b3cdf2b..a254c0c 100644
--- a/src/app/dashboard/page.tsx
+++ b/src/app/dashboard/page.tsx
@@ -26,131 +26,10 @@ import { MonthlyMetricsChart } from "~/app/dashboard/_components/monthly-metrics
import { AnimatedStatsCard } from "~/app/dashboard/_components/animated-stats-card";
// Hero section with clean mono design
-function DashboardHero({ firstName }: { firstName: string }) {
- return (
-
-
Welcome back, {firstName}!
-
- Here's what's happening with your business today
-
-
- );
-}
+
// Enhanced stats cards with better visuals
-async function DashboardStats() {
- const [clients, invoices] = await Promise.all([
- api.clients.getAll(),
- api.invoices.getAll(),
- ]);
-
- const totalClients = clients.length;
- const paidInvoices = invoices.filter(
- (invoice) =>
- getEffectiveInvoiceStatus(
- invoice.status as StoredInvoiceStatus,
- invoice.dueDate,
- ) === "paid",
- );
- const totalRevenue = paidInvoices.reduce(
- (sum, invoice) => sum + invoice.totalAmount,
- 0,
- );
-
- const pendingInvoices = invoices.filter((invoice) => {
- const effectiveStatus = getEffectiveInvoiceStatus(
- invoice.status as StoredInvoiceStatus,
- invoice.dueDate,
- );
- return effectiveStatus === "sent" || effectiveStatus === "overdue";
- });
- const pendingAmount = pendingInvoices.reduce(
- (sum, invoice) => sum + invoice.totalAmount,
- 0,
- );
-
- const overdueInvoices = invoices.filter(
- (invoice) =>
- getEffectiveInvoiceStatus(
- invoice.status as StoredInvoiceStatus,
- invoice.dueDate,
- ) === "overdue",
- );
-
- // Calculate month-over-month trends
- const now = new Date();
- const currentMonth = new Date(now.getFullYear(), now.getMonth(), 1);
- const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1);
-
- // Current month data
- const currentMonthInvoices = invoices.filter(
- (invoice) => new Date(invoice.issueDate) >= currentMonth,
- );
- const currentMonthRevenue = currentMonthInvoices
- .filter(
- (invoice) =>
- getEffectiveInvoiceStatus(
- invoice.status as StoredInvoiceStatus,
- invoice.dueDate,
- ) === "paid",
- )
- .reduce((sum, invoice) => sum + invoice.totalAmount, 0);
-
- // Last month data
- const lastMonthInvoices = invoices.filter((invoice) => {
- const date = new Date(invoice.issueDate);
- return date >= lastMonth && date < currentMonth;
- });
- const lastMonthRevenue = lastMonthInvoices
- .filter(
- (invoice) =>
- getEffectiveInvoiceStatus(
- invoice.status as StoredInvoiceStatus,
- invoice.dueDate,
- ) === "paid",
- )
- .reduce((sum, invoice) => sum + invoice.totalAmount, 0);
-
- // Previous month data for clients
- const prevMonthClients = clients.filter(
- (client) => new Date(client.createdAt) < currentMonth,
- ).length;
-
- // Calculate trends
- const revenueChange =
- lastMonthRevenue > 0
- ? ((currentMonthRevenue - lastMonthRevenue) / lastMonthRevenue) * 100
- : currentMonthRevenue > 0
- ? 100
- : 0;
-
- const pendingChange =
- lastMonthInvoices.length > 0
- ? ((pendingInvoices.length -
- lastMonthInvoices.filter((invoice) => {
- const status = getEffectiveInvoiceStatus(
- invoice.status as StoredInvoiceStatus,
- invoice.dueDate,
- );
- return status === "sent" || status === "overdue";
- }).length) /
- lastMonthInvoices.length) *
- 100
- : pendingInvoices.length > 0
- ? 100
- : 0;
-
- const clientChange = totalClients - prevMonthClients;
-
- const lastMonthOverdue = lastMonthInvoices.filter(
- (invoice) =>
- getEffectiveInvoiceStatus(
- invoice.status as StoredInvoiceStatus,
- invoice.dueDate,
- ) === "overdue",
- ).length;
- const overdueChange = overdueInvoices.length - lastMonthOverdue;
-
+function DashboardStats({ stats }: { stats: any }) { // TODO: Import RouterOutput type
const formatTrend = (value: number, isCount = false) => {
if (isCount) {
return value > 0 ? `+${value}` : value.toString();
@@ -158,66 +37,52 @@ 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 = [
+ const statCards = [
{
title: "Total Revenue",
- value: `$${totalRevenue.toLocaleString("en-US", { minimumFractionDigits: 2 })}`,
- numericValue: totalRevenue,
+ value: `$${stats.totalRevenue.toLocaleString("en-US", { minimumFractionDigits: 2 })}`,
+ numericValue: stats.totalRevenue,
isCurrency: true,
- change: formatTrend(revenueChange),
- trend: revenueChange >= 0 ? ("up" as const) : ("down" as const),
+ change: formatTrend(stats.revenueChange),
+ trend: stats.revenueChange >= 0 ? ("up" as const) : ("down" as const),
iconName: "DollarSign" as const,
- description: `From ${paidInvoices.length} paid invoices`,
+ description: "Total collected revenue",
},
{
title: "Pending Amount",
- value: `$${pendingAmount.toLocaleString("en-US", { minimumFractionDigits: 2 })}`,
- numericValue: pendingAmount,
+ value: `$${stats.pendingAmount.toLocaleString("en-US", { minimumFractionDigits: 2 })}`,
+ numericValue: stats.pendingAmount,
isCurrency: true,
- change: formatTrend(pendingChange),
- trend: pendingChange >= 0 ? ("up" as const) : ("down" as const),
+ change: "0%", // TODO: Calculate pending change if needed
+ trend: "neutral" as const,
iconName: "Clock" as const,
- description: `${pendingInvoices.length} invoices awaiting payment`,
+ description: "Invoices awaiting payment",
},
{
title: "Active Clients",
- value: totalClients.toString(),
- numericValue: totalClients,
+ value: stats.totalClients.toString(),
+ numericValue: stats.totalClients,
isCurrency: false,
- change: formatTrend(clientChange, true),
- trend: clientChange >= 0 ? ("up" as const) : ("down" as const),
+ change: "0", // TODO: Calculate client change if needed
+ trend: "neutral" as const,
iconName: "Users" as const,
description: "Total registered clients",
},
{
title: "Overdue Invoices",
- value: overdueInvoices.length.toString(),
- numericValue: overdueInvoices.length,
+ value: stats.overdueCount.toString(),
+ numericValue: stats.overdueCount,
isCurrency: false,
- change: formatTrend(overdueChange, true),
- trend: overdueChange <= 0 ? ("up" as const) : ("down" as const),
+ change: "0", // TODO: Calculate overdue change if needed
+ trend: "neutral" as const,
iconName: "TrendingDown" as const,
description: "Invoices past due date",
},
];
return (
-
- {stats.map((stat, index) => (
+
+ {statCards.map((stat, index) => (
+
{/* Revenue Trend Chart */}
@@ -250,7 +118,7 @@ async function ChartsSection() {
-
+
@@ -441,14 +309,8 @@ async function CurrentWork() {
}
// Enhanced recent activity
-async function RecentActivity() {
- const invoices = await api.invoices.getAll();
- const recentInvoices = invoices
- .sort(
- (a, b) =>
- new Date(b.issueDate).getTime() - new Date(a.issueDate).getTime(),
- )
- .slice(0, 5);
+async function RecentActivity({ recentInvoices }: { recentInvoices: any[] }) {
+ // Use passed recentInvoices instead of fetching all
const getStatusStyle = (status: string) => {
switch (status) {
@@ -558,7 +420,7 @@ async function RecentActivity() {
// Loading skeletons
function StatsSkeleton() {
return (
-
+
{Array.from({ length: 4 }).map((_, i) => (
@@ -577,7 +439,7 @@ function StatsSkeleton() {
function ChartsSkeleton() {
return (
-
+
@@ -623,30 +485,40 @@ function CardSkeleton() {
);
}
+import { DashboardPageHeader } from "~/components/layout/page-header";
+
+// ... imports
+
export default async function DashboardPage() {
const session = await auth.api.getSession({
headers: await headers(),
});
const firstName = session?.user?.name?.split(" ")[0] ?? "User";
+ // Fetch stats centrally
+ const stats = await api.dashboard.getStats();
+
return (
-
-
+
+
}>
-
+
}>
-
+
-
-
+
+
}>
@@ -657,7 +529,7 @@ export default async function DashboardPage() {
}>
-
+
diff --git a/src/app/dashboard/settings/page.tsx b/src/app/dashboard/settings/page.tsx
index 6540398..7117c66 100644
--- a/src/app/dashboard/settings/page.tsx
+++ b/src/app/dashboard/settings/page.tsx
@@ -3,21 +3,26 @@ import { HydrateClient } from "~/trpc/server";
import { PageHeader } from "~/components/layout/page-header";
import { DataTableSkeleton } from "~/components/data/data-table";
import { SettingsContent } from "./_components/settings-content";
+import { Card, CardContent } from "~/components/ui/card";
export default async function SettingsPage() {
return (
-
+
-
- }>
-
-
-
+
+
+
+ }>
+
+
+
+
+
);
}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index b524e3f..5341be3 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -6,6 +6,7 @@ import { Geist, Geist_Mono, Instrument_Serif } from "next/font/google";
import { TRPCReactProvider } from "~/trpc/react";
import { Toaster } from "~/components/ui/sonner";
import { AnimationPreferencesProvider } from "~/components/providers/animation-preferences-provider";
+import { MotionBackground } from "~/components/layout/motion-background";
import { ThemeProvider } from "~/components/providers/theme-provider";
import { ColorThemeProvider } from "~/components/providers/color-theme-provider";
@@ -141,6 +142,7 @@ export default function RootLayout({
+
{children}
diff --git a/src/components/branding/logo.tsx b/src/components/branding/logo.tsx
index 3912b9d..f3b62ea 100644
--- a/src/components/branding/logo.tsx
+++ b/src/components/branding/logo.tsx
@@ -5,30 +5,29 @@ import { cn } from "~/lib/utils";
interface LogoProps {
className?: string;
- size?: "sm" | "md" | "lg" | "xl";
+ size?: "sm" | "md" | "lg" | "xl" | "icon";
animated?: boolean;
}
export function Logo({ className, size = "md", animated = true }: LogoProps) {
const sizeClasses = {
- sm: "text-sm",
- md: "text-lg",
- lg: "text-2xl",
- xl: "text-4xl",
+ sm: "text-base",
+ md: "text-xl",
+ lg: "text-3xl",
+ xl: "text-5xl",
+ icon: "text-2xl",
};
if (!animated) {
return ;
}
-
-
return (
$
-
-
- been
-
-
- voice
-
+ {size !== "icon" && (
+ <>
+
+
+ been
+
+
+ voice
+
+ >
+ )}
);
}
@@ -70,15 +73,19 @@ function LogoContent({
sizeClasses,
}: {
className?: string;
- size: "sm" | "md" | "lg" | "xl";
+ size: "sm" | "md" | "lg" | "xl" | "icon";
sizeClasses: Record;
}) {
return (
-
+
$
-
- been
- voice
+ {size !== "icon" && (
+ <>
+
+ been
+ voice
+ >
+ )}
);
}
diff --git a/src/components/layout/dashboard-shell.tsx b/src/components/layout/dashboard-shell.tsx
new file mode 100644
index 0000000..06fbe39
--- /dev/null
+++ b/src/components/layout/dashboard-shell.tsx
@@ -0,0 +1,70 @@
+"use client";
+
+import * as React from "react";
+import { Sidebar } from "~/components/layout/sidebar";
+import { SidebarProvider, useSidebar } from "~/components/layout/sidebar-provider";
+import { cn } from "~/lib/utils";
+import { Menu } from "lucide-react";
+import { Button } from "~/components/ui/button";
+import { Sheet, SheetContent, SheetTrigger } from "~/components/ui/sheet";
+import { DashboardBreadcrumbs } from "~/components/navigation/dashboard-breadcrumbs";
+
+function DashboardContent({ children }: { children: React.ReactNode }) {
+ const { isCollapsed } = useSidebar();
+ const [isMobileOpen, setIsMobileOpen] = React.useState(false);
+
+ return (
+
+ {/* Desktop Sidebar */}
+
+
+
+
+ {/* Mobile Sidebar (Sheet) */}
+
+
+
+
+
+
+ setIsMobileOpen(false)} />
+
+
+
+
+ {/* Main Content */}
+
+
+ {/* Mobile header spacer is handled by pt-16 on mobile */}
+
+ {/* Mobile Breadcrumbs could go here or be part of the page */}
+
+ {children}
+
+
+
+ );
+}
+
+export function DashboardShell({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/components/layout/motion-background.tsx b/src/components/layout/motion-background.tsx
new file mode 100644
index 0000000..8aab9b5
--- /dev/null
+++ b/src/components/layout/motion-background.tsx
@@ -0,0 +1,27 @@
+"use client";
+
+import { cn } from "~/lib/utils";
+
+export function MotionBackground() {
+ return (
+
+ );
+}
diff --git a/src/components/layout/page-header.tsx b/src/components/layout/page-header.tsx
index 6c97360..209dd75 100644
--- a/src/components/layout/page-header.tsx
+++ b/src/components/layout/page-header.tsx
@@ -1,4 +1,5 @@
import React from "react";
+import { DashboardBreadcrumbs } from "~/components/navigation/dashboard-breadcrumbs";
interface PageHeaderProps {
title: string;
@@ -39,24 +40,51 @@ export function PageHeader({
};
return (
-
-
-
-
{title}
- {description && (
-
- {description}
-
- )}
-
- {children && (
-
- {children}
+
+ {variant === "large-gradient" || variant === "gradient" ? (
+
+
+
+
+
+
+
{title}
+ {description && (
+
+ {description}
+
+ )}
+
+ {children && (
+
+ {children}
+
+ )}
+
- )}
-
+
+ ) : (
+ <>
+
+
+
+
{title}
+ {description && (
+
+ {description}
+
+ )}
+
+ {children && (
+
+ {children}
+
+ )}
+
+ >
+ )}
);
}
diff --git a/src/components/layout/sidebar-provider.tsx b/src/components/layout/sidebar-provider.tsx
new file mode 100644
index 0000000..4e134b7
--- /dev/null
+++ b/src/components/layout/sidebar-provider.tsx
@@ -0,0 +1,60 @@
+"use client";
+
+import * as React from "react";
+
+interface SidebarContextType {
+ isCollapsed: boolean;
+ toggleCollapse: () => void;
+ expand: () => void;
+ collapse: () => void;
+}
+
+const SidebarContext = React.createContext
(
+ undefined,
+);
+
+export function SidebarProvider({ children }: { children: React.ReactNode }) {
+ const [isCollapsed, setIsCollapsed] = React.useState(false);
+
+ // Persist state if needed, for now just local state
+ React.useEffect(() => {
+ const saved = localStorage.getItem("sidebar-collapsed");
+ if (saved) {
+ setIsCollapsed(JSON.parse(saved));
+ }
+ }, []);
+
+ const toggleCollapse = React.useCallback(() => {
+ setIsCollapsed((prev) => {
+ const next = !prev;
+ localStorage.setItem("sidebar-collapsed", JSON.stringify(next));
+ return next;
+ });
+ }, []);
+
+ const expand = React.useCallback(() => {
+ setIsCollapsed(false);
+ localStorage.setItem("sidebar-collapsed", JSON.stringify(false));
+ }, []);
+
+ const collapse = React.useCallback(() => {
+ setIsCollapsed(true);
+ localStorage.setItem("sidebar-collapsed", JSON.stringify(true));
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useSidebar() {
+ const context = React.useContext(SidebarContext);
+ if (context === undefined) {
+ throw new Error("useSidebar must be used within a SidebarProvider");
+ }
+ return context;
+}
diff --git a/src/components/layout/sidebar.tsx b/src/components/layout/sidebar.tsx
index 52033ae..bb17677 100644
--- a/src/components/layout/sidebar.tsx
+++ b/src/components/layout/sidebar.tsx
@@ -5,102 +5,218 @@ import { usePathname } from "next/navigation";
import { authClient } from "~/lib/auth-client";
import { Skeleton } from "~/components/ui/skeleton";
import { Button } from "~/components/ui/button";
-import { LogOut, User } from "lucide-react";
+import {
+ LogOut,
+ User,
+ ChevronLeft,
+ ChevronRight,
+ PanelLeftClose,
+ PanelLeftOpen,
+ Settings
+} from "lucide-react";
import { navigationConfig } from "~/lib/navigation";
+import { useSidebar } from "./sidebar-provider";
+import { cn } from "~/lib/utils";
+import { Logo } from "~/components/branding/logo";
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "~/components/ui/tooltip";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "~/components/ui/dropdown-menu";
+import { getGravatarUrl } from "~/lib/gravatar";
+import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar";
-export function Sidebar() {
+interface SidebarProps {
+ mobile?: boolean;
+ onClose?: () => void;
+}
+
+export function Sidebar({ mobile, onClose }: SidebarProps) {
const pathname = usePathname();
const { data: session, isPending } = authClient.useSession();
+ const { isCollapsed, toggleCollapse } = useSidebar();
- return (
-