updated email verification and page layout

This commit is contained in:
nvrl committed 2026-02-15 21:22:35 +01:00
1 parent 2befe3eb7d
commit 0ed8fc9ac0
9 files changed
+619 -106

No files matched your search

@@ -0,0 +1,23 @@
# Generated by Django 6.0.2 on 2026-02-15 19:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0003_flavor_shop_url'),
]
operations = [
migrations.AddField(
model_name='user',
name='email_confirmation_code',
field=models.CharField(blank=True, max_length=6, null=True),
),
migrations.AddField(
model_name='user',
name='pending_email',
field=models.EmailField(blank=True, max_length=254, null=True),
),
]
+2
View File
@@ -10,6 +10,8 @@ class User(AbstractUser):
('mocha', 'Mocha'),
]
theme = models.CharField(max_length=20, choices=THEME_CHOICES, default='mocha')
pending_email = models.EmailField(max_length=254, blank=True, null=True)
email_confirmation_code = models.CharField(max_length=6, blank=True, null=True)
def __str__(self):
return self.username
+135 -12
View File
@@ -41,35 +41,100 @@ class UserViewSet(viewsets.ModelViewSet):
permission_classes = [permissions.IsAuthenticated]
def get_permissions(self):
if self.action == 'signup':
if self.action in ['signup', 'verify_signup', 'request_password_reset', 'complete_password_reset']:
return [permissions.AllowAny()]
return super().get_permissions()
@action(detail=False, methods=['post'])
@action(detail=False, methods=['post'], permission_classes=[permissions.AllowAny])
def signup(self, request):
username = request.data.get('username')
email = request.data.get('email')
if not username or not email:
return Response({'error': 'Username and email required'}, status=status.HTTP_400_BAD_REQUEST)
password = request.data.get('password')
if not username or not email or not password:
return Response({'error': 'Username, email and password required'}, status=status.HTTP_400_BAD_REQUEST)
if User.objects.filter(username=username).exists():
return Response({'error': 'Username already exists'}, status=status.HTTP_400_BAD_REQUEST)
if User.objects.filter(email=email).exists():
return Response({'error': 'Email already exists'}, status=status.HTTP_400_BAD_REQUEST)
# Create user with random password
import secrets
password = secrets.token_urlsafe(10)
user = User.objects.create_user(username=username, email=email, password=password)
import string
code = ''.join(secrets.choice(string.digits) for _ in range(6))
user = User.objects.create_user(
username=username,
email=email,
password=password,
is_active=False,
email_confirmation_code=code
)
# Send "Password Reset" / Welcome Email
send_mail(
'Welcome to Holy Flavors!',
f'Hi {username},\n\nYour account has been created. Your temporary password is: {password}\n\nPlease login and change it.',
'from@holyflavors.com',
'Verify your Holy Flavors account',
f'Hi {username},\n\nYour verification code is: {code}',
'noreply@holyflavors.com',
[email],
fail_silently=False,
)
return Response({'status': 'User created, check console/email for temporary password'}, status=status.HTTP_201_CREATED)
return Response({'status': 'User created, please verify your email'}, status=status.HTTP_201_CREATED)
@action(detail=False, methods=['post'], permission_classes=[permissions.AllowAny])
def verify_signup(self, request):
username = request.data.get('username')
code = request.data.get('code')
try:
user = User.objects.get(username=username, is_active=False)
except User.DoesNotExist:
return Response({'error': 'User not found or already active'}, status=status.HTTP_404_NOT_FOUND)
if user.email_confirmation_code == code:
user.is_active = True
user.email_confirmation_code = None
user.save()
return Response({'status': 'Account verified successfully!'})
else:
return Response({'error': 'Invalid verification code'}, status=status.HTTP_400_BAD_REQUEST)
@action(detail=False, methods=['post'], permission_classes=[permissions.AllowAny])
def request_password_reset(self, request):
email = request.data.get('email')
try:
user = User.objects.get(email=email, is_active=True)
except User.DoesNotExist:
return Response({'status': 'If an account exists with this email, a reset code has been sent.'})
import secrets
import string
code = ''.join(secrets.choice(string.digits) for _ in range(6))
user.email_confirmation_code = code
user.save()
send_mail(
'Password Reset Request',
f'Hi {user.username},\n\nYour password reset code is: {code}',
'noreply@holyflavors.com',
[email],
fail_silently=False,
)
return Response({'status': 'If an account exists with this email, a reset code has been sent.'})
@action(detail=False, methods=['post'], permission_classes=[permissions.AllowAny])
def complete_password_reset(self, request):
email = request.data.get('email')
code = request.data.get('code')
new_password = request.data.get('password')
try:
user = User.objects.get(email=email, email_confirmation_code=code)
except User.DoesNotExist:
return Response({'error': 'Invalid email or code'}, status=status.HTTP_400_BAD_REQUEST)
user.set_password(new_password)
user.email_confirmation_code = None
user.save()
return Response({'status': 'Password reset successful!'})
@action(detail=False, methods=['get'])
def me(self, request):
@@ -86,6 +151,64 @@ class UserViewSet(viewsets.ModelViewSet):
return Response({'status': 'theme updated', 'theme': theme})
return Response({'error': 'Invalid theme'}, status=status.HTTP_400_BAD_REQUEST)
@action(detail=False, methods=['post'])
def change_password(self, request):
user = request.user
old_password = request.data.get('old_password')
new_password = request.data.get('new_password')
if not user.check_password(old_password):
return Response({'error': 'Wrong old password'}, status=status.HTTP_400_BAD_REQUEST)
user.set_password(new_password)
user.save()
return Response({'status': 'password changed'})
@action(detail=False, methods=['patch'])
def update_profile(self, request):
user = request.user
username = request.data.get('username')
email = request.data.get('email')
message = ""
if username and username != user.username:
if User.objects.filter(username=username).exists():
return Response({'error': 'Username already taken'}, status=status.HTTP_400_BAD_REQUEST)
user.username = username
if email and email != user.email:
import secrets
import string
code = ''.join(secrets.choice(string.digits) for _ in range(6))
user.pending_email = email
user.email_confirmation_code = code
send_mail(
'Confirm your new email',
f'Hi {user.username},\n\nYour confirmation code is: {code}',
'noreply@holyflavors.com',
[email],
fail_silently=False,
)
message = " Confirmation code sent to new email."
user.save()
serializer = self.get_serializer(user)
data = serializer.data
if message:
data['message'] = message
return Response(data)
@action(detail=False, methods=['post'])
def confirm_email(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.email = user.pending_email
user.pending_email = None
user.email_confirmation_code = None
user.save()
return Response({'status': 'Email confirmed', 'email': user.email})
@action(detail=False, methods=['get'])
def dashboard(self, request):
user = request.user
+108 -32
View File
@@ -11,20 +11,32 @@ import {
Box,
IconButton,
Menu,
MenuItem
MenuItem,
Drawer,
Divider,
List,
ListItem,
ListItemButton,
ListItemText,
useMediaQuery
} from '@mui/material';
import PaletteIcon from '@mui/icons-material/Palette';
import MenuIcon from '@mui/icons-material/Menu';
import { getTheme, type CatppuccinTheme } from './theme';
import CategoryList from './pages/CategoryList';
import CategoryFlavors from './pages/categories/CategoryFlavors';
import Dashboard from './pages/Dashboard';
import Login from './pages/Login';
import Settings from './pages/Settings';
import api from './api';
const App: React.FC = () => {
const [themeName, setThemeName] = useState<CatppuccinTheme>((localStorage.getItem('theme') as CatppuccinTheme) || 'mocha');
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [user, setUser] = useState<{username: string} | null>(null);
const [loadingUser, setLoadingUser] = useState(true);
const [drawerOpen, setDrawerOpen] = useState(false);
const isMobile = useMediaQuery('(max-width:600px)');
useEffect(() => {
const fetchUser = async () => {
@@ -36,10 +48,14 @@ const App: React.FC = () => {
}
} catch (err) {
setUser(null);
} finally {
setLoadingUser(false);
}
};
if (localStorage.getItem('token')) {
fetchUser();
} else {
setLoadingUser(false);
}
}, []);
@@ -64,49 +80,109 @@ const App: React.FC = () => {
window.location.href = '/';
};
const navItems = user ? [
{ label: 'Categories', path: '/' },
{ label: 'Dashboard', path: '/dashboard' },
{ label: 'Settings', path: '/settings' },
] : [
{ label: 'Categories', path: '/' },
{ label: 'Login', path: '/login' },
];
const drawer = (
<Box onClick={() => setDrawerOpen(false)} sx={{ textAlign: 'center' }}>
<Typography variant="h6" sx={{ my: 2 }}>Holy Flavors</Typography>
<Divider />
<List>
{navItems.map((item) => (
<ListItem key={item.label} disablePadding>
<ListItemButton component={Link} to={item.path} sx={{ textAlign: 'center' }}>
<ListItemText primary={item.label} />
</ListItemButton>
</ListItem>
))}
{user && (
<ListItem disablePadding>
<ListItemButton onClick={handleLogout} sx={{ textAlign: 'center' }}>
<ListItemText primary="Logout" />
</ListItemButton>
</ListItem>
)}
</List>
</Box>
);
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<Router>
<AppBar position="static">
<Toolbar>
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
<Link to="/" style={{ color: 'inherit', textDecoration: 'none' }}>Holy Flavors</Link>
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Button color="inherit" component={Link} to="/">Categories</Button>
{user ? (
<>
<Button color="inherit" component={Link} to="/dashboard">Dashboard</Button>
<Button color="inherit" onClick={handleLogout}>Logout ({user.username})</Button>
</>
) : (
<Button color="inherit" component={Link} to="/login">Login</Button>
<AppBar position="sticky" elevation={1}>
<Container maxWidth={false} sx={{ px: { xs: 2, sm: 4, md: 6 } }}>
<Toolbar disableGutters sx={{ minHeight: { xs: 56, sm: 64 } }}>
{isMobile && (
<IconButton
color="inherit"
aria-label="open drawer"
edge="start"
onClick={() => setDrawerOpen(true)}
sx={{ mr: 2 }}
>
<MenuIcon />
</IconButton>
)}
<Typography variant="h6" component="div" sx={{ flexGrow: 1, fontWeight: 'bold' }}>
<Link to="/" style={{ color: 'inherit', textDecoration: 'none' }}>Holy Flavors</Link>
</Typography>
<IconButton color="inherit" onClick={(e) => setAnchorEl(e.currentTarget)}>
<PaletteIcon />
</IconButton>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={() => setAnchorEl(null)}
>
<MenuItem onClick={() => handleThemeChange('latte')}>Latte</MenuItem>
<MenuItem onClick={() => handleThemeChange('frappe')}>Frappé</MenuItem>
<MenuItem onClick={() => handleThemeChange('macchiato')}>Macchiato</MenuItem>
<MenuItem onClick={() => handleThemeChange('mocha')}>Mocha</MenuItem>
</Menu>
</Box>
</Toolbar>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
{!isMobile && !loadingUser && (
<Box sx={{ display: 'flex', gap: 1 }}>
{navItems.map((item) => (
<Button key={item.label} color="inherit" component={Link} to={item.path}>{item.label}</Button>
))}
{user && (
<Button color="inherit" onClick={handleLogout} sx={{ ml: 1 }}>Logout</Button>
)}
</Box>
)}
<IconButton color="inherit" onClick={(e) => setAnchorEl(e.currentTarget)} sx={{ ml: 1 }}>
<PaletteIcon />
</IconButton>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={() => setAnchorEl(null)}
>
<MenuItem onClick={() => handleThemeChange('latte')}>Latte</MenuItem>
<MenuItem onClick={() => handleThemeChange('frappe')}>Frappé</MenuItem>
<MenuItem onClick={() => handleThemeChange('macchiato')}>Macchiato</MenuItem>
<MenuItem onClick={() => handleThemeChange('mocha')}>Mocha</MenuItem>
</Menu>
</Box>
</Toolbar>
</Container>
</AppBar>
<Container sx={{ mt: 4 }}>
<Drawer
variant="temporary"
open={drawerOpen}
onClose={() => setDrawerOpen(false)}
ModalProps={{ keepMounted: true }}
sx={{
display: { xs: 'block', sm: 'none' },
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: 240 },
}}
>
{drawer}
</Drawer>
<Container maxWidth={false} sx={{ mt: 4, pb: 4, px: { xs: 2, sm: 4, md: 6 } }}>
<Routes>
<Route path="/" element={<CategoryList />} />
<Route path="/category/:slug" element={<CategoryFlavors />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/login" element={<Login />} />
</Routes>
</Container>
+1 -1
View File
@@ -66,7 +66,7 @@ const CategoryList: React.FC = () => {
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 3 }}>
{filteredCategories.map(category => (
<Box key={category.id} sx={{ flex: '1 1 45%', minWidth: 280 }}>
<Box key={category.id} sx={{ flex: { xs: '1 1 100%', sm: '1 1 45%', lg: '1 1 23%' }, minWidth: 280 }}>
<Link to={`/category/${category.slug}`} style={{ textDecoration: 'none' }}>
<Card sx={{
height: 150,
+2 -2
View File
@@ -36,7 +36,7 @@ const Dashboard: React.FC = () => {
<Box>
<Typography variant="h4" gutterBottom>Personal Dashboard</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
<Box sx={{ flex: '1 1 45%', minWidth: 300 }}>
<Box sx={{ flex: { xs: '1 1 100%', md: '1 1 45%' }, minWidth: 300 }}>
<Typography variant="h5" color="secondary" gutterBottom>My Ratings ({data.rated_count})</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{data.my_ratings.map((rating: any) => (
@@ -66,7 +66,7 @@ const Dashboard: React.FC = () => {
))}
</Box>
</Box>
<Box sx={{ flex: '1 1 45%', minWidth: 300 }}>
<Box sx={{ flex: { xs: '1 1 100%', md: '1 1 45%' }, minWidth: 300 }}>
<Typography variant="h5" color="primary" gutterBottom>Missing Flavors ({data.missing_count})</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{data.missing_flavors.map((flavor: any) => (
+187 -58
View File
@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { Typography, TextField, Button, Box, Paper, Tab, Tabs } from '@mui/material';
import { Typography, TextField, Button, Box, Paper, Tab, Tabs, Alert } from '@mui/material';
import api from '../api';
import { useTitle } from '../hooks/useTitle';
@@ -12,9 +12,15 @@ const Login: React.FC<LoginProps> = () => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [email, setEmail] = useState('');
const [verificationCode, setVerificationCode] = useState('');
const [isVerifying, setIsVerifying] = useState(false);
const [isResetting, setIsResetting] = useState(false);
const [resetStep, setResetStep] = useState(1); // 1: email, 2: code/new pass
const [message, setMessage] = useState<{type: 'success' | 'error', text: string} | null>(null);
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setMessage(null);
try {
const res = await api.post('token/', {
username,
@@ -22,76 +28,199 @@ const Login: React.FC<LoginProps> = () => {
});
localStorage.setItem('token', res.data.token);
window.location.href = '/';
} catch (err) {
alert('Login failed');
} catch (err: any) {
setMessage({ type: 'error', text: 'Login failed. Please check your credentials or verify your account.' });
}
};
const handleSignup = async (e: React.FormEvent) => {
e.preventDefault();
setMessage(null);
try {
await api.post('users/signup/', { username, email });
alert('Signup request sent! Check your email (or the backend console) for your temporary password.');
setTab(0);
await api.post('users/signup/', { username, email, password });
setIsVerifying(true);
setMessage({ type: 'success', text: 'Signup successful! Please enter the verification code sent to your email.' });
} catch (err: any) {
alert(err.response?.data?.error || 'Signup failed');
setMessage({ type: 'error', text: err.response?.data?.error || 'Signup failed' });
}
};
const handleVerify = async (e: React.FormEvent) => {
e.preventDefault();
setMessage(null);
try {
await api.post('users/verify_signup/', { username, code: verificationCode });
setMessage({ type: 'success', text: 'Account verified! You can now login.' });
setIsVerifying(false);
setTab(0);
setPassword('');
} catch (err: any) {
setMessage({ type: 'error', text: err.response?.data?.error || 'Verification failed' });
}
};
const handleRequestReset = async (e: React.FormEvent) => {
e.preventDefault();
setMessage(null);
try {
await api.post('users/request_password_reset/', { email });
setResetStep(2);
setMessage({ type: 'success', text: 'If an account exists, a reset code has been sent.' });
} catch (err: any) {
setMessage({ type: 'error', text: 'An error occurred. Please try again later.' });
}
};
const handleCompleteReset = async (e: React.FormEvent) => {
e.preventDefault();
setMessage(null);
try {
await api.post('users/complete_password_reset/', { email, code: verificationCode, password });
setMessage({ type: 'success', text: 'Password reset successful! You can now login.' });
setIsResetting(false);
setResetStep(1);
setTab(0);
setPassword('');
setVerificationCode('');
} catch (err: any) {
setMessage({ type: 'error', text: err.response?.data?.error || 'Reset failed' });
}
};
const renderContent = () => {
if (isVerifying) {
return (
<form onSubmit={handleVerify}>
<Typography variant="h6" gutterBottom>Verify Account</Typography>
<Typography variant="body2" sx={{ mb: 2 }}>Enter the code sent to {email}</Typography>
<TextField
fullWidth
label="Verification Code"
margin="normal"
value={verificationCode}
onChange={(e) => setVerificationCode(e.target.value)}
/>
<Button fullWidth variant="contained" type="submit" sx={{ mt: 2 }}>
Verify
</Button>
<Button fullWidth onClick={() => setIsVerifying(false)} sx={{ mt: 1 }}>
Back to Signup
</Button>
</form>
);
}
if (isResetting) {
return (
<form onSubmit={resetStep === 1 ? handleRequestReset : handleCompleteReset}>
<Typography variant="h6" gutterBottom>Reset Password</Typography>
<TextField
fullWidth
label="Email"
margin="normal"
value={email}
onChange={(e) => setEmail(e.target.value)}
disabled={resetStep === 2}
/>
{resetStep === 2 && (
<>
<TextField
fullWidth
label="Reset Code"
margin="normal"
value={verificationCode}
onChange={(e) => setVerificationCode(e.target.value)}
/>
<TextField
fullWidth
label="New Password"
type="password"
margin="normal"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</>
)}
<Button fullWidth variant="contained" type="submit" sx={{ mt: 2 }}>
{resetStep === 1 ? 'Send Reset Code' : 'Reset Password'}
</Button>
<Button fullWidth onClick={() => { setIsResetting(false); setResetStep(1); setMessage(null); }} sx={{ mt: 1 }}>
Back to Login
</Button>
</form>
);
}
return (
<>
<Tabs value={tab} onChange={(_, v) => { setTab(v); setMessage(null); }} sx={{ mb: 2 }}>
<Tab label="Login" />
<Tab label="Signup" />
</Tabs>
{tab === 0 ? (
<form onSubmit={handleLogin}>
<TextField
fullWidth
label="Username"
margin="normal"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<TextField
fullWidth
label="Password"
type="password"
margin="normal"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<Button fullWidth variant="contained" type="submit" sx={{ mt: 2 }}>
Login
</Button>
<Button fullWidth onClick={() => { setIsResetting(true); setMessage(null); }} sx={{ mt: 1, textTransform: 'none' }} size="small">
Forgot Password?
</Button>
</form>
) : (
<form onSubmit={handleSignup}>
<TextField
fullWidth
label="Username"
margin="normal"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<TextField
fullWidth
label="Email"
type="email"
margin="normal"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<TextField
fullWidth
label="Password"
type="password"
margin="normal"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<Button fullWidth variant="contained" type="submit" sx={{ mt: 2 }}>
Signup
</Button>
</form>
)}
</>
);
};
return (
<Box sx={{ maxWidth: 400, mx: 'auto', mt: 8 }}>
<Paper sx={{ p: 3 }}>
<Tabs value={tab} onChange={(_, v) => setTab(v)} sx={{ mb: 2 }}>
<Tab label="Login" />
<Tab label="Signup" />
</Tabs>
{tab === 0 ? (
<form onSubmit={handleLogin}>
<TextField
fullWidth
label="Username"
margin="normal"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<TextField
fullWidth
label="Password"
type="password"
margin="normal"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<Button fullWidth variant="contained" type="submit" sx={{ mt: 2 }}>
Login
</Button>
</form>
) : (
<form onSubmit={handleSignup}>
<TextField
fullWidth
label="Username"
margin="normal"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<TextField
fullWidth
label="Email"
type="email"
margin="normal"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<Typography variant="caption" color="text.secondary">
A password reset link will be sent to your email to set your initial password.
</Typography>
<Button fullWidth variant="contained" type="submit" sx={{ mt: 2 }}>
Signup
</Button>
</form>
)}
{message && <Alert severity={message.type} sx={{ mb: 2 }}>{message.text}</Alert>}
{renderContent()}
</Paper>
<Box sx={{ mt: 2, textAlign: 'center' }}>
<Typography variant="body2" color="text.secondary">
+160
View File
@@ -0,0 +1,160 @@
import React, { useState, useEffect } from 'react';
import {
Typography,
Box,
TextField,
Button,
Paper,
Alert
} from '@mui/material';
import api from '../api';
import { useTitle } from '../hooks/useTitle';
const Settings: React.FC = () => {
useTitle('Account Settings');
const [username, setUsername] = useState('');
const [email, setEmail] = useState('');
const [currentEmail, setCurrentEmail] = useState('');
const [confirmationCode, setConfirmationCode] = useState('');
const [pendingConfirmation, setPendingConfirmation] = useState(false);
const [oldPassword, setOldPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);
useEffect(() => {
const fetchUser = async () => {
try {
const res = await api.get('users/me/');
setUsername(res.data.username);
setEmail(res.data.email);
setCurrentEmail(res.data.email);
} catch (err) {
console.error(err);
}
};
fetchUser();
}, []);
const handleUpdateProfile = async (e: React.FormEvent) => {
e.preventDefault();
try {
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}` });
} else {
setMessage({ type: 'success', text: 'Profile updated successfully!' });
setCurrentEmail(email);
}
} catch (err: any) {
setMessage({ type: 'error', text: err.response?.data?.error || 'Failed to update profile' });
}
};
const handleConfirmEmail = async (e: React.FormEvent) => {
e.preventDefault();
try {
const res = await api.post('users/confirm_email/', { code: confirmationCode });
setMessage({ type: 'success', text: 'Email confirmed and updated!' });
setCurrentEmail(res.data.email);
setPendingConfirmation(false);
setConfirmationCode('');
} catch (err: any) {
setMessage({ type: 'error', text: err.response?.data?.error || 'Confirmation failed' });
}
};
const handleChangePassword = async (e: React.FormEvent) => {
e.preventDefault();
try {
await api.post('users/change_password/', { old_password: oldPassword, new_password: newPassword });
setMessage({ type: 'success', text: 'Password changed successfully!' });
setOldPassword('');
setNewPassword('');
} catch (err: any) {
setMessage({ type: 'error', text: err.response?.data?.error || 'Failed to change password' });
}
};
return (
<Box sx={{ maxWidth: 600, mx: 'auto' }}>
<Typography variant="h4" gutterBottom>Account Settings</Typography>
{message && (
<Alert severity={message.type} sx={{ mb: 3 }} onClose={() => setMessage(null)}>
{message.text}
</Alert>
)}
<Paper sx={{ p: 3, mb: 4 }}>
<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 }}>
<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>
</Box>
);
};
export default Settings;
@@ -107,7 +107,7 @@ const CategoryFlavors: React.FC = () => {
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 3 }}>
{filteredFlavors.map(flavor => (
<Box sx={{ flex: '1 1 30%', minWidth: 280 }} key={flavor.id}>
<Box sx={{ flex: { xs: '1 1 100%', sm: '1 1 45%', md: '1 1 30%', lg: '1 1 23%', xl: '1 1 18%' }, minWidth: 280 }} key={flavor.id}>
<Card sx={{ height: '100%', display: 'flex', flexDirection: 'column', position: 'relative' }}>
{!flavor.is_available && (
<Box sx={{ position: 'absolute', top: 10, right: 10, bgcolor: 'error.main', color: 'white', px: 1, borderRadius: 1, fontSize: '0.75rem', zIndex: 1 }}>