inital working version
This commit is contained in:
276
frontend/src/app/settings/page.tsx
Normal file
276
frontend/src/app/settings/page.tsx
Normal file
@@ -0,0 +1,276 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import AppLayout from '../../components/layout/AppLayout';
|
||||
import { useTheme } from '../../lib/theme';
|
||||
import Button from '../../components/ui/Button';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const defaultSettings: Settings = {
|
||||
theme: 'light',
|
||||
fontFamily: 'serif',
|
||||
fontSize: 'medium',
|
||||
readingWidth: 'medium',
|
||||
};
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const [settings, setSettings] = useState<Settings>(defaultSettings);
|
||||
const [saved, setSaved] = useState(false);
|
||||
|
||||
// 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 }));
|
||||
};
|
||||
|
||||
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>
|
||||
</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>
|
||||
|
||||
{/* 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user