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" />
<link rel="icon" href="/favicon.ico" sizes="48x48" />
<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>
<!-- 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="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
@@ -21,8 +27,16 @@
<!-- Dynamic theme-color - will be updated by JavaScript -->
<meta name="theme-color" content="#0b74de" id="theme-color-meta" />
<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" />
<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 -->
<link rel="apple-touch-icon" href="/apple-touch-icon-180x180.png" />

View File

@@ -144,7 +144,8 @@ export default function AccountSettings() {
<CardHeader>
<CardTitle>Account Management</CardTitle>
<CardDescription>
Manage your connected bank accounts and customize their display names
Manage your connected bank accounts and customize their display
names
</CardDescription>
</CardHeader>
@@ -324,7 +325,8 @@ export default function AccountSettings() {
<CardHeader>
<CardTitle>Add New Bank Account</CardTitle>
<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>
</CardHeader>
<CardContent className="p-6">
@@ -332,7 +334,8 @@ export default function AccountSettings() {
<div className="p-4 bg-muted rounded-lg">
<Plus className="h-8 w-8 text-muted-foreground mx-auto mb-2" />
<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>
</div>
<Button disabled variant="outline">

View File

@@ -69,7 +69,11 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
asChild
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} />
<span className="text-base font-semibold">Leggen</span>
</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">
{accounts.map((account) => {
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 (
<div
@@ -151,7 +158,9 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
</div>
<div className="space-y-1 min-w-0 flex-1">
<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 className="text-xs font-semibold text-foreground">
{formatCurrency(primaryBalance, currency)}

View File

@@ -36,7 +36,11 @@ import {
SelectTrigger,
SelectValue,
} from "./ui/select";
import type { NotificationSettings, NotificationService, SyncOperationsResponse } from "../types/api";
import type {
NotificationSettings,
NotificationService,
SyncOperationsResponse,
} from "../types/api";
export default function Notifications() {
const [testService, setTestService] = useState("");
@@ -163,7 +167,8 @@ export default function Notifications() {
No sync operations yet
</h3>
<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>
</div>
) : (
@@ -171,9 +176,9 @@ export default function Notifications() {
{syncOperations.operations.slice(0, 5).map((operation) => {
const startedAt = new Date(operation.started_at);
const isRunning = !operation.completed_at;
const duration = operation.duration_seconds
const duration = operation.duration_seconds
? `${Math.round(operation.duration_seconds)}s`
: '';
: "";
return (
<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"
>
<div className="flex items-center space-x-4">
<div className={`p-2 rounded-full ${
isRunning
? 'bg-blue-100 text-blue-600'
: operation.success
? 'bg-green-100 text-green-600'
: 'bg-red-100 text-red-600'
}`}>
<div
className={`p-2 rounded-full ${
isRunning
? "bg-blue-100 text-blue-600"
: operation.success
? "bg-green-100 text-green-600"
: "bg-red-100 text-red-600"
}`}
>
{isRunning ? (
<RefreshCw className="h-4 w-4 animate-spin" />
) : operation.success ? (
@@ -199,7 +206,11 @@ export default function Notifications() {
<div>
<div className="flex items-center space-x-2">
<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>
<Badge variant="outline" className="text-xs">
{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">
<span className="flex items-center space-x-1">
<Clock className="h-3 w-3" />
<span>{startedAt.toLocaleDateString()} {startedAt.toLocaleTimeString()}</span>
<span>
{startedAt.toLocaleDateString()}{" "}
{startedAt.toLocaleTimeString()}
</span>
</span>
{duration && (
<span>Duration: {duration}</span>
)}
{duration && <span>Duration: {duration}</span>}
</div>
</div>
</div>
@@ -223,7 +235,9 @@ export default function Notifications() {
</div>
<div className="flex items-center space-x-2 mt-1">
<TrendingUp className="h-3 w-3" />
<span>{operation.transactions_added} new transactions</span>
<span>
{operation.transactions_added} new transactions
</span>
</div>
{operation.errors.length > 0 && (
<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 {
prompt(): Promise<void>;
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
userChoice: Promise<{ outcome: "accepted" | "dismissed" }>;
}
interface PWAPromptProps {
@@ -11,7 +11,8 @@ interface 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);
useEffect(() => {
@@ -96,7 +97,10 @@ interface PWAUpdatePromptProps {
onUpdate: () => void;
}
export function PWAUpdatePrompt({ updateAvailable, onUpdate }: PWAUpdatePromptProps) {
export function PWAUpdatePrompt({
updateAvailable,
onUpdate,
}: PWAUpdatePromptProps) {
const [showPrompt, setShowPrompt] = useState(false);
useEffect(() => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
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",
@@ -31,27 +31,27 @@ const buttonVariants = cva(
variant: "default",
size: "default",
},
}
)
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...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">>(
({ className, type, ...props }, ref) => {
@@ -9,14 +9,14 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
type={type}
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",
className
className,
)}
ref={ref}
{...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"
>
<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>
<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 SeparatorPrimitive from "@radix-ui/react-separator"
import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
@@ -9,7 +9,7 @@ const Separator = React.forwardRef<
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
ref,
) => (
<SeparatorPrimitive.Root
ref={ref}
@@ -18,12 +18,12 @@ const Separator = React.forwardRef<
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
className,
)}
{...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 SheetPrimitive from "@radix-ui/react-dialog"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import * as React from "react";
import * as SheetPrimitive from "@radix-ui/react-dialog";
import { cva, type VariantProps } from "class-variance-authority";
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<
React.ElementRef<typeof SheetPrimitive.Overlay>,
@@ -22,13 +22,13 @@ const SheetOverlay = React.forwardRef<
<SheetPrimitive.Overlay
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",
className
className,
)}
{...props}
ref={ref}
/>
))
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
));
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
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",
@@ -46,8 +46,8 @@ const sheetVariants = cva(
defaultVariants: {
side: "right",
},
}
)
},
);
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
@@ -71,8 +71,8 @@ const SheetContent = React.forwardRef<
{children}
</SheetPrimitive.Content>
</SheetPortal>
))
SheetContent.displayName = SheetPrimitive.Content.displayName
));
SheetContent.displayName = SheetPrimitive.Content.displayName;
const SheetHeader = ({
className,
@@ -81,12 +81,12 @@ const SheetHeader = ({
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
className,
)}
{...props}
/>
)
SheetHeader.displayName = "SheetHeader"
);
SheetHeader.displayName = "SheetHeader";
const SheetFooter = ({
className,
@@ -95,12 +95,12 @@ const SheetFooter = ({
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
className,
)}
{...props}
/>
)
SheetFooter.displayName = "SheetFooter"
);
SheetFooter.displayName = "SheetFooter";
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
@@ -111,8 +111,8 @@ const SheetTitle = React.forwardRef<
className={cn("text-lg font-semibold text-foreground", className)}
{...props}
/>
))
SheetTitle.displayName = SheetPrimitive.Title.displayName
));
SheetTitle.displayName = SheetPrimitive.Title.displayName;
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
@@ -123,8 +123,8 @@ const SheetDescription = React.forwardRef<
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
SheetDescription.displayName = SheetPrimitive.Description.displayName
));
SheetDescription.displayName = SheetPrimitive.Description.displayName;
export {
Sheet,
@@ -137,4 +137,4 @@ export {
SheetFooter,
SheetTitle,
SheetDescription,
}
};

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import * as React from "react";
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<
React.ElementRef<typeof TabsPrimitive.List>,
@@ -13,12 +13,12 @@ const TabsList = React.forwardRef<
ref={ref}
className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
className
className,
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
@@ -28,12 +28,12 @@ const TabsTrigger = React.forwardRef<
ref={ref}
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",
className
className,
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
@@ -43,11 +43,11 @@ const TabsContent = React.forwardRef<
ref={ref}
className={cn(
"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}
/>
))
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 TooltipPrimitive from "@radix-ui/react-tooltip"
import * as React from "react";
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<
React.ElementRef<typeof TooltipPrimitive.Content>,
@@ -19,12 +19,12 @@ const TooltipContent = React.forwardRef<
sideOffset={sideOffset}
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]",
className
className,
)}
{...props}
/>
</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];
// 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) {
themeColorMeta.content = themeColor;
}
// 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) {
msThemeColorMeta.content = themeColor;
}
// 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) {
// 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() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
undefined,
);
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
}
mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener("change", onChange)
}, [])
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener("change", onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener("change", onChange);
}, []);
return !!isMobile
return !!isMobile;
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,12 @@ export default defineConfig({
react(),
VitePWA({
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: {
name: "Leggen",
short_name: "Leggen",
@@ -28,38 +33,38 @@ export default defineConfig({
short_name: "Transactions",
description: "View and manage transactions",
url: "/transactions",
icons: [{ src: "/pwa-192x192.png", sizes: "192x192" }]
icons: [{ src: "/pwa-192x192.png", sizes: "192x192" }],
},
{
name: "Analytics",
short_name: "Analytics",
description: "View financial analytics",
url: "/analytics",
icons: [{ src: "/pwa-192x192.png", sizes: "192x192" }]
}
icons: [{ src: "/pwa-192x192.png", sizes: "192x192" }],
},
],
icons: [
{
src: "pwa-64x64.png",
sizes: "64x64",
type: "image/png"
type: "image/png",
},
{
src: "pwa-192x192.png",
sizes: "192x192",
type: "image/png"
type: "image/png",
},
{
src: "pwa-512x512.png",
sizes: "512x512",
type: "image/png"
type: "image/png",
},
{
src: "maskable-icon-512x512.png",
sizes: "512x512",
type: "image/png",
purpose: "maskable"
}
purpose: "maskable",
},
],
},
workbox: {

View File

@@ -216,13 +216,13 @@ async def stop_scheduler() -> APIResponse:
@router.get("/sync/operations", response_model=APIResponse)
async def get_sync_operations(
limit: int = 50, offset: int = 0
) -> APIResponse:
async def get_sync_operations(limit: int = 50, offset: int = 0) -> APIResponse:
"""Get sync operations history"""
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(
success=True,
data={"operations": operations, "count": len(operations)},

View File

@@ -20,7 +20,9 @@ class SyncService:
"""Get current 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"""
if self._sync_status.is_running and not force:
raise Exception("Sync is already running")
@@ -149,21 +151,25 @@ class SyncService:
self._sync_status.last_sync = end_time
# Update sync operation with final results
sync_operation.update({
"completed_at": end_time.isoformat(),
"success": len(errors) == 0,
"accounts_processed": accounts_processed,
"transactions_added": transactions_added,
"transactions_updated": transactions_updated,
"balances_updated": balances_updated,
"duration_seconds": duration,
"errors": errors,
"logs": logs,
})
sync_operation.update(
{
"completed_at": end_time.isoformat(),
"success": len(errors) == 0,
"accounts_processed": accounts_processed,
"transactions_added": transactions_added,
"transactions_updated": transactions_updated,
"balances_updated": balances_updated,
"duration_seconds": duration,
"errors": errors,
"logs": logs,
}
)
# Persist sync operation to database
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}")
except Exception as e:
logger.error(f"Failed to persist sync operation: {e}")
@@ -183,7 +189,9 @@ class SyncService:
logger.info(
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
except Exception as e:
@@ -195,23 +203,29 @@ class SyncService:
# Save failed sync operation
end_time = datetime.now()
duration = (end_time - start_time).total_seconds()
sync_operation.update({
"completed_at": end_time.isoformat(),
"success": False,
"accounts_processed": accounts_processed,
"transactions_added": transactions_added,
"transactions_updated": transactions_updated,
"balances_updated": balances_updated,
"duration_seconds": duration,
"errors": errors,
"logs": logs,
})
sync_operation.update(
{
"completed_at": end_time.isoformat(),
"success": False,
"accounts_processed": accounts_processed,
"transactions_added": transactions_added,
"transactions_updated": transactions_updated,
"balances_updated": balances_updated,
"duration_seconds": duration,
"errors": errors,
"logs": logs,
}
)
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}")
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
finally:
@@ -229,8 +243,10 @@ class SyncService:
try:
# For now, delegate to sync_all_accounts but with specific filtering
# 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
# For simplicity, we'll return the full result for now
return result