mirror of
https://github.com/narrrl/holy-flavor-rater.git
synced 2026-11-04 00:47:02 +01:00
added more translations
This commit is contained in:
1 parent
8fa463c298
commit
6c30ea1f4c
5 files changed
+298
-209
No files matched your search
@@ -59,6 +59,7 @@ interface SearchResult {
|
||||
}
|
||||
|
||||
const GlobalSearch = () => {
|
||||
const { t } = useTranslation();
|
||||
const [query, setQuery] = useState('');
|
||||
const [options, setOptions] = useState<SearchResult[]>([]);
|
||||
const navigate = useNavigate();
|
||||
@@ -120,7 +121,7 @@ const GlobalSearch = () => {
|
||||
<TextField
|
||||
{...params}
|
||||
fullWidth
|
||||
placeholder="Search..."
|
||||
placeholder={t('common.search')}
|
||||
sx={{
|
||||
bgcolor: 'action.hover',
|
||||
borderRadius: 2,
|
||||
@@ -304,7 +305,7 @@ const App: React.FC = () => {
|
||||
{following.length > 0 && (
|
||||
<>
|
||||
<Divider sx={{ my: 1 }} />
|
||||
<ListSubheader sx={{ bgcolor: 'transparent', fontWeight: 'bold', lineHeight: '32px' }}>Following</ListSubheader>
|
||||
<ListSubheader sx={{ bgcolor: 'transparent', fontWeight: 'bold', lineHeight: '32px' }}>{t('nav.following')}</ListSubheader>
|
||||
{following.map(f => (
|
||||
<ListItem key={f.id} disablePadding>
|
||||
<ListItemButton component={Link} to={`/profile/${f.username}`} onClick={() => setDrawerOpen(false)}>
|
||||
@@ -419,10 +420,10 @@ const App: React.FC = () => {
|
||||
elevation={3}
|
||||
sx={{ mt: 1 }}
|
||||
>
|
||||
<MenuItem component={Link} to="/dashboard" onClick={() => setAnchorEl(null)}>Dashboard</MenuItem>
|
||||
<MenuItem component={Link} to="/settings" onClick={() => setAnchorEl(null)}>Settings</MenuItem>
|
||||
<MenuItem component={Link} to="/dashboard" onClick={() => setAnchorEl(null)}>{t('nav.dashboard')}</MenuItem>
|
||||
<MenuItem component={Link} to="/settings" onClick={() => setAnchorEl(null)}>{t('nav.settings')}</MenuItem>
|
||||
<Divider />
|
||||
<MenuItem onClick={handleLogout} sx={{ color: 'error.main' }}>Logout</MenuItem>
|
||||
<MenuItem onClick={handleLogout} sx={{ color: 'error.main' }}>{t('nav.logout')}</MenuItem>
|
||||
</Menu>
|
||||
</Box>
|
||||
) : (
|
||||
|
||||
+88
-2
@@ -12,7 +12,50 @@ const resources = {
|
||||
"dashboard": "Dashboard",
|
||||
"settings": "Settings",
|
||||
"logout": "Logout",
|
||||
"login": "Login / Signup"
|
||||
"login": "Login / Signup",
|
||||
"following": "Following"
|
||||
},
|
||||
"dashboard": {
|
||||
"welcome": "Welcome back, {{username}}!",
|
||||
"shareProfile": "Share Profile:",
|
||||
"copySuccess": "Link copied to clipboard!",
|
||||
"myRatings": "My Ratings",
|
||||
"missing": "Missing",
|
||||
"noRatings": "You haven't rated any flavors yet.",
|
||||
"exploreFlavors": "Go explore flavors",
|
||||
"allRated": "Amazing! You've rated everything! 🏆"
|
||||
},
|
||||
"settings": {
|
||||
"subtitle": "Manage your account and preferences",
|
||||
"appearanceTitle": "Appearance & Language",
|
||||
"themeLabel": "Theme",
|
||||
"langLabel": "Language",
|
||||
"avatarTitle": "Profile Picture",
|
||||
"avatarButton": "Upload New Avatar",
|
||||
"avatarHint": "Max size: 2MB. JPG, PNG or WEBP.",
|
||||
"avatarSuccess": "Avatar updated successfully!",
|
||||
"infoTitle": "Profile Information",
|
||||
"usernameLabel": "Username",
|
||||
"emailLabel": "Email",
|
||||
"emailHint": "Change will require confirmation",
|
||||
"updateButton": "Update Profile",
|
||||
"updateSuccess": "Profile updated successfully!",
|
||||
"confirmEmailTitle": "Confirm Email Change",
|
||||
"confirmEmailHint": "Enter the 6-digit code sent to {{email}}",
|
||||
"confirmButton": "Verify & Update Email",
|
||||
"passwordTitle": "Change Password",
|
||||
"oldPasswordLabel": "Old Password",
|
||||
"newPasswordLabel": "New Password",
|
||||
"passwordButton": "Change Password",
|
||||
"passwordSuccess": "Password changed successfully!",
|
||||
"dangerTitle": "Danger Zone",
|
||||
"dangerDesc": "Permanently delete your account and all associated data. This action is irreversible.",
|
||||
"deleteButton": "Delete My Account",
|
||||
"deletionCodeLabel": "Deletion Code",
|
||||
"deletionCodeHint": "Enter verification code:",
|
||||
"confirmDeleteButton": "Confirm Permanent Deletion",
|
||||
"cancelButton": "Cancel",
|
||||
"codeSent": "Verification code sent to your email."
|
||||
},
|
||||
"hero": {
|
||||
"title": "The Ultimate Holy Archive",
|
||||
@@ -70,7 +113,50 @@ const resources = {
|
||||
"dashboard": "Dashboard",
|
||||
"settings": "Einstellungen",
|
||||
"logout": "Abmelden",
|
||||
"login": "Anmelden / Registrieren"
|
||||
"login": "Anmelden / Registrieren",
|
||||
"following": "Folge ich"
|
||||
},
|
||||
"dashboard": {
|
||||
"welcome": "Willkommen zurück, {{username}}!",
|
||||
"shareProfile": "Profil teilen:",
|
||||
"copySuccess": "Link in die Zwischenablage kopiert!",
|
||||
"myRatings": "Meine Bewertungen",
|
||||
"missing": "Fehlend",
|
||||
"noRatings": "Du hast noch keine Sorten bewertet.",
|
||||
"exploreFlavors": "Sorten entdecken",
|
||||
"allRated": "Wahnsinn! Du hast alles bewertet! 🏆"
|
||||
},
|
||||
"settings": {
|
||||
"subtitle": "Verwalte dein Konto und deine Einstellungen",
|
||||
"appearanceTitle": "Erscheinungsbild & Sprache",
|
||||
"themeLabel": "Design",
|
||||
"langLabel": "Sprache",
|
||||
"avatarTitle": "Profilbild",
|
||||
"avatarButton": "Neues Bild hochladen",
|
||||
"avatarHint": "Max. Größe: 2MB. JPG, PNG oder WEBP.",
|
||||
"avatarSuccess": "Profilbild erfolgreich aktualisiert!",
|
||||
"infoTitle": "Profil-Informationen",
|
||||
"usernameLabel": "Benutzername",
|
||||
"emailLabel": "E-Mail",
|
||||
"emailHint": "Änderung erfordert Bestätigung",
|
||||
"updateButton": "Profil aktualisieren",
|
||||
"updateSuccess": "Profil erfolgreich aktualisiert!",
|
||||
"confirmEmailTitle": "E-Mail-Änderung bestätigen",
|
||||
"confirmEmailHint": "Gib den 6-stelligen Code ein, der an {{email}} gesendet wurde",
|
||||
"confirmButton": "E-Mail verifizieren & aktualisieren",
|
||||
"passwordTitle": "Passwort ändern",
|
||||
"oldPasswordLabel": "Altes Passwort",
|
||||
"newPasswordLabel": "Neues Passwort",
|
||||
"passwordButton": "Passwort ändern",
|
||||
"passwordSuccess": "Passwort erfolgreich geändert!",
|
||||
"dangerTitle": "Gefahrenzone",
|
||||
"dangerDesc": "Lösche dein Konto und alle damit verbundenen Daten dauerhaft. Dies kann nicht rückgängig gemacht werden.",
|
||||
"deleteButton": "Mein Konto löschen",
|
||||
"deletionCodeLabel": "Löschcode",
|
||||
"deletionCodeHint": "Bestätigungscode eingeben:",
|
||||
"confirmDeleteButton": "Dauerhafte Löschung bestätigen",
|
||||
"cancelButton": "Abbrechen",
|
||||
"codeSent": "Bestätigungscode an deine E-Mail gesendet."
|
||||
},
|
||||
"hero": {
|
||||
"title": "Das ultimative Holy-Archiv",
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import api from '../api';
|
||||
import { useTitle } from '../hooks/useTitle';
|
||||
|
||||
@@ -29,7 +30,8 @@ interface DashboardData {
|
||||
}
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
useTitle('Dashboard');
|
||||
const { t } = useTranslation();
|
||||
useTitle(t('nav.dashboard'));
|
||||
const [data, setData] = useState<DashboardData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
@@ -38,7 +40,7 @@ const Dashboard: React.FC = () => {
|
||||
|
||||
const copyToClipboard = () => {
|
||||
navigator.clipboard.writeText(shareUrl);
|
||||
alert('Link copied to clipboard!');
|
||||
alert(t('dashboard.copySuccess'));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -78,7 +80,7 @@ const Dashboard: React.FC = () => {
|
||||
startIcon={<ArrowBackIcon />}
|
||||
sx={{ mb: 4, textTransform: 'none', borderRadius: 2 }}
|
||||
>
|
||||
Back to Home
|
||||
{t('common.backToHome')}
|
||||
</Button>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 6, flexWrap: 'wrap', gap: 2 }}>
|
||||
@@ -87,12 +89,12 @@ const Dashboard: React.FC = () => {
|
||||
{!data.user.avatar && data.user.username.charAt(0).toUpperCase()}
|
||||
</Avatar>
|
||||
<Box>
|
||||
<Typography variant="h4" sx={{ fontWeight: 'bold' }}>Dashboard</Typography>
|
||||
<Typography variant="body2" color="text.secondary">Welcome back, {data.user.username}!</Typography>
|
||||
<Typography variant="h4" sx={{ fontWeight: 'bold' }}>{t('nav.dashboard')}</Typography>
|
||||
<Typography variant="body2" color="text.secondary">{t('dashboard.welcome', { username: data.user.username })}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, bgcolor: 'background.paper', p: 1, borderRadius: 2, border: '1px solid', borderColor: 'divider' }}>
|
||||
<Typography variant="caption" sx={{ ml: 1, mr: 2, display: { xs: 'none', md: 'block' } }}>Share Profile:</Typography>
|
||||
<Typography variant="caption" sx={{ ml: 1, mr: 2, display: { xs: 'none', md: 'block' } }}>{t('dashboard.shareProfile')}</Typography>
|
||||
<TextField
|
||||
size="small"
|
||||
variant="standard"
|
||||
@@ -108,8 +110,8 @@ const Dashboard: React.FC = () => {
|
||||
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 4 }}>
|
||||
<Tabs value={activeTab} onChange={(_, v) => setActiveTab(v)} textColor="primary" indicatorColor="primary">
|
||||
<Tab label={`My Ratings (${data.rated_count})`} sx={{ fontWeight: 'bold' }} />
|
||||
<Tab label={`Missing (${data.missing_count})`} sx={{ fontWeight: 'bold' }} />
|
||||
<Tab label={`${t('dashboard.myRatings')} (${data.rated_count})`} sx={{ fontWeight: 'bold' }} />
|
||||
<Tab label={`${t('dashboard.missing')} (${data.missing_count})`} sx={{ fontWeight: 'bold' }} />
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
@@ -117,8 +119,8 @@ const Dashboard: React.FC = () => {
|
||||
<Box>
|
||||
{Object.keys(ratedGrouped).length === 0 ? (
|
||||
<Box sx={{ textAlign: 'center', py: 8 }}>
|
||||
<Typography color="text.secondary">You haven't rated any flavors yet.</Typography>
|
||||
<Button component={Link} to="/" sx={{ mt: 2 }}>Go explore flavors</Button>
|
||||
<Typography color="text.secondary">{t('dashboard.noRatings')}</Typography>
|
||||
<Button component={Link} to="/" sx={{ mt: 2 }}>{t('dashboard.exploreFlavors')}</Button>
|
||||
</Box>
|
||||
) : (
|
||||
Object.entries(ratedGrouped).map(([category, items]: [string, any]) => (
|
||||
@@ -189,7 +191,7 @@ const Dashboard: React.FC = () => {
|
||||
<Box>
|
||||
{Object.keys(missingGrouped).length === 0 ? (
|
||||
<Box sx={{ textAlign: 'center', py: 8 }}>
|
||||
<Typography variant="h5" color="primary">Amazing! You've rated everything! 🏆</Typography>
|
||||
<Typography variant="h5" color="primary">{t('dashboard.allRated')}</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
Object.entries(missingGrouped).map(([category, flavors]: [string, any]) => (
|
||||
|
||||
+184
-186
@@ -13,7 +13,8 @@ import {
|
||||
Select,
|
||||
MenuItem,
|
||||
ListSubheader,
|
||||
Divider
|
||||
Divider,
|
||||
Grid
|
||||
} from '@mui/material';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import { Link } from 'react-router-dom';
|
||||
@@ -78,7 +79,7 @@ const Settings: React.FC<SettingsProps> = ({ themeName, onThemeChange }) => {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
});
|
||||
setAvatar(res.data.avatar);
|
||||
setMessage({ type: 'success', text: 'Avatar updated successfully!' });
|
||||
setMessage({ type: 'success', text: t('settings.avatarSuccess') });
|
||||
} catch (err: any) {
|
||||
setMessage({ type: 'error', text: err.response?.data?.error || 'Failed to upload avatar' });
|
||||
}
|
||||
@@ -100,9 +101,9 @@ const Settings: React.FC<SettingsProps> = ({ themeName, onThemeChange }) => {
|
||||
const res = await api.patch('users/update_profile/', { username, email });
|
||||
if (res.data.message) {
|
||||
setPendingConfirmation(true);
|
||||
setMessage({ type: 'success', text: `Profile updated.${res.data.message}` });
|
||||
setMessage({ type: 'success', text: `${t('settings.updateSuccess')} ${res.data.message}` });
|
||||
} else {
|
||||
setMessage({ type: 'success', text: 'Profile updated successfully!' });
|
||||
setMessage({ type: 'success', text: t('settings.updateSuccess') });
|
||||
setCurrentEmail(email);
|
||||
}
|
||||
} catch (err: any) {
|
||||
@@ -114,7 +115,7 @@ const Settings: React.FC<SettingsProps> = ({ themeName, onThemeChange }) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const res = await api.post('users/confirm_email/', { code: confirmationCode });
|
||||
setMessage({ type: 'success', text: 'Email confirmed and updated!' });
|
||||
setMessage({ type: 'success', text: t('settings.updateSuccess') });
|
||||
setCurrentEmail(res.data.email);
|
||||
setPendingConfirmation(false);
|
||||
setConfirmationCode('');
|
||||
@@ -127,7 +128,7 @@ const Settings: React.FC<SettingsProps> = ({ themeName, onThemeChange }) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await api.post('users/change_password/', { old_password: oldPassword, new_password: newPassword });
|
||||
setMessage({ type: 'success', text: 'Password changed successfully!' });
|
||||
setMessage({ type: 'success', text: t('settings.passwordSuccess') });
|
||||
setOldPassword('');
|
||||
setNewPassword('');
|
||||
} catch (err: any) {
|
||||
@@ -139,7 +140,7 @@ const Settings: React.FC<SettingsProps> = ({ themeName, onThemeChange }) => {
|
||||
try {
|
||||
await api.post('users/request_account_deletion/');
|
||||
setIsDeleting(true);
|
||||
setMessage({ type: 'success', text: 'Verification code sent to your email.' });
|
||||
setMessage({ type: 'success', text: t('settings.codeSent') });
|
||||
} catch (err: any) {
|
||||
setMessage({ type: 'error', text: 'Failed to request deletion.' });
|
||||
}
|
||||
@@ -158,7 +159,7 @@ const Settings: React.FC<SettingsProps> = ({ themeName, onThemeChange }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Container maxWidth="sm" sx={{ py: 4 }}>
|
||||
<Container maxWidth="lg" sx={{ py: 4 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
component={Link}
|
||||
@@ -166,202 +167,199 @@ const Settings: React.FC<SettingsProps> = ({ themeName, onThemeChange }) => {
|
||||
startIcon={<ArrowBackIcon />}
|
||||
sx={{ mb: 4, textTransform: 'none', borderRadius: 2 }}
|
||||
>
|
||||
Back to Home
|
||||
{t('common.backToHome')}
|
||||
</Button>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, mb: 4, mt: 4 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, mb: 6 }}>
|
||||
<Avatar src={avatar || undefined} sx={{ width: 80, height: 80, border: '3px solid', borderColor: 'primary.main' }}>
|
||||
{!avatar && username.charAt(0).toUpperCase()}
|
||||
</Avatar>
|
||||
<Box>
|
||||
<Typography variant="h4" sx={{ fontWeight: 'bold' }}>{username}</Typography>
|
||||
<Typography variant="body2" color="text.secondary">Manage your account and preferences</Typography>
|
||||
<Typography variant="body2" color="text.secondary">{t('settings.subtitle')}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{message && (
|
||||
<Alert severity={message.type} sx={{ mb: 3 }} onClose={() => setMessage(null)}>
|
||||
<Alert severity={message.type} sx={{ mb: 4 }} onClose={() => setMessage(null)}>
|
||||
{message.text}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Paper sx={{
|
||||
p: 3, mb: 4,
|
||||
'& .MuiInputLabel-root': { bgcolor: 'background.paper', px: 0.5 }
|
||||
}}>
|
||||
<Typography variant="h6" gutterBottom>Appearance & Language</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 2, flexDirection: { xs: 'column', sm: 'row' } }}>
|
||||
<FormControl fullWidth margin="normal">
|
||||
<InputLabel>Theme</InputLabel>
|
||||
<Select
|
||||
value={themeName}
|
||||
label="Theme"
|
||||
onChange={(e) => onThemeChange(e.target.value as CatppuccinTheme)}
|
||||
>
|
||||
<ListSubheader>Light Themes</ListSubheader>
|
||||
<MenuItem value="latte">Latte</MenuItem>
|
||||
<MenuItem value="pink">Pastel Pink</MenuItem>
|
||||
<MenuItem value="solarized_light">Solarized Light</MenuItem>
|
||||
<MenuItem value="one_light">One Light</MenuItem>
|
||||
<MenuItem value="paper">Paper White</MenuItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListSubheader>Dark Themes</ListSubheader>
|
||||
<MenuItem value="mocha">Mocha (Default)</MenuItem>
|
||||
<MenuItem value="frappe">Frappé</MenuItem>
|
||||
<MenuItem value="macchiato">Macchiato</MenuItem>
|
||||
<MenuItem value="atom">Atom One Dark</MenuItem>
|
||||
<MenuItem value="dracula">Dracula</MenuItem>
|
||||
<MenuItem value="gruvbox">Gruvbox Dark</MenuItem>
|
||||
<MenuItem value="nord">Nord</MenuItem>
|
||||
<MenuItem value="cyberpunk">Cyberpunk</MenuItem>
|
||||
<MenuItem value="forest">Forest Dark</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Grid container spacing={4}>
|
||||
{/* Left Column: Visuals & Language */}
|
||||
<Grid size={{ xs: 12, md: 5 }}>
|
||||
<Paper sx={{ p: 3, mb: 4, borderRadius: 3, '& .MuiInputLabel-root': { bgcolor: 'background.paper', px: 0.5 } }}>
|
||||
<Typography variant="h6" gutterBottom sx={{ fontWeight: 'bold' }}>{t('settings.appearanceTitle')}</Typography>
|
||||
<FormControl fullWidth margin="normal">
|
||||
<InputLabel>{t('settings.themeLabel')}</InputLabel>
|
||||
<Select
|
||||
value={themeName}
|
||||
label={t('settings.themeLabel')}
|
||||
onChange={(e) => onThemeChange(e.target.value as CatppuccinTheme)}
|
||||
>
|
||||
<ListSubheader>Light Themes</ListSubheader>
|
||||
<MenuItem value="latte">Latte</MenuItem>
|
||||
<MenuItem value="pink">Pastel Pink</MenuItem>
|
||||
<MenuItem value="solarized_light">Solarized Light</MenuItem>
|
||||
<MenuItem value="one_light">One Light</MenuItem>
|
||||
<MenuItem value="paper">Paper White</MenuItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListSubheader>Dark Themes</ListSubheader>
|
||||
<MenuItem value="mocha">Mocha (Default)</MenuItem>
|
||||
<MenuItem value="frappe">Frappé</MenuItem>
|
||||
<MenuItem value="macchiato">Macchiato</MenuItem>
|
||||
<MenuItem value="atom">Atom One Dark</MenuItem>
|
||||
<MenuItem value="dracula">Dracula</MenuItem>
|
||||
<MenuItem value="gruvbox">Gruvbox Dark</MenuItem>
|
||||
<MenuItem value="nord">Nord</MenuItem>
|
||||
<MenuItem value="cyberpunk">Cyberpunk</MenuItem>
|
||||
<MenuItem value="forest">Forest Dark</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControl fullWidth margin="normal">
|
||||
<InputLabel>Language</InputLabel>
|
||||
<Select
|
||||
value={language}
|
||||
label="Language"
|
||||
onChange={(e) => handleLanguageChange(e.target.value)}
|
||||
>
|
||||
<MenuItem value="en">English</MenuItem>
|
||||
<MenuItem value="de">Deutsch</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
</Paper>
|
||||
<FormControl fullWidth margin="normal">
|
||||
<InputLabel>{t('settings.langLabel')}</InputLabel>
|
||||
<Select
|
||||
value={language}
|
||||
label={t('settings.langLabel')}
|
||||
onChange={(e) => handleLanguageChange(e.target.value)}
|
||||
>
|
||||
<MenuItem value="en">English</MenuItem>
|
||||
<MenuItem value="de">Deutsch</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Paper>
|
||||
|
||||
<Paper sx={{
|
||||
p: 3, mb: 4,
|
||||
'& .MuiInputLabel-root': { bgcolor: 'background.paper', px: 0.5 }
|
||||
}}>
|
||||
<Typography variant="h6" gutterBottom>Profile Picture</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, mb: 2 }}>
|
||||
<Avatar src={avatar || undefined} sx={{ width: 100, height: 100 }}>
|
||||
{!avatar && username.charAt(0).toUpperCase()}
|
||||
</Avatar>
|
||||
<Box>
|
||||
<Button variant="outlined" component="label">
|
||||
Upload New Avatar
|
||||
<input type="file" hidden accept="image/*" onChange={handleAvatarUpload} />
|
||||
</Button>
|
||||
<Typography variant="caption" display="block" sx={{ mt: 1 }} color="text.secondary">
|
||||
Max size: 2MB. JPG, PNG or WEBP.
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
<Paper sx={{
|
||||
p: 3, mb: 4,
|
||||
'& .MuiInputLabel-root': { bgcolor: 'background.paper', px: 0.5 }
|
||||
}}>
|
||||
<Typography variant="h6" gutterBottom>Profile Information</Typography>
|
||||
<form onSubmit={handleUpdateProfile}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Username"
|
||||
margin="normal"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Email"
|
||||
margin="normal"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
helperText={email !== currentEmail ? "Change will require confirmation" : ""}
|
||||
/>
|
||||
<Button variant="contained" type="submit" sx={{ mt: 2 }}>
|
||||
Update Profile
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
{pendingConfirmation && (
|
||||
<Box sx={{ mt: 4, pt: 3, borderTop: '1px solid rgba(0,0,0,0.1)' }}>
|
||||
<Typography variant="h6" color="primary" gutterBottom>Confirm Email Change</Typography>
|
||||
<Typography variant="body2" sx={{ mb: 2 }}>Enter the 6-digit code sent to {email}</Typography>
|
||||
<form onSubmit={handleConfirmEmail}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Confirmation Code"
|
||||
margin="normal"
|
||||
value={confirmationCode}
|
||||
onChange={(e) => setConfirmationCode(e.target.value)}
|
||||
/>
|
||||
<Button variant="contained" type="submit" sx={{ mt: 1 }} color="secondary">
|
||||
Verify & Update Email
|
||||
</Button>
|
||||
</form>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
<Paper sx={{
|
||||
p: 3,
|
||||
'& .MuiInputLabel-root': { bgcolor: 'background.paper', px: 0.5 }
|
||||
}}>
|
||||
<Typography variant="h6" gutterBottom>Change Password</Typography>
|
||||
<form onSubmit={handleChangePassword}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Old Password"
|
||||
type="password"
|
||||
margin="normal"
|
||||
value={oldPassword}
|
||||
onChange={(e) => setOldPassword(e.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="New Password"
|
||||
type="password"
|
||||
margin="normal"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
/>
|
||||
<Button variant="contained" type="submit" sx={{ mt: 2 }} color="warning">
|
||||
Change Password
|
||||
</Button>
|
||||
</form>
|
||||
</Paper>
|
||||
|
||||
<Paper sx={{
|
||||
p: 3, mt: 4,
|
||||
'& .MuiInputLabel-root': { bgcolor: 'background.paper', px: 0.5 }
|
||||
}}>
|
||||
<Typography variant="h6" gutterBottom color="error" sx={{ fontWeight: 'bold' }}>Danger Zone</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||
Permanently delete your account and all associated data. This action is irreversible.
|
||||
</Typography>
|
||||
|
||||
{!isDeleting ? (
|
||||
<Button variant="outlined" color="error" onClick={handleRequestDeletion}>
|
||||
Delete My Account
|
||||
</Button>
|
||||
) : (
|
||||
<form onSubmit={handleConfirmDeletion}>
|
||||
<Typography variant="subtitle2" gutterBottom sx={{ mt: 2 }}>Enter verification code:</Typography>
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
label="Deletion Code"
|
||||
value={deletionCode}
|
||||
onChange={(e) => setDeletionCode(e.target.value)}
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button variant="contained" color="error" type="submit" disabled={!deletionCode}>
|
||||
Confirm Permanent Deletion
|
||||
</Button>
|
||||
<Button onClick={() => setIsDeleting(false)}>Cancel</Button>
|
||||
<Paper sx={{ p: 3, borderRadius: 3 }}>
|
||||
<Typography variant="h6" gutterBottom sx={{ fontWeight: 'bold' }}>{t('settings.avatarTitle')}</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, my: 2 }}>
|
||||
<Avatar src={avatar || undefined} sx={{ width: 100, height: 100 }}>
|
||||
{!avatar && username.charAt(0).toUpperCase()}
|
||||
</Avatar>
|
||||
<Box>
|
||||
<Button variant="outlined" component="label" size="small">
|
||||
{t('settings.avatarButton')}
|
||||
<input type="file" hidden accept="image/*" onChange={handleAvatarUpload} />
|
||||
</Button>
|
||||
<Typography variant="caption" display="block" sx={{ mt: 1 }} color="text.secondary">
|
||||
{t('settings.avatarHint')}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</form>
|
||||
)}
|
||||
</Paper>
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
{/* Right Column: Account Info & Security */}
|
||||
<Grid size={{ xs: 12, md: 7 }}>
|
||||
<Paper sx={{ p: 3, mb: 4, borderRadius: 3, '& .MuiInputLabel-root': { bgcolor: 'background.paper', px: 0.5 } }}>
|
||||
<Typography variant="h6" gutterBottom sx={{ fontWeight: 'bold' }}>{t('settings.infoTitle')}</Typography>
|
||||
<form onSubmit={handleUpdateProfile}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('settings.usernameLabel')}
|
||||
margin="normal"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('settings.emailLabel')}
|
||||
margin="normal"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
helperText={email !== currentEmail ? t('settings.emailHint') : ""}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
<Button variant="contained" type="submit" sx={{ mt: 2, borderRadius: 2 }}>
|
||||
{t('settings.updateButton')}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
{pendingConfirmation && (
|
||||
<Box sx={{ mt: 4, pt: 3, borderTop: '1px solid', borderColor: 'divider' }}>
|
||||
<Typography variant="h6" color="primary" gutterBottom>{t('settings.confirmEmailTitle')}</Typography>
|
||||
<Typography variant="body2" sx={{ mb: 2 }}>{t('settings.confirmEmailHint', { email })}</Typography>
|
||||
<form onSubmit={handleConfirmEmail}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('settings.deletionCodeLabel')}
|
||||
margin="normal"
|
||||
value={confirmationCode}
|
||||
onChange={(e) => setConfirmationCode(e.target.value)}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
<Button variant="contained" type="submit" sx={{ mt: 1, borderRadius: 2 }} color="secondary">
|
||||
{t('settings.confirmButton')}
|
||||
</Button>
|
||||
</form>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
<Paper sx={{ p: 3, mb: 4, borderRadius: 3, '& .MuiInputLabel-root': { bgcolor: 'background.paper', px: 0.5 } }}>
|
||||
<Typography variant="h6" gutterBottom sx={{ fontWeight: 'bold' }}>{t('settings.passwordTitle')}</Typography>
|
||||
<form onSubmit={handleChangePassword}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('settings.oldPasswordLabel')}
|
||||
type="password"
|
||||
margin="normal"
|
||||
value={oldPassword}
|
||||
onChange={(e) => setOldPassword(e.target.value)}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('settings.newPasswordLabel')}
|
||||
type="password"
|
||||
margin="normal"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
<Button variant="contained" type="submit" sx={{ mt: 2, borderRadius: 2 }} color="warning">
|
||||
{t('settings.passwordButton')}
|
||||
</Button>
|
||||
</form>
|
||||
</Paper>
|
||||
|
||||
<Paper sx={{ p: 3, borderRadius: 3, border: '1px solid', borderColor: 'error.main', bgcolor: (theme) => theme.palette.mode === 'dark' ? 'rgba(255,0,0,0.05)' : 'rgba(255,0,0,0.02)', '& .MuiInputLabel-root': { bgcolor: 'background.paper', px: 0.5 } }}>
|
||||
<Typography variant="h6" gutterBottom color="error" sx={{ fontWeight: 'bold' }}>{t('settings.dangerTitle')}</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||
{t('settings.dangerDesc')}
|
||||
</Typography>
|
||||
|
||||
{!isDeleting ? (
|
||||
<Button variant="outlined" color="error" onClick={handleRequestDeletion} sx={{ borderRadius: 2 }}>
|
||||
{t('settings.deleteButton')}
|
||||
</Button>
|
||||
) : (
|
||||
<form onSubmit={handleConfirmDeletion}>
|
||||
<Typography variant="subtitle2" gutterBottom sx={{ mt: 2 }}>{t('settings.deletionCodeHint')}</Typography>
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
label={t('settings.deletionCodeLabel')}
|
||||
value={deletionCode}
|
||||
onChange={(e) => setDeletionCode(e.target.value)}
|
||||
sx={{ mb: 2 }}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button variant="contained" color="error" type="submit" disabled={!deletionCode} sx={{ borderRadius: 2 }}>
|
||||
{t('settings.confirmDeleteButton')}
|
||||
</Button>
|
||||
<Button onClick={() => setIsDeleting(false)} sx={{ borderRadius: 2 }}>{t('settings.cancelButton')}</Button>
|
||||
</Box>
|
||||
</form>
|
||||
)}
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useParams, Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Typography,
|
||||
Box,
|
||||
@@ -51,6 +52,7 @@ interface Flavor {
|
||||
}
|
||||
|
||||
const FlavorDetail: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [flavor, setFlavor] = useState<Flavor | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -88,7 +90,7 @@ const FlavorDetail: React.FC = () => {
|
||||
fetchFlavor();
|
||||
}, [id]);
|
||||
|
||||
useTitle(flavor?.name || 'Flavor Detail');
|
||||
useTitle(flavor?.name || t('common.loading'));
|
||||
|
||||
const handleRatingSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -156,7 +158,7 @@ const FlavorDetail: React.FC = () => {
|
||||
startIcon={<ArrowBackIcon />}
|
||||
sx={{ mb: 2, textTransform: 'none', borderRadius: 2 }}
|
||||
>
|
||||
Back to {flavor.category_name}
|
||||
{t('common.backTo')} {flavor.category_name}
|
||||
</Button>
|
||||
|
||||
<Card sx={{ display: 'flex', flexDirection: { xs: 'column', md: 'row' }, mb: 4, overflow: 'hidden', borderRadius: { xs: 2, md: 4 } }}>
|
||||
@@ -212,7 +214,7 @@ const FlavorDetail: React.FC = () => {
|
||||
|
||||
{flavor.shop_url && (
|
||||
<Button variant="contained" size="large" component="a" href={flavor.shop_url} target="_blank" fullWidth sx={{ maxWidth: { md: 200 }, borderRadius: 2 }}>
|
||||
Buy Now
|
||||
{t('common.buyNow')}
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
@@ -248,11 +250,11 @@ const FlavorDetail: React.FC = () => {
|
||||
)}
|
||||
|
||||
<Typography variant="h4" gutterBottom sx={{ mt: 6, mb: 3, fontWeight: 'bold', fontSize: { xs: '1.75rem', md: '2.125rem' } }}>
|
||||
Community Reviews
|
||||
{t('home.communityVoice')}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||
{flavor.ratings.length === 0 ? (
|
||||
<Typography color="text.secondary">No reviews yet. Be the first to rate it!</Typography>
|
||||
<Typography color="text.secondary">{t('dashboard.noRatings')}</Typography>
|
||||
) : (
|
||||
flavor.ratings.map((rating: Rating) => (
|
||||
<Card key={rating.id} variant="outlined" sx={{ borderRadius: 2 }}>
|
||||
|
||||
Reference in new issue
Block a user