mirror of
https://github.com/narrrl/holy-flavor-rater.git
synced 2026-11-03 16:41:27 +01:00
added delete option to user settings and updated privacy
This commit is contained in:
1 parent
8d5d20577c
commit
3046103d48
12 files changed
+588
-61
No files matched your search
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 6.0.2 on 2026-02-17 02:16
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('api', '0012_user_following'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='language',
|
||||
field=models.CharField(choices=[('en', 'English'), ('de', 'Deutsch')], default='en', max_length=2),
|
||||
),
|
||||
]
|
||||
@@ -19,7 +19,12 @@ class User(AbstractUser):
|
||||
('cyberpunk', 'Cyberpunk'),
|
||||
('forest', 'Forest Dark'),
|
||||
]
|
||||
LANGUAGE_CHOICES = [
|
||||
('en', 'English'),
|
||||
('de', 'Deutsch'),
|
||||
]
|
||||
theme = models.CharField(max_length=20, choices=THEME_CHOICES, default='mocha')
|
||||
language = models.CharField(max_length=2, choices=LANGUAGE_CHOICES, default='en')
|
||||
avatar = models.ImageField(upload_to='avatars/', blank=True, null=True)
|
||||
pending_email = models.EmailField(max_length=254, blank=True, null=True)
|
||||
email_confirmation_code = models.CharField(max_length=6, blank=True, null=True)
|
||||
|
||||
@@ -9,7 +9,7 @@ class UserSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['id', 'username', 'email', 'theme', 'avatar', 'following_count', 'followers_count', 'is_following']
|
||||
fields = ['id', 'username', 'email', 'theme', 'language', 'avatar', 'following_count', 'followers_count', 'is_following']
|
||||
|
||||
def get_avatar(self, obj):
|
||||
if obj.avatar:
|
||||
|
||||
+42
-3
@@ -242,14 +242,23 @@ class UserViewSet(viewsets.ModelViewSet):
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=False, methods=['patch'])
|
||||
def update_theme(self, request):
|
||||
def update_preferences(self, request):
|
||||
user = request.user
|
||||
theme = request.data.get('theme')
|
||||
language = request.data.get('language')
|
||||
|
||||
updated = False
|
||||
if theme in dict(User.THEME_CHOICES):
|
||||
user.theme = theme
|
||||
updated = True
|
||||
if language in dict(User.LANGUAGE_CHOICES):
|
||||
user.language = language
|
||||
updated = True
|
||||
|
||||
if updated:
|
||||
user.save()
|
||||
return Response({'status': 'theme updated', 'theme': theme})
|
||||
return Response({'error': 'Invalid theme'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response({'status': 'preferences updated', 'theme': user.theme, 'language': user.language})
|
||||
return Response({'error': 'No valid updates provided'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@action(detail=False, methods=['post'])
|
||||
def update_avatar(self, request):
|
||||
@@ -380,6 +389,36 @@ class UserViewSet(viewsets.ModelViewSet):
|
||||
user.save()
|
||||
return Response({'status': 'Password reset successful!'})
|
||||
|
||||
@action(detail=False, methods=['post'])
|
||||
@method_decorator(ratelimit(key='user', rate='3/h', method='POST', block=True))
|
||||
def request_account_deletion(self, request):
|
||||
import secrets
|
||||
import string
|
||||
code = ''.join(secrets.choice(string.digits) for _ in range(6))
|
||||
|
||||
user = request.user
|
||||
user.email_confirmation_code = code
|
||||
user.save()
|
||||
|
||||
send_mail(
|
||||
'Confirm Account Deletion - Holy Flavors Archive',
|
||||
f'Hi {user.username},\n\nYou requested to delete your account. This action is permanent.\n\nYour deletion code is: {code}',
|
||||
'noreply@holyflavors.com',
|
||||
[user.email],
|
||||
fail_silently=False,
|
||||
)
|
||||
return Response({'status': 'Code sent to your email'})
|
||||
|
||||
@action(detail=False, methods=['post'])
|
||||
def confirm_account_deletion(self, request):
|
||||
user = request.user
|
||||
code = request.data.get('code')
|
||||
if not user.email_confirmation_code or code != user.email_confirmation_code:
|
||||
return Response({'error': 'Invalid or expired code'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
user.delete()
|
||||
return Response({'status': 'Account deleted'})
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def dashboard(self, request):
|
||||
user = request.user
|
||||
|
||||
Generated
+88
-1
@@ -14,8 +14,11 @@
|
||||
"@mui/icons-material": "^7.3.8",
|
||||
"@mui/material": "^7.3.8",
|
||||
"axios": "^1.13.5",
|
||||
"i18next": "^24.2.2",
|
||||
"i18next-browser-languagedetector": "^8.0.2",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-i18next": "^15.4.0",
|
||||
"react-router-dom": "^7.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -3197,6 +3200,55 @@
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/html-parse-stringify": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
||||
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"void-elements": "3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "24.2.3",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.3.tgz",
|
||||
"integrity": "sha512-lfbf80OzkocvX7nmZtu7nSTNbrTYR52sLWxPtlXX1zAhVw8WEnFk4puUkCR4B1dNQwbSpEHHHemcZu//7EcB7A==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com/i18next.html"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/i18next-browser-languagedetector": {
|
||||
"version": "8.2.1",
|
||||
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.1.tgz",
|
||||
"integrity": "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.2"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
@@ -3748,6 +3800,32 @@
|
||||
"react": "^19.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "15.7.4",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.7.4.tgz",
|
||||
"integrity": "sha512-nyU8iKNrI5uDJch0z9+Y5XEr34b0wkyYj3Rp+tfbahxtlswxSCjcUL9H0nqXo9IR3/t5Y5PKIA3fx3MfUyR9Xw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"html-parse-stringify": "^3.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"i18next": ">= 23.4.0",
|
||||
"react": ">= 16.8.0",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
},
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "19.2.4",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz",
|
||||
@@ -4047,7 +4125,7 @@
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@@ -4204,6 +4282,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/void-elements": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
@@ -16,8 +16,11 @@
|
||||
"@mui/icons-material": "^7.3.8",
|
||||
"@mui/material": "^7.3.8",
|
||||
"axios": "^1.13.5",
|
||||
"i18next": "^24.2.2",
|
||||
"i18next-browser-languagedetector": "^8.0.2",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-i18next": "^15.4.0",
|
||||
"react-router-dom": "^7.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
+18
-9
@@ -33,6 +33,9 @@ import ExpandLess from '@mui/icons-material/ExpandLess';
|
||||
import ExpandMore from '@mui/icons-material/ExpandMore';
|
||||
import { getTheme, type CatppuccinTheme } from './theme';
|
||||
import api from './api';
|
||||
import './i18n';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Footer from './components/Footer';
|
||||
|
||||
// Code splitting
|
||||
const MainPage = lazy(() => import('./pages/MainPage'));
|
||||
@@ -42,6 +45,7 @@ const Dashboard = lazy(() => import('./pages/Dashboard'));
|
||||
const PublicProfile = lazy(() => import('./pages/PublicProfile'));
|
||||
const CommunityFeed = lazy(() => import('./pages/CommunityFeed'));
|
||||
const About = lazy(() => import('./pages/About'));
|
||||
const Privacy = lazy(() => import('./pages/Privacy'));
|
||||
const Login = lazy(() => import('./pages/Login'));
|
||||
const Settings = lazy(() => import('./pages/Settings'));
|
||||
|
||||
@@ -159,6 +163,7 @@ const GlobalSearch = () => {
|
||||
};
|
||||
|
||||
const App: React.FC = () => {
|
||||
const { i18n, t } = useTranslation();
|
||||
const [themeName, setThemeName] = useState<CatppuccinTheme>((localStorage.getItem('theme') as CatppuccinTheme) || 'mocha');
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [catAnchorEl, setCatAnchorEl] = useState<null | HTMLElement>(null);
|
||||
@@ -181,6 +186,7 @@ const App: React.FC = () => {
|
||||
if (userRes.data && userRes.data.username) {
|
||||
setUser(userRes.data);
|
||||
if (userRes.data.theme) setThemeName(userRes.data.theme);
|
||||
if (userRes.data.language) i18n.changeLanguage(userRes.data.language);
|
||||
|
||||
// Also fetch following list for the mobile sidebar
|
||||
try {
|
||||
@@ -223,6 +229,7 @@ const App: React.FC = () => {
|
||||
localStorage.removeItem('token');
|
||||
setUser(null);
|
||||
setFollowing([]);
|
||||
i18n.changeLanguage(navigator.language.split('-')[0] || 'en');
|
||||
window.location.href = '/';
|
||||
};
|
||||
|
||||
@@ -243,19 +250,19 @@ const App: React.FC = () => {
|
||||
<List>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton component={Link} to="/" onClick={() => setDrawerOpen(false)}>
|
||||
<ListItemText primary="Home" />
|
||||
<ListItemText primary={t('nav.home')} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton component={Link} to="/community" onClick={() => setDrawerOpen(false)}>
|
||||
<ListItemText primary="Community" />
|
||||
<ListItemText primary={t('nav.community')} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton component={Link} to="/about" onClick={() => setDrawerOpen(false)}>
|
||||
<ListItemText primary="About" />
|
||||
<ListItemText primary={t('nav.about')} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
|
||||
@@ -285,12 +292,12 @@ const App: React.FC = () => {
|
||||
<>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton component={Link} to="/dashboard" onClick={() => setDrawerOpen(false)}>
|
||||
<ListItemText primary="Dashboard" />
|
||||
<ListItemText primary={t('nav.dashboard')} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton component={Link} to="/settings" onClick={() => setDrawerOpen(false)}>
|
||||
<ListItemText primary="Account Settings" />
|
||||
<ListItemText primary={t('nav.settings')} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
|
||||
@@ -315,14 +322,14 @@ const App: React.FC = () => {
|
||||
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton onClick={handleLogout}>
|
||||
<ListItemText primary="Logout" sx={{ color: 'error.main' }} />
|
||||
<ListItemText primary={t('nav.logout')} sx={{ color: 'error.main' }} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
</>
|
||||
) : (
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton component={Link} to="/login" onClick={() => setDrawerOpen(false)}>
|
||||
<ListItemText primary="Login / Signup" />
|
||||
<ListItemText primary={t('nav.login')} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
)}
|
||||
@@ -356,7 +363,7 @@ const App: React.FC = () => {
|
||||
to="/community"
|
||||
sx={{ fontWeight: 'bold', textTransform: 'none', fontSize: '1rem', mr: 2 }}
|
||||
>
|
||||
Community
|
||||
{t('nav.community')}
|
||||
</Button>
|
||||
<Button
|
||||
color="inherit"
|
||||
@@ -364,7 +371,7 @@ const App: React.FC = () => {
|
||||
to="/about"
|
||||
sx={{ fontWeight: 'bold', textTransform: 'none', fontSize: '1rem', mr: 2 }}
|
||||
>
|
||||
About
|
||||
{t('nav.about')}
|
||||
</Button>
|
||||
<Button
|
||||
color="inherit"
|
||||
@@ -457,6 +464,7 @@ const App: React.FC = () => {
|
||||
<Route path="/profile/:username" element={<PublicProfile />} />
|
||||
<Route path="/community" element={<CommunityFeed />} />
|
||||
<Route path="/about" element={<About />} />
|
||||
<Route path="/privacy" element={<Privacy />} />
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/settings" element={<Settings themeName={themeName} onThemeChange={handleThemeChange} />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
@@ -464,6 +472,7 @@ const App: React.FC = () => {
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</Box>
|
||||
<Footer />
|
||||
</Box>
|
||||
</Router>
|
||||
</ThemeProvider>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import { Box, Container, Typography, Link as MuiLink, Divider } from '@mui/material';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const Footer: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
return (
|
||||
<Box component="footer" sx={{ bgcolor: 'background.paper', pt: 6, pb: 4, mt: 'auto', borderTop: '1px solid', borderColor: 'divider' }}>
|
||||
<Container maxWidth="lg">
|
||||
<Box sx={{ display: 'flex', flexDirection: { xs: 'column', sm: 'row' }, justifyContent: 'space-between', alignItems: 'center', gap: 2, mb: 2 }}>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ fontWeight: 'bold' }}>
|
||||
© {year} Holy Flavors Archive
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 3 }}>
|
||||
<MuiLink component={Link} to="/about" color="inherit" sx={{ fontSize: '0.8rem', textDecoration: 'none', '&:hover': { textDecoration: 'underline' } }}>
|
||||
Impressum
|
||||
</MuiLink>
|
||||
<MuiLink component={Link} to="/privacy" color="inherit" sx={{ fontSize: '0.8rem', textDecoration: 'none', '&:hover': { textDecoration: 'underline' } }}>
|
||||
Privacy Policy
|
||||
</MuiLink>
|
||||
</Box>
|
||||
</Box>
|
||||
<Divider sx={{ my: 2, opacity: 0.5 }} />
|
||||
<Typography variant="caption" color="text.secondary" align="center" display="block">
|
||||
{t('nav.about')} • Fan Project
|
||||
</Typography>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
@@ -0,0 +1,109 @@
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
|
||||
const resources = {
|
||||
en: {
|
||||
translation: {
|
||||
"nav": {
|
||||
"home": "Home",
|
||||
"community": "Community",
|
||||
"about": "About",
|
||||
"dashboard": "Dashboard",
|
||||
"settings": "Settings",
|
||||
"logout": "Logout",
|
||||
"login": "Login / Signup"
|
||||
},
|
||||
"hero": {
|
||||
"title": "The Ultimate Holy Archive",
|
||||
"subtitle": "Browse every flavor ever released, discover new favorites, and share your ratings with the community.",
|
||||
"join": "Join Community",
|
||||
"whatIsThis": "What is this for?"
|
||||
},
|
||||
"home": {
|
||||
"activity": "Community Activity",
|
||||
"activitySubtitle": "Latest reviews from people you follow",
|
||||
"viewFullFeed": "View Full Feed",
|
||||
"quietFeed": "Your feed is a bit quiet",
|
||||
"followMore": "Follow more users to see their activity here!",
|
||||
"exploreFlavors": "Explore Flavors",
|
||||
"hallOfFame": "Hall of Fame",
|
||||
"communityVoice": "Community Voice",
|
||||
"newArrivals": "New Arrivals",
|
||||
"viewAllReviews": "View All Reviews",
|
||||
"viewMoreReviews": "View more reviews",
|
||||
"rated": "rated"
|
||||
},
|
||||
"common": {
|
||||
"search": "Search...",
|
||||
"backToArchive": "Back to Archive",
|
||||
"backToHome": "Back to Home",
|
||||
"backTo": "Back to",
|
||||
"buyNow": "Buy Now",
|
||||
"reviews": "reviews",
|
||||
"rank": "Rank",
|
||||
"loading": "Loading..."
|
||||
}
|
||||
}
|
||||
},
|
||||
de: {
|
||||
translation: {
|
||||
"nav": {
|
||||
"home": "Startseite",
|
||||
"community": "Community",
|
||||
"about": "Über uns",
|
||||
"dashboard": "Dashboard",
|
||||
"settings": "Einstellungen",
|
||||
"logout": "Abmelden",
|
||||
"login": "Anmelden / Registrieren"
|
||||
},
|
||||
"hero": {
|
||||
"title": "Das ultimative Holy-Archiv",
|
||||
"subtitle": "Durchsuche jede jemals veröffentlichte Sorte, entdecke neue Favoriten und teile deine Bewertungen mit der Community.",
|
||||
"join": "Community beitreten",
|
||||
"whatIsThis": "Was ist das?"
|
||||
},
|
||||
"home": {
|
||||
"activity": "Community-Aktivität",
|
||||
"activitySubtitle": "Neueste Bewertungen von Personen, denen du folgst",
|
||||
"viewFullFeed": "Vollständiger Feed",
|
||||
"quietFeed": "Dein Feed ist noch etwas ruhig",
|
||||
"followMore": "Folge mehr Benutzern, um deren Aktivitäten hier zu sehen!",
|
||||
"exploreFlavors": "Sorten entdecken",
|
||||
"hallOfFame": "Hall of Fame",
|
||||
"communityVoice": "Community-Stimmen",
|
||||
"newArrivals": "Neuheiten",
|
||||
"viewAllReviews": "Alle Bewertungen ansehen",
|
||||
"viewMoreReviews": "Mehr Bewertungen",
|
||||
"rated": "bewertete"
|
||||
},
|
||||
"common": {
|
||||
"search": "Suchen...",
|
||||
"backToArchive": "Zurück zum Archiv",
|
||||
"backToHome": "Zurück zur Startseite",
|
||||
"backTo": "Zurück zu",
|
||||
"buyNow": "Jetzt kaufen",
|
||||
"reviews": "Bewertungen",
|
||||
"rank": "Rang",
|
||||
"loading": "Wird geladen..."
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources,
|
||||
fallbackLng: 'en',
|
||||
interpolation: {
|
||||
escapeValue: false
|
||||
},
|
||||
detection: {
|
||||
order: ['localStorage', 'navigator'],
|
||||
caches: ['localStorage']
|
||||
}
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
useMediaQuery
|
||||
} from '@mui/material';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import StarIcon from '@mui/icons-material/Star';
|
||||
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
|
||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
||||
@@ -46,6 +47,7 @@ interface Review {
|
||||
}
|
||||
|
||||
const MainPage: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const isMobileSize = useMediaQuery('(max-width:600px)');
|
||||
const [topFlavors, setTopFlavors] = useState<Flavor[]>([]);
|
||||
const [newestFlavors, setNewestFlavors] = useState<Flavor[]>([]);
|
||||
@@ -218,19 +220,19 @@ const MainPage: React.FC = () => {
|
||||
<Container maxWidth="lg">
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', mb: 4, flexWrap: 'wrap', gap: 2 }}>
|
||||
<Box>
|
||||
<Typography variant="h3" sx={{ fontWeight: 'bold', mb: 1, fontSize: { xs: '2rem', md: '3rem' } }}>Community Activity</Typography>
|
||||
<Typography variant="body1" color="text.secondary">Latest reviews from people you follow</Typography>
|
||||
<Typography variant="h3" sx={{ fontWeight: 'bold', mb: 1, fontSize: { xs: '2rem', md: '3rem' } }}>{t('home.activity')}</Typography>
|
||||
<Typography variant="body1" color="text.secondary">{t('home.activitySubtitle')}</Typography>
|
||||
</Box>
|
||||
<Button component={Link} to="/community" variant="outlined" sx={{ borderRadius: 2, textTransform: 'none' }}>
|
||||
View Full Feed
|
||||
{t('home.viewFullFeed')}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{feedRatings.length === 0 ? (
|
||||
<Paper sx={{ p: 4, textAlign: 'center', borderRadius: 4, bgcolor: 'action.hover', border: '2px dashed', borderColor: 'divider', elevation: 0 }}>
|
||||
<Typography variant="h6">Your feed is a bit quiet</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>Follow more users to see their activity here!</Typography>
|
||||
<Button variant="contained" component={Link} to="/" size="small" sx={{ borderRadius: 2 }}>Explore Flavors</Button>
|
||||
<Typography variant="h6">{t('home.quietFeed')}</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>{t('home.followMore')}</Typography>
|
||||
<Button variant="contained" component={Link} to="/" size="small" sx={{ borderRadius: 2 }}>{t('home.exploreFlavors')}</Button>
|
||||
</Paper>
|
||||
) : (
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', sm: 'repeat(2, 1fr)', lg: 'repeat(3, 1fr)' }, gap: 3 }}>
|
||||
@@ -300,17 +302,17 @@ const MainPage: React.FC = () => {
|
||||
}}
|
||||
>
|
||||
<Typography variant="h2" component="h1" gutterBottom sx={{ fontWeight: 'bold', fontSize: { xs: '2.2rem', sm: '3rem', md: '4.5rem' }, overflowWrap: 'break-word', px: 2 }}>
|
||||
The Ultimate Holy Archive
|
||||
{t('hero.title')}
|
||||
</Typography>
|
||||
<Typography variant="h5" color="text.secondary" sx={{ mb: 4, maxWidth: 800, mx: 'auto', px: 2 }}>
|
||||
Browse every flavor ever released, discover new favorites, and share your ratings with the community.
|
||||
{t('hero.subtitle')}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', gap: 2, flexWrap: 'wrap', px: 2 }}>
|
||||
<Button variant="contained" size="large" component={Link} to="/login" sx={{ px: 4, py: 1.5, borderRadius: 2 }}>
|
||||
Join Community
|
||||
{t('hero.join')}
|
||||
</Button>
|
||||
<Button variant="outlined" size="large" component={Link} to="/about" sx={{ px: 4, py: 1.5, borderRadius: 2 }}>
|
||||
What is this for?
|
||||
{t('hero.whatIsThis')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
@@ -325,7 +327,7 @@ const MainPage: React.FC = () => {
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 4, justifyContent: 'space-between' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<StarIcon color="primary" sx={{ mr: 1, fontSize: '2rem' }} />
|
||||
<Typography variant="h4" sx={{ fontWeight: 'bold' }}>Hall of Fame</Typography>
|
||||
<Typography variant="h4" sx={{ fontWeight: 'bold' }}>{t('home.hallOfFame')}</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<IconButton onClick={handlePrev} sx={{ border: '1px solid', borderColor: 'divider' }}><ChevronLeftIcon /></IconButton>
|
||||
@@ -362,8 +364,9 @@ const MainPage: React.FC = () => {
|
||||
loading="lazy"
|
||||
/>
|
||||
</Box>
|
||||
<CardContent sx={{ flex: 1, p: { xs: 3, sm: 4, md: 6 }, display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
|
||||
<Chip label={`Rank #${activeIndex + 1}`} color="primary" size="small" sx={{ width: 'fit-content', mb: 1, fontWeight: 'bold' }} />
|
||||
<CardContent sx={{ flex: 1, p: { xs: 3, sm: 4, md: 6 }, display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
|
||||
<Chip label={`${t('common.rank')} #${activeIndex + 1}`} color="primary" size="small" sx={{ width: 'fit-content', mb: 1, fontWeight: 'bold' }} />
|
||||
|
||||
<Typography variant="h3" gutterBottom sx={{ fontWeight: 'bold', fontSize: { xs: '1.75rem', sm: '2.5rem', md: '3rem' } }}>
|
||||
{currentTop.name}
|
||||
</Typography>
|
||||
@@ -395,7 +398,7 @@ const MainPage: React.FC = () => {
|
||||
to={`/flavor/${currentTop.id}`}
|
||||
sx={{ mt: { xs: 3, md: 4 }, width: { xs: '100%', sm: 'fit-content' }, px: 4, py: 1.5, borderRadius: 2 }}
|
||||
>
|
||||
View All Reviews
|
||||
{t('home.viewAllReviews')}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -423,7 +426,7 @@ const MainPage: React.FC = () => {
|
||||
{/* Recent Reviews - SIDE BY SIDE */}
|
||||
{recentReviews.length > 0 && (
|
||||
<Box sx={{ flex: { xs: '1 1 100%', xl: '1 1 35%' }, minWidth: 0 }}>
|
||||
<Typography variant="h4" sx={{ mb: 4, fontWeight: 'bold' }}>Community Voice</Typography>
|
||||
<Typography variant="h4" sx={{ mb: 4, fontWeight: 'bold' }}>{t('home.communityVoice')}</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
{recentReviews.slice(0, 5).map(review => (
|
||||
<Paper
|
||||
@@ -457,7 +460,7 @@ const MainPage: React.FC = () => {
|
||||
</Paper>
|
||||
))}
|
||||
<Button component={Link} to="/community" variant="text" size="small" sx={{ alignSelf: 'center', mt: 1 }}>
|
||||
View more reviews
|
||||
{t('home.viewMoreReviews')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -467,7 +470,7 @@ const MainPage: React.FC = () => {
|
||||
{/* Newest Arrivals */}
|
||||
{newestFlavors.length > 0 && (
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="h4" sx={{ mb: 4, fontWeight: 'bold' }}>New Arrivals</Typography>
|
||||
<Typography variant="h4" sx={{ mb: 4, fontWeight: 'bold' }}>{t('home.newArrivals')}</Typography>
|
||||
<Box sx={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: {
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import React from 'react';
|
||||
import { Typography, Box, Container, Paper, Button, ToggleButton, ToggleButtonGroup } from '@mui/material';
|
||||
import { Link } from 'react-router-dom';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import { useTitle } from '../hooks/useTitle';
|
||||
|
||||
const Privacy: React.FC = () => {
|
||||
useTitle('Privacy Policy / Datenschutzerklärung');
|
||||
const [lang, setLang] = React.useState<'de' | 'en'>('de');
|
||||
|
||||
return (
|
||||
<Container maxWidth="md" sx={{ py: 8 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 4, flexWrap: 'wrap', gap: 2 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
component={Link}
|
||||
to="/"
|
||||
startIcon={<ArrowBackIcon />}
|
||||
sx={{ textTransform: 'none', borderRadius: 2 }}
|
||||
>
|
||||
{lang === 'de' ? 'Zurück' : 'Back'}
|
||||
</Button>
|
||||
|
||||
<ToggleButtonGroup
|
||||
value={lang}
|
||||
exclusive
|
||||
onChange={(_, v) => v && setLang(v)}
|
||||
size="small"
|
||||
>
|
||||
<ToggleButton value="de">Deutsch</ToggleButton>
|
||||
<ToggleButton value="en">English</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Box>
|
||||
|
||||
<Paper sx={{ p: { xs: 3, md: 6 }, borderRadius: 4 }}>
|
||||
{lang === 'de' ? (
|
||||
<Box>
|
||||
<Typography variant="h3" gutterBottom sx={{ fontWeight: 'bold' }}>Datenschutzerklärung</Typography>
|
||||
<Typography variant="body1" paragraph>
|
||||
Der Schutz Ihrer persönlichen Daten ist uns ein wichtiges Anliegen. Nachfolgend informieren wir Sie
|
||||
über die Verarbeitung Ihrer Daten auf unserer Webseite gemäß den Anforderungen der DSGVO.
|
||||
</Typography>
|
||||
|
||||
<Typography variant="h5" gutterBottom sx={{ mt: 4, fontWeight: 'bold' }}>1. Verantwortlicher</Typography>
|
||||
<Typography variant="body2" paragraph>
|
||||
Verantwortlich für die Datenverarbeitung auf dieser Webseite ist:<br />
|
||||
{import.meta.env.VITE_IMPRESSUM_NAME || '[Ihr Name]'}<br />
|
||||
{import.meta.env.VITE_IMPRESSUM_STREET || '[Ihre Straße]'}<br />
|
||||
{import.meta.env.VITE_IMPRESSUM_CITY || '[Ihre Stadt]'}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="h5" gutterBottom sx={{ mt: 4, fontWeight: 'bold' }}>2. Erfassung und Speicherung personenbezogener Daten</Typography>
|
||||
<Typography variant="body2" paragraph>
|
||||
<strong>Registrierung:</strong> Wenn Sie ein Konto erstellen, speichern wir Ihren Benutzernamen,
|
||||
Ihre E-Mail-Adresse und Ihr Passwort (verschlüsselt). Diese Daten werden zur Bereitstellung der
|
||||
Community-Funktionen (Bewertungen, Folgen von Nutzern) verwendet. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO.
|
||||
</Typography>
|
||||
<Typography variant="body2" paragraph>
|
||||
<strong>Öffentliches Profil:</strong> Ihr Benutzername, Ihr Avatar und Ihre Bewertungen sind für
|
||||
andere Nutzer öffentlich sichtbar. Sie können diese Daten jederzeit in den Einstellungen ändern oder löschen.
|
||||
</Typography>
|
||||
<Typography variant="body2" paragraph>
|
||||
<strong>Kontolöschung:</strong> Sie können Ihr Konto und alle damit verbundenen Daten jederzeit
|
||||
in den Kontoeinstellungen dauerhaft löschen. Zur Sicherheit ist hierfür eine Verifizierung per E-Mail-Code erforderlich.
|
||||
</Typography>
|
||||
|
||||
<Typography variant="h5" gutterBottom sx={{ mt: 4, fontWeight: 'bold' }}>3. Datenübermittlung an Dritte</Typography>
|
||||
<Typography variant="body2" paragraph>
|
||||
Wir geben Ihre Daten nicht ohne Ihre ausdrückliche Einwilligung an Dritte weiter, es sei denn,
|
||||
dies ist zur Vertragserfüllung oder zur Erfüllung gesetzlicher Verpflichtungen erforderlich.
|
||||
</Typography>
|
||||
|
||||
<Typography variant="h5" gutterBottom sx={{ mt: 4, fontWeight: 'bold' }}>4. Ihre Rechte</Typography>
|
||||
<Typography variant="body2" paragraph>
|
||||
Sie haben das Recht auf Auskunft (Art. 15 DSGVO), Berichtigung (Art. 16 DSGVO), Löschung (Art. 17 DSGVO)
|
||||
und Einschränkung der Verarbeitung Ihrer Daten. Bitte kontaktieren Sie uns hierzu unter:
|
||||
{import.meta.env.VITE_IMPRESSUM_EMAIL}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<Box>
|
||||
<Typography variant="h3" gutterBottom sx={{ fontWeight: 'bold' }}>Privacy Policy</Typography>
|
||||
<Typography variant="body1" paragraph>
|
||||
Protecting your personal data is a top priority for us. Below we inform you about the processing
|
||||
of your data on our website in accordance with the requirements of the GDPR.
|
||||
</Typography>
|
||||
|
||||
<Typography variant="h5" gutterBottom sx={{ mt: 4, fontWeight: 'bold' }}>1. Controller</Typography>
|
||||
<Typography variant="body2" paragraph>
|
||||
The party responsible for data processing on this website is:<br />
|
||||
{import.meta.env.VITE_IMPRESSUM_NAME || '[Your Name]'}<br />
|
||||
{import.meta.env.VITE_IMPRESSUM_STREET || '[Your Street]'}<br />
|
||||
{import.meta.env.VITE_IMPRESSUM_CITY || '[Your City]'}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="h5" gutterBottom sx={{ mt: 4, fontWeight: 'bold' }}>2. Collection and Storage of Personal Data</Typography>
|
||||
<Typography variant="body2" paragraph>
|
||||
<strong>Registration:</strong> When you create an account, we store your username, email address,
|
||||
and password (encrypted). This data is used to provide community features (ratings, following users).
|
||||
The legal basis is Art. 6 Para. 1 lit. b GDPR.
|
||||
</Typography>
|
||||
<Typography variant="body2" paragraph>
|
||||
<strong>Public Profile:</strong> Your username, avatar, and ratings are publicly visible to other users.
|
||||
You can change or delete this data at any time in your settings.
|
||||
</Typography>
|
||||
<Typography variant="body2" paragraph>
|
||||
<strong>Account Deletion:</strong> You can permanently delete your account and all associated data
|
||||
at any time in your account settings. For security reasons, this requires verification via an email code.
|
||||
</Typography>
|
||||
|
||||
<Typography variant="h5" gutterBottom sx={{ mt: 4, fontWeight: 'bold' }}>3. Data Transfer to Third Parties</Typography>
|
||||
<Typography variant="body2" paragraph>
|
||||
We do not pass your data on to third parties without your express consent, unless this is necessary
|
||||
to fulfill a contract or a legal obligation.
|
||||
</Typography>
|
||||
|
||||
<Typography variant="h5" gutterBottom sx={{ mt: 4, fontWeight: 'bold' }}>4. Your Rights</Typography>
|
||||
<Typography variant="body2" paragraph>
|
||||
You have the right to information (Art. 15 GDPR), correction (Art. 16 GDPR), deletion (Art. 17 GDPR),
|
||||
and restriction of the processing of your data. Please contact us at:
|
||||
{import.meta.env.VITE_IMPRESSUM_EMAIL}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default Privacy;
|
||||
+118
-30
@@ -18,6 +18,7 @@ import {
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import { Link } from 'react-router-dom';
|
||||
import api from '../api';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useTitle } from '../hooks/useTitle';
|
||||
import type { CatppuccinTheme } from '../theme';
|
||||
|
||||
@@ -27,15 +28,19 @@ interface SettingsProps {
|
||||
}
|
||||
|
||||
const Settings: React.FC<SettingsProps> = ({ themeName, onThemeChange }) => {
|
||||
useTitle('Account Settings');
|
||||
const { t, i18n } = useTranslation();
|
||||
useTitle(t('nav.settings'));
|
||||
const [username, setUsername] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [currentEmail, setCurrentEmail] = useState('');
|
||||
const [avatar, setAvatar] = useState<string | null>(null);
|
||||
const [language, setLanguage] = useState(i18n.language.split('-')[0]);
|
||||
const [confirmationCode, setConfirmationCode] = useState('');
|
||||
const [pendingConfirmation, setPendingConfirmation] = useState(false);
|
||||
const [oldPassword, setOldPassword] = useState('');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [deletionCode, setDeletionCode] = useState('');
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -46,6 +51,9 @@ const Settings: React.FC<SettingsProps> = ({ themeName, onThemeChange }) => {
|
||||
setEmail(res.data.email);
|
||||
setCurrentEmail(res.data.email);
|
||||
setAvatar(res.data.avatar);
|
||||
if (res.data.language) {
|
||||
setLanguage(res.data.language);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
@@ -76,6 +84,16 @@ const Settings: React.FC<SettingsProps> = ({ themeName, onThemeChange }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleLanguageChange = async (newLang: string) => {
|
||||
setLanguage(newLang);
|
||||
i18n.changeLanguage(newLang);
|
||||
try {
|
||||
await api.patch('users/update_preferences/', { language: newLang });
|
||||
} catch (err) {
|
||||
console.error('Failed to update language on server');
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateProfile = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
@@ -117,6 +135,28 @@ const Settings: React.FC<SettingsProps> = ({ themeName, onThemeChange }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleRequestDeletion = async () => {
|
||||
try {
|
||||
await api.post('users/request_account_deletion/');
|
||||
setIsDeleting(true);
|
||||
setMessage({ type: 'success', text: 'Verification code sent to your email.' });
|
||||
} catch (err: any) {
|
||||
setMessage({ type: 'error', text: 'Failed to request deletion.' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmDeletion = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!window.confirm('Are you absolutely sure? This cannot be undone.')) return;
|
||||
try {
|
||||
await api.post('users/confirm_account_deletion/', { code: deletionCode });
|
||||
localStorage.removeItem('token');
|
||||
window.location.href = '/';
|
||||
} catch (err: any) {
|
||||
setMessage({ type: 'error', text: 'Invalid code. Deletion failed.' });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Container maxWidth="sm" sx={{ py: 4 }}>
|
||||
<Button
|
||||
@@ -149,35 +189,49 @@ const Settings: React.FC<SettingsProps> = ({ themeName, onThemeChange }) => {
|
||||
p: 3, mb: 4,
|
||||
'& .MuiInputLabel-root': { bgcolor: 'background.paper', px: 0.5 }
|
||||
}}>
|
||||
<Typography variant="h6" gutterBottom>Appearance</Typography>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<Paper sx={{
|
||||
@@ -274,6 +328,40 @@ const Settings: React.FC<SettingsProps> = ({ themeName, onThemeChange }) => {
|
||||
</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>
|
||||
</Box>
|
||||
</form>
|
||||
)}
|
||||
</Paper>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user