Begin dark mode!

This commit is contained in:
2025-07-12 21:46:26 -04:00
parent 07f190bce2
commit fa4bd886b3
23 changed files with 2189 additions and 1030 deletions

View File

@@ -1,27 +1,32 @@
"use client"
"use client";
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon, Search } from "lucide-react"
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import {
CheckIcon,
ChevronDownIcon,
ChevronUpIcon,
Search,
} from "lucide-react";
import { cn } from "~/lib/utils"
import { cn } from "~/lib/utils";
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />
return <SelectPrimitive.Root data-slot="select" {...props} />;
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
}
function SelectTrigger({
@@ -30,15 +35,15 @@ function SelectTrigger({
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default"
size?: "sm" | "default";
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"flex w-full items-center justify-between gap-2 rounded-md border border-gray-200 bg-gray-50 px-3 h-10 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-emerald-500 focus-visible:ring-emerald-500 focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[placeholder]:text-muted-foreground",
className
"data-[placeholder]:text-muted-foreground flex h-10 w-full items-center justify-between gap-2 rounded-md border border-gray-200 bg-gray-50 px-3 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-emerald-500 focus-visible:ring-[3px] focus-visible:ring-emerald-500 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-700 dark:text-white",
className,
)}
{...props}
>
@@ -47,7 +52,7 @@ function SelectTrigger({
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
);
}
function SelectContent({
@@ -64,7 +69,7 @@ function SelectContent({
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
className,
)}
position={position}
{...props}
@@ -74,7 +79,7 @@ function SelectContent({
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
)}
>
{children}
@@ -82,7 +87,7 @@ function SelectContent({
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
);
}
function SelectLabel({
@@ -95,7 +100,7 @@ function SelectLabel({
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
)
);
}
function SelectItem({
@@ -108,7 +113,7 @@ function SelectItem({
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
className,
)}
{...props}
>
@@ -119,7 +124,7 @@ function SelectItem({
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
);
}
function SelectSeparator({
@@ -132,7 +137,7 @@ function SelectSeparator({
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
)
);
}
function SelectScrollUpButton({
@@ -144,13 +149,13 @@ function SelectScrollUpButton({
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
className,
)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
)
);
}
function SelectScrollDownButton({
@@ -162,13 +167,13 @@ function SelectScrollDownButton({
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
className,
)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
)
);
}
// Enhanced SelectContent with search functionality
@@ -208,7 +213,7 @@ function SelectContentWithSearch({
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-96 min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-hidden rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
className,
)}
position={position}
onEscapeKeyDown={(e) => {
@@ -226,11 +231,11 @@ function SelectContentWithSearch({
{...props}
>
{onSearchChange && (
<div className="flex items-center px-3 py-2 border-b">
<div className="border-border flex items-center border-b px-3 py-2">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<input
ref={searchInputRef}
className="flex h-8 w-full rounded-md bg-transparent py-2 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50 border-0 focus:ring-0 focus:outline-none"
className="placeholder:text-muted-foreground text-foreground flex h-8 w-full rounded-md border-0 bg-transparent py-2 text-sm outline-none focus:ring-0 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
placeholder={searchPlaceholder}
value={searchValue}
onChange={(e) => onSearchChange(e.target.value)}
@@ -240,7 +245,11 @@ function SelectContentWithSearch({
e.stopPropagation();
}
// Prevent arrow keys from moving focus away from search
if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) {
if (
["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(
e.key,
)
) {
e.stopPropagation();
}
}}
@@ -255,7 +264,9 @@ function SelectContentWithSearch({
<SelectScrollUpButton />
<SelectPrimitive.Viewport className="p-1">
{filteredOptions && filteredOptions.length === 0 ? (
<div className="px-3 py-2 text-sm text-muted-foreground select-none">No results found</div>
<div className="text-muted-foreground px-3 py-2 text-sm select-none">
No results found
</div>
) : (
children
)}
@@ -263,7 +274,7 @@ function SelectContentWithSearch({
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
);
}
// Searchable Select component
@@ -284,21 +295,21 @@ function SearchableSelect({
options,
searchPlaceholder = "Search...",
className,
disabled
disabled,
}: SearchableSelectProps) {
const [searchValue, setSearchValue] = React.useState("");
const [isOpen, setIsOpen] = React.useState(false);
const filteredOptions = React.useMemo(() => {
if (!searchValue) return options;
return options.filter(option =>
option.label.toLowerCase().includes(searchValue.toLowerCase())
return options.filter((option) =>
option.label.toLowerCase().includes(searchValue.toLowerCase()),
);
}, [options, searchValue]);
// Convert empty string to placeholder value for display
const displayValue = value === "" ? "__placeholder__" : value;
// Convert placeholder value back to empty string when selected
const handleValueChange = (newValue: string) => {
const actualValue = newValue === "__placeholder__" ? "" : newValue;
@@ -309,9 +320,9 @@ function SearchableSelect({
};
return (
<Select
value={displayValue}
onValueChange={handleValueChange}
<Select
value={displayValue}
onValueChange={handleValueChange}
disabled={disabled}
open={isOpen}
onOpenChange={setIsOpen}
@@ -353,4 +364,4 @@ export {
SelectTrigger,
SelectValue,
SearchableSelect,
}
};

View File

@@ -1,4 +1,4 @@
import { cn } from "~/lib/utils"
import { cn } from "~/lib/utils";
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
@@ -7,37 +7,43 @@ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
className={cn("bg-muted animate-pulse rounded-md", className)}
{...props}
/>
)
);
}
// Dashboard skeleton components
export function DashboardStatsSkeleton() {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div className="mb-8 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="shadow-xl border-0 bg-white/80 backdrop-blur-sm rounded-xl p-6">
<div className="flex items-center justify-between mb-4">
<div
key={i}
className="rounded-xl border-0 bg-white/80 p-6 shadow-xl backdrop-blur-sm dark:bg-gray-800/80"
>
<div className="mb-4 flex items-center justify-between">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-8 w-8 rounded-lg" />
</div>
<Skeleton className="h-8 w-16 mb-2" />
<Skeleton className="mb-2 h-8 w-16" />
<Skeleton className="h-3 w-32" />
</div>
))}
</div>
)
);
}
export function DashboardCardsSkeleton() {
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
<div className="mb-8 grid grid-cols-1 gap-8 lg:grid-cols-2">
{Array.from({ length: 2 }).map((_, i) => (
<div key={i} className="shadow-xl border-0 bg-white/80 backdrop-blur-sm rounded-xl p-6">
<div className="flex items-center gap-2 mb-4">
<div
key={i}
className="rounded-xl border-0 bg-white/80 p-6 shadow-xl backdrop-blur-sm dark:bg-gray-800/80"
>
<div className="mb-4 flex items-center gap-2">
<Skeleton className="h-8 w-8 rounded-lg" />
<Skeleton className="h-6 w-32" />
</div>
<Skeleton className="h-4 w-full mb-4" />
<Skeleton className="mb-4 h-4 w-full" />
<div className="flex gap-3">
<Skeleton className="h-10 w-24" />
<Skeleton className="h-10 w-32" />
@@ -45,20 +51,20 @@ export function DashboardCardsSkeleton() {
</div>
))}
</div>
)
);
}
export function DashboardActivitySkeleton() {
return (
<div className="shadow-xl border-0 bg-white/80 backdrop-blur-sm rounded-xl p-6">
<Skeleton className="h-6 w-32 mb-6" />
<div className="text-center py-12">
<Skeleton className="h-20 w-20 rounded-full mx-auto mb-4" />
<Skeleton className="h-6 w-48 mx-auto mb-2" />
<Skeleton className="h-4 w-64 mx-auto" />
<div className="rounded-xl border-0 bg-white/80 p-6 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
<Skeleton className="mb-6 h-6 w-32" />
<div className="py-12 text-center">
<Skeleton className="mx-auto mb-4 h-20 w-20 rounded-full" />
<Skeleton className="mx-auto mb-2 h-6 w-48" />
<Skeleton className="mx-auto h-4 w-64" />
</div>
</div>
)
);
}
// Table skeleton components
@@ -66,17 +72,17 @@ export function TableSkeleton({ rows = 5 }: { rows?: number }) {
return (
<div className="space-y-4">
{/* Search and filters */}
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex flex-col gap-4 sm:flex-row">
<Skeleton className="h-10 w-64" />
<div className="flex gap-2">
<Skeleton className="h-10 w-24" />
<Skeleton className="h-10 w-24" />
</div>
</div>
{/* Table */}
<div className="border rounded-lg">
<div className="p-4 border-b">
<div className="rounded-lg border">
<div className="border-b p-4">
<div className="flex items-center justify-between">
<Skeleton className="h-4 w-32" />
<div className="flex gap-2">
@@ -85,7 +91,7 @@ export function TableSkeleton({ rows = 5 }: { rows?: number }) {
</div>
</div>
</div>
<div className="p-4">
<div className="space-y-3">
{Array.from({ length: rows }).map((_, i) => (
@@ -101,7 +107,7 @@ export function TableSkeleton({ rows = 5 }: { rows?: number }) {
</div>
</div>
</div>
{/* Pagination */}
<div className="flex items-center justify-between">
<Skeleton className="h-4 w-32" />
@@ -112,7 +118,7 @@ export function TableSkeleton({ rows = 5 }: { rows?: number }) {
</div>
</div>
</div>
)
);
}
// Form skeleton components
@@ -121,36 +127,36 @@ export function FormSkeleton() {
<div className="space-y-6">
<div className="space-y-4">
<div>
<Skeleton className="h-4 w-20 mb-2" />
<Skeleton className="mb-2 h-4 w-20" />
<Skeleton className="h-10 w-full" />
</div>
<div>
<Skeleton className="h-4 w-24 mb-2" />
<Skeleton className="mb-2 h-4 w-24" />
<Skeleton className="h-10 w-full" />
</div>
<div>
<Skeleton className="h-4 w-16 mb-2" />
<Skeleton className="mb-2 h-4 w-16" />
<Skeleton className="h-10 w-full" />
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<Skeleton className="h-4 w-20 mb-2" />
<Skeleton className="mb-2 h-4 w-20" />
<Skeleton className="h-10 w-full" />
</div>
<div>
<Skeleton className="h-4 w-16 mb-2" />
<Skeleton className="mb-2 h-4 w-16" />
<Skeleton className="h-10 w-full" />
</div>
</div>
<div className="flex gap-3">
<Skeleton className="h-10 w-24" />
<Skeleton className="h-10 w-24" />
</div>
</div>
)
);
}
// Invoice view skeleton
@@ -158,16 +164,16 @@ export function InvoiceViewSkeleton() {
return (
<div className="space-y-6">
{/* Header */}
<div className="flex justify-between items-start">
<div className="flex items-start justify-between">
<div className="space-y-2">
<Skeleton className="h-8 w-48" />
<Skeleton className="h-4 w-64" />
</div>
<Skeleton className="h-10 w-32" />
</div>
{/* Client info */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<div className="space-y-3">
<Skeleton className="h-5 w-24" />
<Skeleton className="h-4 w-full" />
@@ -180,10 +186,10 @@ export function InvoiceViewSkeleton() {
<Skeleton className="h-4 w-3/4" />
</div>
</div>
{/* Items table */}
<div className="border rounded-lg">
<div className="p-4 border-b">
<div className="rounded-lg border">
<div className="border-b p-4">
<Skeleton className="h-5 w-32" />
</div>
<div className="p-4">
@@ -200,7 +206,7 @@ export function InvoiceViewSkeleton() {
</div>
</div>
</div>
{/* Total */}
<div className="flex justify-end">
<div className="space-y-2">
@@ -209,7 +215,7 @@ export function InvoiceViewSkeleton() {
</div>
</div>
</div>
)
);
}
export { Skeleton }
export { Skeleton };

View File

@@ -104,10 +104,10 @@ interface Business {
}
const statusColors = {
draft: "bg-gray-100 text-gray-800",
sent: "bg-blue-100 text-blue-800",
paid: "bg-green-100 text-green-800",
overdue: "bg-red-100 text-red-800",
draft: "bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300",
sent: "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400",
paid: "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400",
overdue: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400",
} as const;
const statusLabels = {
@@ -503,7 +503,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
/>
</TableHead>
<TableHead
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50"
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800"
onClick={() => handleSort("name")}
>
<div className="flex items-center gap-1">
@@ -515,10 +515,10 @@ export function UniversalTable({ resource }: UniversalTableProps) {
)}
</div>
</TableHead>
<TableHead className="px-4 py-4 text-base font-semibold text-gray-700">
<TableHead className="px-4 py-4 text-base font-semibold text-gray-700 dark:text-gray-300">
Email
</TableHead>
<TableHead className="px-4 py-4 text-base font-semibold text-gray-700">
<TableHead className="px-4 py-4 text-base font-semibold text-gray-700 dark:text-gray-300">
Phone
</TableHead>
<TableHead className="w-8 px-4 py-4"></TableHead>
@@ -536,7 +536,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
/>
</TableHead>
<TableHead
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50"
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800"
onClick={() => handleSort("invoiceNumber")}
>
<div className="flex items-center gap-1">
@@ -549,7 +549,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
</div>
</TableHead>
<TableHead
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50"
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800"
onClick={() => handleSort("client.name")}
>
<div className="flex items-center gap-1">
@@ -562,7 +562,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
</div>
</TableHead>
<TableHead
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50"
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800"
onClick={() => handleSort("status")}
>
<div className="flex items-center gap-1">
@@ -575,7 +575,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
</div>
</TableHead>
<TableHead
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50"
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800"
onClick={() => handleSort("totalAmount")}
>
<div className="flex items-center gap-1">
@@ -588,7 +588,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
</div>
</TableHead>
<TableHead
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50"
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800"
onClick={() => handleSort("dueDate")}
>
<div className="flex items-center gap-1">
@@ -615,21 +615,21 @@ export function UniversalTable({ resource }: UniversalTableProps) {
/>
</TableHead>
<TableHead
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50"
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800"
onClick={() => handleSort("name")}
>
Name
</TableHead>
<TableHead className="px-4 py-4 text-base font-semibold text-gray-700">
<TableHead className="px-4 py-4 text-base font-semibold text-gray-700 dark:text-gray-300">
Email
</TableHead>
<TableHead className="px-4 py-4 text-base font-semibold text-gray-700">
<TableHead className="px-4 py-4 text-base font-semibold text-gray-700 dark:text-gray-300">
Phone
</TableHead>
<TableHead className="px-4 py-4 text-base font-semibold text-gray-700">
<TableHead className="px-4 py-4 text-base font-semibold text-gray-700 dark:text-gray-300">
Website
</TableHead>
<TableHead className="px-4 py-4 text-base font-semibold text-gray-700">
<TableHead className="px-4 py-4 text-base font-semibold text-gray-700 dark:text-gray-300">
Default
</TableHead>
<TableHead className="w-8 px-4 py-4"></TableHead>
@@ -691,7 +691,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
<TableRow>
<TableCell
colSpan={colSpan}
className="py-12 text-center text-gray-500"
className="py-12 text-center text-gray-500 dark:text-gray-400"
>
<div className="flex flex-col items-center gap-2">
{resource === "clients" ? (
@@ -701,8 +701,10 @@ export function UniversalTable({ resource }: UniversalTableProps) {
) : (
<FileText className="mb-2 h-8 w-8 text-emerald-400" />
)}
<div className="text-lg font-semibold">No {resource} found</div>
<div className="mb-2 text-gray-500">
<div className="text-lg font-semibold dark:text-gray-300">
No {resource} found
</div>
<div className="mb-2 text-gray-500 dark:text-gray-400">
Get started by adding your first{" "}
{getSingularResourceName(resource).toLowerCase()}.
</div>
@@ -728,7 +730,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
<TableRow
key={client.id}
data-selected={selected.includes(client.id)}
className="group cursor-pointer transition-colors hover:bg-emerald-50/60"
className="group cursor-pointer transition-colors hover:bg-emerald-50/60 dark:hover:bg-emerald-900/20"
onClick={(e) => {
if (
(e.target as HTMLElement).closest(
@@ -750,7 +752,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
className="data-[state=checked]:border-emerald-600 data-[state=checked]:bg-emerald-600"
/>
</TableCell>
<TableCell className="px-4 py-4 text-base font-medium text-gray-900 group-hover:text-emerald-700">
<TableCell className="px-4 py-4 text-base font-medium text-gray-900 group-hover:text-emerald-700 dark:text-white dark:group-hover:text-emerald-400">
<Link
href={`/dashboard/clients/${client.id}/edit`}
className="hover:underline"
@@ -758,10 +760,10 @@ export function UniversalTable({ resource }: UniversalTableProps) {
{client.name}
</Link>
</TableCell>
<TableCell className="px-4 py-4 text-gray-700">
<TableCell className="px-4 py-4 text-gray-700 dark:text-gray-300">
{client.email}
</TableCell>
<TableCell className="px-4 py-4 text-gray-700">
<TableCell className="px-4 py-4 text-gray-700 dark:text-gray-300">
{client.phone}
</TableCell>
<TableCell
@@ -797,7 +799,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
<TableRow
key={invoice.id}
data-selected={selected.includes(invoice.id)}
className="group cursor-pointer transition-colors hover:bg-emerald-50/60"
className="group cursor-pointer transition-colors hover:bg-emerald-50/60 dark:hover:bg-emerald-900/20"
onClick={(e) => {
if (
(e.target as HTMLElement).closest(
@@ -819,7 +821,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
className="data-[state=checked]:border-emerald-600 data-[state=checked]:bg-emerald-600"
/>
</TableCell>
<TableCell className="px-4 py-4 text-base font-medium text-gray-900 group-hover:text-emerald-700">
<TableCell className="px-4 py-4 text-base font-medium text-gray-900 group-hover:text-emerald-700 dark:text-white dark:group-hover:text-emerald-400">
<Link
href={`/dashboard/invoices/${invoice.id}`}
className="hover:underline"
@@ -827,7 +829,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
{invoice.invoiceNumber}
</Link>
</TableCell>
<TableCell className="px-4 py-4 text-gray-700">
<TableCell className="px-4 py-4 text-gray-700 dark:text-gray-300">
{invoice.client?.name}
</TableCell>
<TableCell className="px-4 py-4">
@@ -837,10 +839,10 @@ export function UniversalTable({ resource }: UniversalTableProps) {
{statusLabels[invoice.status]}
</span>
</TableCell>
<TableCell className="px-4 py-4 font-medium text-gray-700">
<TableCell className="px-4 py-4 font-medium text-gray-700 dark:text-gray-300">
{formatCurrency(invoice.totalAmount)}
</TableCell>
<TableCell className="px-4 py-4 text-gray-700">
<TableCell className="px-4 py-4 text-gray-700 dark:text-gray-300">
{formatDate(invoice.dueDate)}
</TableCell>
<TableCell
@@ -885,7 +887,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
<TableRow
key={business.id}
data-selected={selected.includes(business.id)}
className="group cursor-pointer transition-colors hover:bg-emerald-50/60"
className="group cursor-pointer transition-colors hover:bg-emerald-50/60 dark:hover:bg-emerald-900/20"
onClick={(e) => {
if (
(e.target as HTMLElement).closest(
@@ -907,7 +909,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
className="data-[state=checked]:border-emerald-600 data-[state=checked]:bg-emerald-600"
/>
</TableCell>
<TableCell className="px-4 py-4 text-base font-medium text-gray-900 group-hover:text-emerald-700">
<TableCell className="px-4 py-4 text-base font-medium text-gray-900 group-hover:text-emerald-700 dark:text-white dark:group-hover:text-emerald-400">
<Link
href={`/dashboard/businesses/${business.id}/edit`}
className="hover:underline"
@@ -915,18 +917,18 @@ export function UniversalTable({ resource }: UniversalTableProps) {
{business.name}
</Link>
</TableCell>
<TableCell className="px-4 py-4 text-gray-700">
<TableCell className="px-4 py-4 text-gray-700 dark:text-gray-300">
{business.email}
</TableCell>
<TableCell className="px-4 py-4 text-gray-700">
<TableCell className="px-4 py-4 text-gray-700 dark:text-gray-300">
{business.phone}
</TableCell>
<TableCell className="px-4 py-4 text-gray-700">
<TableCell className="px-4 py-4 text-gray-700 dark:text-gray-300">
{business.website}
</TableCell>
<TableCell className="px-4 py-4 text-gray-700">
<TableCell className="px-4 py-4 text-gray-700 dark:text-gray-300">
{business.isDefault ? (
<span className="rounded-full bg-emerald-100 px-2 py-1 text-xs font-medium text-emerald-800">
<span className="rounded-full bg-emerald-100 px-2 py-1 text-xs font-medium text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-400">
Default
</span>
) : (
@@ -968,7 +970,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
return (
<div className="w-full">
{/* Controls */}
<div className="mb-4 flex flex-wrap items-center gap-3 rounded-lg border border-gray-200 bg-white/90 p-4 shadow-sm">
<div className="mb-4 flex flex-wrap items-center gap-3 rounded-lg border border-gray-200 bg-white/90 p-4 shadow-sm dark:border-gray-700 dark:bg-gray-800/90">
{/* Left side - View controls and filters */}
<div className="flex items-center gap-2">
<Button
@@ -996,7 +998,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-48">
<DropdownMenuItem className="font-medium text-gray-700">
<DropdownMenuItem className="font-medium text-gray-700 dark:text-gray-300">
Filters
</DropdownMenuItem>
{resource === "invoices" && (
@@ -1005,7 +1007,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
onClick={() => setStatusFilter("all")}
className={
statusFilter === "all"
? "bg-emerald-50 text-emerald-700"
? "bg-emerald-50 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400"
: ""
}
>
@@ -1015,7 +1017,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
onClick={() => setStatusFilter("draft")}
className={
statusFilter === "draft"
? "bg-emerald-50 text-emerald-700"
? "bg-emerald-50 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400"
: ""
}
>
@@ -1025,7 +1027,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
onClick={() => setStatusFilter("sent")}
className={
statusFilter === "sent"
? "bg-emerald-50 text-emerald-700"
? "bg-emerald-50 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400"
: ""
}
>
@@ -1035,7 +1037,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
onClick={() => setStatusFilter("paid")}
className={
statusFilter === "paid"
? "bg-emerald-50 text-emerald-700"
? "bg-emerald-50 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400"
: ""
}
>
@@ -1045,7 +1047,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
onClick={() => setStatusFilter("overdue")}
className={
statusFilter === "overdue"
? "bg-emerald-50 text-emerald-700"
? "bg-emerald-50 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400"
: ""
}
>
@@ -1065,7 +1067,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
placeholder={`Search ${resource}...`}
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-48 sm:w-64"
className="w-48 sm:w-64 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
/>
<Button variant="outline" size="icon">
<Search className="h-4 w-4" />
@@ -1075,7 +1077,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
{/* Batch actions */}
{selected.length > 0 && (
<>
<span className="hidden text-sm text-gray-500 sm:inline">
<span className="hidden text-sm text-gray-500 sm:inline dark:text-gray-400">
{selected.length} selected
</span>
{resource === "invoices" && (
@@ -1124,7 +1126,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
</div>
{/* Table View */}
{view === "table" && (
<div className="overflow-hidden rounded-2xl border border-gray-200 bg-white/90 shadow-xl">
<div className="overflow-hidden rounded-2xl border border-gray-200 bg-white/90 shadow-xl dark:border-gray-700 dark:bg-gray-800/90">
<Table className="w-full">
<TableHeader>
<TableRow>{renderTableHeaders()}</TableRow>
@@ -1135,9 +1137,9 @@ export function UniversalTable({ resource }: UniversalTableProps) {
)}
{/* Pagination Controls */}
{view === "table" && totalPages > 1 && (
<div className="mt-4 mb-4 flex flex-wrap items-center justify-between gap-3 rounded-2xl border border-gray-200 bg-white/90 p-4 shadow-sm">
<div className="mt-4 mb-4 flex flex-wrap items-center justify-between gap-3 rounded-2xl border border-gray-200 bg-white/90 p-4 shadow-sm dark:border-gray-700 dark:bg-gray-800/90">
{/* Left side - Page info and items per page */}
<div className="flex items-center gap-3 text-sm text-gray-600">
<div className="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
<span className="hidden sm:inline">
Showing {startIndex + 1} to{" "}
{Math.min(endIndex, filteredAndSortedData.length)} of{" "}
@@ -1154,7 +1156,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
setItemsPerPage(Number(e.target.value));
setCurrentPage(1);
}}
className="h-8 w-20 rounded-md border border-gray-300 bg-white px-2 py-1 text-sm focus:border-emerald-500 focus:ring-emerald-500 sm:w-28"
className="h-8 w-20 rounded-md border border-gray-300 bg-white px-2 py-1 text-sm focus:border-emerald-500 focus:ring-emerald-500 sm:w-28 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
>
<option value={5}>5</option>
<option value={10}>10</option>
@@ -1220,7 +1222,10 @@ export function UniversalTable({ resource }: UniversalTableProps) {
pageNum === currentPage + 2
) {
return (
<span key={pageNum} className="px-1 text-gray-400 sm:px-2">
<span
key={pageNum}
className="px-1 text-gray-400 sm:px-2 dark:text-gray-500"
>
...
</span>
);
@@ -1251,7 +1256,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
Array.from({ length: 6 }).map((_, index) => (
<div
key={`skeleton-card-${index}`}
className="flex flex-col gap-2 rounded-2xl border border-gray-200 bg-white/90 p-4 shadow-xl"
className="flex flex-col gap-2 rounded-2xl border border-gray-200 bg-white/90 p-4 shadow-xl dark:border-gray-700 dark:bg-gray-800/90"
>
<Skeleton className="h-6 w-32" />
<Skeleton className="h-4 w-40" />
@@ -1259,7 +1264,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
</div>
))
) : filteredAndSortedData.length === 0 ? (
<div className="col-span-full flex flex-col items-center py-16 text-gray-500">
<div className="col-span-full flex flex-col items-center py-16 text-gray-500 dark:text-gray-400">
{resource === "clients" ? (
<UserPlus className="mb-2 h-8 w-8 text-emerald-400" />
) : resource === "businesses" ? (
@@ -1267,8 +1272,10 @@ export function UniversalTable({ resource }: UniversalTableProps) {
) : (
<FileText className="mb-2 h-8 w-8 text-emerald-400" />
)}
<div className="text-lg font-semibold">No {resource} found</div>
<div className="mb-2 text-gray-500">
<div className="text-lg font-semibold dark:text-gray-300">
No {resource} found
</div>
<div className="mb-2 text-gray-500 dark:text-gray-400">
Get started by adding your first{" "}
{getSingularResourceName(resource).toLowerCase()}.
</div>
@@ -1289,13 +1296,17 @@ export function UniversalTable({ resource }: UniversalTableProps) {
return (
<div
key={client.id}
className="flex cursor-pointer flex-col gap-2 rounded-2xl border border-gray-200 bg-white/90 p-4 shadow-xl transition-colors hover:bg-emerald-50/60"
className="flex cursor-pointer flex-col gap-2 rounded-2xl border border-gray-200 bg-white/90 p-4 shadow-xl transition-colors hover:bg-emerald-50/60 dark:border-gray-700 dark:bg-gray-800/90 dark:hover:bg-emerald-900/20"
>
<div className="text-lg font-semibold text-gray-900 group-hover:text-emerald-700">
<div className="text-lg font-semibold text-gray-900 group-hover:text-emerald-700 dark:text-white dark:group-hover:text-emerald-400">
{client.name}
</div>
<div className="text-sm text-gray-700">{client.email}</div>
<div className="text-sm text-gray-700">{client.phone}</div>
<div className="text-sm text-gray-700 dark:text-gray-300">
{client.email}
</div>
<div className="text-sm text-gray-700 dark:text-gray-300">
{client.phone}
</div>
</div>
);
} else if (resource === "invoices") {
@@ -1303,15 +1314,15 @@ export function UniversalTable({ resource }: UniversalTableProps) {
return (
<div
key={invoice.id}
className="flex cursor-pointer flex-col gap-2 rounded-2xl border border-gray-200 bg-white/90 p-4 shadow-xl transition-colors hover:bg-emerald-50/60"
className="flex cursor-pointer flex-col gap-2 rounded-2xl border border-gray-200 bg-white/90 p-4 shadow-xl transition-colors hover:bg-emerald-50/60 dark:border-gray-700 dark:bg-gray-800/90 dark:hover:bg-emerald-900/20"
>
<div className="text-lg font-semibold text-gray-900 group-hover:text-emerald-700">
<div className="text-lg font-semibold text-gray-900 group-hover:text-emerald-700 dark:text-white dark:group-hover:text-emerald-400">
{invoice.invoiceNumber}
</div>
<div className="text-sm text-gray-700">
<div className="text-sm text-gray-700 dark:text-gray-300">
{invoice.client?.name}
</div>
<div className="text-sm text-gray-700">
<div className="text-sm text-gray-700 dark:text-gray-300">
{formatCurrency(invoice.totalAmount)}
</div>
</div>
@@ -1321,15 +1332,15 @@ export function UniversalTable({ resource }: UniversalTableProps) {
return (
<div
key={business.id}
className="flex cursor-pointer flex-col gap-2 rounded-2xl border border-gray-200 bg-white/90 p-4 shadow-xl transition-colors hover:bg-emerald-50/60"
className="flex cursor-pointer flex-col gap-2 rounded-2xl border border-gray-200 bg-white/90 p-4 shadow-xl transition-colors hover:bg-emerald-50/60 dark:border-gray-700 dark:bg-gray-800/90 dark:hover:bg-emerald-900/20"
>
<div className="text-lg font-semibold text-gray-900 group-hover:text-emerald-700">
<div className="text-lg font-semibold text-gray-900 group-hover:text-emerald-700 dark:text-white dark:group-hover:text-emerald-400">
{business.name}
</div>
<div className="text-sm text-gray-700">
<div className="text-sm text-gray-700 dark:text-gray-300">
{business.email}
</div>
<div className="text-sm text-gray-700">
<div className="text-sm text-gray-700 dark:text-gray-300">
{business.phone}
</div>
</div>
@@ -1341,15 +1352,15 @@ export function UniversalTable({ resource }: UniversalTableProps) {
)}
{/* Confirmation Dialog */}
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<DialogContent className="border-0 bg-white/95 shadow-2xl backdrop-blur-sm">
<DialogContent className="border-0 bg-white/95 shadow-2xl backdrop-blur-sm dark:bg-gray-800/95">
<DialogHeader>
<DialogTitle className="text-xl font-bold text-gray-800">
<DialogTitle className="text-xl font-bold text-gray-800 dark:text-white">
Delete{" "}
{resource.slice(0, -1).charAt(0).toUpperCase() +
resource.slice(0, -1).slice(1)}
{itemToDelete === "batch" ? "s" : ""}
</DialogTitle>
<DialogDescription className="text-gray-600">
<DialogDescription className="text-gray-600 dark:text-gray-300">
{itemToDelete === "batch"
? `Are you sure you want to delete the selected ${resource}? This action cannot be undone.`
: `Are you sure you want to delete this ${resource.slice(0, -1)}? This action cannot be undone.`}
@@ -1359,7 +1370,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
<Button
variant="outline"
onClick={() => setDeleteDialogOpen(false)}
className="border-gray-300 text-gray-700 hover:bg-gray-50"
className="border-gray-300 text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800"
>
Cancel
</Button>