'use client'; import { useState, useEffect } from 'react'; import AppLayout from '../../components/layout/AppLayout'; import { useTheme } from '../../lib/theme'; import Button from '../../components/ui/Button'; import { storyApi, authorApi, databaseApi } from '../../lib/api'; type FontFamily = 'serif' | 'sans' | 'mono'; type FontSize = 'small' | 'medium' | 'large' | 'extra-large'; type ReadingWidth = 'narrow' | 'medium' | 'wide'; interface Settings { theme: 'light' | 'dark'; fontFamily: FontFamily; fontSize: FontSize; readingWidth: ReadingWidth; readingSpeed: number; // words per minute } const defaultSettings: Settings = { theme: 'light', fontFamily: 'serif', fontSize: 'medium', readingWidth: 'medium', readingSpeed: 200, }; export default function SettingsPage() { const { theme, setTheme } = useTheme(); const [settings, setSettings] = useState(defaultSettings); const [saved, setSaved] = useState(false); const [typesenseStatus, setTypesenseStatus] = useState<{ stories: { loading: boolean; message: string; success?: boolean }; authors: { loading: boolean; message: string; success?: boolean }; }>({ stories: { loading: false, message: '' }, authors: { loading: false, message: '' } }); const [authorsSchema, setAuthorsSchema] = useState(null); const [showSchema, setShowSchema] = useState(false); const [databaseStatus, setDatabaseStatus] = useState<{ completeBackup: { loading: boolean; message: string; success?: boolean }; completeRestore: { loading: boolean; message: string; success?: boolean }; completeClear: { loading: boolean; message: string; success?: boolean }; }>({ completeBackup: { loading: false, message: '' }, completeRestore: { loading: false, message: '' }, completeClear: { loading: false, message: '' } }); // Load settings from localStorage on mount useEffect(() => { const savedSettings = localStorage.getItem('storycove-settings'); if (savedSettings) { try { const parsed = JSON.parse(savedSettings); setSettings({ ...defaultSettings, ...parsed, theme }); } catch (error) { console.error('Failed to parse saved settings:', error); setSettings({ ...defaultSettings, theme }); } } else { setSettings({ ...defaultSettings, theme }); } }, [theme]); // Save settings to localStorage const saveSettings = () => { localStorage.setItem('storycove-settings', JSON.stringify(settings)); // Apply theme change setTheme(settings.theme); // Apply font settings to CSS custom properties const root = document.documentElement; const fontFamilyMap = { serif: 'Georgia, Times, serif', sans: 'Inter, system-ui, sans-serif', mono: 'Monaco, Consolas, monospace', }; const fontSizeMap = { small: '14px', medium: '16px', large: '18px', 'extra-large': '20px', }; const readingWidthMap = { narrow: '600px', medium: '800px', wide: '1000px', }; root.style.setProperty('--reading-font-family', fontFamilyMap[settings.fontFamily]); root.style.setProperty('--reading-font-size', fontSizeMap[settings.fontSize]); root.style.setProperty('--reading-max-width', readingWidthMap[settings.readingWidth]); setSaved(true); setTimeout(() => setSaved(false), 2000); }; const updateSetting = (key: K, value: Settings[K]) => { setSettings(prev => ({ ...prev, [key]: value })); }; const handleTypesenseOperation = async ( type: 'stories' | 'authors', operation: 'reindex' | 'recreate', apiCall: () => Promise<{ success: boolean; message: string; count?: number; error?: string }> ) => { setTypesenseStatus(prev => ({ ...prev, [type]: { loading: true, message: 'Processing...', success: undefined } })); try { const result = await apiCall(); setTypesenseStatus(prev => ({ ...prev, [type]: { loading: false, message: result.success ? result.message : result.error || 'Operation failed', success: result.success } })); // Clear message after 5 seconds setTimeout(() => { setTypesenseStatus(prev => ({ ...prev, [type]: { loading: false, message: '', success: undefined } })); }, 5000); } catch (error) { setTypesenseStatus(prev => ({ ...prev, [type]: { loading: false, message: 'Network error occurred', success: false } })); setTimeout(() => { setTypesenseStatus(prev => ({ ...prev, [type]: { loading: false, message: '', success: undefined } })); }, 5000); } }; const fetchAuthorsSchema = async () => { try { const result = await authorApi.getTypesenseSchema(); if (result.success) { setAuthorsSchema(result.schema); } else { setAuthorsSchema({ error: result.error }); } } catch (error) { setAuthorsSchema({ error: 'Failed to fetch schema' }); } }; const handleCompleteBackup = async () => { setDatabaseStatus(prev => ({ ...prev, completeBackup: { loading: true, message: 'Creating complete backup...', success: undefined } })); try { const backupBlob = await databaseApi.backupComplete(); // Create download link const url = window.URL.createObjectURL(backupBlob); const link = document.createElement('a'); link.href = url; const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); link.download = `storycove_complete_backup_${timestamp}.zip`; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(url); setDatabaseStatus(prev => ({ ...prev, completeBackup: { loading: false, message: 'Complete backup downloaded successfully', success: true } })); } catch (error: any) { setDatabaseStatus(prev => ({ ...prev, completeBackup: { loading: false, message: error.message || 'Complete backup failed', success: false } })); } // Clear message after 5 seconds setTimeout(() => { setDatabaseStatus(prev => ({ ...prev, completeBackup: { loading: false, message: '', success: undefined } })); }, 5000); }; const handleCompleteRestore = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; // Reset the input so the same file can be selected again event.target.value = ''; if (!file.name.endsWith('.zip')) { setDatabaseStatus(prev => ({ ...prev, completeRestore: { loading: false, message: 'Please select a .zip file', success: false } })); return; } const confirmed = window.confirm( 'Are you sure you want to restore the complete backup? This will PERMANENTLY DELETE all current data AND files (cover images, avatars) and replace them with the backup data. This action cannot be undone!' ); if (!confirmed) return; setDatabaseStatus(prev => ({ ...prev, completeRestore: { loading: true, message: 'Restoring complete backup...', success: undefined } })); try { const result = await databaseApi.restoreComplete(file); setDatabaseStatus(prev => ({ ...prev, completeRestore: { loading: false, message: result.success ? result.message : result.message, success: result.success } })); } catch (error: any) { setDatabaseStatus(prev => ({ ...prev, completeRestore: { loading: false, message: error.message || 'Complete restore failed', success: false } })); } // Clear message after 10 seconds for restore (longer because it's important) setTimeout(() => { setDatabaseStatus(prev => ({ ...prev, completeRestore: { loading: false, message: '', success: undefined } })); }, 10000); }; const handleCompleteClear = async () => { const confirmed = window.confirm( 'Are you ABSOLUTELY SURE you want to clear the entire database AND all files? This will PERMANENTLY DELETE ALL stories, authors, series, tags, collections, AND all uploaded images (covers, avatars). This action cannot be undone!' ); if (!confirmed) return; const doubleConfirmed = window.confirm( 'This is your final warning! Clicking OK will DELETE EVERYTHING in your StoryCove database AND all uploaded files. Are you completely certain you want to proceed?' ); if (!doubleConfirmed) return; setDatabaseStatus(prev => ({ ...prev, completeClear: { loading: true, message: 'Clearing database and files...', success: undefined } })); try { const result = await databaseApi.clearComplete(); setDatabaseStatus(prev => ({ ...prev, completeClear: { loading: false, message: result.success ? `Database and files cleared successfully. Deleted ${result.deletedRecords} records.` : result.message, success: result.success } })); } catch (error: any) { setDatabaseStatus(prev => ({ ...prev, completeClear: { loading: false, message: error.message || 'Clear operation failed', success: false } })); } // Clear message after 10 seconds for clear (longer because it's important) setTimeout(() => { setDatabaseStatus(prev => ({ ...prev, completeClear: { loading: false, message: '', success: undefined } })); }, 10000); }; return (

Settings

Customize your StoryCove reading experience

{/* Theme Settings */}

Appearance

{/* Reading Settings */}

Reading Experience

{/* Font Family */}
{/* Font Size */}
{(['small', 'medium', 'large', 'extra-large'] as FontSize[]).map((size) => ( ))}
{/* Reading Width */}
{(['narrow', 'medium', 'wide'] as ReadingWidth[]).map((width) => ( ))}
{/* Reading Speed */}
updateSetting('readingSpeed', parseInt(e.target.value))} className="flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" />
{settings.readingSpeed}
WPM
Slow (100) Average (200) Fast (400)
{/* Preview */}

Preview

Sample Story Title

by Sample Author

This is how your story text will look with the current settings. The quick brown fox jumps over the lazy dog. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

{/* Typesense Search Management */}

Search Index Management

Manage the Typesense search indexes for stories and authors. Use these tools if search functionality isn't working properly.

{/* Stories Section */}

Stories Index

{typesenseStatus.stories.message && (
{typesenseStatus.stories.message}
)}
{/* Authors Section */}

Authors Index

{typesenseStatus.authors.message && (
{typesenseStatus.authors.message}
)} {/* Debug Schema Section */}
{showSchema && authorsSchema && (
{JSON.stringify(authorsSchema, null, 2)}
)}

When to use these tools:

  • Reindex: Refresh search data while keeping the existing schema
  • Recreate Collection: Delete and rebuild the entire search index (fixes schema issues)
{/* Database Management */}

Database Management

Backup, restore, or clear your StoryCove database and files. These comprehensive operations include both your data and uploaded images.

{/* Complete Backup Section */}

📦 Create Backup

Download a complete backup as a ZIP file. This includes your database AND all uploaded files (cover images, avatars). This is a comprehensive backup of your entire StoryCove installation.

{databaseStatus.completeBackup.message && (
{databaseStatus.completeBackup.message}
)}
{/* Restore Section */}

📥 Restore Backup

⚠️ Warning: This will completely replace your current database AND all files with the backup. All existing data and uploaded files will be permanently deleted.

{databaseStatus.completeRestore.message && (
{databaseStatus.completeRestore.message}
)} {databaseStatus.completeRestore.loading && (
Restoring backup...
)}
{/* Clear Everything Section */}

🗑️ Clear Everything

⚠️ Danger Zone: This will permanently delete ALL data from your database AND all uploaded files (cover images, avatars). Everything will be completely removed. This action cannot be undone!

{databaseStatus.completeClear.message && (
{databaseStatus.completeClear.message}
)}

💡 Best Practices:

  • Always backup before performing restore or clear operations
  • Store backups safely in multiple locations for important data
  • Test restores in a development environment when possible
  • Backup files (.zip) contain both database and all uploaded files
  • Verify backup files are complete before relying on them
{/* Actions */}
); }