mirror of
https://github.com/johndoe6345789/snippet-pastebin.git
synced 2026-04-24 13:34:55 +00:00
Split SettingsPage.tsx (627 LOC) into 6 smaller components
Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
72
src/components/settings/BackendAutoConfigCard.tsx
Normal file
72
src/components/settings/BackendAutoConfigCard.tsx
Normal file
@@ -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<void>
|
||||
}
|
||||
|
||||
export function BackendAutoConfigCard({
|
||||
envVarSet,
|
||||
flaskUrl,
|
||||
flaskConnectionStatus,
|
||||
testingConnection,
|
||||
onTestConnection
|
||||
}: BackendAutoConfigCardProps) {
|
||||
if (!envVarSet) return null
|
||||
|
||||
return (
|
||||
<Card className="border-accent">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-accent">
|
||||
<CloudCheck weight="fill" size={24} />
|
||||
Backend Auto-Configured
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Flask backend is configured via environment variable
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<span className="text-sm text-muted-foreground">Backend URL</span>
|
||||
<code className="text-sm font-mono bg-muted px-2 py-1 rounded">{flaskUrl}</code>
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<span className="text-sm text-muted-foreground">Configuration Source</span>
|
||||
<code className="text-sm font-mono bg-muted px-2 py-1 rounded">VITE_FLASK_BACKEND_URL</code>
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<span className="text-sm text-muted-foreground">Status</span>
|
||||
{flaskConnectionStatus === 'connected' && (
|
||||
<span className="flex items-center gap-2 text-sm text-green-600">
|
||||
<CloudCheck weight="fill" size={16} />
|
||||
Connected
|
||||
</span>
|
||||
)}
|
||||
{flaskConnectionStatus === 'failed' && (
|
||||
<span className="flex items-center gap-2 text-sm text-destructive">
|
||||
<CloudSlash weight="fill" size={16} />
|
||||
Connection Failed
|
||||
</span>
|
||||
)}
|
||||
{flaskConnectionStatus === 'unknown' && (
|
||||
<Button
|
||||
onClick={onTestConnection}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={testingConnection}
|
||||
>
|
||||
{testingConnection ? 'Testing...' : 'Test Connection'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
84
src/components/settings/DatabaseActionsCard.tsx
Normal file
84
src/components/settings/DatabaseActionsCard.tsx
Normal file
@@ -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<void>
|
||||
onImport: (event: React.ChangeEvent<HTMLInputElement>) => Promise<void>
|
||||
onSeed: () => Promise<void>
|
||||
onClear: () => Promise<void>
|
||||
}
|
||||
|
||||
export function DatabaseActionsCard({
|
||||
onExport,
|
||||
onImport,
|
||||
onSeed,
|
||||
onClear
|
||||
}: DatabaseActionsCardProps) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Database Actions</CardTitle>
|
||||
<CardDescription>
|
||||
Backup, restore, or reset your database
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold mb-2">Export Database</h3>
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
Download your database as a file for backup or transfer to another device
|
||||
</p>
|
||||
<Button onClick={onExport} variant="outline" className="gap-2">
|
||||
<Download weight="bold" size={16} />
|
||||
Export Database
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-border">
|
||||
<h3 className="text-sm font-semibold mb-2">Import Database</h3>
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
Restore a previously exported database file
|
||||
</p>
|
||||
<label>
|
||||
<input
|
||||
type="file"
|
||||
accept=".db"
|
||||
onChange={onImport}
|
||||
className="hidden"
|
||||
id="import-db"
|
||||
/>
|
||||
<Button variant="outline" className="gap-2" asChild>
|
||||
<span>
|
||||
<Upload weight="bold" size={16} />
|
||||
Import Database
|
||||
</span>
|
||||
</Button>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-border">
|
||||
<h3 className="text-sm font-semibold mb-2">Sample Data</h3>
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
Add sample code snippets to get started (only if database is empty)
|
||||
</p>
|
||||
<Button onClick={onSeed} variant="outline" className="gap-2">
|
||||
<Database weight="bold" size={16} />
|
||||
Add Sample Data
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-border">
|
||||
<h3 className="text-sm font-semibold mb-2 text-destructive">Clear All Data</h3>
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
Permanently delete all snippets and templates. This cannot be undone.
|
||||
</p>
|
||||
<Button onClick={onClear} variant="destructive" className="gap-2">
|
||||
<Trash weight="bold" size={16} />
|
||||
Clear Database
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
55
src/components/settings/DatabaseStatsCard.tsx
Normal file
55
src/components/settings/DatabaseStatsCard.tsx
Normal file
@@ -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 (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Database weight="duotone" size={24} />
|
||||
Database Statistics
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Information about your local database storage
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{loading ? (
|
||||
<p className="text-muted-foreground">Loading...</p>
|
||||
) : stats ? (
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-center py-2 border-b border-border">
|
||||
<span className="text-sm text-muted-foreground">Snippets</span>
|
||||
<span className="font-semibold">{stats.snippetCount}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-2 border-b border-border">
|
||||
<span className="text-sm text-muted-foreground">Templates</span>
|
||||
<span className="font-semibold">{stats.templateCount}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-2 border-b border-border">
|
||||
<span className="text-sm text-muted-foreground">Storage Type</span>
|
||||
<span className="font-semibold capitalize">{stats.storageType}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-2">
|
||||
<span className="text-sm text-muted-foreground">Database Size</span>
|
||||
<span className="font-semibold">{formatBytes(stats.databaseSize)}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-destructive">Failed to load statistics</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
67
src/components/settings/SchemaHealthCard.tsx
Normal file
67
src/components/settings/SchemaHealthCard.tsx
Normal file
@@ -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<void>
|
||||
onCheckSchema: () => Promise<void>
|
||||
}
|
||||
|
||||
export function SchemaHealthCard({
|
||||
schemaHealth,
|
||||
checkingSchema,
|
||||
onClear,
|
||||
onCheckSchema
|
||||
}: SchemaHealthCardProps) {
|
||||
if (schemaHealth === 'unknown') return null
|
||||
|
||||
if (schemaHealth === 'corrupted') {
|
||||
return (
|
||||
<Card className="border-destructive bg-destructive/10">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-destructive">
|
||||
<Warning weight="fill" size={24} />
|
||||
Schema Corruption Detected
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Your database schema is outdated or corrupted and needs to be repaired
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Alert className="border-destructive">
|
||||
<AlertDescription>
|
||||
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.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={onClear} variant="destructive" className="gap-2">
|
||||
<FirstAid weight="bold" size={16} />
|
||||
Repair Database (Wipe & Recreate)
|
||||
</Button>
|
||||
<Button onClick={onCheckSchema} variant="outline" disabled={checkingSchema}>
|
||||
{checkingSchema ? 'Checking...' : 'Re-check Schema'}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="border-green-600 bg-green-600/10">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-green-600">
|
||||
<CheckCircle weight="fill" size={24} />
|
||||
Schema Healthy
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Your database schema is up to date and functioning correctly
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
157
src/components/settings/StorageBackendCard.tsx
Normal file
157
src/components/settings/StorageBackendCard.tsx
Normal file
@@ -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<void>
|
||||
onSaveConfig: () => Promise<void>
|
||||
onMigrateToFlask: () => Promise<void>
|
||||
onMigrateToIndexedDB: () => Promise<void>
|
||||
}
|
||||
|
||||
export function StorageBackendCard({
|
||||
storageBackend,
|
||||
flaskUrl,
|
||||
flaskConnectionStatus,
|
||||
testingConnection,
|
||||
envVarSet,
|
||||
onStorageBackendChange,
|
||||
onFlaskUrlChange,
|
||||
onTestConnection,
|
||||
onSaveConfig,
|
||||
onMigrateToFlask,
|
||||
onMigrateToIndexedDB,
|
||||
}: StorageBackendCardProps) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<CloudArrowUp weight="duotone" size={24} />
|
||||
Storage Backend
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Choose where your snippets are stored
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{envVarSet && (
|
||||
<Alert className="border-accent bg-accent/10">
|
||||
<AlertDescription className="flex items-center gap-2">
|
||||
<CloudCheck weight="fill" size={16} className="text-accent" />
|
||||
<span>
|
||||
Storage backend is configured via <code className="px-1.5 py-0.5 rounded bg-muted text-xs font-mono">VITE_FLASK_BACKEND_URL</code> environment variable and cannot be changed here.
|
||||
</span>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<RadioGroup
|
||||
value={storageBackend}
|
||||
onValueChange={(value) => onStorageBackendChange(value as StorageBackend)}
|
||||
disabled={envVarSet}
|
||||
>
|
||||
<div className="flex items-start space-x-3 space-y-0">
|
||||
<RadioGroupItem value="indexeddb" id="storage-indexeddb" disabled={envVarSet} />
|
||||
<div className="flex-1">
|
||||
<Label htmlFor="storage-indexeddb" className={`font-semibold ${envVarSet ? 'opacity-50' : 'cursor-pointer'}`}>
|
||||
IndexedDB (Local Browser Storage)
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Store snippets locally in your browser. Data persists on this device only.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-3 space-y-0 mt-4">
|
||||
<RadioGroupItem value="flask" id="storage-flask" disabled={envVarSet} />
|
||||
<div className="flex-1">
|
||||
<Label htmlFor="storage-flask" className={`font-semibold ${envVarSet ? 'opacity-50' : 'cursor-pointer'}`}>
|
||||
Flask Backend (Remote Server)
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Store snippets on a Flask backend server. Data is accessible from any device.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
|
||||
{storageBackend === 'flask' && (
|
||||
<div className="space-y-4 p-4 border border-border rounded-lg bg-muted/50">
|
||||
<div>
|
||||
<Label htmlFor="flask-url">Flask Backend URL</Label>
|
||||
<div className="flex gap-2 mt-2">
|
||||
<Input
|
||||
id="flask-url"
|
||||
type="url"
|
||||
placeholder="http://localhost:5000"
|
||||
value={flaskUrl}
|
||||
onChange={(e) => onFlaskUrlChange(e.target.value)}
|
||||
disabled={envVarSet}
|
||||
/>
|
||||
<Button
|
||||
onClick={onTestConnection}
|
||||
variant="outline"
|
||||
disabled={testingConnection || !flaskUrl}
|
||||
>
|
||||
{testingConnection ? 'Testing...' : 'Test'}
|
||||
</Button>
|
||||
</div>
|
||||
{flaskConnectionStatus === 'connected' && (
|
||||
<div className="flex items-center gap-2 mt-2 text-sm text-green-600">
|
||||
<CloudCheck weight="fill" size={16} />
|
||||
Connected successfully
|
||||
</div>
|
||||
)}
|
||||
{flaskConnectionStatus === 'failed' && (
|
||||
<div className="flex items-center gap-2 mt-2 text-sm text-destructive">
|
||||
<CloudSlash weight="fill" size={16} />
|
||||
Connection failed
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="pt-2 space-y-2">
|
||||
<Button
|
||||
onClick={onMigrateToFlask}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full gap-2"
|
||||
>
|
||||
<Upload weight="bold" size={16} />
|
||||
Migrate IndexedDB Data to Flask
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onMigrateToIndexedDB}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full gap-2"
|
||||
>
|
||||
<Download weight="bold" size={16} />
|
||||
Migrate Flask Data to IndexedDB
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pt-2">
|
||||
<Button onClick={onSaveConfig} className="gap-2" disabled={envVarSet}>
|
||||
<Database weight="bold" size={16} />
|
||||
Save Storage Settings
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
40
src/components/settings/StorageInfoCard.tsx
Normal file
40
src/components/settings/StorageInfoCard.tsx
Normal file
@@ -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 (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Storage Information</CardTitle>
|
||||
<CardDescription>
|
||||
How your data is stored
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
{storageType === 'indexeddb' ? (
|
||||
<>
|
||||
<strong>IndexedDB</strong> 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' ? (
|
||||
<>
|
||||
<strong>localStorage</strong> 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.
|
||||
</>
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,22 +1,20 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
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 { Database, Download, Upload, Trash, CloudArrowUp, CloudCheck, CloudSlash, FirstAid, CheckCircle, Warning } from '@phosphor-icons/react'
|
||||
import { getDatabaseStats, exportDatabase, importDatabase, clearDatabase, seedDatabase, getAllSnippets, validateDatabaseSchema } from '@/lib/db'
|
||||
import { toast } from 'sonner'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import {
|
||||
getStorageConfig,
|
||||
saveStorageConfig,
|
||||
loadStorageConfig,
|
||||
FlaskStorageAdapter,
|
||||
type StorageBackend
|
||||
} from '@/lib/storage'
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
|
||||
import { PersistenceSettings } from '@/components/demo/PersistenceSettings'
|
||||
import { SchemaHealthCard } from '@/components/settings/SchemaHealthCard'
|
||||
import { BackendAutoConfigCard } from '@/components/settings/BackendAutoConfigCard'
|
||||
import { StorageBackendCard } from '@/components/settings/StorageBackendCard'
|
||||
import { DatabaseStatsCard } from '@/components/settings/DatabaseStatsCard'
|
||||
import { StorageInfoCard } from '@/components/settings/StorageInfoCard'
|
||||
import { DatabaseActionsCard } from '@/components/settings/DatabaseActionsCard'
|
||||
|
||||
export function SettingsPage() {
|
||||
const [stats, setStats] = useState<{
|
||||
@@ -271,356 +269,52 @@ export function SettingsPage() {
|
||||
<div className="grid gap-6 max-w-3xl">
|
||||
<PersistenceSettings />
|
||||
|
||||
{schemaHealth === 'corrupted' && (
|
||||
<Card className="border-destructive bg-destructive/10">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-destructive">
|
||||
<Warning weight="fill" size={24} />
|
||||
Schema Corruption Detected
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Your database schema is outdated or corrupted and needs to be repaired
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Alert className="border-destructive">
|
||||
<AlertDescription>
|
||||
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.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleClear} variant="destructive" className="gap-2">
|
||||
<FirstAid weight="bold" size={16} />
|
||||
Repair Database (Wipe & Recreate)
|
||||
</Button>
|
||||
<Button onClick={checkSchemaHealth} variant="outline" disabled={checkingSchema}>
|
||||
{checkingSchema ? 'Checking...' : 'Re-check Schema'}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
<SchemaHealthCard
|
||||
schemaHealth={schemaHealth}
|
||||
checkingSchema={checkingSchema}
|
||||
onClear={handleClear}
|
||||
onCheckSchema={checkSchemaHealth}
|
||||
/>
|
||||
|
||||
{schemaHealth === 'healthy' && (
|
||||
<Card className="border-green-600 bg-green-600/10">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-green-600">
|
||||
<CheckCircle weight="fill" size={24} />
|
||||
Schema Healthy
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Your database schema is up to date and functioning correctly
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{envVarSet && (
|
||||
<Card className="border-accent">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-accent">
|
||||
<CloudCheck weight="fill" size={24} />
|
||||
Backend Auto-Configured
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Flask backend is configured via environment variable
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<span className="text-sm text-muted-foreground">Backend URL</span>
|
||||
<code className="text-sm font-mono bg-muted px-2 py-1 rounded">{flaskUrl}</code>
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<span className="text-sm text-muted-foreground">Configuration Source</span>
|
||||
<code className="text-sm font-mono bg-muted px-2 py-1 rounded">VITE_FLASK_BACKEND_URL</code>
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<span className="text-sm text-muted-foreground">Status</span>
|
||||
{flaskConnectionStatus === 'connected' && (
|
||||
<span className="flex items-center gap-2 text-sm text-green-600">
|
||||
<CloudCheck weight="fill" size={16} />
|
||||
Connected
|
||||
</span>
|
||||
)}
|
||||
{flaskConnectionStatus === 'failed' && (
|
||||
<span className="flex items-center gap-2 text-sm text-destructive">
|
||||
<CloudSlash weight="fill" size={16} />
|
||||
Connection Failed
|
||||
</span>
|
||||
)}
|
||||
{flaskConnectionStatus === 'unknown' && (
|
||||
<Button
|
||||
onClick={handleTestConnection}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={testingConnection}
|
||||
>
|
||||
{testingConnection ? 'Testing...' : 'Test Connection'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
<BackendAutoConfigCard
|
||||
envVarSet={envVarSet}
|
||||
flaskUrl={flaskUrl}
|
||||
flaskConnectionStatus={flaskConnectionStatus}
|
||||
testingConnection={testingConnection}
|
||||
onTestConnection={handleTestConnection}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<CloudArrowUp weight="duotone" size={24} />
|
||||
Storage Backend
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Choose where your snippets are stored
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{envVarSet && (
|
||||
<Alert className="border-accent bg-accent/10">
|
||||
<AlertDescription className="flex items-center gap-2">
|
||||
<CloudCheck weight="fill" size={16} className="text-accent" />
|
||||
<span>
|
||||
Storage backend is configured via <code className="px-1.5 py-0.5 rounded bg-muted text-xs font-mono">VITE_FLASK_BACKEND_URL</code> environment variable and cannot be changed here.
|
||||
</span>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<RadioGroup
|
||||
value={storageBackend}
|
||||
onValueChange={(value) => setStorageBackend(value as StorageBackend)}
|
||||
disabled={envVarSet}
|
||||
>
|
||||
<div className="flex items-start space-x-3 space-y-0">
|
||||
<RadioGroupItem value="indexeddb" id="storage-indexeddb" disabled={envVarSet} />
|
||||
<div className="flex-1">
|
||||
<Label htmlFor="storage-indexeddb" className={`font-semibold ${envVarSet ? 'opacity-50' : 'cursor-pointer'}`}>
|
||||
IndexedDB (Local Browser Storage)
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Store snippets locally in your browser. Data persists on this device only.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-3 space-y-0 mt-4">
|
||||
<RadioGroupItem value="flask" id="storage-flask" disabled={envVarSet} />
|
||||
<div className="flex-1">
|
||||
<Label htmlFor="storage-flask" className={`font-semibold ${envVarSet ? 'opacity-50' : 'cursor-pointer'}`}>
|
||||
Flask Backend (Remote Server)
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Store snippets on a Flask backend server. Data is accessible from any device.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
<StorageBackendCard
|
||||
storageBackend={storageBackend}
|
||||
flaskUrl={flaskUrl}
|
||||
flaskConnectionStatus={flaskConnectionStatus}
|
||||
testingConnection={testingConnection}
|
||||
envVarSet={envVarSet}
|
||||
onStorageBackendChange={setStorageBackend}
|
||||
onFlaskUrlChange={(url) => {
|
||||
setFlaskUrl(url)
|
||||
setFlaskConnectionStatus('unknown')
|
||||
}}
|
||||
onTestConnection={handleTestConnection}
|
||||
onSaveConfig={handleSaveStorageConfig}
|
||||
onMigrateToFlask={handleMigrateToFlask}
|
||||
onMigrateToIndexedDB={handleMigrateToIndexedDB}
|
||||
/>
|
||||
|
||||
{storageBackend === 'flask' && (
|
||||
<div className="space-y-4 p-4 border border-border rounded-lg bg-muted/50">
|
||||
<div>
|
||||
<Label htmlFor="flask-url">Flask Backend URL</Label>
|
||||
<div className="flex gap-2 mt-2">
|
||||
<Input
|
||||
id="flask-url"
|
||||
type="url"
|
||||
placeholder="http://localhost:5000"
|
||||
value={flaskUrl}
|
||||
onChange={(e) => {
|
||||
setFlaskUrl(e.target.value)
|
||||
setFlaskConnectionStatus('unknown')
|
||||
}}
|
||||
disabled={envVarSet}
|
||||
/>
|
||||
<Button
|
||||
onClick={handleTestConnection}
|
||||
variant="outline"
|
||||
disabled={testingConnection || !flaskUrl}
|
||||
>
|
||||
{testingConnection ? 'Testing...' : 'Test'}
|
||||
</Button>
|
||||
</div>
|
||||
{flaskConnectionStatus === 'connected' && (
|
||||
<div className="flex items-center gap-2 mt-2 text-sm text-green-600">
|
||||
<CloudCheck weight="fill" size={16} />
|
||||
Connected successfully
|
||||
</div>
|
||||
)}
|
||||
{flaskConnectionStatus === 'failed' && (
|
||||
<div className="flex items-center gap-2 mt-2 text-sm text-destructive">
|
||||
<CloudSlash weight="fill" size={16} />
|
||||
Connection failed
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DatabaseStatsCard
|
||||
loading={loading}
|
||||
stats={stats}
|
||||
formatBytes={formatBytes}
|
||||
/>
|
||||
|
||||
<div className="pt-2 space-y-2">
|
||||
<Button
|
||||
onClick={handleMigrateToFlask}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full gap-2"
|
||||
>
|
||||
<Upload weight="bold" size={16} />
|
||||
Migrate IndexedDB Data to Flask
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleMigrateToIndexedDB}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full gap-2"
|
||||
>
|
||||
<Download weight="bold" size={16} />
|
||||
Migrate Flask Data to IndexedDB
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<StorageInfoCard storageType={stats?.storageType} />
|
||||
|
||||
<div className="pt-2">
|
||||
<Button onClick={handleSaveStorageConfig} className="gap-2" disabled={envVarSet}>
|
||||
<Database weight="bold" size={16} />
|
||||
Save Storage Settings
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Database weight="duotone" size={24} />
|
||||
Database Statistics
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Information about your local database storage
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{loading ? (
|
||||
<p className="text-muted-foreground">Loading...</p>
|
||||
) : stats ? (
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-center py-2 border-b border-border">
|
||||
<span className="text-sm text-muted-foreground">Snippets</span>
|
||||
<span className="font-semibold">{stats.snippetCount}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-2 border-b border-border">
|
||||
<span className="text-sm text-muted-foreground">Templates</span>
|
||||
<span className="font-semibold">{stats.templateCount}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-2 border-b border-border">
|
||||
<span className="text-sm text-muted-foreground">Storage Type</span>
|
||||
<span className="font-semibold capitalize">{stats.storageType}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-2">
|
||||
<span className="text-sm text-muted-foreground">Database Size</span>
|
||||
<span className="font-semibold">{formatBytes(stats.databaseSize)}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-destructive">Failed to load statistics</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Storage Information</CardTitle>
|
||||
<CardDescription>
|
||||
How your data is stored
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
{stats?.storageType === 'indexeddb' ? (
|
||||
<>
|
||||
<strong>IndexedDB</strong> 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' ? (
|
||||
<>
|
||||
<strong>localStorage</strong> 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.
|
||||
</>
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Database Actions</CardTitle>
|
||||
<CardDescription>
|
||||
Backup, restore, or reset your database
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold mb-2">Export Database</h3>
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
Download your database as a file for backup or transfer to another device
|
||||
</p>
|
||||
<Button onClick={handleExport} variant="outline" className="gap-2">
|
||||
<Download weight="bold" size={16} />
|
||||
Export Database
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-border">
|
||||
<h3 className="text-sm font-semibold mb-2">Import Database</h3>
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
Restore a previously exported database file
|
||||
</p>
|
||||
<label>
|
||||
<input
|
||||
type="file"
|
||||
accept=".db"
|
||||
onChange={handleImport}
|
||||
className="hidden"
|
||||
id="import-db"
|
||||
/>
|
||||
<Button variant="outline" className="gap-2" asChild>
|
||||
<span>
|
||||
<Upload weight="bold" size={16} />
|
||||
Import Database
|
||||
</span>
|
||||
</Button>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-border">
|
||||
<h3 className="text-sm font-semibold mb-2">Sample Data</h3>
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
Add sample code snippets to get started (only if database is empty)
|
||||
</p>
|
||||
<Button onClick={handleSeed} variant="outline" className="gap-2">
|
||||
<Database weight="bold" size={16} />
|
||||
Add Sample Data
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-border">
|
||||
<h3 className="text-sm font-semibold mb-2 text-destructive">Clear All Data</h3>
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
Permanently delete all snippets and templates. This cannot be undone.
|
||||
</p>
|
||||
<Button onClick={handleClear} variant="destructive" className="gap-2">
|
||||
<Trash weight="bold" size={16} />
|
||||
Clear Database
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<DatabaseActionsCard
|
||||
onExport={handleExport}
|
||||
onImport={handleImport}
|
||||
onSeed={handleSeed}
|
||||
onClear={handleClear}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user