@@ -192,128 +231,24 @@ export function StorageSettingsPanel() {
return (
-
-
-
- Storage Settings
-
-
- Manage your local data storage preferences
-
-
+
-
-
-
- Current Backend:
- {getBackendIcon()}
- {getBackendLabel()}
-
-
{backend?.toUpperCase() || 'UNKNOWN'}
-
-
{getBackendDescription()}
-
-
-
-
Switch Storage Backend
-
-
-
-
-
setFlaskUrl(e.target.value)}
- placeholder="http://localhost:5001"
- className="mt-1"
- />
-
- Enter Flask backend URL or set VITE_FLASK_BACKEND_URL environment variable
-
-
-
-
-
-
-
-
-
-
- IndexedDB is the default and works offline. Flask backend enables cross-device sync.
-
-
-
-
-
Data Management
-
-
-
-
-
- Export your data as a JSON file or import from a previous backup
-
-
+
+
+
)
diff --git a/src/components/molecules/StorageSettings.tsx b/src/components/molecules/StorageSettings.tsx
index f50a166..20fa6b0 100644
--- a/src/components/molecules/StorageSettings.tsx
+++ b/src/components/molecules/StorageSettings.tsx
@@ -5,8 +5,165 @@ import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Badge } from '@/components/ui/badge'
import { useStorageBackend } from '@/hooks/use-unified-storage'
-import { toast } from 'sonner'
import { Database, HardDrive, Cloud, Cpu, Download, Upload } from '@phosphor-icons/react'
+import {
+ storageSettingsCopy,
+ getBackendCopy,
+ type StorageBackendKey,
+} from '@/components/storage/storageSettingsConfig'
+import { useStorageSwitchHandlers } from '@/components/storage/useStorageSwitchHandlers'
+import { useStorageDataHandlers } from '@/components/storage/useStorageDataHandlers'
+
+const getBackendIcon = (backend: StorageBackendKey | null) => {
+ switch (backend) {
+ case 'flask':
+ return
+ case 'indexeddb':
+ return
+ case 'sqlite':
+ return
+ case 'sparkkv':
+ return
+ default:
+ return
+ }
+}
+
+type BackendCardProps = {
+ backend: StorageBackendKey | null
+ isLoading: boolean
+ flaskUrl: string
+ isSwitching: boolean
+ onFlaskUrlChange: (value: string) => void
+ onSwitchToFlask: () => void
+ onSwitchToIndexedDB: () => void
+ onSwitchToSQLite: () => void
+}
+
+const BackendCard = ({
+ backend,
+ isLoading,
+ flaskUrl,
+ isSwitching,
+ onFlaskUrlChange,
+ onSwitchToFlask,
+ onSwitchToIndexedDB,
+ onSwitchToSQLite,
+}: BackendCardProps) => {
+ const backendCopy = getBackendCopy(backend)
+
+ return (
+
+
+
+ {getBackendIcon(backend)}
+ {storageSettingsCopy.molecule.title}
+
+ {storageSettingsCopy.molecule.description}
+
+
+
+ {storageSettingsCopy.molecule.currentBackendLabel}
+
+ {getBackendIcon(backend)}
+ {backendCopy.moleculeLabel}
+
+
+
+
+
+
+
+ onFlaskUrlChange(e.target.value)}
+ placeholder={storageSettingsCopy.molecule.flaskUrlPlaceholder}
+ disabled={isSwitching || isLoading}
+ />
+
+
+
{storageSettingsCopy.molecule.flaskHelp}
+
+
+
+
+
+
+
+
+
{storageSettingsCopy.molecule.backendDetails.indexeddb}
+
{storageSettingsCopy.molecule.backendDetails.sqlite}
+
{storageSettingsCopy.molecule.backendDetails.flask}
+
+
+
+
+ )
+}
+
+type DataManagementCardProps = {
+ isExporting: boolean
+ isImporting: boolean
+ onExport: () => void
+ onImport: () => void
+}
+
+const DataManagementCard = ({
+ isExporting,
+ isImporting,
+ onExport,
+ onImport,
+}: DataManagementCardProps) => (
+
+
+ {storageSettingsCopy.molecule.dataTitle}
+ {storageSettingsCopy.molecule.dataDescription}
+
+
+
+
+
+
+ {storageSettingsCopy.molecule.dataHelp}
+
+
+)
export function StorageSettings() {
const {
@@ -20,214 +177,43 @@ export function StorageSettings() {
} = useStorageBackend()
const [flaskUrl, setFlaskUrl] = useState(
- localStorage.getItem('codeforge-flask-url') || 'http://localhost:5001'
+ localStorage.getItem('codeforge-flask-url') || storageSettingsCopy.molecule.flaskUrlPlaceholder
)
- const [isSwitching, setIsSwitching] = useState(false)
- const handleSwitchToFlask = async () => {
- setIsSwitching(true)
- try {
- await switchToFlask(flaskUrl)
- toast.success('Switched to Flask backend')
- } catch (error) {
- toast.error(`Failed to switch to Flask: ${error}`)
- } finally {
- setIsSwitching(false)
- }
- }
+ const { isSwitching, handleSwitchToFlask, handleSwitchToSQLite, handleSwitchToIndexedDB } =
+ useStorageSwitchHandlers({
+ backend,
+ flaskUrl,
+ switchToFlask,
+ switchToSQLite,
+ switchToIndexedDB,
+ })
- const handleSwitchToIndexedDB = async () => {
- setIsSwitching(true)
- try {
- await switchToIndexedDB()
- toast.success('Switched to IndexedDB')
- } catch (error) {
- toast.error(`Failed to switch to IndexedDB: ${error}`)
- } finally {
- setIsSwitching(false)
- }
- }
-
- const handleSwitchToSQLite = async () => {
- setIsSwitching(true)
- try {
- await switchToSQLite()
- toast.success('Switched to SQLite')
- } catch (error) {
- toast.error(`Failed to switch to SQLite: ${error}`)
- } finally {
- setIsSwitching(false)
- }
- }
-
- const handleExport = async () => {
- try {
- const data = await exportData()
- const json = JSON.stringify(data, null, 2)
- const blob = new Blob([json], { type: 'application/json' })
- const url = URL.createObjectURL(blob)
- const a = document.createElement('a')
- a.href = url
- a.download = `codeforge-backup-${Date.now()}.json`
- document.body.appendChild(a)
- a.click()
- document.body.removeChild(a)
- URL.revokeObjectURL(url)
- toast.success('Data exported successfully')
- } catch (error) {
- toast.error(`Failed to export data: ${error}`)
- }
- }
-
- const handleImport = () => {
- const input = document.createElement('input')
- input.type = 'file'
- input.accept = '.json'
- input.onchange = async (e) => {
- const file = (e.target as HTMLInputElement).files?.[0]
- if (!file) return
-
- try {
- const text = await file.text()
- const data = JSON.parse(text)
- await importData(data)
- toast.success('Data imported successfully')
- } catch (error) {
- toast.error(`Failed to import data: ${error}`)
- }
- }
- input.click()
- }
-
- const getBackendIcon = (backendType: string | null) => {
- switch (backendType) {
- case 'flask':
- return
- case 'indexeddb':
- return
- case 'sqlite':
- return
- case 'sparkkv':
- return
- default:
- return
- }
- }
-
- const getBackendLabel = (backendType: string | null) => {
- switch (backendType) {
- case 'flask':
- return 'Flask Backend'
- case 'indexeddb':
- return 'IndexedDB'
- case 'sqlite':
- return 'SQLite'
- case 'sparkkv':
- return 'Spark KV'
- default:
- return 'Unknown'
- }
- }
+ const { isExporting, isImporting, handleExport, handleImport } = useStorageDataHandlers({
+ exportData,
+ importData,
+ exportFilename: () => `${storageSettingsCopy.molecule.exportFilenamePrefix}-${Date.now()}.json`,
+ importAccept: '.json',
+ })
return (
-
-
-
- {getBackendIcon(backend)}
- Storage Backend
-
-
- Choose where your data is stored
-
-
-
-
- Current backend:
-
- {getBackendIcon(backend)}
- {getBackendLabel(backend)}
-
-
-
-
-
-
-
- setFlaskUrl(e.target.value)}
- placeholder="http://localhost:5001"
- disabled={isSwitching || isLoading}
- />
-
-
-
- Store data on a Flask server (persistent across devices)
-
-
-
-
-
-
-
-
-
-
IndexedDB (Default): Browser storage, large capacity, works offline
-
SQLite: Browser storage with SQL queries, requires sql.js package
-
Flask: Server storage, persistent across devices, requires backend
-
-
-
-
-
-
-
- Data Management
-
- Export or import your data
-
-
-
-
-
-
-
-
- Backup your data to a JSON file or restore from a previous backup
-
-
-
+
+
)
}
diff --git a/src/components/storage/storageSettingsConfig.ts b/src/components/storage/storageSettingsConfig.ts
new file mode 100644
index 0000000..34e28f7
--- /dev/null
+++ b/src/components/storage/storageSettingsConfig.ts
@@ -0,0 +1,13 @@
+import copy from './storageSettingsCopy.json'
+
+export type StorageBackendKey = 'flask' | 'sqlite' | 'indexeddb' | 'sparkkv'
+
+export const storageSettingsCopy = copy
+
+export const getBackendCopy = (backend: StorageBackendKey | null) => {
+ if (!backend) {
+ return storageSettingsCopy.backends.unknown
+ }
+
+ return storageSettingsCopy.backends[backend] ?? storageSettingsCopy.backends.unknown
+}
diff --git a/src/components/storage/storageSettingsCopy.json b/src/components/storage/storageSettingsCopy.json
new file mode 100644
index 0000000..1f11e0d
--- /dev/null
+++ b/src/components/storage/storageSettingsCopy.json
@@ -0,0 +1,106 @@
+{
+ "panel": {
+ "title": "Storage Settings",
+ "description": "Manage your local data storage preferences",
+ "loadingDescription": "Detecting storage backend...",
+ "currentBackendLabel": "Current Backend:",
+ "switchTitle": "Switch Storage Backend",
+ "flaskUrlLabel": "Flask Backend URL (Optional)",
+ "flaskUrlPlaceholder": "http://localhost:5001",
+ "flaskUrlHelp": "Enter Flask backend URL or set VITE_FLASK_BACKEND_URL environment variable",
+ "switchHelp": "IndexedDB is the default and works offline. Flask backend enables cross-device sync.",
+ "dataTitle": "Data Management",
+ "dataHelp": "Export your data as a JSON file or import from a previous backup",
+ "buttons": {
+ "indexeddb": "IndexedDB (Default)",
+ "flask": "Flask Backend",
+ "sqlite": "SQLite",
+ "export": "Export Data",
+ "import": "Import Data"
+ },
+ "exportFilenamePrefix": "codeforge-data"
+ },
+ "molecule": {
+ "title": "Storage Backend",
+ "description": "Choose where your data is stored",
+ "currentBackendLabel": "Current backend:",
+ "flaskUrlLabel": "Flask Backend URL",
+ "flaskUrlPlaceholder": "http://localhost:5001",
+ "flaskHelp": "Store data on a Flask server (persistent across devices)",
+ "dataTitle": "Data Management",
+ "dataDescription": "Export or import your data",
+ "dataHelp": "Backup your data to a JSON file or restore from a previous backup",
+ "buttons": {
+ "flaskActive": "Active",
+ "flaskUse": "Use Flask",
+ "indexeddbActive": "Active",
+ "indexeddbUse": "Use IndexedDB",
+ "sqliteActive": "Active",
+ "sqliteUse": "Use SQLite",
+ "export": "Export Data",
+ "import": "Import Data"
+ },
+ "backendDetails": {
+ "indexeddb": "IndexedDB (Default): Browser storage, large capacity, works offline",
+ "sqlite": "SQLite: Browser storage with SQL queries, requires sql.js package",
+ "flask": "Flask: Server storage, persistent across devices, requires backend"
+ },
+ "exportFilenamePrefix": "codeforge-backup"
+ },
+ "backends": {
+ "flask": {
+ "panelLabel": "Flask Backend (Remote)",
+ "panelDescription": "Data stored on Flask server with SQLite (cross-device sync)",
+ "moleculeLabel": "Flask Backend",
+ "badgeLabel": "Flask"
+ },
+ "sqlite": {
+ "panelLabel": "SQLite (On-disk)",
+ "panelDescription": "Data stored in SQLite database persisted to localStorage",
+ "moleculeLabel": "SQLite",
+ "badgeLabel": "SQLite"
+ },
+ "indexeddb": {
+ "panelLabel": "IndexedDB (Browser) - Default",
+ "panelDescription": "Data stored in browser IndexedDB (default, recommended for most users)",
+ "moleculeLabel": "IndexedDB",
+ "badgeLabel": "IndexedDB"
+ },
+ "sparkkv": {
+ "panelLabel": "Spark KV (Cloud)",
+ "panelDescription": "Data stored in Spark cloud key-value store",
+ "moleculeLabel": "Spark KV",
+ "badgeLabel": "Spark KV"
+ },
+ "unknown": {
+ "panelLabel": "Unknown",
+ "panelDescription": "No storage backend detected",
+ "moleculeLabel": "Unknown",
+ "badgeLabel": "Unknown"
+ }
+ },
+ "toasts": {
+ "alreadyUsing": {
+ "flask": "Already using Flask backend",
+ "sqlite": "Already using SQLite",
+ "indexeddb": "Already using IndexedDB"
+ },
+ "errors": {
+ "missingFlaskUrl": "Please enter a Flask backend URL"
+ },
+ "success": {
+ "switchFlask": "Switched to Flask backend",
+ "switchSQLite": "Switched to SQLite storage",
+ "switchIndexedDB": "Switched to IndexedDB storage (default)",
+ "export": "Data exported successfully",
+ "import": "Data imported successfully"
+ },
+ "failure": {
+ "switchFlask": "Failed to switch to Flask",
+ "switchSQLite": "Failed to switch to SQLite",
+ "switchIndexedDB": "Failed to switch to IndexedDB",
+ "export": "Failed to export data",
+ "import": "Failed to import data"
+ }
+ }
+}
diff --git a/src/components/storage/storageSettingsUtils.ts b/src/components/storage/storageSettingsUtils.ts
new file mode 100644
index 0000000..cfe819f
--- /dev/null
+++ b/src/components/storage/storageSettingsUtils.ts
@@ -0,0 +1,34 @@
+export const formatStorageError = (error: unknown) => {
+ if (error instanceof Error) {
+ return error.message
+ }
+
+ return String(error)
+}
+
+export const downloadJson = (data: unknown, filename: string) => {
+ const json = JSON.stringify(data, null, 2)
+ const blob = new Blob([json], { type: 'application/json' })
+ const url = URL.createObjectURL(blob)
+ const anchor = document.createElement('a')
+
+ anchor.href = url
+ anchor.download = filename
+ document.body.appendChild(anchor)
+ anchor.click()
+ document.body.removeChild(anchor)
+ URL.revokeObjectURL(url)
+}
+
+export const createJsonFileInput = (accept: string, onFileLoaded: (file: File) => void) => {
+ const input = document.createElement('input')
+ input.type = 'file'
+ input.accept = accept
+ input.onchange = (event) => {
+ const file = (event.target as HTMLInputElement).files?.[0]
+ if (file) {
+ onFileLoaded(file)
+ }
+ }
+ input.click()
+}
diff --git a/src/components/storage/useStorageDataHandlers.ts b/src/components/storage/useStorageDataHandlers.ts
new file mode 100644
index 0000000..6689bad
--- /dev/null
+++ b/src/components/storage/useStorageDataHandlers.ts
@@ -0,0 +1,57 @@
+import { useCallback, useState } from 'react'
+import { toast } from 'sonner'
+import { createJsonFileInput, downloadJson, formatStorageError } from './storageSettingsUtils'
+import { storageSettingsCopy } from './storageSettingsConfig'
+
+type DataHandlers = {
+ exportData: () => Promise
+ importData: (data: unknown) => Promise
+ exportFilename: () => string
+ importAccept: string
+}
+
+export const useStorageDataHandlers = ({
+ exportData,
+ importData,
+ exportFilename,
+ importAccept,
+}: DataHandlers) => {
+ const [isExporting, setIsExporting] = useState(false)
+ const [isImporting, setIsImporting] = useState(false)
+
+ const handleExport = useCallback(async () => {
+ setIsExporting(true)
+ try {
+ const data = await exportData()
+ downloadJson(data, exportFilename())
+ toast.success(storageSettingsCopy.toasts.success.export)
+ } catch (error) {
+ toast.error(`${storageSettingsCopy.toasts.failure.export}: ${formatStorageError(error)}`)
+ } finally {
+ setIsExporting(false)
+ }
+ }, [exportData, exportFilename])
+
+ const handleImport = useCallback(() => {
+ createJsonFileInput(importAccept, async (file) => {
+ setIsImporting(true)
+ try {
+ const text = await file.text()
+ const data = JSON.parse(text)
+ await importData(data)
+ toast.success(storageSettingsCopy.toasts.success.import)
+ } catch (error) {
+ toast.error(`${storageSettingsCopy.toasts.failure.import}: ${formatStorageError(error)}`)
+ } finally {
+ setIsImporting(false)
+ }
+ })
+ }, [importAccept, importData])
+
+ return {
+ isExporting,
+ isImporting,
+ handleExport,
+ handleImport,
+ }
+}
diff --git a/src/components/storage/useStorageSwitchHandlers.ts b/src/components/storage/useStorageSwitchHandlers.ts
new file mode 100644
index 0000000..5f52c4c
--- /dev/null
+++ b/src/components/storage/useStorageSwitchHandlers.ts
@@ -0,0 +1,85 @@
+import { useCallback, useState } from 'react'
+import { toast } from 'sonner'
+import { formatStorageError } from './storageSettingsUtils'
+import { storageSettingsCopy, type StorageBackendKey } from './storageSettingsConfig'
+
+type SwitchHandlers = {
+ backend: StorageBackendKey | null
+ flaskUrl: string
+ switchToFlask: (url: string) => Promise
+ switchToSQLite: () => Promise
+ switchToIndexedDB: () => Promise
+}
+
+export const useStorageSwitchHandlers = ({
+ backend,
+ flaskUrl,
+ switchToFlask,
+ switchToSQLite,
+ switchToIndexedDB,
+}: SwitchHandlers) => {
+ const [isSwitching, setIsSwitching] = useState(false)
+
+ const handleSwitchToFlask = useCallback(async () => {
+ if (backend === 'flask') {
+ toast.info(storageSettingsCopy.toasts.alreadyUsing.flask)
+ return
+ }
+
+ if (!flaskUrl) {
+ toast.error(storageSettingsCopy.toasts.errors.missingFlaskUrl)
+ return
+ }
+
+ setIsSwitching(true)
+ try {
+ await switchToFlask(flaskUrl)
+ toast.success(storageSettingsCopy.toasts.success.switchFlask)
+ } catch (error) {
+ toast.error(`${storageSettingsCopy.toasts.failure.switchFlask}: ${formatStorageError(error)}`)
+ } finally {
+ setIsSwitching(false)
+ }
+ }, [backend, flaskUrl, switchToFlask])
+
+ const handleSwitchToSQLite = useCallback(async () => {
+ if (backend === 'sqlite') {
+ toast.info(storageSettingsCopy.toasts.alreadyUsing.sqlite)
+ return
+ }
+
+ setIsSwitching(true)
+ try {
+ await switchToSQLite()
+ toast.success(storageSettingsCopy.toasts.success.switchSQLite)
+ } catch (error) {
+ toast.error(`${storageSettingsCopy.toasts.failure.switchSQLite}: ${formatStorageError(error)}`)
+ } finally {
+ setIsSwitching(false)
+ }
+ }, [backend, switchToSQLite])
+
+ const handleSwitchToIndexedDB = useCallback(async () => {
+ if (backend === 'indexeddb') {
+ toast.info(storageSettingsCopy.toasts.alreadyUsing.indexeddb)
+ return
+ }
+
+ setIsSwitching(true)
+ try {
+ await switchToIndexedDB()
+ toast.success(storageSettingsCopy.toasts.success.switchIndexedDB)
+ } catch (error) {
+ toast.error(`${storageSettingsCopy.toasts.failure.switchIndexedDB}: ${formatStorageError(error)}`)
+ } finally {
+ setIsSwitching(false)
+ }
+ }, [backend, switchToIndexedDB])
+
+ return {
+ isSwitching,
+ handleSwitchToFlask,
+ handleSwitchToSQLite,
+ handleSwitchToIndexedDB,
+ }
+}