mirror of
https://github.com/elisiariocouto/leggen.git
synced 2025-12-14 15:32:21 +00:00
Reformat files.
This commit is contained in:
committed by
Elisiário Couto
parent
65404848aa
commit
eb38264c68
@@ -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" />
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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>
|
||||||
) : (
|
) : (
|
||||||
@@ -171,9 +176,9 @@ export default function Notifications() {
|
|||||||
{syncOperations.operations.slice(0, 5).map((operation) => {
|
{syncOperations.operations.slice(0, 5).map((operation) => {
|
||||||
const startedAt = new Date(operation.started_at);
|
const startedAt = new Date(operation.started_at);
|
||||||
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
|
||||||
isRunning
|
className={`p-2 rounded-full ${
|
||||||
? 'bg-blue-100 text-blue-600'
|
isRunning
|
||||||
: operation.success
|
? "bg-blue-100 text-blue-600"
|
||||||
? 'bg-green-100 text-green-600'
|
: operation.success
|
||||||
: 'bg-red-100 text-red-600'
|
? "bg-green-100 text-green-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>
|
</span>
|
||||||
{duration && (
|
{duration && <span>Duration: {duration}</span>}
|
||||||
<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">
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
isRunning
|
className={`p-2 rounded-full ${
|
||||||
? 'bg-blue-100 text-blue-600'
|
isRunning
|
||||||
: operation.success
|
? "bg-blue-100 text-blue-600"
|
||||||
? 'bg-green-100 text-green-600'
|
: operation.success
|
||||||
: 'bg-red-100 text-red-600'
|
? "bg-green-100 text-green-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>
|
</span>
|
||||||
{duration && (
|
{duration && <span>Duration: {duration}</span>}
|
||||||
<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>
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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
|
||||||
"p-3 rounded-full",
|
className={cn(
|
||||||
iconColor === "green" && "bg-green-100 dark:bg-green-900/20",
|
"p-3 rounded-full",
|
||||||
iconColor === "blue" && "bg-blue-100 dark:bg-blue-900/20",
|
iconColor === "green" && "bg-green-100 dark:bg-green-900/20",
|
||||||
iconColor === "red" && "bg-red-100 dark:bg-red-900/20",
|
iconColor === "blue" && "bg-blue-100 dark:bg-blue-900/20",
|
||||||
iconColor === "purple" && "bg-purple-100 dark:bg-purple-900/20",
|
iconColor === "red" && "bg-red-100 dark:bg-red-900/20",
|
||||||
iconColor === "orange" && "bg-orange-100 dark:bg-orange-900/20",
|
iconColor === "purple" && "bg-purple-100 dark:bg-purple-900/20",
|
||||||
iconColor === "default" && "bg-muted"
|
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",
|
<Icon
|
||||||
iconColor === "blue" && "text-blue-600",
|
className={cn(
|
||||||
iconColor === "red" && "text-red-600",
|
"h-6 w-6",
|
||||||
iconColor === "purple" && "text-purple-600",
|
iconColor === "green" && "text-green-600",
|
||||||
iconColor === "orange" && "text-orange-600",
|
iconColor === "blue" && "text-blue-600",
|
||||||
iconColor === "default" && "text-muted-foreground"
|
iconColor === "red" && "text-red-600",
|
||||||
)} />
|
iconColor === "purple" && "text-purple-600",
|
||||||
|
iconColor === "orange" && "text-orange-600",
|
||||||
|
iconColor === "default" && "text-muted-foreground",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -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":
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,26 +7,30 @@ 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")
|
||||||
const updateSWFunction = registerSW({
|
.then(({ registerSW }) => {
|
||||||
onNeedRefresh() {
|
const updateSWFunction = registerSW({
|
||||||
setUpdateAvailable(true);
|
onNeedRefresh() {
|
||||||
setUpdateSW(() => updateSWFunction);
|
setUpdateAvailable(true);
|
||||||
},
|
setUpdateSW(() => updateSWFunction);
|
||||||
onOfflineReady() {
|
},
|
||||||
console.log("App ready to work offline");
|
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");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -3,71 +3,71 @@ export default {
|
|||||||
darkMode: ["class"],
|
darkMode: ["class"],
|
||||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||||
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")],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -216,13 +216,13 @@ 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,
|
||||||
data={"operations": operations, "count": len(operations)},
|
data={"operations": operations, "count": len(operations)},
|
||||||
|
|||||||
@@ -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,21 +151,25 @@ 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(),
|
{
|
||||||
"success": len(errors) == 0,
|
"completed_at": end_time.isoformat(),
|
||||||
"accounts_processed": accounts_processed,
|
"success": len(errors) == 0,
|
||||||
"transactions_added": transactions_added,
|
"accounts_processed": accounts_processed,
|
||||||
"transactions_updated": transactions_updated,
|
"transactions_added": transactions_added,
|
||||||
"balances_updated": balances_updated,
|
"transactions_updated": transactions_updated,
|
||||||
"duration_seconds": duration,
|
"balances_updated": balances_updated,
|
||||||
"errors": errors,
|
"duration_seconds": duration,
|
||||||
"logs": logs,
|
"errors": errors,
|
||||||
})
|
"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,23 +203,29 @@ 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(),
|
{
|
||||||
"success": False,
|
"completed_at": end_time.isoformat(),
|
||||||
"accounts_processed": accounts_processed,
|
"success": False,
|
||||||
"transactions_added": transactions_added,
|
"accounts_processed": accounts_processed,
|
||||||
"transactions_updated": transactions_updated,
|
"transactions_added": transactions_added,
|
||||||
"balances_updated": balances_updated,
|
"transactions_updated": transactions_updated,
|
||||||
"duration_seconds": duration,
|
"balances_updated": balances_updated,
|
||||||
"errors": errors,
|
"duration_seconds": duration,
|
||||||
"logs": logs,
|
"errors": errors,
|
||||||
})
|
"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,8 +243,10 @@ 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
|
||||||
return result
|
return result
|
||||||
|
|||||||
Reference in New Issue
Block a user