Generated by Spark: Ok I figured it out. Make a backend folder, put a flask backend in it with a Dockerfile. Main UI uses IndexedDB then with a UI setting, it can be moved to the flask backend.

This commit is contained in:
2026-01-17 18:30:19 +00:00
committed by GitHub
parent 88a9225524
commit 3c3be9c8ed
13 changed files with 915 additions and 35 deletions

46
PRD.md
View File

@@ -60,28 +60,49 @@ A code snippet management application with an integrated component library showc
- Success criteria: Demo loads with working example code, users can edit and see instant changes, educational cards explain key features
**Database Management & Settings**
- Functionality: Settings page with database statistics, backup/restore, and storage information
- Purpose: Enable users to manage their local data, export/import snippets, and understand storage mechanism
- Functionality: Settings page with storage backend selection (IndexedDB or Flask), database statistics, backup/restore, and data migration
- Purpose: Enable users to choose between local browser storage or remote Flask backend, manage their data, export/import snippets, and migrate between storage backends
- Trigger: Navigate to "Settings" via hamburger menu
- Progression: User opens settings → Views database stats → Exports backup if needed → Can import previous backups → Manages sample data → Can clear all data if needed
- Success criteria: Shows accurate statistics, export creates valid .db file, import restores data correctly, clear operation requires confirmation
- Progression: User opens settings → Selects storage backend (IndexedDB or Flask) → Configures Flask URL if needed → Tests connection → Migrates data if switching backends → Views database stats → Exports backup if needed → Can import previous backups → Manages sample data → Can clear all data if needed
- Success criteria: Backend switching works seamlessly, Flask connection test validates server availability, data migration preserves all snippets, shows accurate statistics, export creates valid .db file, import restores data correctly, clear operation requires confirmation
## Data Persistence
The application uses **SQL.js** (SQLite compiled to WebAssembly) for local database management with the following storage strategy:
The application supports **flexible data storage** with two backend options:
1. **Primary Storage: IndexedDB** - Used when available for better performance and larger storage capacity (typically 50MB+ depending on browser)
2. **Fallback: localStorage** - Used when IndexedDB is unavailable (typically 5-10MB limit)
3. **Database Structure**: Two tables - `snippets` (user-created snippets) and `snippet_templates` (reusable templates)
4. **Automatic Persistence**: Database is automatically saved after every create, update, or delete operation
5. **Export/Import**: Users can export their entire database as a .db file for backup or transfer to another device
### Storage Backends
1. **IndexedDB (Local Browser Storage) - Default**
- Uses SQL.js (SQLite compiled to WebAssembly) for local database management
- Primary Storage: IndexedDB - Used when available for better performance and larger storage capacity (typically 50MB+)
- Fallback: localStorage - Used when IndexedDB is unavailable (typically 5-10MB limit)
- Database Structure: Two tables - `snippets` (user-created snippets) and `snippet_templates` (reusable templates)
- Automatic Persistence: Database is automatically saved after every create, update, or delete operation
- Export/Import: Users can export their entire database as a .db file for backup or transfer
2. **Flask Backend (Remote Server) - Optional**
- Snippets stored on a Flask REST API server with SQLite database
- Allows access to snippets from any device
- Requires running the Flask backend (Docker support included)
- RESTful API endpoints for all CRUD operations
- Data migration tools to move snippets between IndexedDB and Flask
### Switching Between Backends
Users can switch storage backends from the Settings page:
- Select desired backend (IndexedDB or Flask)
- Configure Flask URL if using remote backend
- Test connection to Flask server
- Migrate existing snippets between backends
- Configuration persists in localStorage
This approach provides:
- Full SQL query capabilities for complex filtering and sorting
- Choice between local-only or remote storage
- Reliable persistence across browser sessions
- No external dependencies or server requirements
- Easy backup and restore functionality
- Protection against localStorage quota exceeded errors
- Multi-device access when using Flask backend
## Edge Case Handling
- **No Search Results**: Friendly message encouraging users to refine their search
@@ -91,6 +112,9 @@ This approach provides:
- **Storage Quota Exceeded**: Automatically switches from localStorage to IndexedDB if available, warns user if both are full
- **Database Corruption**: Gracefully handles corrupted database files, creates new database if loading fails
- **Import Invalid Database**: Validates imported files, shows clear error message if file is invalid
- **Flask Connection Failure**: Tests connection before switching to Flask backend, shows clear error if server is unreachable
- **Data Migration Errors**: Validates data during migration, provides clear feedback on success or failure
- **Network Errors with Flask**: Shows informative error messages when Flask API calls fail, suggests checking server status
## Design Direction
The design should evoke **precision, technical craftsmanship, and modern developer tools**.

View File

@@ -14,33 +14,70 @@ A powerful code snippet management application with an integrated component libr
## Data Storage
CodeSnippet uses a robust local storage solution that works entirely in your browser:
CodeSnippet offers flexible data storage with two backend options:
### Storage Strategy
1. **Primary: IndexedDB** - Used when available for better performance and larger storage capacity (typically 50MB+)
2. **Fallback: localStorage** - Used when IndexedDB is unavailable (typically 5-10MB limit)
3. **Technology: SQL.js** - SQLite compiled to WebAssembly for full SQL query capabilities
### Storage Backends
### Database Structure
- **snippets** table - Stores user-created code snippets with metadata
- **snippet_templates** table - Stores reusable snippet templates
#### 1. **IndexedDB (Local Browser Storage)** - Default
- All data stored locally in your browser
- No server required
- Data persists on this device only
- Uses SQLite compiled to WebAssembly for full SQL capabilities
- Primary: IndexedDB for better performance and larger storage capacity (typically 50MB+)
- Fallback: localStorage when IndexedDB is unavailable (typically 5-10MB limit)
### Features
- ✅ Automatic persistence after every operation
- ✅ Full SQL query capabilities for complex filtering and sorting
- ✅ No external dependencies or server requirements
- ✅ Export/import functionality for backup and transfer
- ✅ Graceful fallback between storage mechanisms
- ✅ Protection against quota exceeded errors
#### 2. **Flask Backend (Remote Server)** - Optional
- Snippets stored on a remote Flask server with SQLite database
- Access your snippets from any device
- Requires running the Flask backend (see Backend Setup below)
- Supports data migration between IndexedDB and Flask
### Managing Your Data
### Switching Storage Backends
Visit the **Settings** page (accessible from the hamburger menu) to:
- View database statistics (snippet count, template count, storage type, database size)
- Export your database as a backup file
- Import a previously exported database
- Add sample data to get started
- Clear all data if needed
Visit the **Settings** page to:
- Choose between IndexedDB and Flask backend
- Configure Flask backend URL
- Test connection to Flask server
- Migrate data between storage backends
- View database statistics
- Export/import database backups
## Backend Setup
### Running Flask Backend Locally
```bash
cd backend
pip install -r requirements.txt
python app.py
```
Server runs on `http://localhost:5000` by default.
### Running with Docker
Build and run:
```bash
docker build -t codesnippet-backend ./backend
docker run -p 5000:5000 -v $(pwd)/data:/data codesnippet-backend
```
Or use docker-compose:
```bash
docker-compose up -d
```
### Backend API
The Flask backend provides a REST API:
- `GET /health` - Health check
- `GET /api/snippets` - Get all snippets
- `GET /api/snippets/:id` - Get a specific snippet
- `POST /api/snippets` - Create a new snippet
- `PUT /api/snippets/:id` - Update a snippet
- `DELETE /api/snippets/:id` - Delete a snippet
See `backend/README.md` for more details.
## Getting Started

13
backend/.dockerignore Normal file
View File

@@ -0,0 +1,13 @@
__pycache__
*.pyc
*.pyo
*.pyd
.Python
*.so
*.egg
*.egg-info
dist
build
.env
venv
env

25
backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,25 @@
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
*.db
venv/
env/
ENV/
.venv

16
backend/Dockerfile Normal file
View File

@@ -0,0 +1,16 @@
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
RUN mkdir -p /data
EXPOSE 5000
ENV DB_PATH=/data/snippets.db
CMD ["python", "app.py"]

154
backend/README.md Normal file
View File

@@ -0,0 +1,154 @@
# CodeSnippet Backend
Flask-based REST API for managing code snippets with SQLite database.
## Quick Start
### Option 1: Docker (Recommended)
```bash
# From project root
docker-compose up -d
```
The backend will be available at `http://localhost:5000`
### Option 2: Local Python
```bash
cd backend
pip install -r requirements.txt
python app.py
```
Server runs on `http://localhost:5000` by default.
## Configuration
Set the `DB_PATH` environment variable to change the database location:
```bash
export DB_PATH=/path/to/snippets.db
python app.py
```
Default: `/data/snippets.db`
## API Endpoints
### Health Check
- `GET /health` - Returns server health status
### Snippets
- `GET /api/snippets` - Get all snippets
- `GET /api/snippets/:id` - Get a specific snippet
- `POST /api/snippets` - Create a new snippet
- `PUT /api/snippets/:id` - Update a snippet
- `DELETE /api/snippets/:id` - Delete a snippet
### Request/Response Format
#### Snippet Object
```json
{
"id": "unique-id",
"title": "Snippet Title",
"code": "const example = 'code';",
"language": "javascript",
"description": "Optional description",
"tags": ["array", "of", "tags"],
"category": "general",
"componentName": "ComponentName",
"previewParams": [],
"createdAt": "2024-01-01T00:00:00.000Z",
"updatedAt": "2024-01-01T00:00:00.000Z"
}
```
## Frontend Integration
In the CodeSnippet app:
1. Navigate to Settings page
2. Select "Flask Backend (Remote Server)"
3. Enter backend URL (e.g., `http://localhost:5000`)
4. Click "Test" to verify connection
5. Click "Save Storage Settings"
6. Optionally migrate existing IndexedDB data to Flask
## Docker Details
### Building the Image
```bash
docker build -t codesnippet-backend ./backend
```
### Running the Container
```bash
# With volume for persistent data
docker run -p 5000:5000 -v $(pwd)/data:/data codesnippet-backend
# With custom database path
docker run -p 5000:5000 -e DB_PATH=/data/custom.db -v $(pwd)/data:/data codesnippet-backend
```
### Using Docker Compose
```bash
# Start
docker-compose up -d
# Stop
docker-compose down
# View logs
docker-compose logs -f backend
# Rebuild after changes
docker-compose up -d --build
```
## Development
The backend uses:
- **Flask 3.0** - Web framework
- **Flask-CORS** - Cross-origin resource sharing
- **SQLite3** - Database (built into Python)
### Database Schema
```sql
CREATE TABLE snippets (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
code TEXT NOT NULL,
language TEXT NOT NULL,
description TEXT,
tags TEXT,
category TEXT,
componentName TEXT,
previewParams TEXT,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL
)
```
## Environment Variables
- `DB_PATH` - Path to SQLite database file (default: `/data/snippets.db`)
## Troubleshooting
### Connection Refused
- Ensure the Flask server is running
- Check firewall settings
- Verify the port (5000) is not in use
### CORS Errors
- The backend allows all origins by default
- Modify `CORS(app)` in `app.py` if you need to restrict origins
### Database Locked
- Ensure only one instance of the backend is running
- Check file permissions on the database file

181
backend/app.py Normal file
View File

@@ -0,0 +1,181 @@
from flask import Flask, request, jsonify
from flask_cors import CORS
import sqlite3
import json
from datetime import datetime
import os
app = Flask(__name__)
CORS(app)
DB_PATH = os.environ.get('DB_PATH', '/data/snippets.db')
def get_db():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def init_db():
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
conn = get_db()
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS snippets (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
code TEXT NOT NULL,
language TEXT NOT NULL,
description TEXT,
tags TEXT,
category TEXT,
componentName TEXT,
previewParams TEXT,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL
)
''')
conn.commit()
conn.close()
@app.route('/health', methods=['GET'])
def health():
return jsonify({'status': 'healthy', 'timestamp': datetime.utcnow().isoformat()})
@app.route('/api/snippets', methods=['GET'])
def get_snippets():
try:
conn = get_db()
cursor = conn.cursor()
cursor.execute('SELECT * FROM snippets ORDER BY updatedAt DESC')
rows = cursor.fetchall()
conn.close()
snippets = []
for row in rows:
snippet = dict(row)
if snippet.get('tags'):
snippet['tags'] = json.loads(snippet['tags'])
if snippet.get('previewParams'):
snippet['previewParams'] = json.loads(snippet['previewParams'])
snippets.append(snippet)
return jsonify(snippets)
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/snippets/<snippet_id>', methods=['GET'])
def get_snippet(snippet_id):
try:
conn = get_db()
cursor = conn.cursor()
cursor.execute('SELECT * FROM snippets WHERE id = ?', (snippet_id,))
row = cursor.fetchone()
conn.close()
if not row:
return jsonify({'error': 'Snippet not found'}), 404
snippet = dict(row)
if snippet.get('tags'):
snippet['tags'] = json.loads(snippet['tags'])
if snippet.get('previewParams'):
snippet['previewParams'] = json.loads(snippet['previewParams'])
return jsonify(snippet)
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/snippets', methods=['POST'])
def create_snippet():
try:
data = request.json
conn = get_db()
cursor = conn.cursor()
tags_json = json.dumps(data.get('tags', []))
preview_params_json = json.dumps(data.get('previewParams', []))
cursor.execute('''
INSERT INTO snippets (id, title, code, language, description, tags, category, componentName, previewParams, createdAt, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
data['id'],
data['title'],
data['code'],
data['language'],
data.get('description', ''),
tags_json,
data.get('category', 'general'),
data.get('componentName', ''),
preview_params_json,
data['createdAt'],
data['updatedAt']
))
conn.commit()
conn.close()
return jsonify(data), 201
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/snippets/<snippet_id>', methods=['PUT'])
def update_snippet(snippet_id):
try:
data = request.json
conn = get_db()
cursor = conn.cursor()
tags_json = json.dumps(data.get('tags', []))
preview_params_json = json.dumps(data.get('previewParams', []))
cursor.execute('''
UPDATE snippets
SET title = ?, code = ?, language = ?, description = ?, tags = ?, category = ?, componentName = ?, previewParams = ?, updatedAt = ?
WHERE id = ?
''', (
data['title'],
data['code'],
data['language'],
data.get('description', ''),
tags_json,
data.get('category', 'general'),
data.get('componentName', ''),
preview_params_json,
data['updatedAt'],
snippet_id
))
conn.commit()
conn.close()
if cursor.rowcount == 0:
return jsonify({'error': 'Snippet not found'}), 404
return jsonify(data)
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/snippets/<snippet_id>', methods=['DELETE'])
def delete_snippet(snippet_id):
try:
conn = get_db()
cursor = conn.cursor()
cursor.execute('DELETE FROM snippets WHERE id = ?', (snippet_id,))
conn.commit()
conn.close()
if cursor.rowcount == 0:
return jsonify({'error': 'Snippet not found'}), 404
return jsonify({'success': True})
except Exception as e:
return jsonify({'error': str(e)}), 500
if __name__ == '__main__':
init_db()
app.run(host='0.0.0.0', port=5000, debug=True)

2
backend/requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
Flask==3.0.0
flask-cors==4.0.0

15
docker-compose.yml Normal file
View File

@@ -0,0 +1,15 @@
version: '3.8'
services:
backend:
build: ./backend
ports:
- "5000:5000"
volumes:
- snippet-data:/data
environment:
- DB_PATH=/data/snippets.db
restart: unless-stopped
volumes:
snippet-data:

View File

@@ -1,8 +1,10 @@
import initSqlJs, { Database } from 'sql.js'
import type { Snippet, SnippetTemplate } from './types'
import { getStorageConfig, FlaskStorageAdapter } from './storage'
let dbInstance: Database | null = null
let sqlInstance: any = null
let flaskAdapter: FlaskStorageAdapter | null = null
const DB_KEY = 'codesnippet-db'
const IDB_NAME = 'CodeSnippetDB'
@@ -192,7 +194,23 @@ async function saveDB() {
}
}
function getFlaskAdapter(): FlaskStorageAdapter | null {
const config = getStorageConfig()
if (config.backend === 'flask' && config.flaskUrl) {
if (!flaskAdapter || flaskAdapter['baseUrl'] !== config.flaskUrl) {
flaskAdapter = new FlaskStorageAdapter(config.flaskUrl)
}
return flaskAdapter
}
return null
}
export async function getAllSnippets(): Promise<Snippet[]> {
const adapter = getFlaskAdapter()
if (adapter) {
return await adapter.getAllSnippets()
}
const db = await initDB()
const results = db.exec('SELECT * FROM snippets ORDER BY updatedAt DESC')
@@ -218,6 +236,11 @@ export async function getAllSnippets(): Promise<Snippet[]> {
}
export async function getSnippet(id: string): Promise<Snippet | null> {
const adapter = getFlaskAdapter()
if (adapter) {
return await adapter.getSnippet(id)
}
const db = await initDB()
const results = db.exec('SELECT * FROM snippets WHERE id = ?', [id])
@@ -242,6 +265,11 @@ export async function getSnippet(id: string): Promise<Snippet | null> {
}
export async function createSnippet(snippet: Snippet): Promise<void> {
const adapter = getFlaskAdapter()
if (adapter) {
return await adapter.createSnippet(snippet)
}
const db = await initDB()
db.run(
@@ -266,6 +294,11 @@ export async function createSnippet(snippet: Snippet): Promise<void> {
}
export async function updateSnippet(snippet: Snippet): Promise<void> {
const adapter = getFlaskAdapter()
if (adapter) {
return await adapter.updateSnippet(snippet)
}
const db = await initDB()
db.run(
@@ -290,6 +323,11 @@ export async function updateSnippet(snippet: Snippet): Promise<void> {
}
export async function deleteSnippet(id: string): Promise<void> {
const adapter = getFlaskAdapter()
if (adapter) {
return await adapter.deleteSnippet(id)
}
const db = await initDB()
db.run('DELETE FROM snippets WHERE id = ?', [id])

138
src/lib/storage.ts Normal file
View File

@@ -0,0 +1,138 @@
import type { Snippet } from './types'
export type StorageBackend = 'indexeddb' | 'flask'
export interface StorageConfig {
backend: StorageBackend
flaskUrl?: string
}
let currentConfig: StorageConfig = {
backend: 'indexeddb'
}
const STORAGE_CONFIG_KEY = 'codesnippet-storage-config'
export function loadStorageConfig(): StorageConfig {
try {
const saved = localStorage.getItem(STORAGE_CONFIG_KEY)
if (saved) {
currentConfig = JSON.parse(saved)
}
} catch (error) {
console.warn('Failed to load storage config:', error)
}
return currentConfig
}
export function saveStorageConfig(config: StorageConfig): void {
currentConfig = config
try {
localStorage.setItem(STORAGE_CONFIG_KEY, JSON.stringify(config))
} catch (error) {
console.warn('Failed to save storage config:', error)
}
}
export function getStorageConfig(): StorageConfig {
return currentConfig
}
export class FlaskStorageAdapter {
private baseUrl: string
constructor(baseUrl: string) {
this.baseUrl = baseUrl.replace(/\/$/, '')
}
async testConnection(): Promise<boolean> {
try {
const response = await fetch(`${this.baseUrl}/health`, {
method: 'GET',
signal: AbortSignal.timeout(5000)
})
return response.ok
} catch (error) {
console.error('Flask connection test failed:', error)
return false
}
}
async getAllSnippets(): Promise<Snippet[]> {
const response = await fetch(`${this.baseUrl}/api/snippets`)
if (!response.ok) {
throw new Error(`Failed to fetch snippets: ${response.statusText}`)
}
const data = await response.json()
return data.map((s: any) => ({
...s,
createdAt: typeof s.createdAt === 'string' ? new Date(s.createdAt).getTime() : s.createdAt,
updatedAt: typeof s.updatedAt === 'string' ? new Date(s.updatedAt).getTime() : s.updatedAt
}))
}
async getSnippet(id: string): Promise<Snippet | null> {
const response = await fetch(`${this.baseUrl}/api/snippets/${id}`)
if (response.status === 404) {
return null
}
if (!response.ok) {
throw new Error(`Failed to fetch snippet: ${response.statusText}`)
}
const data = await response.json()
return {
...data,
createdAt: typeof data.createdAt === 'string' ? new Date(data.createdAt).getTime() : data.createdAt,
updatedAt: typeof data.updatedAt === 'string' ? new Date(data.updatedAt).getTime() : data.updatedAt
}
}
async createSnippet(snippet: Snippet): Promise<void> {
const response = await fetch(`${this.baseUrl}/api/snippets`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...snippet,
createdAt: new Date(snippet.createdAt).toISOString(),
updatedAt: new Date(snippet.updatedAt).toISOString()
})
})
if (!response.ok) {
throw new Error(`Failed to create snippet: ${response.statusText}`)
}
}
async updateSnippet(snippet: Snippet): Promise<void> {
const response = await fetch(`${this.baseUrl}/api/snippets/${snippet.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...snippet,
createdAt: new Date(snippet.createdAt).toISOString(),
updatedAt: new Date(snippet.updatedAt).toISOString()
})
})
if (!response.ok) {
throw new Error(`Failed to update snippet: ${response.statusText}`)
}
}
async deleteSnippet(id: string): Promise<void> {
const response = await fetch(`${this.baseUrl}/api/snippets/${id}`, {
method: 'DELETE'
})
if (!response.ok) {
throw new Error(`Failed to delete snippet: ${response.statusText}`)
}
}
async migrateFromIndexedDB(snippets: Snippet[]): Promise<void> {
for (const snippet of snippets) {
await this.createSnippet(snippet)
}
}
async migrateToIndexedDB(): Promise<Snippet[]> {
return this.getAllSnippets()
}
}

View File

@@ -2,6 +2,7 @@ import { createRoot } from 'react-dom/client'
import { ErrorBoundary } from "react-error-boundary";
import "@github/spark/spark"
import { Toaster } from '@/components/ui/sonner'
import { loadStorageConfig } from '@/lib/storage'
import App from './App.tsx'
import { ErrorFallback } from './ErrorFallback.tsx'
@@ -10,6 +11,8 @@ import "./main.css"
import "./styles/theme.css"
import "./index.css"
loadStorageConfig()
const logErrorToConsole = (error: Error, info: { componentStack?: string }) => {
console.error('Application Error:', error);
if (info.componentStack) {

View File

@@ -2,10 +2,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 { Database, Download, Upload, Trash } from '@phosphor-icons/react'
import { getDatabaseStats, exportDatabase, importDatabase, clearDatabase, seedDatabase } from '@/lib/db'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Database, Download, Upload, Trash, CloudArrowUp, CloudCheck, CloudSlash } from '@phosphor-icons/react'
import { getDatabaseStats, exportDatabase, importDatabase, clearDatabase, seedDatabase, getAllSnippets } 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'
export function SettingsPage() {
const [stats, setStats] = useState<{
@@ -15,6 +25,10 @@ export function SettingsPage() {
databaseSize: number
} | null>(null)
const [loading, setLoading] = useState(true)
const [storageBackend, setStorageBackend] = useState<StorageBackend>('indexeddb')
const [flaskUrl, setFlaskUrl] = useState('')
const [flaskConnectionStatus, setFlaskConnectionStatus] = useState<'unknown' | 'connected' | 'failed'>('unknown')
const [testingConnection, setTestingConnection] = useState(false)
const loadStats = async () => {
setLoading(true)
@@ -29,8 +43,30 @@ export function SettingsPage() {
}
}
const testFlaskConnection = async (url: string) => {
setTestingConnection(true)
try {
const adapter = new FlaskStorageAdapter(url)
const connected = await adapter.testConnection()
setFlaskConnectionStatus(connected ? 'connected' : 'failed')
return connected
} catch (error) {
console.error('Connection test failed:', error)
setFlaskConnectionStatus('failed')
return false
} finally {
setTestingConnection(false)
}
}
useEffect(() => {
loadStats()
const config = loadStorageConfig()
setStorageBackend(config.backend)
setFlaskUrl(config.flaskUrl || 'http://localhost:5000')
if (config.backend === 'flask' && config.flaskUrl) {
testFlaskConnection(config.flaskUrl)
}
}, [])
const handleExport = async () => {
@@ -104,6 +140,98 @@ export function SettingsPage() {
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]
}
const handleTestConnection = async () => {
await testFlaskConnection(flaskUrl)
}
const handleSaveStorageConfig = async () => {
if (storageBackend === 'flask') {
if (!flaskUrl) {
toast.error('Please enter a Flask backend URL')
return
}
const connected = await testFlaskConnection(flaskUrl)
if (!connected) {
toast.error('Cannot connect to Flask backend. Please check the URL and ensure the server is running.')
return
}
}
saveStorageConfig({
backend: storageBackend,
flaskUrl: storageBackend === 'flask' ? flaskUrl : undefined
})
toast.success('Storage backend updated successfully')
await loadStats()
}
const handleMigrateToFlask = async () => {
if (!flaskUrl) {
toast.error('Please enter a Flask backend URL')
return
}
try {
const adapter = new FlaskStorageAdapter(flaskUrl)
const connected = await adapter.testConnection()
if (!connected) {
toast.error('Cannot connect to Flask backend')
return
}
const snippets = await getAllSnippets()
if (snippets.length === 0) {
toast.info('No snippets to migrate')
return
}
await adapter.migrateFromIndexedDB(snippets)
saveStorageConfig({
backend: 'flask',
flaskUrl
})
toast.success(`Successfully migrated ${snippets.length} snippets to Flask backend`)
await loadStats()
} catch (error) {
console.error('Migration failed:', error)
toast.error('Failed to migrate data to Flask backend')
}
}
const handleMigrateToIndexedDB = async () => {
if (!flaskUrl) {
toast.error('Please enter a Flask backend URL')
return
}
try {
const adapter = new FlaskStorageAdapter(flaskUrl)
const snippets = await adapter.migrateToIndexedDB()
if (snippets.length === 0) {
toast.info('No snippets to migrate')
return
}
saveStorageConfig({
backend: 'indexeddb'
})
window.location.reload()
toast.success(`Successfully migrated ${snippets.length} snippets to IndexedDB`)
} catch (error) {
console.error('Migration failed:', error)
toast.error('Failed to migrate data from Flask backend')
}
}
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
@@ -116,6 +244,112 @@ export function SettingsPage() {
</div>
<div className="grid gap-6 max-w-3xl">
<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">
<RadioGroup value={storageBackend} onValueChange={(value) => setStorageBackend(value as StorageBackend)}>
<div className="flex items-start space-x-3 space-y-0">
<RadioGroupItem value="indexeddb" id="storage-indexeddb" />
<div className="flex-1">
<Label htmlFor="storage-indexeddb" className="font-semibold 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" />
<div className="flex-1">
<Label htmlFor="storage-flask" className="font-semibold 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) => {
setFlaskUrl(e.target.value)
setFlaskConnectionStatus('unknown')
}}
/>
<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>
<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>
)}
<div className="pt-2">
<Button onClick={handleSaveStorageConfig} className="gap-2">
<Database weight="bold" size={16} />
Save Storage Settings
</Button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">