743 lines
30 KiB
TypeScript
743 lines
30 KiB
TypeScript
'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<Settings>(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<any>(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 = <K extends keyof Settings>(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<HTMLInputElement>) => {
|
||
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 (
|
||
<AppLayout>
|
||
<div className="max-w-2xl mx-auto space-y-8">
|
||
<div>
|
||
<h1 className="text-3xl font-bold theme-header">Settings</h1>
|
||
<p className="theme-text mt-2">
|
||
Customize your StoryCove reading experience
|
||
</p>
|
||
</div>
|
||
|
||
<div className="space-y-6">
|
||
{/* Theme Settings */}
|
||
<div className="theme-card theme-shadow rounded-lg p-6">
|
||
<h2 className="text-xl font-semibold theme-header mb-4">Appearance</h2>
|
||
|
||
<div className="space-y-4">
|
||
<div>
|
||
<label className="block text-sm font-medium theme-header mb-2">
|
||
Theme
|
||
</label>
|
||
<div className="flex gap-4">
|
||
<button
|
||
onClick={() => updateSetting('theme', 'light')}
|
||
className={`px-4 py-2 rounded-lg border transition-colors ${
|
||
settings.theme === 'light'
|
||
? 'theme-accent-bg text-white border-transparent'
|
||
: 'theme-card theme-text theme-border hover:border-gray-400'
|
||
}`}
|
||
>
|
||
☀️ Light
|
||
</button>
|
||
<button
|
||
onClick={() => updateSetting('theme', 'dark')}
|
||
className={`px-4 py-2 rounded-lg border transition-colors ${
|
||
settings.theme === 'dark'
|
||
? 'theme-accent-bg text-white border-transparent'
|
||
: 'theme-card theme-text theme-border hover:border-gray-400'
|
||
}`}
|
||
>
|
||
🌙 Dark
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Reading Settings */}
|
||
<div className="theme-card theme-shadow rounded-lg p-6">
|
||
<h2 className="text-xl font-semibold theme-header mb-4">Reading Experience</h2>
|
||
|
||
<div className="space-y-6">
|
||
{/* Font Family */}
|
||
<div>
|
||
<label className="block text-sm font-medium theme-header mb-2">
|
||
Font Family
|
||
</label>
|
||
<div className="flex gap-4 flex-wrap">
|
||
<button
|
||
onClick={() => updateSetting('fontFamily', 'serif')}
|
||
className={`px-4 py-2 rounded-lg border transition-colors font-serif ${
|
||
settings.fontFamily === 'serif'
|
||
? 'theme-accent-bg text-white border-transparent'
|
||
: 'theme-card theme-text theme-border hover:border-gray-400'
|
||
}`}
|
||
>
|
||
Serif
|
||
</button>
|
||
<button
|
||
onClick={() => updateSetting('fontFamily', 'sans')}
|
||
className={`px-4 py-2 rounded-lg border transition-colors font-sans ${
|
||
settings.fontFamily === 'sans'
|
||
? 'theme-accent-bg text-white border-transparent'
|
||
: 'theme-card theme-text theme-border hover:border-gray-400'
|
||
}`}
|
||
>
|
||
Sans Serif
|
||
</button>
|
||
<button
|
||
onClick={() => updateSetting('fontFamily', 'mono')}
|
||
className={`px-4 py-2 rounded-lg border transition-colors font-mono ${
|
||
settings.fontFamily === 'mono'
|
||
? 'theme-accent-bg text-white border-transparent'
|
||
: 'theme-card theme-text theme-border hover:border-gray-400'
|
||
}`}
|
||
>
|
||
Monospace
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Font Size */}
|
||
<div>
|
||
<label className="block text-sm font-medium theme-header mb-2">
|
||
Font Size
|
||
</label>
|
||
<div className="flex gap-4 flex-wrap">
|
||
{(['small', 'medium', 'large', 'extra-large'] as FontSize[]).map((size) => (
|
||
<button
|
||
key={size}
|
||
onClick={() => updateSetting('fontSize', size)}
|
||
className={`px-4 py-2 rounded-lg border transition-colors capitalize ${
|
||
settings.fontSize === size
|
||
? 'theme-accent-bg text-white border-transparent'
|
||
: 'theme-card theme-text theme-border hover:border-gray-400'
|
||
}`}
|
||
>
|
||
{size.replace('-', ' ')}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Reading Width */}
|
||
<div>
|
||
<label className="block text-sm font-medium theme-header mb-2">
|
||
Reading Width
|
||
</label>
|
||
<div className="flex gap-4">
|
||
{(['narrow', 'medium', 'wide'] as ReadingWidth[]).map((width) => (
|
||
<button
|
||
key={width}
|
||
onClick={() => updateSetting('readingWidth', width)}
|
||
className={`px-4 py-2 rounded-lg border transition-colors capitalize ${
|
||
settings.readingWidth === width
|
||
? 'theme-accent-bg text-white border-transparent'
|
||
: 'theme-card theme-text theme-border hover:border-gray-400'
|
||
}`}
|
||
>
|
||
{width}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Reading Speed */}
|
||
<div>
|
||
<label className="block text-sm font-medium theme-header mb-2">
|
||
Reading Speed (words per minute)
|
||
</label>
|
||
<div className="flex items-center gap-4">
|
||
<input
|
||
type="range"
|
||
min="100"
|
||
max="400"
|
||
step="25"
|
||
value={settings.readingSpeed}
|
||
onChange={(e) => updateSetting('readingSpeed', parseInt(e.target.value))}
|
||
className="flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
|
||
/>
|
||
<div className="min-w-[80px] text-center">
|
||
<span className="text-lg font-medium theme-header">{settings.readingSpeed}</span>
|
||
<div className="text-xs theme-text">WPM</div>
|
||
</div>
|
||
</div>
|
||
<div className="flex justify-between text-xs theme-text mt-1">
|
||
<span>Slow (100)</span>
|
||
<span>Average (200)</span>
|
||
<span>Fast (400)</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Preview */}
|
||
<div className="theme-card theme-shadow rounded-lg p-6">
|
||
<h2 className="text-xl font-semibold theme-header mb-4">Preview</h2>
|
||
|
||
<div
|
||
className="p-4 theme-card border theme-border rounded-lg"
|
||
style={{
|
||
fontFamily: settings.fontFamily === 'serif' ? 'Georgia, Times, serif'
|
||
: settings.fontFamily === 'sans' ? 'Inter, system-ui, sans-serif'
|
||
: 'Monaco, Consolas, monospace',
|
||
fontSize: settings.fontSize === 'small' ? '14px'
|
||
: settings.fontSize === 'medium' ? '16px'
|
||
: settings.fontSize === 'large' ? '18px'
|
||
: '20px',
|
||
maxWidth: settings.readingWidth === 'narrow' ? '600px'
|
||
: settings.readingWidth === 'medium' ? '800px'
|
||
: '1000px',
|
||
}}
|
||
>
|
||
<h3 className="text-xl font-bold theme-header mb-2">Sample Story Title</h3>
|
||
<p className="theme-text mb-4">by Sample Author</p>
|
||
<p className="theme-text leading-relaxed">
|
||
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.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Typesense Search Management */}
|
||
<div className="theme-card theme-shadow rounded-lg p-6">
|
||
<h2 className="text-xl font-semibold theme-header mb-4">Search Index Management</h2>
|
||
<p className="theme-text mb-6">
|
||
Manage the Typesense search indexes for stories and authors. Use these tools if search functionality isn't working properly.
|
||
</p>
|
||
|
||
<div className="space-y-6">
|
||
{/* Stories Section */}
|
||
<div className="border theme-border rounded-lg p-4">
|
||
<h3 className="text-lg font-semibold theme-header mb-3">Stories Index</h3>
|
||
<div className="flex flex-col sm:flex-row gap-3 mb-3">
|
||
<Button
|
||
onClick={() => handleTypesenseOperation('stories', 'reindex', storyApi.reindexTypesense)}
|
||
disabled={typesenseStatus.stories.loading}
|
||
loading={typesenseStatus.stories.loading}
|
||
variant="ghost"
|
||
className="flex-1"
|
||
>
|
||
{typesenseStatus.stories.loading ? 'Reindexing...' : 'Reindex Stories'}
|
||
</Button>
|
||
<Button
|
||
onClick={() => handleTypesenseOperation('stories', 'recreate', storyApi.recreateTypesenseCollection)}
|
||
disabled={typesenseStatus.stories.loading}
|
||
loading={typesenseStatus.stories.loading}
|
||
variant="secondary"
|
||
className="flex-1"
|
||
>
|
||
{typesenseStatus.stories.loading ? 'Recreating...' : 'Recreate Collection'}
|
||
</Button>
|
||
</div>
|
||
{typesenseStatus.stories.message && (
|
||
<div className={`text-sm p-2 rounded ${
|
||
typesenseStatus.stories.success
|
||
? 'bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200'
|
||
: 'bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200'
|
||
}`}>
|
||
{typesenseStatus.stories.message}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Authors Section */}
|
||
<div className="border theme-border rounded-lg p-4">
|
||
<h3 className="text-lg font-semibold theme-header mb-3">Authors Index</h3>
|
||
<div className="flex flex-col sm:flex-row gap-3 mb-3">
|
||
<Button
|
||
onClick={() => handleTypesenseOperation('authors', 'reindex', authorApi.reindexTypesense)}
|
||
disabled={typesenseStatus.authors.loading}
|
||
loading={typesenseStatus.authors.loading}
|
||
variant="ghost"
|
||
className="flex-1"
|
||
>
|
||
{typesenseStatus.authors.loading ? 'Reindexing...' : 'Reindex Authors'}
|
||
</Button>
|
||
<Button
|
||
onClick={() => handleTypesenseOperation('authors', 'recreate', authorApi.recreateTypesenseCollection)}
|
||
disabled={typesenseStatus.authors.loading}
|
||
loading={typesenseStatus.authors.loading}
|
||
variant="secondary"
|
||
className="flex-1"
|
||
>
|
||
{typesenseStatus.authors.loading ? 'Recreating...' : 'Recreate Collection'}
|
||
</Button>
|
||
</div>
|
||
{typesenseStatus.authors.message && (
|
||
<div className={`text-sm p-2 rounded ${
|
||
typesenseStatus.authors.success
|
||
? 'bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200'
|
||
: 'bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200'
|
||
}`}>
|
||
{typesenseStatus.authors.message}
|
||
</div>
|
||
)}
|
||
|
||
{/* Debug Schema Section */}
|
||
<div className="border-t theme-border pt-3">
|
||
<div className="flex items-center gap-2 mb-2">
|
||
<Button
|
||
onClick={fetchAuthorsSchema}
|
||
variant="ghost"
|
||
className="text-xs"
|
||
>
|
||
Inspect Schema
|
||
</Button>
|
||
<Button
|
||
onClick={() => setShowSchema(!showSchema)}
|
||
variant="ghost"
|
||
className="text-xs"
|
||
disabled={!authorsSchema}
|
||
>
|
||
{showSchema ? 'Hide' : 'Show'} Schema
|
||
</Button>
|
||
</div>
|
||
|
||
{showSchema && authorsSchema && (
|
||
<div className="text-xs theme-text bg-gray-50 dark:bg-gray-800 p-3 rounded border overflow-auto max-h-48">
|
||
<pre>{JSON.stringify(authorsSchema, null, 2)}</pre>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="text-sm theme-text bg-blue-50 dark:bg-blue-900/20 p-3 rounded-lg">
|
||
<p className="font-medium mb-1">When to use these tools:</p>
|
||
<ul className="text-xs space-y-1 ml-4">
|
||
<li>• <strong>Reindex:</strong> Refresh search data while keeping the existing schema</li>
|
||
<li>• <strong>Recreate Collection:</strong> Delete and rebuild the entire search index (fixes schema issues)</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Database Management */}
|
||
<div className="theme-card theme-shadow rounded-lg p-6">
|
||
<h2 className="text-xl font-semibold theme-header mb-4">Database Management</h2>
|
||
<p className="theme-text mb-6">
|
||
Backup, restore, or clear your StoryCove database and files. These comprehensive operations include both your data and uploaded images.
|
||
</p>
|
||
|
||
<div className="space-y-6">
|
||
{/* Complete Backup Section */}
|
||
<div className="border theme-border rounded-lg p-4 border-blue-200 dark:border-blue-800">
|
||
<h3 className="text-lg font-semibold theme-header mb-3">📦 Create Backup</h3>
|
||
<p className="text-sm theme-text mb-3">
|
||
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.
|
||
</p>
|
||
<Button
|
||
onClick={handleCompleteBackup}
|
||
disabled={databaseStatus.completeBackup.loading}
|
||
loading={databaseStatus.completeBackup.loading}
|
||
variant="primary"
|
||
className="w-full sm:w-auto"
|
||
>
|
||
{databaseStatus.completeBackup.loading ? 'Creating Backup...' : 'Download Backup'}
|
||
</Button>
|
||
{databaseStatus.completeBackup.message && (
|
||
<div className={`text-sm p-2 rounded mt-3 ${
|
||
databaseStatus.completeBackup.success
|
||
? 'bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200'
|
||
: 'bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200'
|
||
}`}>
|
||
{databaseStatus.completeBackup.message}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Restore Section */}
|
||
<div className="border theme-border rounded-lg p-4 border-orange-200 dark:border-orange-800">
|
||
<h3 className="text-lg font-semibold theme-header mb-3">📥 Restore Backup</h3>
|
||
<p className="text-sm theme-text mb-3">
|
||
<strong className="text-orange-600 dark:text-orange-400">⚠️ Warning:</strong> This will completely replace your current database AND all files with the backup. All existing data and uploaded files will be permanently deleted.
|
||
</p>
|
||
<div className="flex items-center gap-3">
|
||
<input
|
||
type="file"
|
||
accept=".zip"
|
||
onChange={handleCompleteRestore}
|
||
disabled={databaseStatus.completeRestore.loading}
|
||
className="flex-1 text-sm theme-text file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-medium file:theme-accent-bg file:text-white hover:file:bg-opacity-90 file:cursor-pointer"
|
||
/>
|
||
</div>
|
||
{databaseStatus.completeRestore.message && (
|
||
<div className={`text-sm p-2 rounded mt-3 ${
|
||
databaseStatus.completeRestore.success
|
||
? 'bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200'
|
||
: 'bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200'
|
||
}`}>
|
||
{databaseStatus.completeRestore.message}
|
||
</div>
|
||
)}
|
||
{databaseStatus.completeRestore.loading && (
|
||
<div className="text-sm theme-text mt-3 flex items-center gap-2">
|
||
<div className="animate-spin w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full"></div>
|
||
Restoring backup...
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Clear Everything Section */}
|
||
<div className="border theme-border rounded-lg p-4 border-red-200 dark:border-red-800 bg-red-50 dark:bg-red-900/10">
|
||
<h3 className="text-lg font-semibold theme-header mb-3">🗑️ Clear Everything</h3>
|
||
<p className="text-sm theme-text mb-3">
|
||
<strong className="text-red-600 dark:text-red-400">⚠️ Danger Zone:</strong> 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!
|
||
</p>
|
||
<Button
|
||
onClick={handleCompleteClear}
|
||
disabled={databaseStatus.completeClear.loading}
|
||
loading={databaseStatus.completeClear.loading}
|
||
variant="secondary"
|
||
className="w-full sm:w-auto bg-red-700 hover:bg-red-800 text-white border-red-700"
|
||
>
|
||
{databaseStatus.completeClear.loading ? 'Clearing Everything...' : 'Clear Everything'}
|
||
</Button>
|
||
{databaseStatus.completeClear.message && (
|
||
<div className={`text-sm p-2 rounded mt-3 ${
|
||
databaseStatus.completeClear.success
|
||
? 'bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200'
|
||
: 'bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200'
|
||
}`}>
|
||
{databaseStatus.completeClear.message}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="text-sm theme-text bg-blue-50 dark:bg-blue-900/20 p-3 rounded-lg">
|
||
<p className="font-medium mb-1">💡 Best Practices:</p>
|
||
<ul className="text-xs space-y-1 ml-4">
|
||
<li>• <strong>Always backup</strong> before performing restore or clear operations</li>
|
||
<li>• <strong>Store backups safely</strong> in multiple locations for important data</li>
|
||
<li>• <strong>Test restores</strong> in a development environment when possible</li>
|
||
<li>• <strong>Backup files (.zip)</strong> contain both database and all uploaded files</li>
|
||
<li>• <strong>Verify backup files</strong> are complete before relying on them</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Actions */}
|
||
<div className="flex justify-end gap-4">
|
||
<Button
|
||
variant="ghost"
|
||
onClick={() => {
|
||
setSettings({ ...defaultSettings, theme });
|
||
}}
|
||
>
|
||
Reset to Defaults
|
||
</Button>
|
||
|
||
<Button
|
||
onClick={saveSettings}
|
||
className={saved ? 'bg-green-600 hover:bg-green-700' : ''}
|
||
>
|
||
{saved ? '✓ Saved!' : 'Save Settings'}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</AppLayout>
|
||
);
|
||
} |