Reformat files.

This commit is contained in:
Elisiário Couto
2025-09-22 23:00:27 +01:00
committed by Elisiário Couto
parent 65404848aa
commit eb38264c68
29 changed files with 627 additions and 509 deletions

View File

@@ -4,11 +4,17 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" sizes="48x48" /> <link rel="icon" href="/favicon.ico" sizes="48x48" />
<link rel="icon" href="/favicon.svg" sizes="any" type="image/svg+xml" /> <link rel="icon" href="/favicon.svg" sizes="any" type="image/svg+xml" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" /> <meta
name="viewport"
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
/>
<title>Leggen</title> <title>Leggen</title>
<!-- PWA Meta Tags --> <!-- PWA Meta Tags -->
<meta name="description" content="Personal finance management application" /> <meta
name="description"
content="Personal finance management application"
/>
<meta name="application-name" content="Leggen" /> <meta name="application-name" content="Leggen" />
<meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" /> <meta name="apple-mobile-web-app-status-bar-style" content="default" />
@@ -21,8 +27,16 @@
<!-- Dynamic theme-color - will be updated by JavaScript --> <!-- Dynamic theme-color - will be updated by JavaScript -->
<meta name="theme-color" content="#0b74de" id="theme-color-meta" /> <meta name="theme-color" content="#0b74de" id="theme-color-meta" />
<meta name="msapplication-navbutton-color" content="#0b74de" id="ms-theme-color-meta" /> <meta
<meta name="apple-mobile-web-app-status-bar-style" content="default" id="apple-status-bar-meta" /> name="msapplication-navbutton-color"
content="#0b74de"
id="ms-theme-color-meta"
/>
<meta
name="apple-mobile-web-app-status-bar-style"
content="default"
id="apple-status-bar-meta"
/>
<!-- Icons --> <!-- Icons -->
<link rel="apple-touch-icon" href="/apple-touch-icon-180x180.png" /> <link rel="apple-touch-icon" href="/apple-touch-icon-180x180.png" />

View File

@@ -144,7 +144,8 @@ export default function AccountSettings() {
<CardHeader> <CardHeader>
<CardTitle>Account Management</CardTitle> <CardTitle>Account Management</CardTitle>
<CardDescription> <CardDescription>
Manage your connected bank accounts and customize their display names Manage your connected bank accounts and customize their display
names
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
@@ -324,7 +325,8 @@ export default function AccountSettings() {
<CardHeader> <CardHeader>
<CardTitle>Add New Bank Account</CardTitle> <CardTitle>Add New Bank Account</CardTitle>
<CardDescription> <CardDescription>
Connect additional bank accounts to track all your finances in one place Connect additional bank accounts to track all your finances in one
place
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="p-6"> <CardContent className="p-6">
@@ -332,7 +334,8 @@ export default function AccountSettings() {
<div className="p-4 bg-muted rounded-lg"> <div className="p-4 bg-muted rounded-lg">
<Plus className="h-8 w-8 text-muted-foreground mx-auto mb-2" /> <Plus className="h-8 w-8 text-muted-foreground mx-auto mb-2" />
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Bank connection functionality is coming soon. Stay tuned for updates! Bank connection functionality is coming soon. Stay tuned for
updates!
</p> </p>
</div> </div>
<Button disabled variant="outline"> <Button disabled variant="outline">

View File

@@ -69,7 +69,11 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
asChild asChild
className="data-[slot=sidebar-menu-button]:!p-1.5" className="data-[slot=sidebar-menu-button]:!p-1.5"
> >
<Link to="/" className="flex items-center space-x-2" onClick={handleNavigationClick}> <Link
to="/"
className="flex items-center space-x-2"
onClick={handleNavigationClick}
>
<Logo size={24} /> <Logo size={24} />
<span className="text-base font-semibold">Leggen</span> <span className="text-base font-semibold">Leggen</span>
</Link> </Link>
@@ -138,7 +142,10 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<div className="border-t border-border/50 max-h-48 overflow-y-auto"> <div className="border-t border-border/50 max-h-48 overflow-y-auto">
{accounts.map((account) => { {accounts.map((account) => {
const primaryBalance = account.balances?.[0]?.amount || 0; const primaryBalance = account.balances?.[0]?.amount || 0;
const currency = account.balances?.[0]?.currency || account.currency || "EUR"; const currency =
account.balances?.[0]?.currency ||
account.currency ||
"EUR";
return ( return (
<div <div
@@ -151,7 +158,9 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
</div> </div>
<div className="space-y-1 min-w-0 flex-1"> <div className="space-y-1 min-w-0 flex-1">
<p className="text-xs font-medium text-foreground truncate"> <p className="text-xs font-medium text-foreground truncate">
{account.display_name || account.name || "Unnamed Account"} {account.display_name ||
account.name ||
"Unnamed Account"}
</p> </p>
<p className="text-xs font-semibold text-foreground"> <p className="text-xs font-semibold text-foreground">
{formatCurrency(primaryBalance, currency)} {formatCurrency(primaryBalance, currency)}

View File

@@ -36,7 +36,11 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "./ui/select"; } from "./ui/select";
import type { NotificationSettings, NotificationService, SyncOperationsResponse } from "../types/api"; import type {
NotificationSettings,
NotificationService,
SyncOperationsResponse,
} from "../types/api";
export default function Notifications() { export default function Notifications() {
const [testService, setTestService] = useState(""); const [testService, setTestService] = useState("");
@@ -163,7 +167,8 @@ export default function Notifications() {
No sync operations yet No sync operations yet
</h3> </h3>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Sync operations will appear here once you start syncing your accounts. Sync operations will appear here once you start syncing your
accounts.
</p> </p>
</div> </div>
) : ( ) : (
@@ -173,7 +178,7 @@ export default function Notifications() {
const isRunning = !operation.completed_at; const isRunning = !operation.completed_at;
const duration = operation.duration_seconds const duration = operation.duration_seconds
? `${Math.round(operation.duration_seconds)}s` ? `${Math.round(operation.duration_seconds)}s`
: ''; : "";
return ( return (
<div <div
@@ -181,13 +186,15 @@ export default function Notifications() {
className="flex items-center justify-between p-4 border rounded-lg hover:bg-accent transition-colors" className="flex items-center justify-between p-4 border rounded-lg hover:bg-accent transition-colors"
> >
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<div className={`p-2 rounded-full ${ <div
className={`p-2 rounded-full ${
isRunning isRunning
? 'bg-blue-100 text-blue-600' ? "bg-blue-100 text-blue-600"
: operation.success : operation.success
? 'bg-green-100 text-green-600' ? "bg-green-100 text-green-600"
: 'bg-red-100 text-red-600' : "bg-red-100 text-red-600"
}`}> }`}
>
{isRunning ? ( {isRunning ? (
<RefreshCw className="h-4 w-4 animate-spin" /> <RefreshCw className="h-4 w-4 animate-spin" />
) : operation.success ? ( ) : operation.success ? (
@@ -199,7 +206,11 @@ export default function Notifications() {
<div> <div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<h4 className="text-sm font-medium text-foreground"> <h4 className="text-sm font-medium text-foreground">
{isRunning ? 'Sync Running' : operation.success ? 'Sync Completed' : 'Sync Failed'} {isRunning
? "Sync Running"
: operation.success
? "Sync Completed"
: "Sync Failed"}
</h4> </h4>
<Badge variant="outline" className="text-xs"> <Badge variant="outline" className="text-xs">
{operation.trigger_type} {operation.trigger_type}
@@ -208,11 +219,12 @@ export default function Notifications() {
<div className="flex items-center space-x-4 mt-1 text-xs text-muted-foreground"> <div className="flex items-center space-x-4 mt-1 text-xs text-muted-foreground">
<span className="flex items-center space-x-1"> <span className="flex items-center space-x-1">
<Clock className="h-3 w-3" /> <Clock className="h-3 w-3" />
<span>{startedAt.toLocaleDateString()} {startedAt.toLocaleTimeString()}</span> <span>
{startedAt.toLocaleDateString()}{" "}
{startedAt.toLocaleTimeString()}
</span> </span>
{duration && ( </span>
<span>Duration: {duration}</span> {duration && <span>Duration: {duration}</span>}
)}
</div> </div>
</div> </div>
</div> </div>
@@ -223,7 +235,9 @@ export default function Notifications() {
</div> </div>
<div className="flex items-center space-x-2 mt-1"> <div className="flex items-center space-x-2 mt-1">
<TrendingUp className="h-3 w-3" /> <TrendingUp className="h-3 w-3" />
<span>{operation.transactions_added} new transactions</span> <span>
{operation.transactions_added} new transactions
</span>
</div> </div>
{operation.errors.length > 0 && ( {operation.errors.length > 0 && (
<div className="flex items-center space-x-2 mt-1 text-red-600"> <div className="flex items-center space-x-2 mt-1 text-red-600">

View File

@@ -3,7 +3,7 @@ import { X, Download, RotateCcw } from "lucide-react";
interface BeforeInstallPromptEvent extends Event { interface BeforeInstallPromptEvent extends Event {
prompt(): Promise<void>; prompt(): Promise<void>;
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>; userChoice: Promise<{ outcome: "accepted" | "dismissed" }>;
} }
interface PWAPromptProps { interface PWAPromptProps {
@@ -11,7 +11,8 @@ interface PWAPromptProps {
} }
export function PWAInstallPrompt({ onInstall }: PWAPromptProps) { export function PWAInstallPrompt({ onInstall }: PWAPromptProps) {
const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null); const [deferredPrompt, setDeferredPrompt] =
useState<BeforeInstallPromptEvent | null>(null);
const [showPrompt, setShowPrompt] = useState(false); const [showPrompt, setShowPrompt] = useState(false);
useEffect(() => { useEffect(() => {
@@ -96,7 +97,10 @@ interface PWAUpdatePromptProps {
onUpdate: () => void; onUpdate: () => void;
} }
export function PWAUpdatePrompt({ updateAvailable, onUpdate }: PWAUpdatePromptProps) { export function PWAUpdatePrompt({
updateAvailable,
onUpdate,
}: PWAUpdatePromptProps) {
const [showPrompt, setShowPrompt] = useState(false); const [showPrompt, setShowPrompt] = useState(false);
useEffect(() => { useEffect(() => {

View File

@@ -43,7 +43,12 @@ import {
} from "./ui/select"; } from "./ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs";
import AccountsSkeleton from "./AccountsSkeleton"; import AccountsSkeleton from "./AccountsSkeleton";
import type { Account, Balance, NotificationSettings, NotificationService } from "../types/api"; import type {
Account,
Balance,
NotificationSettings,
NotificationService,
} from "../types/api";
// Helper function to get status indicator color and styles // Helper function to get status indicator color and styles
const getStatusIndicator = (status: string) => { const getStatusIndicator = (status: string) => {
@@ -83,7 +88,9 @@ export default function Settings() {
const [editingAccountId, setEditingAccountId] = useState<string | null>(null); const [editingAccountId, setEditingAccountId] = useState<string | null>(null);
const [editingName, setEditingName] = useState(""); const [editingName, setEditingName] = useState("");
const [testService, setTestService] = useState(""); const [testService, setTestService] = useState("");
const [testMessage, setTestMessage] = useState("Test notification from Leggen"); const [testMessage, setTestMessage] = useState(
"Test notification from Leggen",
);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -239,7 +246,10 @@ export default function Settings() {
<User className="h-4 w-4" /> <User className="h-4 w-4" />
<span>Accounts</span> <span>Accounts</span>
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="notifications" className="flex items-center space-x-2"> <TabsTrigger
value="notifications"
className="flex items-center space-x-2"
>
<Bell className="h-4 w-4" /> <Bell className="h-4 w-4" />
<span>Notifications</span> <span>Notifications</span>
</TabsTrigger> </TabsTrigger>
@@ -251,7 +261,8 @@ export default function Settings() {
<CardHeader> <CardHeader>
<CardTitle>Account Management</CardTitle> <CardTitle>Account Management</CardTitle>
<CardDescription> <CardDescription>
Manage your connected bank accounts and customize their display names Manage your connected bank accounts and customize their display
names
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
@@ -319,7 +330,8 @@ export default function Settings() {
autoComplete="off" autoComplete="off"
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter") handleEditSave(); if (e.key === "Enter") handleEditSave();
if (e.key === "Escape") handleEditCancel(); if (e.key === "Escape")
handleEditCancel();
}} }}
autoFocus autoFocus
/> />
@@ -428,7 +440,8 @@ export default function Settings() {
<CardHeader> <CardHeader>
<CardTitle>Add New Bank Account</CardTitle> <CardTitle>Add New Bank Account</CardTitle>
<CardDescription> <CardDescription>
Connect additional bank accounts to track all your finances in one place Connect additional bank accounts to track all your finances in
one place
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="p-6"> <CardContent className="p-6">
@@ -436,7 +449,8 @@ export default function Settings() {
<div className="p-4 bg-muted rounded-lg"> <div className="p-4 bg-muted rounded-lg">
<Plus className="h-8 w-8 text-muted-foreground mx-auto mb-2" /> <Plus className="h-8 w-8 text-muted-foreground mx-auto mb-2" />
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Bank connection functionality is coming soon. Stay tuned for updates! Bank connection functionality is coming soon. Stay tuned for
updates!
</p> </p>
</div> </div>
<Button disabled variant="outline"> <Button disabled variant="outline">
@@ -498,7 +512,9 @@ export default function Settings() {
disabled={!testService || testMutation.isPending} disabled={!testService || testMutation.isPending}
> >
<Send className="h-4 w-4 mr-2" /> <Send className="h-4 w-4 mr-2" />
{testMutation.isPending ? "Sending..." : "Send Test Notification"} {testMutation.isPending
? "Sending..."
: "Send Test Notification"}
</Button> </Button>
</div> </div>
</CardContent> </CardContent>
@@ -511,7 +527,9 @@ export default function Settings() {
<Bell className="h-5 w-5 text-primary" /> <Bell className="h-5 w-5 text-primary" />
<span>Notification Services</span> <span>Notification Services</span>
</CardTitle> </CardTitle>
<CardDescription>Manage your notification services</CardDescription> <CardDescription>
Manage your notification services
</CardDescription>
</CardHeader> </CardHeader>
{!services || services.length === 0 ? ( {!services || services.length === 0 ? (
@@ -521,7 +539,8 @@ export default function Settings() {
No notification services configured No notification services configured
</h3> </h3>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Configure notification services in your backend to receive alerts. Configure notification services in your backend to receive
alerts.
</p> </p>
</CardContent> </CardContent>
) : ( ) : (
@@ -537,7 +556,9 @@ export default function Settings() {
<div className="p-3 bg-muted rounded-full"> <div className="p-3 bg-muted rounded-full">
{service.name.toLowerCase().includes("discord") ? ( {service.name.toLowerCase().includes("discord") ? (
<MessageSquare className="h-6 w-6 text-muted-foreground" /> <MessageSquare className="h-6 w-6 text-muted-foreground" />
) : service.name.toLowerCase().includes("telegram") ? ( ) : service.name
.toLowerCase()
.includes("telegram") ? (
<Send className="h-6 w-6 text-muted-foreground" /> <Send className="h-6 w-6 text-muted-foreground" />
) : ( ) : (
<Bell className="h-6 w-6 text-muted-foreground" /> <Bell className="h-6 w-6 text-muted-foreground" />
@@ -612,8 +633,11 @@ export default function Settings() {
Case Insensitive Filters Case Insensitive Filters
</Label> </Label>
<p className="text-sm text-foreground"> <p className="text-sm text-foreground">
{notificationSettings.filters.case_insensitive.length > 0 {notificationSettings.filters.case_insensitive
? notificationSettings.filters.case_insensitive.join(", ") .length > 0
? notificationSettings.filters.case_insensitive.join(
", ",
)
: "None"} : "None"}
</p> </p>
</div> </div>
@@ -623,8 +647,11 @@ export default function Settings() {
</Label> </Label>
<p className="text-sm text-foreground"> <p className="text-sm text-foreground">
{notificationSettings.filters.case_sensitive && {notificationSettings.filters.case_sensitive &&
notificationSettings.filters.case_sensitive.length > 0 notificationSettings.filters.case_sensitive.length >
? notificationSettings.filters.case_sensitive.join(", ") 0
? notificationSettings.filters.case_sensitive.join(
", ",
)
: "None"} : "None"}
</p> </p>
</div> </div>
@@ -634,8 +661,8 @@ export default function Settings() {
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
<p> <p>
Configure notification settings through your backend API to Configure notification settings through your backend API
customize filters and service configurations. to customize filters and service configurations.
</p> </p>
</div> </div>
</div> </div>

View File

@@ -39,7 +39,9 @@ export default function System() {
<CardContent className="p-6"> <CardContent className="p-6">
<div className="flex items-center justify-center"> <div className="flex items-center justify-center">
<RefreshCw className="h-6 w-6 animate-spin text-muted-foreground" /> <RefreshCw className="h-6 w-6 animate-spin text-muted-foreground" />
<span className="ml-2 text-muted-foreground">Loading system status...</span> <span className="ml-2 text-muted-foreground">
Loading system status...
</span>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@@ -55,8 +57,8 @@ export default function System() {
<AlertTitle>Failed to load system data</AlertTitle> <AlertTitle>Failed to load system data</AlertTitle>
<AlertDescription className="space-y-3"> <AlertDescription className="space-y-3">
<p> <p>
Unable to connect to the Leggen API. Please check your configuration Unable to connect to the Leggen API. Please check your
and ensure the API server is running. configuration and ensure the API server is running.
</p> </p>
<Button <Button
onClick={() => refetchSyncOperations()} onClick={() => refetchSyncOperations()}
@@ -81,7 +83,9 @@ export default function System() {
<Activity className="h-5 w-5 text-primary" /> <Activity className="h-5 w-5 text-primary" />
<span>Recent Sync Operations</span> <span>Recent Sync Operations</span>
</CardTitle> </CardTitle>
<CardDescription>Latest synchronization activities and their status</CardDescription> <CardDescription>
Latest synchronization activities and their status
</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{!syncOperations || syncOperations.operations.length === 0 ? ( {!syncOperations || syncOperations.operations.length === 0 ? (
@@ -91,7 +95,8 @@ export default function System() {
No sync operations yet No sync operations yet
</h3> </h3>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Sync operations will appear here once you start syncing your accounts. Sync operations will appear here once you start syncing your
accounts.
</p> </p>
</div> </div>
) : ( ) : (
@@ -101,7 +106,7 @@ export default function System() {
const isRunning = !operation.completed_at; const isRunning = !operation.completed_at;
const duration = operation.duration_seconds const duration = operation.duration_seconds
? `${Math.round(operation.duration_seconds)}s` ? `${Math.round(operation.duration_seconds)}s`
: ''; : "";
return ( return (
<div <div
@@ -109,13 +114,15 @@ export default function System() {
className="flex items-center justify-between p-4 border rounded-lg hover:bg-accent transition-colors" className="flex items-center justify-between p-4 border rounded-lg hover:bg-accent transition-colors"
> >
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<div className={`p-2 rounded-full ${ <div
className={`p-2 rounded-full ${
isRunning isRunning
? 'bg-blue-100 text-blue-600' ? "bg-blue-100 text-blue-600"
: operation.success : operation.success
? 'bg-green-100 text-green-600' ? "bg-green-100 text-green-600"
: 'bg-red-100 text-red-600' : "bg-red-100 text-red-600"
}`}> }`}
>
{isRunning ? ( {isRunning ? (
<RefreshCw className="h-4 w-4 animate-spin" /> <RefreshCw className="h-4 w-4 animate-spin" />
) : operation.success ? ( ) : operation.success ? (
@@ -127,7 +134,11 @@ export default function System() {
<div> <div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<h4 className="text-sm font-medium text-foreground"> <h4 className="text-sm font-medium text-foreground">
{isRunning ? 'Sync Running' : operation.success ? 'Sync Completed' : 'Sync Failed'} {isRunning
? "Sync Running"
: operation.success
? "Sync Completed"
: "Sync Failed"}
</h4> </h4>
<Badge variant="outline" className="text-xs"> <Badge variant="outline" className="text-xs">
{operation.trigger_type} {operation.trigger_type}
@@ -136,11 +147,12 @@ export default function System() {
<div className="flex items-center space-x-4 mt-1 text-xs text-muted-foreground"> <div className="flex items-center space-x-4 mt-1 text-xs text-muted-foreground">
<span className="flex items-center space-x-1"> <span className="flex items-center space-x-1">
<Clock className="h-3 w-3" /> <Clock className="h-3 w-3" />
<span>{startedAt.toLocaleDateString()} {startedAt.toLocaleTimeString()}</span> <span>
{startedAt.toLocaleDateString()}{" "}
{startedAt.toLocaleTimeString()}
</span> </span>
{duration && ( </span>
<span>Duration: {duration}</span> {duration && <span>Duration: {duration}</span>}
)}
</div> </div>
</div> </div>
</div> </div>
@@ -151,7 +163,9 @@ export default function System() {
</div> </div>
<div className="flex items-center space-x-2 mt-1"> <div className="flex items-center space-x-2 mt-1">
<TrendingUp className="h-3 w-3" /> <TrendingUp className="h-3 w-3" />
<span>{operation.transactions_added} new transactions</span> <span>
{operation.transactions_added} new transactions
</span>
</div> </div>
{operation.errors.length > 0 && ( {operation.errors.length > 0 && (
<div className="flex items-center space-x-2 mt-1 text-red-600"> <div className="flex items-center space-x-2 mt-1 text-red-600">
@@ -175,25 +189,31 @@ export default function System() {
<CheckCircle className="h-5 w-5 text-green-500" /> <CheckCircle className="h-5 w-5 text-green-500" />
<span>System Health</span> <span>System Health</span>
</CardTitle> </CardTitle>
<CardDescription>Overall system status and performance</CardDescription> <CardDescription>
Overall system status and performance
</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="text-center p-4 bg-green-50 rounded-lg border border-green-200"> <div className="text-center p-4 bg-green-50 rounded-lg border border-green-200">
<div className="text-2xl font-bold text-green-700"> <div className="text-2xl font-bold text-green-700">
{syncOperations?.operations.filter(op => op.success).length || 0} {syncOperations?.operations.filter((op) => op.success).length ||
0}
</div> </div>
<div className="text-sm text-green-600">Successful Syncs</div> <div className="text-sm text-green-600">Successful Syncs</div>
</div> </div>
<div className="text-center p-4 bg-red-50 rounded-lg border border-red-200"> <div className="text-center p-4 bg-red-50 rounded-lg border border-red-200">
<div className="text-2xl font-bold text-red-700"> <div className="text-2xl font-bold text-red-700">
{syncOperations?.operations.filter(op => !op.success && op.completed_at).length || 0} {syncOperations?.operations.filter(
(op) => !op.success && op.completed_at,
).length || 0}
</div> </div>
<div className="text-sm text-red-600">Failed Syncs</div> <div className="text-sm text-red-600">Failed Syncs</div>
</div> </div>
<div className="text-center p-4 bg-blue-50 rounded-lg border border-blue-200"> <div className="text-center p-4 bg-blue-50 rounded-lg border border-blue-200">
<div className="text-2xl font-bold text-blue-700"> <div className="text-2xl font-bold text-blue-700">
{syncOperations?.operations.filter(op => !op.completed_at).length || 0} {syncOperations?.operations.filter((op) => !op.completed_at)
.length || 0}
</div> </div>
<div className="text-sm text-blue-600">Running Operations</div> <div className="text-sm text-blue-600">Running Operations</div>
</div> </div>

View File

@@ -97,7 +97,6 @@ export default function TransactionsTable() {
queryFn: apiClient.getAccounts, queryFn: apiClient.getAccounts,
}); });
const { const {
data: transactionsResponse, data: transactionsResponse,
isLoading: transactionsLoading, isLoading: transactionsLoading,
@@ -141,11 +140,7 @@ export default function TransactionsTable() {
// Reset pagination when filters change // Reset pagination when filters change
useEffect(() => { useEffect(() => {
setCurrentPage(1); setCurrentPage(1);
}, [ }, [filterState.selectedAccount, filterState.startDate, filterState.endDate]);
filterState.selectedAccount,
filterState.startDate,
filterState.endDate,
]);
const handleViewRaw = (transaction: Transaction) => { const handleViewRaw = (transaction: Transaction) => {
setSelectedTransaction(transaction); setSelectedTransaction(transaction);
@@ -163,7 +158,6 @@ export default function TransactionsTable() {
filterState.startDate || filterState.startDate ||
filterState.endDate; filterState.endDate;
// Define columns // Define columns
const columns: ColumnDef<Transaction>[] = [ const columns: ColumnDef<Transaction>[] = [
{ {
@@ -427,10 +421,7 @@ export default function TransactionsTable() {
table.getRowModel().rows.map((row) => ( table.getRowModel().rows.map((row) => (
<tr key={row.id} className="hover:bg-muted/50"> <tr key={row.id} className="hover:bg-muted/50">
{row.getVisibleCells().map((cell) => ( {row.getVisibleCells().map((cell) => (
<td <td key={cell.id} className="px-6 py-4 whitespace-nowrap">
key={cell.id}
className="px-6 py-4 whitespace-nowrap"
>
{flexRender( {flexRender(
cell.column.columnDef.cell, cell.column.columnDef.cell,
cell.getContext(), cell.getContext(),

View File

@@ -29,13 +29,9 @@ export default function StatCard({
<CardContent className="p-6"> <CardContent className="p-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-sm font-medium text-muted-foreground"> <p className="text-sm font-medium text-muted-foreground">{title}</p>
{title}
</p>
<div className="flex items-baseline"> <div className="flex items-baseline">
<p className="text-2xl font-bold text-foreground"> <p className="text-2xl font-bold text-foreground">{value}</p>
{value}
</p>
{trend && ( {trend && (
<div <div
className={cn( className={cn(
@@ -51,29 +47,31 @@ export default function StatCard({
)} )}
</div> </div>
{subtitle && ( {subtitle && (
<p className="text-sm text-muted-foreground mt-1"> <p className="text-sm text-muted-foreground mt-1">{subtitle}</p>
{subtitle}
</p>
)} )}
</div> </div>
<div className={cn( <div
className={cn(
"p-3 rounded-full", "p-3 rounded-full",
iconColor === "green" && "bg-green-100 dark:bg-green-900/20", iconColor === "green" && "bg-green-100 dark:bg-green-900/20",
iconColor === "blue" && "bg-blue-100 dark:bg-blue-900/20", iconColor === "blue" && "bg-blue-100 dark:bg-blue-900/20",
iconColor === "red" && "bg-red-100 dark:bg-red-900/20", iconColor === "red" && "bg-red-100 dark:bg-red-900/20",
iconColor === "purple" && "bg-purple-100 dark:bg-purple-900/20", iconColor === "purple" && "bg-purple-100 dark:bg-purple-900/20",
iconColor === "orange" && "bg-orange-100 dark:bg-orange-900/20", iconColor === "orange" && "bg-orange-100 dark:bg-orange-900/20",
iconColor === "default" && "bg-muted" iconColor === "default" && "bg-muted",
)}> )}
<Icon className={cn( >
<Icon
className={cn(
"h-6 w-6", "h-6 w-6",
iconColor === "green" && "text-green-600", iconColor === "green" && "text-green-600",
iconColor === "blue" && "text-blue-600", iconColor === "blue" && "text-blue-600",
iconColor === "red" && "text-red-600", iconColor === "red" && "text-red-600",
iconColor === "purple" && "text-purple-600", iconColor === "purple" && "text-purple-600",
iconColor === "orange" && "text-orange-600", iconColor === "orange" && "text-orange-600",
iconColor === "default" && "text-muted-foreground" iconColor === "default" && "text-muted-foreground",
)} /> )}
/>
</div> </div>
</div> </div>
</CardContent> </CardContent>

View File

@@ -70,7 +70,6 @@ export function ActiveFilterChips({
}); });
} }
const handleRemoveChip = (key: keyof FilterState) => { const handleRemoveChip = (key: keyof FilterState) => {
switch (key) { switch (key) {
case "startDate": case "startDate":

View File

@@ -91,7 +91,6 @@ export function FilterBar({
className="w-[220px]" className="w-[220px]"
/> />
</div> </div>
</div> </div>
{/* Mobile Layout */} {/* Mobile Layout */}
@@ -129,7 +128,6 @@ export function FilterBar({
onDateRangeChange={handleDateRangeChange} onDateRangeChange={handleDateRangeChange}
className="w-full" className="w-full"
/> />
</div> </div>
</div> </div>

View File

@@ -1,8 +1,8 @@
import * as React from "react" import * as React from "react";
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
@@ -31,27 +31,27 @@ const buttonVariants = cva(
variant: "default", variant: "default",
size: "default", size: "default",
}, },
} },
) );
export interface ButtonProps export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>, extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> { VariantProps<typeof buttonVariants> {
asChild?: boolean asChild?: boolean;
} }
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => { ({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button" const Comp = asChild ? Slot : "button";
return ( return (
<Comp <Comp
className={cn(buttonVariants({ variant, size, className }))} className={cn(buttonVariants({ variant, size, className }))}
ref={ref} ref={ref}
{...props} {...props}
/> />
) );
} },
) );
Button.displayName = "Button" Button.displayName = "Button";
export { Button, buttonVariants } export { Button, buttonVariants };

View File

@@ -1,6 +1,6 @@
import * as React from "react" import * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>( const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => { ({ className, type, ...props }, ref) => {
@@ -9,14 +9,14 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
type={type} type={type}
className={cn( className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className className,
)} )}
ref={ref} ref={ref}
{...props} {...props}
/> />
) );
} },
) );
Input.displayName = "Input" Input.displayName = "Input";
export { Input } export { Input };

View File

@@ -15,7 +15,9 @@ export function Logo({ className = "", size = 32 }: LogoProps) {
aria-labelledby="logo-title logo-desc" aria-labelledby="logo-title logo-desc"
> >
<title id="logo-title">leggen stylized italic L</title> <title id="logo-title">leggen stylized italic L</title>
<desc id="logo-desc">Square gradient background with italic white L.</desc> <desc id="logo-desc">
Square gradient background with italic white L.
</desc>
<defs> <defs>
<linearGradient id="logo-bg" x1="0" y1="0" x2="1" y2="1"> <linearGradient id="logo-bg" x1="0" y1="0" x2="1" y2="1">

View File

@@ -1,7 +1,7 @@
import * as React from "react" import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator" import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const Separator = React.forwardRef< const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>, React.ElementRef<typeof SeparatorPrimitive.Root>,
@@ -9,7 +9,7 @@ const Separator = React.forwardRef<
>( >(
( (
{ className, orientation = "horizontal", decorative = true, ...props }, { className, orientation = "horizontal", decorative = true, ...props },
ref ref,
) => ( ) => (
<SeparatorPrimitive.Root <SeparatorPrimitive.Root
ref={ref} ref={ref}
@@ -18,12 +18,12 @@ const Separator = React.forwardRef<
className={cn( className={cn(
"shrink-0 bg-border", "shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]", orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className className,
)} )}
{...props} {...props}
/> />
) ),
) );
Separator.displayName = SeparatorPrimitive.Root.displayName Separator.displayName = SeparatorPrimitive.Root.displayName;
export { Separator } export { Separator };

View File

@@ -1,19 +1,19 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as SheetPrimitive from "@radix-ui/react-dialog" import * as SheetPrimitive from "@radix-ui/react-dialog";
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority";
import { X } from "lucide-react" import { X } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const Sheet = SheetPrimitive.Root const Sheet = SheetPrimitive.Root;
const SheetTrigger = SheetPrimitive.Trigger const SheetTrigger = SheetPrimitive.Trigger;
const SheetClose = SheetPrimitive.Close const SheetClose = SheetPrimitive.Close;
const SheetPortal = SheetPrimitive.Portal const SheetPortal = SheetPrimitive.Portal;
const SheetOverlay = React.forwardRef< const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>, React.ElementRef<typeof SheetPrimitive.Overlay>,
@@ -22,13 +22,13 @@ const SheetOverlay = React.forwardRef<
<SheetPrimitive.Overlay <SheetPrimitive.Overlay
className={cn( className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className className,
)} )}
{...props} {...props}
ref={ref} ref={ref}
/> />
)) ));
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva( const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out", "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
@@ -46,8 +46,8 @@ const sheetVariants = cva(
defaultVariants: { defaultVariants: {
side: "right", side: "right",
}, },
} },
) );
interface SheetContentProps interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>, extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
@@ -71,8 +71,8 @@ const SheetContent = React.forwardRef<
{children} {children}
</SheetPrimitive.Content> </SheetPrimitive.Content>
</SheetPortal> </SheetPortal>
)) ));
SheetContent.displayName = SheetPrimitive.Content.displayName SheetContent.displayName = SheetPrimitive.Content.displayName;
const SheetHeader = ({ const SheetHeader = ({
className, className,
@@ -81,12 +81,12 @@ const SheetHeader = ({
<div <div
className={cn( className={cn(
"flex flex-col space-y-2 text-center sm:text-left", "flex flex-col space-y-2 text-center sm:text-left",
className className,
)} )}
{...props} {...props}
/> />
) );
SheetHeader.displayName = "SheetHeader" SheetHeader.displayName = "SheetHeader";
const SheetFooter = ({ const SheetFooter = ({
className, className,
@@ -95,12 +95,12 @@ const SheetFooter = ({
<div <div
className={cn( className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className className,
)} )}
{...props} {...props}
/> />
) );
SheetFooter.displayName = "SheetFooter" SheetFooter.displayName = "SheetFooter";
const SheetTitle = React.forwardRef< const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>, React.ElementRef<typeof SheetPrimitive.Title>,
@@ -111,8 +111,8 @@ const SheetTitle = React.forwardRef<
className={cn("text-lg font-semibold text-foreground", className)} className={cn("text-lg font-semibold text-foreground", className)}
{...props} {...props}
/> />
)) ));
SheetTitle.displayName = SheetPrimitive.Title.displayName SheetTitle.displayName = SheetPrimitive.Title.displayName;
const SheetDescription = React.forwardRef< const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>, React.ElementRef<typeof SheetPrimitive.Description>,
@@ -123,8 +123,8 @@ const SheetDescription = React.forwardRef<
className={cn("text-sm text-muted-foreground", className)} className={cn("text-sm text-muted-foreground", className)}
{...props} {...props}
/> />
)) ));
SheetDescription.displayName = SheetPrimitive.Description.displayName SheetDescription.displayName = SheetPrimitive.Description.displayName;
export { export {
Sheet, Sheet,
@@ -137,4 +137,4 @@ export {
SheetFooter, SheetFooter,
SheetTitle, SheetTitle,
SheetDescription, SheetDescription,
} };

View File

@@ -1,62 +1,62 @@
import * as React from "react" import * as React from "react";
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority";
import { PanelLeft } from "lucide-react" import { PanelLeft } from "lucide-react";
import { useIsMobile } from "@/hooks/use-mobile" import { useIsMobile } from "@/hooks/use-mobile";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator" import { Separator } from "@/components/ui/separator";
import { import {
Sheet, Sheet,
SheetContent, SheetContent,
SheetDescription, SheetDescription,
SheetHeader, SheetHeader,
SheetTitle, SheetTitle,
} from "@/components/ui/sheet" } from "@/components/ui/sheet";
import { Skeleton } from "@/components/ui/skeleton" import { Skeleton } from "@/components/ui/skeleton";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip" } from "@/components/ui/tooltip";
const SIDEBAR_COOKIE_NAME = "sidebar_state" const SIDEBAR_COOKIE_NAME = "sidebar_state";
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = "16rem" const SIDEBAR_WIDTH = "16rem";
const SIDEBAR_WIDTH_MOBILE = "18rem" const SIDEBAR_WIDTH_MOBILE = "18rem";
const SIDEBAR_WIDTH_ICON = "3rem" const SIDEBAR_WIDTH_ICON = "3rem";
const SIDEBAR_KEYBOARD_SHORTCUT = "b" const SIDEBAR_KEYBOARD_SHORTCUT = "b";
type SidebarContextProps = { type SidebarContextProps = {
state: "expanded" | "collapsed" state: "expanded" | "collapsed";
open: boolean open: boolean;
setOpen: (open: boolean) => void setOpen: (open: boolean) => void;
openMobile: boolean openMobile: boolean;
setOpenMobile: (open: boolean) => void setOpenMobile: (open: boolean) => void;
isMobile: boolean isMobile: boolean;
toggleSidebar: () => void toggleSidebar: () => void;
} };
const SidebarContext = React.createContext<SidebarContextProps | null>(null) const SidebarContext = React.createContext<SidebarContextProps | null>(null);
function useSidebar() { function useSidebar() {
const context = React.useContext(SidebarContext) const context = React.useContext(SidebarContext);
if (!context) { if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.") throw new Error("useSidebar must be used within a SidebarProvider.");
} }
return context return context;
} }
const SidebarProvider = React.forwardRef< const SidebarProvider = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.ComponentProps<"div"> & { React.ComponentProps<"div"> & {
defaultOpen?: boolean defaultOpen?: boolean;
open?: boolean open?: boolean;
onOpenChange?: (open: boolean) => void onOpenChange?: (open: boolean) => void;
} }
>( >(
( (
@@ -69,36 +69,36 @@ const SidebarProvider = React.forwardRef<
children, children,
...props ...props
}, },
ref ref,
) => { ) => {
const isMobile = useIsMobile() const isMobile = useIsMobile();
const [openMobile, setOpenMobile] = React.useState(false) const [openMobile, setOpenMobile] = React.useState(false);
// This is the internal state of the sidebar. // This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component. // We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen) const [_open, _setOpen] = React.useState(defaultOpen);
const open = openProp ?? _open const open = openProp ?? _open;
const setOpen = React.useCallback( const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => { (value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === "function" ? value(open) : value const openState = typeof value === "function" ? value(open) : value;
if (setOpenProp) { if (setOpenProp) {
setOpenProp(openState) setOpenProp(openState);
} else { } else {
_setOpen(openState) _setOpen(openState);
} }
// This sets the cookie to keep the sidebar state. // This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}` document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
}, },
[setOpenProp, open] [setOpenProp, open],
) );
// Helper to toggle the sidebar. // Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => { const toggleSidebar = React.useCallback(() => {
return isMobile return isMobile
? setOpenMobile((open) => !open) ? setOpenMobile((open) => !open)
: setOpen((open) => !open) : setOpen((open) => !open);
}, [isMobile, setOpen, setOpenMobile]) }, [isMobile, setOpen, setOpenMobile]);
// Adds a keyboard shortcut to toggle the sidebar. // Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => { React.useEffect(() => {
@@ -107,18 +107,18 @@ const SidebarProvider = React.forwardRef<
event.key === SIDEBAR_KEYBOARD_SHORTCUT && event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
(event.metaKey || event.ctrlKey) (event.metaKey || event.ctrlKey)
) { ) {
event.preventDefault() event.preventDefault();
toggleSidebar() toggleSidebar();
}
} }
};
window.addEventListener("keydown", handleKeyDown) window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown) return () => window.removeEventListener("keydown", handleKeyDown);
}, [toggleSidebar]) }, [toggleSidebar]);
// We add a state so that we can do data-state="expanded" or "collapsed". // We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes. // This makes it easier to style the sidebar with Tailwind classes.
const state = open ? "expanded" : "collapsed" const state = open ? "expanded" : "collapsed";
const contextValue = React.useMemo<SidebarContextProps>( const contextValue = React.useMemo<SidebarContextProps>(
() => ({ () => ({
@@ -130,8 +130,16 @@ const SidebarProvider = React.forwardRef<
setOpenMobile, setOpenMobile,
toggleSidebar, toggleSidebar,
}), }),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] [
) state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
],
);
return ( return (
<SidebarContext.Provider value={contextValue}> <SidebarContext.Provider value={contextValue}>
@@ -146,7 +154,7 @@ const SidebarProvider = React.forwardRef<
} }
className={cn( className={cn(
"group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar", "group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar",
className className,
)} )}
ref={ref} ref={ref}
{...props} {...props}
@@ -155,17 +163,17 @@ const SidebarProvider = React.forwardRef<
</div> </div>
</TooltipProvider> </TooltipProvider>
</SidebarContext.Provider> </SidebarContext.Provider>
) );
} },
) );
SidebarProvider.displayName = "SidebarProvider" SidebarProvider.displayName = "SidebarProvider";
const Sidebar = React.forwardRef< const Sidebar = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.ComponentProps<"div"> & { React.ComponentProps<"div"> & {
side?: "left" | "right" side?: "left" | "right";
variant?: "sidebar" | "floating" | "inset" variant?: "sidebar" | "floating" | "inset";
collapsible?: "offcanvas" | "icon" | "none" collapsible?: "offcanvas" | "icon" | "none";
} }
>( >(
( (
@@ -177,23 +185,23 @@ const Sidebar = React.forwardRef<
children, children,
...props ...props
}, },
ref ref,
) => { ) => {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar() const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
if (collapsible === "none") { if (collapsible === "none") {
return ( return (
<div <div
className={cn( className={cn(
"flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground", "flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground",
className className,
)} )}
ref={ref} ref={ref}
{...props} {...props}
> >
{children} {children}
</div> </div>
) );
} }
if (isMobile) { if (isMobile) {
@@ -217,7 +225,7 @@ const Sidebar = React.forwardRef<
<div className="flex h-full w-full flex-col">{children}</div> <div className="flex h-full w-full flex-col">{children}</div>
</SheetContent> </SheetContent>
</Sheet> </Sheet>
) );
} }
return ( return (
@@ -237,7 +245,7 @@ const Sidebar = React.forwardRef<
"group-data-[side=right]:rotate-180", "group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset" variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]" ? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]" : "group-data-[collapsible=icon]:w-[--sidebar-width-icon]",
)} )}
/> />
<div <div
@@ -250,7 +258,7 @@ const Sidebar = React.forwardRef<
variant === "floating" || variant === "inset" variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]" ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l", : "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
className className,
)} )}
{...props} {...props}
> >
@@ -262,16 +270,16 @@ const Sidebar = React.forwardRef<
</div> </div>
</div> </div>
</div> </div>
) );
} },
) );
Sidebar.displayName = "Sidebar" Sidebar.displayName = "Sidebar";
const SidebarTrigger = React.forwardRef< const SidebarTrigger = React.forwardRef<
React.ElementRef<typeof Button>, React.ElementRef<typeof Button>,
React.ComponentProps<typeof Button> React.ComponentProps<typeof Button>
>(({ className, onClick, ...props }, ref) => { >(({ className, onClick, ...props }, ref) => {
const { toggleSidebar } = useSidebar() const { toggleSidebar } = useSidebar();
return ( return (
<Button <Button
@@ -281,23 +289,23 @@ const SidebarTrigger = React.forwardRef<
size="icon" size="icon"
className={cn("h-7 w-7", className)} className={cn("h-7 w-7", className)}
onClick={(event) => { onClick={(event) => {
onClick?.(event) onClick?.(event);
toggleSidebar() toggleSidebar();
}} }}
{...props} {...props}
> >
<PanelLeft /> <PanelLeft />
<span className="sr-only">Toggle Sidebar</span> <span className="sr-only">Toggle Sidebar</span>
</Button> </Button>
) );
}) });
SidebarTrigger.displayName = "SidebarTrigger" SidebarTrigger.displayName = "SidebarTrigger";
const SidebarRail = React.forwardRef< const SidebarRail = React.forwardRef<
HTMLButtonElement, HTMLButtonElement,
React.ComponentProps<"button"> React.ComponentProps<"button">
>(({ className, ...props }, ref) => { >(({ className, ...props }, ref) => {
const { toggleSidebar } = useSidebar() const { toggleSidebar } = useSidebar();
return ( return (
<button <button
@@ -314,13 +322,13 @@ const SidebarRail = React.forwardRef<
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar", "group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2", "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2", "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className className,
)} )}
{...props} {...props}
/> />
) );
}) });
SidebarRail.displayName = "SidebarRail" SidebarRail.displayName = "SidebarRail";
const SidebarInset = React.forwardRef< const SidebarInset = React.forwardRef<
HTMLDivElement, HTMLDivElement,
@@ -332,13 +340,13 @@ const SidebarInset = React.forwardRef<
className={cn( className={cn(
"relative flex w-full flex-1 flex-col bg-background", "relative flex w-full flex-1 flex-col bg-background",
"md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow", "md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
className className,
)} )}
{...props} {...props}
/> />
) );
}) });
SidebarInset.displayName = "SidebarInset" SidebarInset.displayName = "SidebarInset";
const SidebarInput = React.forwardRef< const SidebarInput = React.forwardRef<
React.ElementRef<typeof Input>, React.ElementRef<typeof Input>,
@@ -350,13 +358,13 @@ const SidebarInput = React.forwardRef<
data-sidebar="input" data-sidebar="input"
className={cn( className={cn(
"h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring", "h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
className className,
)} )}
{...props} {...props}
/> />
) );
}) });
SidebarInput.displayName = "SidebarInput" SidebarInput.displayName = "SidebarInput";
const SidebarHeader = React.forwardRef< const SidebarHeader = React.forwardRef<
HTMLDivElement, HTMLDivElement,
@@ -369,9 +377,9 @@ const SidebarHeader = React.forwardRef<
className={cn("flex flex-col gap-2 p-2", className)} className={cn("flex flex-col gap-2 p-2", className)}
{...props} {...props}
/> />
) );
}) });
SidebarHeader.displayName = "SidebarHeader" SidebarHeader.displayName = "SidebarHeader";
const SidebarFooter = React.forwardRef< const SidebarFooter = React.forwardRef<
HTMLDivElement, HTMLDivElement,
@@ -384,9 +392,9 @@ const SidebarFooter = React.forwardRef<
className={cn("flex flex-col gap-2 p-2", className)} className={cn("flex flex-col gap-2 p-2", className)}
{...props} {...props}
/> />
) );
}) });
SidebarFooter.displayName = "SidebarFooter" SidebarFooter.displayName = "SidebarFooter";
const SidebarSeparator = React.forwardRef< const SidebarSeparator = React.forwardRef<
React.ElementRef<typeof Separator>, React.ElementRef<typeof Separator>,
@@ -399,9 +407,9 @@ const SidebarSeparator = React.forwardRef<
className={cn("mx-2 w-auto bg-sidebar-border", className)} className={cn("mx-2 w-auto bg-sidebar-border", className)}
{...props} {...props}
/> />
) );
}) });
SidebarSeparator.displayName = "SidebarSeparator" SidebarSeparator.displayName = "SidebarSeparator";
const SidebarContent = React.forwardRef< const SidebarContent = React.forwardRef<
HTMLDivElement, HTMLDivElement,
@@ -413,13 +421,13 @@ const SidebarContent = React.forwardRef<
data-sidebar="content" data-sidebar="content"
className={cn( className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden", "flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className className,
)} )}
{...props} {...props}
/> />
) );
}) });
SidebarContent.displayName = "SidebarContent" SidebarContent.displayName = "SidebarContent";
const SidebarGroup = React.forwardRef< const SidebarGroup = React.forwardRef<
HTMLDivElement, HTMLDivElement,
@@ -432,15 +440,15 @@ const SidebarGroup = React.forwardRef<
className={cn("relative flex w-full min-w-0 flex-col p-2", className)} className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...props} {...props}
/> />
) );
}) });
SidebarGroup.displayName = "SidebarGroup" SidebarGroup.displayName = "SidebarGroup";
const SidebarGroupLabel = React.forwardRef< const SidebarGroupLabel = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.ComponentProps<"div"> & { asChild?: boolean } React.ComponentProps<"div"> & { asChild?: boolean }
>(({ className, asChild = false, ...props }, ref) => { >(({ className, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "div" const Comp = asChild ? Slot : "div";
return ( return (
<Comp <Comp
@@ -449,19 +457,19 @@ const SidebarGroupLabel = React.forwardRef<
className={cn( className={cn(
"flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", "flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0", "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className className,
)} )}
{...props} {...props}
/> />
) );
}) });
SidebarGroupLabel.displayName = "SidebarGroupLabel" SidebarGroupLabel.displayName = "SidebarGroupLabel";
const SidebarGroupAction = React.forwardRef< const SidebarGroupAction = React.forwardRef<
HTMLButtonElement, HTMLButtonElement,
React.ComponentProps<"button"> & { asChild?: boolean } React.ComponentProps<"button"> & { asChild?: boolean }
>(({ className, asChild = false, ...props }, ref) => { >(({ className, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button" const Comp = asChild ? Slot : "button";
return ( return (
<Comp <Comp
@@ -472,13 +480,13 @@ const SidebarGroupAction = React.forwardRef<
// Increases the hit area of the button on mobile. // Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 after:md:hidden", "after:absolute after:-inset-2 after:md:hidden",
"group-data-[collapsible=icon]:hidden", "group-data-[collapsible=icon]:hidden",
className className,
)} )}
{...props} {...props}
/> />
) );
}) });
SidebarGroupAction.displayName = "SidebarGroupAction" SidebarGroupAction.displayName = "SidebarGroupAction";
const SidebarGroupContent = React.forwardRef< const SidebarGroupContent = React.forwardRef<
HTMLDivElement, HTMLDivElement,
@@ -490,8 +498,8 @@ const SidebarGroupContent = React.forwardRef<
className={cn("w-full text-sm", className)} className={cn("w-full text-sm", className)}
{...props} {...props}
/> />
)) ));
SidebarGroupContent.displayName = "SidebarGroupContent" SidebarGroupContent.displayName = "SidebarGroupContent";
const SidebarMenu = React.forwardRef< const SidebarMenu = React.forwardRef<
HTMLUListElement, HTMLUListElement,
@@ -503,8 +511,8 @@ const SidebarMenu = React.forwardRef<
className={cn("flex w-full min-w-0 flex-col gap-1", className)} className={cn("flex w-full min-w-0 flex-col gap-1", className)}
{...props} {...props}
/> />
)) ));
SidebarMenu.displayName = "SidebarMenu" SidebarMenu.displayName = "SidebarMenu";
const SidebarMenuItem = React.forwardRef< const SidebarMenuItem = React.forwardRef<
HTMLLIElement, HTMLLIElement,
@@ -516,8 +524,8 @@ const SidebarMenuItem = React.forwardRef<
className={cn("group/menu-item relative", className)} className={cn("group/menu-item relative", className)}
{...props} {...props}
/> />
)) ));
SidebarMenuItem.displayName = "SidebarMenuItem" SidebarMenuItem.displayName = "SidebarMenuItem";
const sidebarMenuButtonVariants = cva( const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
@@ -538,15 +546,15 @@ const sidebarMenuButtonVariants = cva(
variant: "default", variant: "default",
size: "default", size: "default",
}, },
} },
) );
const SidebarMenuButton = React.forwardRef< const SidebarMenuButton = React.forwardRef<
HTMLButtonElement, HTMLButtonElement,
React.ComponentProps<"button"> & { React.ComponentProps<"button"> & {
asChild?: boolean asChild?: boolean;
isActive?: boolean isActive?: boolean;
tooltip?: string | React.ComponentProps<typeof TooltipContent> tooltip?: string | React.ComponentProps<typeof TooltipContent>;
} & VariantProps<typeof sidebarMenuButtonVariants> } & VariantProps<typeof sidebarMenuButtonVariants>
>( >(
( (
@@ -559,10 +567,10 @@ const SidebarMenuButton = React.forwardRef<
className, className,
...props ...props
}, },
ref ref,
) => { ) => {
const Comp = asChild ? Slot : "button" const Comp = asChild ? Slot : "button";
const { isMobile, state } = useSidebar() const { isMobile, state } = useSidebar();
const button = ( const button = (
<Comp <Comp
@@ -573,16 +581,16 @@ const SidebarMenuButton = React.forwardRef<
className={cn(sidebarMenuButtonVariants({ variant, size }), className)} className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
{...props} {...props}
/> />
) );
if (!tooltip) { if (!tooltip) {
return button return button;
} }
if (typeof tooltip === "string") { if (typeof tooltip === "string") {
tooltip = { tooltip = {
children: tooltip, children: tooltip,
} };
} }
return ( return (
@@ -595,19 +603,19 @@ const SidebarMenuButton = React.forwardRef<
{...tooltip} {...tooltip}
/> />
</Tooltip> </Tooltip>
) );
} },
) );
SidebarMenuButton.displayName = "SidebarMenuButton" SidebarMenuButton.displayName = "SidebarMenuButton";
const SidebarMenuAction = React.forwardRef< const SidebarMenuAction = React.forwardRef<
HTMLButtonElement, HTMLButtonElement,
React.ComponentProps<"button"> & { React.ComponentProps<"button"> & {
asChild?: boolean asChild?: boolean;
showOnHover?: boolean showOnHover?: boolean;
} }
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => { >(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button" const Comp = asChild ? Slot : "button";
return ( return (
<Comp <Comp
@@ -623,13 +631,13 @@ const SidebarMenuAction = React.forwardRef<
"group-data-[collapsible=icon]:hidden", "group-data-[collapsible=icon]:hidden",
showOnHover && showOnHover &&
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0", "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
className className,
)} )}
{...props} {...props}
/> />
) );
}) });
SidebarMenuAction.displayName = "SidebarMenuAction" SidebarMenuAction.displayName = "SidebarMenuAction";
const SidebarMenuBadge = React.forwardRef< const SidebarMenuBadge = React.forwardRef<
HTMLDivElement, HTMLDivElement,
@@ -645,23 +653,23 @@ const SidebarMenuBadge = React.forwardRef<
"peer-data-[size=default]/menu-button:top-1.5", "peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5", "peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden", "group-data-[collapsible=icon]:hidden",
className className,
)} )}
{...props} {...props}
/> />
)) ));
SidebarMenuBadge.displayName = "SidebarMenuBadge" SidebarMenuBadge.displayName = "SidebarMenuBadge";
const SidebarMenuSkeleton = React.forwardRef< const SidebarMenuSkeleton = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.ComponentProps<"div"> & { React.ComponentProps<"div"> & {
showIcon?: boolean showIcon?: boolean;
} }
>(({ className, showIcon = false, ...props }, ref) => { >(({ className, showIcon = false, ...props }, ref) => {
// Random width between 50 to 90%. // Random width between 50 to 90%.
const width = React.useMemo(() => { const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%` return `${Math.floor(Math.random() * 40) + 50}%`;
}, []) }, []);
return ( return (
<div <div
@@ -686,9 +694,9 @@ const SidebarMenuSkeleton = React.forwardRef<
} }
/> />
</div> </div>
) );
}) });
SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton" SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton";
const SidebarMenuSub = React.forwardRef< const SidebarMenuSub = React.forwardRef<
HTMLUListElement, HTMLUListElement,
@@ -700,28 +708,28 @@ const SidebarMenuSub = React.forwardRef<
className={cn( className={cn(
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5", "mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden", "group-data-[collapsible=icon]:hidden",
className className,
)} )}
{...props} {...props}
/> />
)) ));
SidebarMenuSub.displayName = "SidebarMenuSub" SidebarMenuSub.displayName = "SidebarMenuSub";
const SidebarMenuSubItem = React.forwardRef< const SidebarMenuSubItem = React.forwardRef<
HTMLLIElement, HTMLLIElement,
React.ComponentProps<"li"> React.ComponentProps<"li">
>(({ ...props }, ref) => <li ref={ref} {...props} />) >(({ ...props }, ref) => <li ref={ref} {...props} />);
SidebarMenuSubItem.displayName = "SidebarMenuSubItem" SidebarMenuSubItem.displayName = "SidebarMenuSubItem";
const SidebarMenuSubButton = React.forwardRef< const SidebarMenuSubButton = React.forwardRef<
HTMLAnchorElement, HTMLAnchorElement,
React.ComponentProps<"a"> & { React.ComponentProps<"a"> & {
asChild?: boolean asChild?: boolean;
size?: "sm" | "md" size?: "sm" | "md";
isActive?: boolean isActive?: boolean;
} }
>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => { >(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a" const Comp = asChild ? Slot : "a";
return ( return (
<Comp <Comp
@@ -735,13 +743,13 @@ const SidebarMenuSubButton = React.forwardRef<
size === "sm" && "text-xs", size === "sm" && "text-xs",
size === "md" && "text-sm", size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden", "group-data-[collapsible=icon]:hidden",
className className,
)} )}
{...props} {...props}
/> />
) );
}) });
SidebarMenuSubButton.displayName = "SidebarMenuSubButton" SidebarMenuSubButton.displayName = "SidebarMenuSubButton";
export { export {
Sidebar, Sidebar,
@@ -768,4 +776,4 @@ export {
SidebarSeparator, SidebarSeparator,
SidebarTrigger, SidebarTrigger,
useSidebar, useSidebar,
} };

View File

@@ -1,4 +1,4 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Skeleton({ function Skeleton({
className, className,
@@ -9,7 +9,7 @@ function Skeleton({
className={cn("animate-pulse rounded-md bg-primary/10", className)} className={cn("animate-pulse rounded-md bg-primary/10", className)}
{...props} {...props}
/> />
) );
} }
export { Skeleton } export { Skeleton };

View File

@@ -1,9 +1,9 @@
import * as React from "react" import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs" import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const Tabs = TabsPrimitive.Root const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef< const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>, React.ElementRef<typeof TabsPrimitive.List>,
@@ -13,12 +13,12 @@ const TabsList = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground", "inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
className className,
)} )}
{...props} {...props}
/> />
)) ));
TabsList.displayName = TabsPrimitive.List.displayName TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef< const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>, React.ElementRef<typeof TabsPrimitive.Trigger>,
@@ -28,12 +28,12 @@ const TabsTrigger = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow", "inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
className className,
)} )}
{...props} {...props}
/> />
)) ));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef< const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>, React.ElementRef<typeof TabsPrimitive.Content>,
@@ -43,11 +43,11 @@ const TabsContent = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className className,
)} )}
{...props} {...props}
/> />
)) ));
TabsContent.displayName = TabsPrimitive.Content.displayName TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent } export { Tabs, TabsList, TabsTrigger, TabsContent };

View File

@@ -1,13 +1,13 @@
import * as React from "react" import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip" import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const TooltipProvider = TooltipPrimitive.Provider const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef< const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>, React.ElementRef<typeof TooltipPrimitive.Content>,
@@ -19,12 +19,12 @@ const TooltipContent = React.forwardRef<
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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 origin-[--radix-tooltip-content-transform-origin]", "z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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 origin-[--radix-tooltip-content-transform-origin]",
className className,
)} )}
{...props} {...props}
/> />
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>
)) ));
TooltipContent.displayName = TooltipPrimitive.Content.displayName TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };

View File

@@ -51,22 +51,29 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
const themeColor = THEME_COLORS[resolvedTheme]; const themeColor = THEME_COLORS[resolvedTheme];
// Update theme-color meta tag // Update theme-color meta tag
const themeColorMeta = document.getElementById("theme-color-meta") as HTMLMetaElement; const themeColorMeta = document.getElementById(
"theme-color-meta",
) as HTMLMetaElement;
if (themeColorMeta) { if (themeColorMeta) {
themeColorMeta.content = themeColor; themeColorMeta.content = themeColor;
} }
// Update Microsoft tile color // Update Microsoft tile color
const msThemeColorMeta = document.getElementById("ms-theme-color-meta") as HTMLMetaElement; const msThemeColorMeta = document.getElementById(
"ms-theme-color-meta",
) as HTMLMetaElement;
if (msThemeColorMeta) { if (msThemeColorMeta) {
msThemeColorMeta.content = themeColor; msThemeColorMeta.content = themeColor;
} }
// Update Apple status bar style for better iOS integration // Update Apple status bar style for better iOS integration
const appleStatusBarMeta = document.getElementById("apple-status-bar-meta") as HTMLMetaElement; const appleStatusBarMeta = document.getElementById(
"apple-status-bar-meta",
) as HTMLMetaElement;
if (appleStatusBarMeta) { if (appleStatusBarMeta) {
// Use 'black-translucent' for dark theme, 'default' for light theme // Use 'black-translucent' for dark theme, 'default' for light theme
appleStatusBarMeta.content = resolvedTheme === "dark" ? "black-translucent" : "default"; appleStatusBarMeta.content =
resolvedTheme === "dark" ? "black-translucent" : "default";
} }
}; };

View File

@@ -1,19 +1,21 @@
import * as React from "react" import * as React from "react";
const MOBILE_BREAKPOINT = 768 const MOBILE_BREAKPOINT = 768;
export function useIsMobile() { export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined) const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
undefined,
);
React.useEffect(() => { React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => { const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
} };
mql.addEventListener("change", onChange) mql.addEventListener("change", onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener("change", onChange) return () => mql.removeEventListener("change", onChange);
}, []) }, []);
return !!isMobile return !!isMobile;
} }

View File

@@ -7,13 +7,16 @@ interface PWAUpdate {
export function usePWA(): PWAUpdate { export function usePWA(): PWAUpdate {
const [updateAvailable, setUpdateAvailable] = useState(false); const [updateAvailable, setUpdateAvailable] = useState(false);
const [updateSW, setUpdateSW] = useState<() => Promise<void>>(() => async () => {}); const [updateSW, setUpdateSW] = useState<() => Promise<void>>(
() => async () => {},
);
useEffect(() => { useEffect(() => {
// Check if SW registration is available // Check if SW registration is available
if ("serviceWorker" in navigator) { if ("serviceWorker" in navigator) {
// Import the registerSW function // Import the registerSW function
import("virtual:pwa-register").then(({ registerSW }) => { import("virtual:pwa-register")
.then(({ registerSW }) => {
const updateSWFunction = registerSW({ const updateSWFunction = registerSW({
onNeedRefresh() { onNeedRefresh() {
setUpdateAvailable(true); setUpdateAvailable(true);
@@ -23,7 +26,8 @@ export function usePWA(): PWAUpdate {
console.log("App ready to work offline"); console.log("App ready to work offline");
}, },
}); });
}).catch(() => { })
.catch(() => {
// PWA not available in development mode or when disabled // PWA not available in development mode or when disabled
console.log("PWA registration not available"); console.log("PWA registration not available");
}); });

View File

@@ -3,10 +3,7 @@ import { AppSidebar } from "../components/AppSidebar";
import { SiteHeader } from "../components/SiteHeader"; import { SiteHeader } from "../components/SiteHeader";
import { PWAInstallPrompt, PWAUpdatePrompt } from "../components/PWAPrompts"; import { PWAInstallPrompt, PWAUpdatePrompt } from "../components/PWAPrompts";
import { usePWA } from "../hooks/usePWA"; import { usePWA } from "../hooks/usePWA";
import { import { SidebarInset, SidebarProvider } from "../components/ui/sidebar";
SidebarInset,
SidebarProvider,
} from "../components/ui/sidebar";
function RootLayout() { function RootLayout() {
const { updateAvailable, updateSW } = usePWA(); const { updateAvailable, updateSW } = usePWA();

View File

@@ -234,7 +234,7 @@ export interface SyncOperation {
duration_seconds?: number; duration_seconds?: number;
errors: string[]; errors: string[];
logs: string[]; logs: string[];
trigger_type: 'manual' | 'scheduled' | 'api'; trigger_type: "manual" | "scheduled" | "api";
} }
export interface SyncOperationsResponse { export interface SyncOperationsResponse {

View File

@@ -5,69 +5,69 @@ export default {
theme: { theme: {
extend: { extend: {
borderRadius: { borderRadius: {
lg: 'var(--radius)', lg: "var(--radius)",
md: 'calc(var(--radius) - 2px)', md: "calc(var(--radius) - 2px)",
sm: 'calc(var(--radius) - 4px)' sm: "calc(var(--radius) - 4px)",
}, },
spacing: { spacing: {
'safe-top': 'var(--safe-area-inset-top)', "safe-top": "var(--safe-area-inset-top)",
'safe-bottom': 'var(--safe-area-inset-bottom)', "safe-bottom": "var(--safe-area-inset-bottom)",
'safe-left': 'var(--safe-area-inset-left)', "safe-left": "var(--safe-area-inset-left)",
'safe-right': 'var(--safe-area-inset-right)' "safe-right": "var(--safe-area-inset-right)",
}, },
colors: { colors: {
background: 'hsl(var(--background))', background: "hsl(var(--background))",
foreground: 'hsl(var(--foreground))', foreground: "hsl(var(--foreground))",
card: { card: {
DEFAULT: 'hsl(var(--card))', DEFAULT: "hsl(var(--card))",
foreground: 'hsl(var(--card-foreground))' foreground: "hsl(var(--card-foreground))",
}, },
popover: { popover: {
DEFAULT: 'hsl(var(--popover))', DEFAULT: "hsl(var(--popover))",
foreground: 'hsl(var(--popover-foreground))' foreground: "hsl(var(--popover-foreground))",
}, },
primary: { primary: {
DEFAULT: 'hsl(var(--primary))', DEFAULT: "hsl(var(--primary))",
foreground: 'hsl(var(--primary-foreground))' foreground: "hsl(var(--primary-foreground))",
}, },
secondary: { secondary: {
DEFAULT: 'hsl(var(--secondary))', DEFAULT: "hsl(var(--secondary))",
foreground: 'hsl(var(--secondary-foreground))' foreground: "hsl(var(--secondary-foreground))",
}, },
muted: { muted: {
DEFAULT: 'hsl(var(--muted))', DEFAULT: "hsl(var(--muted))",
foreground: 'hsl(var(--muted-foreground))' foreground: "hsl(var(--muted-foreground))",
}, },
accent: { accent: {
DEFAULT: 'hsl(var(--accent))', DEFAULT: "hsl(var(--accent))",
foreground: 'hsl(var(--accent-foreground))' foreground: "hsl(var(--accent-foreground))",
}, },
destructive: { destructive: {
DEFAULT: 'hsl(var(--destructive))', DEFAULT: "hsl(var(--destructive))",
foreground: 'hsl(var(--destructive-foreground))' foreground: "hsl(var(--destructive-foreground))",
}, },
border: 'hsl(var(--border))', border: "hsl(var(--border))",
input: 'hsl(var(--input))', input: "hsl(var(--input))",
ring: 'hsl(var(--ring))', ring: "hsl(var(--ring))",
chart: { chart: {
'1': 'hsl(var(--chart-1))', 1: "hsl(var(--chart-1))",
'2': 'hsl(var(--chart-2))', 2: "hsl(var(--chart-2))",
'3': 'hsl(var(--chart-3))', 3: "hsl(var(--chart-3))",
'4': 'hsl(var(--chart-4))', 4: "hsl(var(--chart-4))",
'5': 'hsl(var(--chart-5))' 5: "hsl(var(--chart-5))",
}, },
sidebar: { sidebar: {
DEFAULT: 'hsl(var(--sidebar-background))', DEFAULT: "hsl(var(--sidebar-background))",
foreground: 'hsl(var(--sidebar-foreground))', foreground: "hsl(var(--sidebar-foreground))",
primary: 'hsl(var(--sidebar-primary))', primary: "hsl(var(--sidebar-primary))",
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))', "primary-foreground": "hsl(var(--sidebar-primary-foreground))",
accent: 'hsl(var(--sidebar-accent))', accent: "hsl(var(--sidebar-accent))",
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))', "accent-foreground": "hsl(var(--sidebar-accent-foreground))",
border: 'hsl(var(--sidebar-border))', border: "hsl(var(--sidebar-border))",
ring: 'hsl(var(--sidebar-ring))' ring: "hsl(var(--sidebar-ring))",
} },
} },
} },
}, },
plugins: [require("@tailwindcss/forms"), require("tailwindcss-animate")], plugins: [require("@tailwindcss/forms"), require("tailwindcss-animate")],
}; };

View File

@@ -10,7 +10,12 @@ export default defineConfig({
react(), react(),
VitePWA({ VitePWA({
registerType: "autoUpdate", registerType: "autoUpdate",
includeAssets: ["favicon.ico", "apple-touch-icon-180x180.png", "maskable-icon-512x512.png", "robots.txt"], includeAssets: [
"favicon.ico",
"apple-touch-icon-180x180.png",
"maskable-icon-512x512.png",
"robots.txt",
],
manifest: { manifest: {
name: "Leggen", name: "Leggen",
short_name: "Leggen", short_name: "Leggen",
@@ -28,38 +33,38 @@ export default defineConfig({
short_name: "Transactions", short_name: "Transactions",
description: "View and manage transactions", description: "View and manage transactions",
url: "/transactions", url: "/transactions",
icons: [{ src: "/pwa-192x192.png", sizes: "192x192" }] icons: [{ src: "/pwa-192x192.png", sizes: "192x192" }],
}, },
{ {
name: "Analytics", name: "Analytics",
short_name: "Analytics", short_name: "Analytics",
description: "View financial analytics", description: "View financial analytics",
url: "/analytics", url: "/analytics",
icons: [{ src: "/pwa-192x192.png", sizes: "192x192" }] icons: [{ src: "/pwa-192x192.png", sizes: "192x192" }],
} },
], ],
icons: [ icons: [
{ {
src: "pwa-64x64.png", src: "pwa-64x64.png",
sizes: "64x64", sizes: "64x64",
type: "image/png" type: "image/png",
}, },
{ {
src: "pwa-192x192.png", src: "pwa-192x192.png",
sizes: "192x192", sizes: "192x192",
type: "image/png" type: "image/png",
}, },
{ {
src: "pwa-512x512.png", src: "pwa-512x512.png",
sizes: "512x512", sizes: "512x512",
type: "image/png" type: "image/png",
}, },
{ {
src: "maskable-icon-512x512.png", src: "maskable-icon-512x512.png",
sizes: "512x512", sizes: "512x512",
type: "image/png", type: "image/png",
purpose: "maskable" purpose: "maskable",
} },
], ],
}, },
workbox: { workbox: {

View File

@@ -216,12 +216,12 @@ async def stop_scheduler() -> APIResponse:
@router.get("/sync/operations", response_model=APIResponse) @router.get("/sync/operations", response_model=APIResponse)
async def get_sync_operations( async def get_sync_operations(limit: int = 50, offset: int = 0) -> APIResponse:
limit: int = 50, offset: int = 0
) -> APIResponse:
"""Get sync operations history""" """Get sync operations history"""
try: try:
operations = await sync_service.database.get_sync_operations(limit=limit, offset=offset) operations = await sync_service.database.get_sync_operations(
limit=limit, offset=offset
)
return APIResponse( return APIResponse(
success=True, success=True,

View File

@@ -20,7 +20,9 @@ class SyncService:
"""Get current sync status""" """Get current sync status"""
return self._sync_status return self._sync_status
async def sync_all_accounts(self, force: bool = False, trigger_type: str = "manual") -> SyncResult: async def sync_all_accounts(
self, force: bool = False, trigger_type: str = "manual"
) -> SyncResult:
"""Sync all connected accounts""" """Sync all connected accounts"""
if self._sync_status.is_running and not force: if self._sync_status.is_running and not force:
raise Exception("Sync is already running") raise Exception("Sync is already running")
@@ -149,7 +151,8 @@ class SyncService:
self._sync_status.last_sync = end_time self._sync_status.last_sync = end_time
# Update sync operation with final results # Update sync operation with final results
sync_operation.update({ sync_operation.update(
{
"completed_at": end_time.isoformat(), "completed_at": end_time.isoformat(),
"success": len(errors) == 0, "success": len(errors) == 0,
"accounts_processed": accounts_processed, "accounts_processed": accounts_processed,
@@ -159,11 +162,14 @@ class SyncService:
"duration_seconds": duration, "duration_seconds": duration,
"errors": errors, "errors": errors,
"logs": logs, "logs": logs,
}) }
)
# Persist sync operation to database # Persist sync operation to database
try: try:
operation_id = await self.database.persist_sync_operation(sync_operation) operation_id = await self.database.persist_sync_operation(
sync_operation
)
logger.debug(f"Saved sync operation with ID: {operation_id}") logger.debug(f"Saved sync operation with ID: {operation_id}")
except Exception as e: except Exception as e:
logger.error(f"Failed to persist sync operation: {e}") logger.error(f"Failed to persist sync operation: {e}")
@@ -183,7 +189,9 @@ class SyncService:
logger.info( logger.info(
f"Sync completed: {accounts_processed} accounts, {transactions_added} new transactions" f"Sync completed: {accounts_processed} accounts, {transactions_added} new transactions"
) )
logs.append(f"Sync completed: {accounts_processed} accounts, {transactions_added} new transactions") logs.append(
f"Sync completed: {accounts_processed} accounts, {transactions_added} new transactions"
)
return result return result
except Exception as e: except Exception as e:
@@ -195,7 +203,8 @@ class SyncService:
# Save failed sync operation # Save failed sync operation
end_time = datetime.now() end_time = datetime.now()
duration = (end_time - start_time).total_seconds() duration = (end_time - start_time).total_seconds()
sync_operation.update({ sync_operation.update(
{
"completed_at": end_time.isoformat(), "completed_at": end_time.isoformat(),
"success": False, "success": False,
"accounts_processed": accounts_processed, "accounts_processed": accounts_processed,
@@ -205,13 +214,18 @@ class SyncService:
"duration_seconds": duration, "duration_seconds": duration,
"errors": errors, "errors": errors,
"logs": logs, "logs": logs,
}) }
)
try: try:
operation_id = await self.database.persist_sync_operation(sync_operation) operation_id = await self.database.persist_sync_operation(
sync_operation
)
logger.debug(f"Saved failed sync operation with ID: {operation_id}") logger.debug(f"Saved failed sync operation with ID: {operation_id}")
except Exception as persist_error: except Exception as persist_error:
logger.error(f"Failed to persist failed sync operation: {persist_error}") logger.error(
f"Failed to persist failed sync operation: {persist_error}"
)
raise raise
finally: finally:
@@ -229,7 +243,9 @@ class SyncService:
try: try:
# For now, delegate to sync_all_accounts but with specific filtering # For now, delegate to sync_all_accounts but with specific filtering
# This could be optimized later to only process specified accounts # This could be optimized later to only process specified accounts
result = await self.sync_all_accounts(force=force, trigger_type=trigger_type) result = await self.sync_all_accounts(
force=force, trigger_type=trigger_type
)
# Filter results to only specified accounts if needed # Filter results to only specified accounts if needed
# For simplicity, we'll return the full result for now # For simplicity, we'll return the full result for now