CineMatch/frontend/src/pages/Settings.tsx
2026-02-23 13:04:28 +01:00

351 lines
14 KiB
TypeScript

import React, { useState } from 'react';
import { useAppContext } from '../store/AppContext';
import { Sun, Moon, Lock, Trash2, Film, ChevronRight, AlertTriangle } from 'lucide-react';
const TMDB_GENRES = [
{ id: 28, name: 'Action' },
{ id: 12, name: 'Adventure' },
{ id: 16, name: 'Animation' },
{ id: 35, name: 'Comedy' },
{ id: 80, name: 'Crime' },
{ id: 99, name: 'Documentary' },
{ id: 18, name: 'Drama' },
{ id: 10751, name: 'Family' },
{ id: 14, name: 'Fantasy' },
{ id: 36, name: 'History' },
{ id: 27, name: 'Horror' },
{ id: 10402, name: 'Music' },
{ id: 9648, name: 'Mystery' },
{ id: 10749, name: 'Romance' },
{ id: 878, name: 'Science Fiction' },
{ id: 53, name: 'Thriller' },
{ id: 10752, name: 'War' },
{ id: 37, name: 'Western' },
];
type SettingsSection = 'main' | 'genres' | 'password' | 'delete';
export const Settings: React.FC = () => {
const { user, token, theme, toggleTheme, updateGenres, logout } = useAppContext();
const [activeSection, setActiveSection] = useState<SettingsSection>('main');
const [selectedGenres, setSelectedGenres] = useState<number[]>(user?.genres ?? []);
const [genresSaved, setGenresSaved] = useState(false);
// Password reset state
const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [passwordError, setPasswordError] = useState('');
const [passwordSuccess, setPasswordSuccess] = useState('');
// Delete account state
const [deletePassword, setDeletePassword] = useState('');
const [deleteError, setDeleteError] = useState('');
const isGuest = !user?.username;
const toggleGenre = (id: number) => {
setSelectedGenres((prev) =>
prev.includes(id) ? prev.filter((g) => g !== id) : [...prev, id]
);
setGenresSaved(false);
};
const handleSaveGenres = async () => {
if (selectedGenres.length === 0) return;
await updateGenres(selectedGenres);
setGenresSaved(true);
setTimeout(() => setGenresSaved(false), 2000);
};
const handleChangePassword = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setPasswordError('');
setPasswordSuccess('');
if (newPassword.length < 4) {
setPasswordError('New password must be at least 4 characters.');
return;
}
if (newPassword !== confirmPassword) {
setPasswordError('New passwords do not match.');
return;
}
try {
const res = await fetch('/api/auth/password', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ currentPassword, newPassword }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
setPasswordSuccess('Password changed successfully!');
setCurrentPassword('');
setNewPassword('');
setConfirmPassword('');
} catch (err: unknown) {
setPasswordError(err instanceof Error ? err.message : 'An error occurred');
}
};
const handleDeleteAccount = async () => {
if (!deletePassword) {
setDeleteError('Please enter your password to confirm.');
return;
}
setDeleteError('');
try {
const res = await fetch('/api/auth/account', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ password: deletePassword }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
logout();
} catch (err: unknown) {
setDeleteError(err instanceof Error ? err.message : 'An error occurred');
}
};
if (!user) return null;
// ── Sub-sections ──────────────────────────────────────────────
if (activeSection === 'genres') {
return (
<div className="min-h-screen bg-background text-foreground p-6 max-w-md mx-auto pb-24">
<button
onClick={() => setActiveSection('main')}
className="text-sm text-slate-400 hover:text-foreground mb-4 flex items-center gap-1"
>
Back
</button>
<h2 className="text-xl font-bold mb-2 flex items-center gap-2">
<Film className="w-5 h-5 text-primary" />
Movie Styles
</h2>
<p className="text-slate-400 text-sm mb-6">
Select the genres you enjoy. This influences your movie feed.
</p>
<div className="flex flex-wrap gap-3">
{TMDB_GENRES.map((genre) => (
<button
key={genre.id}
onClick={() => toggleGenre(genre.id)}
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors border ${
selectedGenres.includes(genre.id)
? 'bg-primary border-primary text-white'
: 'bg-surface border-border text-muted hover:border-slate-500'
}`}
>
{genre.name}
</button>
))}
</div>
<button
onClick={handleSaveGenres}
disabled={selectedGenres.length === 0}
className="w-full mt-8 py-3 px-4 bg-primary hover:bg-primary-hover text-white rounded-xl font-medium transition-colors disabled:opacity-40"
>
{genresSaved ? '✓ Saved!' : 'Save Preferences'}
</button>
</div>
);
}
if (activeSection === 'password') {
return (
<div className="min-h-screen bg-background text-foreground p-6 max-w-md mx-auto pb-24">
<button
onClick={() => setActiveSection('main')}
className="text-sm text-slate-400 hover:text-foreground mb-4 flex items-center gap-1"
>
Back
</button>
<h2 className="text-xl font-bold mb-2 flex items-center gap-2">
<Lock className="w-5 h-5 text-primary" />
Change Password
</h2>
<p className="text-slate-400 text-sm mb-6">
Update your account password.
</p>
<form onSubmit={handleChangePassword} className="space-y-4">
{passwordError && <p className="text-red-500 text-sm">{passwordError}</p>}
{passwordSuccess && <p className="text-green-500 text-sm">{passwordSuccess}</p>}
<input
type="password"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
placeholder="Current password"
className="w-full bg-surface border border-border rounded-xl p-3 text-foreground placeholder:text-muted focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
required
/>
<input
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
placeholder="New password"
className="w-full bg-surface border border-border rounded-xl p-3 text-foreground placeholder:text-muted focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
required
/>
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="Confirm new password"
className="w-full bg-surface border border-border rounded-xl p-3 text-foreground placeholder:text-muted focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
required
/>
<button
type="submit"
className="w-full py-3 px-4 bg-primary hover:bg-primary-hover text-white rounded-xl font-medium transition-colors"
>
Update Password
</button>
</form>
</div>
);
}
if (activeSection === 'delete') {
return (
<div className="min-h-screen bg-background text-foreground p-6 max-w-md mx-auto pb-24">
<button
onClick={() => setActiveSection('main')}
className="text-sm text-slate-400 hover:text-foreground mb-4 flex items-center gap-1"
>
Back
</button>
<h2 className="text-xl font-bold mb-2 flex items-center gap-2 text-red-500">
<AlertTriangle className="w-5 h-5" />
Delete Account
</h2>
<div className="bg-red-500/10 border border-red-500/30 rounded-xl p-4 mb-6">
<p className="text-red-400 text-sm">
This action is <strong>permanent and irreversible</strong>. All your data swipes,
matches, and account info will be deleted.
</p>
</div>
{deleteError && <p className="text-red-500 text-sm mb-4">{deleteError}</p>}
<label htmlFor="delete-password" className="block text-sm text-slate-400 mb-2">
Enter your password to confirm:
</label>
<input
id="delete-password"
type="password"
value={deletePassword}
onChange={(e) => setDeletePassword(e.target.value)}
placeholder="Your password"
className="w-full bg-surface border border-red-500/40 rounded-xl p-3 text-foreground placeholder:text-muted focus:outline-none focus:ring-2 focus:ring-red-500 focus:border-transparent mb-4"
/>
<button
onClick={handleDeleteAccount}
disabled={!deletePassword}
className="w-full py-3 px-4 bg-red-600 hover:bg-red-700 text-white rounded-xl font-medium transition-colors disabled:opacity-40"
>
Permanently Delete My Account
</button>
</div>
);
}
// ── Main settings menu ────────────────────────────────────────
return (
<div className="min-h-screen bg-background text-foreground p-6 max-w-md mx-auto pb-24">
<h1 className="text-2xl font-bold mb-6">Settings</h1>
<div className="space-y-3">
{/* Theme toggle */}
<div className="bg-surface rounded-2xl border border-border p-4 flex items-center justify-between">
<div className="flex items-center gap-3">
{theme === 'dark' ? (
<Moon className="w-5 h-5 text-primary" />
) : (
<Sun className="w-5 h-5 text-primary" />
)}
<div>
<p className="font-medium">Theme</p>
<p className="text-sm text-muted">{theme === 'dark' ? 'Dark mode' : 'Light mode'}</p>
</div>
</div>
<button
onClick={toggleTheme}
className={`relative inline-flex h-7 w-12 items-center rounded-full transition-colors ${
theme === 'dark' ? 'bg-primary' : 'bg-slate-300'
}`}
>
<span
className={`inline-block h-5 w-5 transform rounded-full bg-white transition-transform ${
theme === 'dark' ? 'translate-x-6' : 'translate-x-1'
}`}
/>
</button>
</div>
{/* Movie Styles */}
<button
onClick={() => setActiveSection('genres')}
className="w-full bg-surface rounded-2xl border border-border p-4 flex items-center justify-between hover:bg-surface-hover transition-colors"
>
<div className="flex items-center gap-3">
<Film className="w-5 h-5 text-primary" />
<div className="text-left">
<p className="font-medium">Movie Styles</p>
<p className="text-sm text-muted">
{user.genres && user.genres.length > 0
? `${user.genres.length} genre${user.genres.length > 1 ? 's' : ''} selected`
: 'No genres selected'}
</p>
</div>
</div>
<ChevronRight className="w-5 h-5 text-muted" />
</button>
{/* Password Reset (only for registered users) */}
{!isGuest && (
<button
onClick={() => setActiveSection('password')}
className="w-full bg-surface rounded-2xl border border-border p-4 flex items-center justify-between hover:bg-surface-hover transition-colors"
>
<div className="flex items-center gap-3">
<Lock className="w-5 h-5 text-primary" />
<div className="text-left">
<p className="font-medium">Change Password</p>
<p className="text-sm text-muted">Update your account password</p>
</div>
</div>
<ChevronRight className="w-5 h-5 text-muted" />
</button>
)}
{/* Delete Account (hidden for guests) */}
{!isGuest && (
<button
onClick={() => setActiveSection('delete')}
className="w-full bg-surface rounded-2xl border border-red-500/20 p-4 flex items-center justify-between hover:bg-red-500/5 transition-colors"
>
<div className="flex items-center gap-3">
<Trash2 className="w-5 h-5 text-red-500" />
<div className="text-left">
<p className="font-medium text-red-500">Delete Account</p>
<p className="text-sm text-muted">Permanently remove your data</p>
</div>
</div>
<ChevronRight className="w-5 h-5 text-red-500" />
</button>
)}
</div>
</div>
);
};