diff --git a/src/components/features/snippet-display/SnippetCard.tsx b/src/components/features/snippet-display/SnippetCard.tsx index b9d5382..a065df4 100644 --- a/src/components/features/snippet-display/SnippetCard.tsx +++ b/src/components/features/snippet-display/SnippetCard.tsx @@ -1,23 +1,12 @@ import { useState, useMemo, useEffect } from 'react' -import { Badge } from '@/components/ui/badge' -import { Button } from '@/components/ui/button' import { Card } from '@/components/ui/card' -import { Checkbox } from '@/components/ui/checkbox' -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuSub, - DropdownMenuSubContent, - DropdownMenuSubTrigger, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu' -import { Copy, Pencil, Trash, Eye, DotsThree, FolderOpen } from '@phosphor-icons/react' import { Snippet, Namespace } from '@/lib/types' -import { strings, appConfig, LANGUAGE_COLORS } from '@/lib/config' +import { strings, appConfig } from '@/lib/config' import { getAllNamespaces, moveSnippetToNamespace } from '@/lib/db' import { toast } from 'sonner' +import { SnippetCardHeader } from './SnippetCardHeader' +import { SnippetCodePreview } from './SnippetCodePreview' +import { SnippetCardActions } from './SnippetCardActions' interface SnippetCardProps { snippet: Snippet @@ -157,127 +146,30 @@ export function SnippetCard({ onClick={handleView} >
-
-
- {selectionMode && ( - e.stopPropagation()} - className="mt-1" - /> - )} -
-

- {snippet.title} -

- {snippetData.description && ( -

- {snippetData.description} -

- )} -
-
- - {snippet.language} - -
+ -
-
-            {snippetData.displayCode}
-          
- {snippetData.isTruncated && ( -

- {strings.snippetCard.viewFullCode} -

- )} -
+ {!selectionMode && ( -
-
- -
-
- - - - - - - - e.stopPropagation()}> - - - - Move to... - - - {availableNamespaces.length === 0 ? ( - - No other namespaces - - ) : ( - availableNamespaces.map((namespace) => ( - handleMoveToNamespace(namespace.id)} - > - {namespace.name} - {namespace.isDefault && ( - (Default) - )} - - )) - )} - - - - - - Delete - - - -
-
+ )}
diff --git a/src/components/features/snippet-display/SnippetCardActions.tsx b/src/components/features/snippet-display/SnippetCardActions.tsx new file mode 100644 index 0000000..95becba --- /dev/null +++ b/src/components/features/snippet-display/SnippetCardActions.tsx @@ -0,0 +1,120 @@ +import { Button } from '@/components/ui/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' +import { Copy, Pencil, Trash, Eye, DotsThree, FolderOpen } from '@phosphor-icons/react' +import { Namespace } from '@/lib/types' +import { strings } from '@/lib/config' + +interface SnippetCardActionsProps { + isCopied: boolean + isMoving: boolean + availableNamespaces: Namespace[] + onView: (e: React.MouseEvent) => void + onCopy: (e: React.MouseEvent) => void + onEdit: (e: React.MouseEvent) => void + onDelete: (e: React.MouseEvent) => void + onMoveToNamespace: (namespaceId: string) => void +} + +export function SnippetCardActions({ + isCopied, + isMoving, + availableNamespaces, + onView, + onCopy, + onEdit, + onDelete, + onMoveToNamespace, +}: SnippetCardActionsProps) { + return ( +
+
+ +
+
+ + + + + + + + e.stopPropagation()}> + + + + Move to... + + + {availableNamespaces.length === 0 ? ( + + No other namespaces + + ) : ( + availableNamespaces.map((namespace) => ( + onMoveToNamespace(namespace.id)} + > + {namespace.name} + {namespace.isDefault && ( + (Default) + )} + + )) + )} + + + + + + Delete + + + +
+
+ ) +} diff --git a/src/components/features/snippet-display/SnippetCardHeader.tsx b/src/components/features/snippet-display/SnippetCardHeader.tsx new file mode 100644 index 0000000..d36be64 --- /dev/null +++ b/src/components/features/snippet-display/SnippetCardHeader.tsx @@ -0,0 +1,50 @@ +import { Badge } from '@/components/ui/badge' +import { Checkbox } from '@/components/ui/checkbox' +import { Snippet } from '@/lib/types' +import { LANGUAGE_COLORS } from '@/lib/config' + +interface SnippetCardHeaderProps { + snippet: Snippet + description: string + selectionMode: boolean + isSelected: boolean + onToggleSelect: () => void +} + +export function SnippetCardHeader({ + snippet, + description, + selectionMode, + isSelected, + onToggleSelect +}: SnippetCardHeaderProps) { + return ( +
+
+ {selectionMode && ( + e.stopPropagation()} + className="mt-1" + /> + )} +
+

+ {snippet.title} +

+ {description && ( +

+ {description} +

+ )} +
+
+ + {snippet.language} + +
+ ) +} diff --git a/src/components/features/snippet-display/SnippetCodePreview.tsx b/src/components/features/snippet-display/SnippetCodePreview.tsx new file mode 100644 index 0000000..ed7818d --- /dev/null +++ b/src/components/features/snippet-display/SnippetCodePreview.tsx @@ -0,0 +1,21 @@ +import { strings } from '@/lib/config' + +interface SnippetCodePreviewProps { + displayCode: string + isTruncated: boolean +} + +export function SnippetCodePreview({ displayCode, isTruncated }: SnippetCodePreviewProps) { + return ( +
+
+        {displayCode}
+      
+ {isTruncated && ( +

+ {strings.snippetCard.viewFullCode} +

+ )} +
+ ) +} diff --git a/src/components/features/snippet-viewer/SnippetViewer.tsx b/src/components/features/snippet-viewer/SnippetViewer.tsx index 70d03b2..3815fe9 100644 --- a/src/components/features/snippet-viewer/SnippetViewer.tsx +++ b/src/components/features/snippet-viewer/SnippetViewer.tsx @@ -2,18 +2,12 @@ import { Dialog, DialogContent, DialogHeader, - DialogTitle, } from '@/components/ui/dialog' -import { Button } from '@/components/ui/button' -import { Badge } from '@/components/ui/badge' -import { Copy, Pencil, Check, SplitVertical } from '@phosphor-icons/react' import { Snippet } from '@/lib/types' -import { MonacoEditor } from '@/components/features/snippet-editor/MonacoEditor' -import { ReactPreview } from '@/components/features/snippet-editor/ReactPreview' -import { PythonOutput } from '@/components/features/python-runner/PythonOutput' -import { cn } from '@/lib/utils' import { useState } from 'react' -import { strings, appConfig, LANGUAGE_COLORS } from '@/lib/config' +import { appConfig } from '@/lib/config' +import { SnippetViewerHeader } from './SnippetViewerHeader' +import { SnippetViewerContent } from './SnippetViewerContent' interface SnippetViewerProps { snippet: Snippet | null @@ -47,110 +41,24 @@ export function SnippetViewer({ snippet, open, onOpenChange, onEdit, onCopy }: S -
-
-
- - {snippet.title} - - - {snippet.language} - -
- {snippet.description && ( -

- {snippet.description} -

- )} -

- {strings.snippetViewer.lastUpdated}: {new Date(snippet.updatedAt).toLocaleString()} -

-
-
- {canPreview && ( - - )} - - -
-
+ setShowPreview(!showPreview)} + />
- {canPreview && showPreview ? ( - <> -
- {}} - language={snippet.language} - height="100%" - readOnly={true} - /> -
-
- {isPython ? ( - - ) : ( - - )} -
- - ) : ( -
- {}} - language={snippet.language} - height="100%" - readOnly={true} - /> -
- )} +
diff --git a/src/components/features/snippet-viewer/SnippetViewerContent.tsx b/src/components/features/snippet-viewer/SnippetViewerContent.tsx new file mode 100644 index 0000000..5524219 --- /dev/null +++ b/src/components/features/snippet-viewer/SnippetViewerContent.tsx @@ -0,0 +1,58 @@ +import { Snippet } from '@/lib/types' +import { MonacoEditor } from '@/components/features/snippet-editor/MonacoEditor' +import { ReactPreview } from '@/components/features/snippet-editor/ReactPreview' +import { PythonOutput } from '@/components/features/python-runner/PythonOutput' + +interface SnippetViewerContentProps { + snippet: Snippet + canPreview: boolean + showPreview: boolean + isPython: boolean +} + +export function SnippetViewerContent({ + snippet, + canPreview, + showPreview, + isPython, +}: SnippetViewerContentProps) { + if (canPreview && showPreview) { + return ( + <> +
+ {}} + language={snippet.language} + height="100%" + readOnly={true} + /> +
+
+ {isPython ? ( + + ) : ( + + )} +
+ + ) + } + + return ( +
+ {}} + language={snippet.language} + height="100%" + readOnly={true} + /> +
+ ) +} diff --git a/src/components/features/snippet-viewer/SnippetViewerHeader.tsx b/src/components/features/snippet-viewer/SnippetViewerHeader.tsx new file mode 100644 index 0000000..f1f7221 --- /dev/null +++ b/src/components/features/snippet-viewer/SnippetViewerHeader.tsx @@ -0,0 +1,96 @@ +import { Button } from '@/components/ui/button' +import { Badge } from '@/components/ui/badge' +import { DialogTitle } from '@/components/ui/dialog' +import { Copy, Pencil, Check, SplitVertical } from '@phosphor-icons/react' +import { Snippet } from '@/lib/types' +import { cn } from '@/lib/utils' +import { strings, LANGUAGE_COLORS } from '@/lib/config' + +interface SnippetViewerHeaderProps { + snippet: Snippet + isCopied: boolean + canPreview: boolean + showPreview: boolean + onCopy: () => void + onEdit: () => void + onTogglePreview: () => void +} + +export function SnippetViewerHeader({ + snippet, + isCopied, + canPreview, + showPreview, + onCopy, + onEdit, + onTogglePreview, +}: SnippetViewerHeaderProps) { + return ( +
+
+
+ + {snippet.title} + + + {snippet.language} + +
+ {snippet.description && ( +

+ {snippet.description} +

+ )} +

+ {strings.snippetViewer.lastUpdated}: {new Date(snippet.updatedAt).toLocaleString()} +

+
+
+ {canPreview && ( + + )} + + +
+
+ ) +} diff --git a/src/components/settings/BackendAutoConfigCard.tsx b/src/components/settings/BackendAutoConfigCard.tsx new file mode 100644 index 0000000..e932227 --- /dev/null +++ b/src/components/settings/BackendAutoConfigCard.tsx @@ -0,0 +1,72 @@ +import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { CloudCheck, CloudSlash } from '@phosphor-icons/react' + +interface BackendAutoConfigCardProps { + envVarSet: boolean + flaskUrl: string + flaskConnectionStatus: 'unknown' | 'connected' | 'failed' + testingConnection: boolean + onTestConnection: () => Promise +} + +export function BackendAutoConfigCard({ + envVarSet, + flaskUrl, + flaskConnectionStatus, + testingConnection, + onTestConnection +}: BackendAutoConfigCardProps) { + if (!envVarSet) return null + + return ( + + + + + Backend Auto-Configured + + + Flask backend is configured via environment variable + + + +
+
+ Backend URL + {flaskUrl} +
+
+ Configuration Source + VITE_FLASK_BACKEND_URL +
+
+ Status + {flaskConnectionStatus === 'connected' && ( + + + Connected + + )} + {flaskConnectionStatus === 'failed' && ( + + + Connection Failed + + )} + {flaskConnectionStatus === 'unknown' && ( + + )} +
+
+
+
+ ) +} diff --git a/src/components/settings/DatabaseActionsCard.tsx b/src/components/settings/DatabaseActionsCard.tsx new file mode 100644 index 0000000..a2b4fce --- /dev/null +++ b/src/components/settings/DatabaseActionsCard.tsx @@ -0,0 +1,84 @@ +import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Database, Download, Upload, Trash } from '@phosphor-icons/react' + +interface DatabaseActionsCardProps { + onExport: () => Promise + onImport: (event: React.ChangeEvent) => Promise + onSeed: () => Promise + onClear: () => Promise +} + +export function DatabaseActionsCard({ + onExport, + onImport, + onSeed, + onClear +}: DatabaseActionsCardProps) { + return ( + + + Database Actions + + Backup, restore, or reset your database + + + +
+

Export Database

+

+ Download your database as a file for backup or transfer to another device +

+ +
+ +
+

Import Database

+

+ Restore a previously exported database file +

+ +
+ +
+

Sample Data

+

+ Add sample code snippets to get started (only if database is empty) +

+ +
+ +
+

Clear All Data

+

+ Permanently delete all snippets and templates. This cannot be undone. +

+ +
+
+
+ ) +} diff --git a/src/components/settings/DatabaseStatsCard.tsx b/src/components/settings/DatabaseStatsCard.tsx new file mode 100644 index 0000000..b9b5b2b --- /dev/null +++ b/src/components/settings/DatabaseStatsCard.tsx @@ -0,0 +1,55 @@ +import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card' +import { Database } from '@phosphor-icons/react' + +interface DatabaseStatsCardProps { + loading: boolean + stats: { + snippetCount: number + templateCount: number + storageType: 'indexeddb' | 'localstorage' | 'none' + databaseSize: number + } | null + formatBytes: (bytes: number) => string +} + +export function DatabaseStatsCard({ loading, stats, formatBytes }: DatabaseStatsCardProps) { + return ( + + + + + Database Statistics + + + Information about your local database storage + + + + {loading ? ( +

Loading...

+ ) : stats ? ( +
+
+ Snippets + {stats.snippetCount} +
+
+ Templates + {stats.templateCount} +
+
+ Storage Type + {stats.storageType} +
+
+ Database Size + {formatBytes(stats.databaseSize)} +
+
+ ) : ( +

Failed to load statistics

+ )} +
+
+ ) +} diff --git a/src/components/settings/SchemaHealthCard.tsx b/src/components/settings/SchemaHealthCard.tsx new file mode 100644 index 0000000..bdb0cff --- /dev/null +++ b/src/components/settings/SchemaHealthCard.tsx @@ -0,0 +1,67 @@ +import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Alert, AlertDescription } from '@/components/ui/alert' +import { Warning, FirstAid, CheckCircle } from '@phosphor-icons/react' + +interface SchemaHealthCardProps { + schemaHealth: 'unknown' | 'healthy' | 'corrupted' + checkingSchema: boolean + onClear: () => Promise + onCheckSchema: () => Promise +} + +export function SchemaHealthCard({ + schemaHealth, + checkingSchema, + onClear, + onCheckSchema +}: SchemaHealthCardProps) { + if (schemaHealth === 'unknown') return null + + if (schemaHealth === 'corrupted') { + return ( + + + + + Schema Corruption Detected + + + Your database schema is outdated or corrupted and needs to be repaired + + + + + + The database schema is missing required tables or columns (likely due to namespace feature addition). + This can cause errors when loading or saving snippets. Click the button below to wipe and recreate the database with the correct schema. + + +
+ + +
+
+
+ ) + } + + return ( + + + + + Schema Healthy + + + Your database schema is up to date and functioning correctly + + + + ) +} diff --git a/src/components/settings/StorageBackendCard.tsx b/src/components/settings/StorageBackendCard.tsx new file mode 100644 index 0000000..80d8d1c --- /dev/null +++ b/src/components/settings/StorageBackendCard.tsx @@ -0,0 +1,157 @@ +import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Alert, AlertDescription } from '@/components/ui/alert' +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' +import { Database, CloudArrowUp, CloudCheck, CloudSlash, Upload, Download } from '@phosphor-icons/react' +import { type StorageBackend } from '@/lib/storage' + +interface StorageBackendCardProps { + storageBackend: StorageBackend + flaskUrl: string + flaskConnectionStatus: 'unknown' | 'connected' | 'failed' + testingConnection: boolean + envVarSet: boolean + onStorageBackendChange: (backend: StorageBackend) => void + onFlaskUrlChange: (url: string) => void + onTestConnection: () => Promise + onSaveConfig: () => Promise + onMigrateToFlask: () => Promise + onMigrateToIndexedDB: () => Promise +} + +export function StorageBackendCard({ + storageBackend, + flaskUrl, + flaskConnectionStatus, + testingConnection, + envVarSet, + onStorageBackendChange, + onFlaskUrlChange, + onTestConnection, + onSaveConfig, + onMigrateToFlask, + onMigrateToIndexedDB, +}: StorageBackendCardProps) { + return ( + + + + + Storage Backend + + + Choose where your snippets are stored + + + + {envVarSet && ( + + + + + Storage backend is configured via VITE_FLASK_BACKEND_URL environment variable and cannot be changed here. + + + + )} + + onStorageBackendChange(value as StorageBackend)} + disabled={envVarSet} + > +
+ +
+ +

+ Store snippets locally in your browser. Data persists on this device only. +

+
+
+ +
+ +
+ +

+ Store snippets on a Flask backend server. Data is accessible from any device. +

+
+
+
+ + {storageBackend === 'flask' && ( +
+
+ +
+ onFlaskUrlChange(e.target.value)} + disabled={envVarSet} + /> + +
+ {flaskConnectionStatus === 'connected' && ( +
+ + Connected successfully +
+ )} + {flaskConnectionStatus === 'failed' && ( +
+ + Connection failed +
+ )} +
+ +
+ + +
+
+ )} + +
+ +
+
+
+ ) +} diff --git a/src/components/settings/StorageInfoCard.tsx b/src/components/settings/StorageInfoCard.tsx new file mode 100644 index 0000000..41d7c76 --- /dev/null +++ b/src/components/settings/StorageInfoCard.tsx @@ -0,0 +1,40 @@ +import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card' +import { Alert, AlertDescription } from '@/components/ui/alert' + +interface StorageInfoCardProps { + storageType?: 'indexeddb' | 'localstorage' | 'none' +} + +export function StorageInfoCard({ storageType }: StorageInfoCardProps) { + return ( + + + Storage Information + + How your data is stored + + + + + + {storageType === 'indexeddb' ? ( + <> + IndexedDB is being used for storage. This provides better performance and + larger storage capacity compared to localStorage. Your data persists locally in your browser. + + ) : storageType === 'localstorage' ? ( + <> + localStorage is being used for storage. IndexedDB is not available in your + browser. Note that localStorage has a smaller storage limit (typically 5-10MB). + + ) : ( + <> + No persistent storage detected. Your data will be lost when you close the browser. + + )} + + + + + ) +} diff --git a/src/components/ui/sidebar-context.tsx b/src/components/ui/sidebar-context.tsx new file mode 100644 index 0000000..7406539 --- /dev/null +++ b/src/components/ui/sidebar-context.tsx @@ -0,0 +1,132 @@ +"use client" + +import { CSSProperties, ComponentProps, createContext, useCallback, useContext, useEffect, useMemo, useState } from "react" +import { useIsMobile } from "@/hooks/use-mobile" +import { cn } from "@/lib/utils" +import { TooltipProvider } from "@/components/ui/tooltip" + +const SIDEBAR_COOKIE_NAME = "sidebar_state" +const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 +export const SIDEBAR_WIDTH = "16rem" +export const SIDEBAR_WIDTH_MOBILE = "18rem" +export const SIDEBAR_WIDTH_ICON = "3rem" +const SIDEBAR_KEYBOARD_SHORTCUT = "b" + +export type SidebarContextProps = { + state: "expanded" | "collapsed" + open: boolean + setOpen: (open: boolean) => void + openMobile: boolean + setOpenMobile: (open: boolean) => void + isMobile: boolean + toggleSidebar: () => void +} + +const SidebarContext = createContext(null) + +export function useSidebar() { + const context = useContext(SidebarContext) + if (!context) { + throw new Error("useSidebar must be used within a SidebarProvider.") + } + + return context +} + +export function SidebarProvider({ + defaultOpen = true, + open: openProp, + onOpenChange: setOpenProp, + className, + style, + children, + ...props +}: ComponentProps<"div"> & { + defaultOpen?: boolean + open?: boolean + onOpenChange?: (open: boolean) => void +}) { + const isMobile = useIsMobile() + const [openMobile, setOpenMobile] = useState(false) + + // This is the internal state of the sidebar. + // We use openProp and setOpenProp for control from outside the component. + const [_open, _setOpen] = useState(defaultOpen) + const open = openProp ?? _open + const setOpen = useCallback( + (value: boolean | ((value: boolean) => boolean)) => { + const openState = typeof value === "function" ? value(open) : value + if (setOpenProp) { + setOpenProp(openState) + } else { + _setOpen(openState) + } + + // This sets the cookie to keep the sidebar state. + document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}` + }, + [setOpenProp, open] + ) + + // Helper to toggle the sidebar. + const toggleSidebar = useCallback(() => { + return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open) + }, [isMobile, setOpen, setOpenMobile]) + + // Adds a keyboard shortcut to toggle the sidebar. + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if ( + event.key === SIDEBAR_KEYBOARD_SHORTCUT && + (event.metaKey || event.ctrlKey) + ) { + event.preventDefault() + toggleSidebar() + } + } + + window.addEventListener("keydown", handleKeyDown) + return () => window.removeEventListener("keydown", handleKeyDown) + }, [toggleSidebar]) + + // We add a state so that we can do data-state="expanded" or "collapsed". + // This makes it easier to style the sidebar with Tailwind classes. + const state = open ? "expanded" : "collapsed" + + const contextValue = useMemo( + () => ({ + state, + open, + setOpen, + isMobile, + openMobile, + setOpenMobile, + toggleSidebar, + }), + [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] + ) + + return ( + + +
+ {children} +
+
+
+ ) +} diff --git a/src/components/ui/sidebar-core.tsx b/src/components/ui/sidebar-core.tsx new file mode 100644 index 0000000..3679772 --- /dev/null +++ b/src/components/ui/sidebar-core.tsx @@ -0,0 +1,181 @@ +"use client" + +import { CSSProperties, ComponentProps } from "react" +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet" +import { useSidebar, SIDEBAR_WIDTH_MOBILE } from "./sidebar-context" +import PanelLeftIcon from "lucide-react/dist/esm/icons/panel-left" + +export function Sidebar({ + side = "left", + variant = "sidebar", + collapsible = "offcanvas", + className, + children, + ...props +}: ComponentProps<"div"> & { + side?: "left" | "right" + variant?: "sidebar" | "floating" | "inset" + collapsible?: "offcanvas" | "icon" | "none" +}) { + const { isMobile, state, openMobile, setOpenMobile } = useSidebar() + + if (collapsible === "none") { + return ( +
+ {children} +
+ ) + } + + if (isMobile) { + return ( + + + + Sidebar + Displays the mobile sidebar. + +
{children}
+
+
+ ) + } + + return ( +
+ {/* This is what handles the sidebar gap on desktop */} +
+ +
+ ) +} + +export function SidebarTrigger({ + className, + onClick, + ...props +}: ComponentProps) { + const { toggleSidebar } = useSidebar() + + return ( + + ) +} + +export function SidebarRail({ className, ...props }: ComponentProps<"button">) { + const { toggleSidebar } = useSidebar() + + return ( + - ) -} - -function SidebarRail({ className, ...props }: ComponentProps<"button">) { - const { toggleSidebar } = useSidebar() - - return ( - - -
- - - )} + - {schemaHealth === 'healthy' && ( - - - - - Schema Healthy - - - Your database schema is up to date and functioning correctly - - - - )} - - {envVarSet && ( - - - - - Backend Auto-Configured - - - Flask backend is configured via environment variable - - - -
-
- Backend URL - {flaskUrl} -
-
- Configuration Source - VITE_FLASK_BACKEND_URL -
-
- Status - {flaskConnectionStatus === 'connected' && ( - - - Connected - - )} - {flaskConnectionStatus === 'failed' && ( - - - Connection Failed - - )} - {flaskConnectionStatus === 'unknown' && ( - - )} -
-
-
-
- )} + - - - - - Storage Backend - - - Choose where your snippets are stored - - - - {envVarSet && ( - - - - - Storage backend is configured via VITE_FLASK_BACKEND_URL environment variable and cannot be changed here. - - - - )} - - setStorageBackend(value as StorageBackend)} - disabled={envVarSet} - > -
- -
- -

- Store snippets locally in your browser. Data persists on this device only. -

-
-
- -
- -
- -

- Store snippets on a Flask backend server. Data is accessible from any device. -

-
-
-
+ { + setFlaskUrl(url) + setFlaskConnectionStatus('unknown') + }} + onTestConnection={handleTestConnection} + onSaveConfig={handleSaveStorageConfig} + onMigrateToFlask={handleMigrateToFlask} + onMigrateToIndexedDB={handleMigrateToIndexedDB} + /> - {storageBackend === 'flask' && ( -
-
- -
- { - setFlaskUrl(e.target.value) - setFlaskConnectionStatus('unknown') - }} - disabled={envVarSet} - /> - -
- {flaskConnectionStatus === 'connected' && ( -
- - Connected successfully -
- )} - {flaskConnectionStatus === 'failed' && ( -
- - Connection failed -
- )} -
+ -
- - -
-
- )} + -
- -
-
-
- - - - - - Database Statistics - - - Information about your local database storage - - - - {loading ? ( -

Loading...

- ) : stats ? ( -
-
- Snippets - {stats.snippetCount} -
-
- Templates - {stats.templateCount} -
-
- Storage Type - {stats.storageType} -
-
- Database Size - {formatBytes(stats.databaseSize)} -
-
- ) : ( -

Failed to load statistics

- )} -
-
- - - - Storage Information - - How your data is stored - - - - - - {stats?.storageType === 'indexeddb' ? ( - <> - IndexedDB is being used for storage. This provides better performance and - larger storage capacity compared to localStorage. Your data persists locally in your browser. - - ) : stats?.storageType === 'localstorage' ? ( - <> - localStorage is being used for storage. IndexedDB is not available in your - browser. Note that localStorage has a smaller storage limit (typically 5-10MB). - - ) : ( - <> - No persistent storage detected. Your data will be lost when you close the browser. - - )} - - - - - - - - Database Actions - - Backup, restore, or reset your database - - - -
-

Export Database

-

- Download your database as a file for backup or transfer to another device -

- -
- -
-

Import Database

-

- Restore a previously exported database file -

- -
- -
-

Sample Data

-

- Add sample code snippets to get started (only if database is empty) -

- -
- -
-

Clear All Data

-

- Permanently delete all snippets and templates. This cannot be undone. -

- -
-
-
+ )