mirror of
https://github.com/narrrl/holy-flavor-rater.git
synced 2026-11-03 16:41:27 +01:00
Merge branch 'main' into narl.io
This commit is contained in:
commit
31a4f1af40
10 files changed
+345
-33
No files matched your search
@@ -0,0 +1,62 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
frontend-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
working-directory: frontend
|
||||
|
||||
- name: Build Frontend
|
||||
run: npm run build
|
||||
working-directory: frontend
|
||||
|
||||
backend-check:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.14.3'
|
||||
cache: 'pip'
|
||||
cache-dependency-path: backend/requirements.txt
|
||||
|
||||
- name: Install Dependencies
|
||||
run: pip install -r requirements.txt
|
||||
working-directory: backend
|
||||
|
||||
- name: Django System Check
|
||||
run: python manage.py check
|
||||
working-directory: backend
|
||||
env:
|
||||
SECRET_KEY: "ci-test-key"
|
||||
DEBUG: "false"
|
||||
ALLOWED_HOSTS: "localhost"
|
||||
|
||||
- name: Check Migrations
|
||||
run: python manage.py makemigrations --check --dry-run
|
||||
working-directory: backend
|
||||
env:
|
||||
SECRET_KEY: "ci-test-key"
|
||||
DEBUG: "false"
|
||||
ALLOWED_HOSTS: "localhost"
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 6.0.2 on 2026-02-19 14:56
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('api', '0026_banner_is_enabled_user_selected_banner_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='notification',
|
||||
name='notification_type',
|
||||
field=models.CharField(choices=[('reply', 'Reply'), ('mention', 'Mention'), ('follow', 'Follow'), ('ticket_new', 'New Ticket'), ('ticket_reply', 'Ticket Reply'), ('profile_comment', 'Profile Comment')], max_length=20),
|
||||
),
|
||||
]
|
||||
@@ -128,6 +128,7 @@ class Notification(models.Model):
|
||||
TYPE_CHOICES = [
|
||||
('reply', 'Reply'),
|
||||
('mention', 'Mention'),
|
||||
('follow', 'Follow'),
|
||||
('ticket_new', 'New Ticket'),
|
||||
('ticket_reply', 'Ticket Reply'),
|
||||
('profile_comment', 'Profile Comment'),
|
||||
|
||||
+49
-3
@@ -307,10 +307,12 @@ class UserViewSet(viewsets.ModelViewSet):
|
||||
email_confirmation_code=code
|
||||
)
|
||||
|
||||
verification_link = f"{settings.FRONTEND_URL}/verify-email?username={username}&code={code}"
|
||||
|
||||
try:
|
||||
send_mail(
|
||||
'Verify your Holy Flavors account',
|
||||
f'Hi {username},\n\nYour verification code is: {code}',
|
||||
f'Hi {username},\n\nYour verification code is: {code}\n\nAlternatively, you can complete your registration by clicking the link below:\n{verification_link}\n\nWelcome to the archive!',
|
||||
settings.DEFAULT_FROM_EMAIL,
|
||||
[email],
|
||||
fail_silently=False,
|
||||
@@ -323,7 +325,42 @@ class UserViewSet(viewsets.ModelViewSet):
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
return Response({'status': 'User created, please verify your email'}, status=status.HTTP_201_CREATED)
|
||||
return Response({'status': 'User created, please verify your email', 'username': username, 'email': email}, status=status.HTTP_201_CREATED)
|
||||
|
||||
@action(detail=False, methods=['post'], permission_classes=[permissions.AllowAny])
|
||||
def resend_verification(self, request):
|
||||
username = request.data.get('username')
|
||||
if not username:
|
||||
return Response({'error': 'Username required'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
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)
|
||||
|
||||
import secrets
|
||||
import string
|
||||
code = ''.join(secrets.choice(string.digits) for _ in range(6))
|
||||
user.email_confirmation_code = code
|
||||
user.save()
|
||||
|
||||
verification_link = f"{settings.FRONTEND_URL}/verify-email?username={user.username}&code={code}"
|
||||
|
||||
try:
|
||||
send_mail(
|
||||
'Verify your Holy Flavors account',
|
||||
f'Hi {user.username},\n\nYour new verification code is: {code}\n\nAlternatively, you can complete your registration by clicking the link below:\n{verification_link}',
|
||||
settings.DEFAULT_FROM_EMAIL,
|
||||
[user.email],
|
||||
fail_silently=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return Response(
|
||||
{'error': f'Failed to send email: {e}'},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
return Response({'status': 'Verification code resent!'})
|
||||
|
||||
@action(detail=False, methods=['post'], permission_classes=[permissions.AllowAny])
|
||||
def verify_signup(self, request):
|
||||
@@ -516,7 +553,16 @@ class UserViewSet(viewsets.ModelViewSet):
|
||||
user_to_follow = self.get_object()
|
||||
if user_to_follow == request.user:
|
||||
return Response({'error': 'You cannot follow yourself'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
request.user.following.add(user_to_follow)
|
||||
|
||||
if not request.user.following.filter(pk=user_to_follow.pk).exists():
|
||||
request.user.following.add(user_to_follow)
|
||||
# Notify the user being followed
|
||||
Notification.objects.create(
|
||||
recipient=user_to_follow,
|
||||
actor=request.user,
|
||||
notification_type='follow'
|
||||
)
|
||||
|
||||
return Response({'status': 'followed'})
|
||||
|
||||
@action(detail=True, methods=['post'], permission_classes=[permissions.IsAuthenticated])
|
||||
|
||||
@@ -191,6 +191,8 @@ REST_FRAMEWORK = {
|
||||
CORS_ALLOWED_ORIGINS_STRING = os.environ.get('CORS_ALLOWED_ORIGINS', 'https://holy.narl.io,http://localhost:5173,http://127.0.0.1:5173')
|
||||
CORS_ALLOWED_ORIGINS = [origin.strip() for origin in CORS_ALLOWED_ORIGINS_STRING.split(',')]
|
||||
|
||||
FRONTEND_URL = os.environ.get('FRONTEND_URL', 'https://holy.narl.io').rstrip('/')
|
||||
|
||||
import os
|
||||
# ... (near top of file)
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ const Privacy = lazy(() => import('./pages/Privacy'));
|
||||
const Login = lazy(() => import('./pages/Login'));
|
||||
const Settings = lazy(() => import('./pages/Settings'));
|
||||
const Support = lazy(() => import('./pages/Support'));
|
||||
const VerifyEmail = lazy(() => import('./pages/VerifyEmail'));
|
||||
const AdminDashboard = lazy(() => import('./pages/AdminDashboard'));
|
||||
const AdminUserDetail = lazy(() => import('./pages/AdminUserDetail'));
|
||||
const AdminRatingDetail = lazy(() => import('./pages/AdminRatingDetail'));
|
||||
@@ -73,7 +74,7 @@ interface Notification {
|
||||
id: number;
|
||||
actor_username: string;
|
||||
actor_avatar: string | null;
|
||||
notification_type: 'reply' | 'mention' | 'ticket_new' | 'ticket_reply' | 'profile_comment';
|
||||
notification_type: 'reply' | 'mention' | 'follow' | 'ticket_new' | 'ticket_reply' | 'profile_comment';
|
||||
rating: number | null;
|
||||
reply: number | null;
|
||||
is_read: boolean;
|
||||
@@ -511,6 +512,9 @@ const App: React.FC = () => {
|
||||
} else if (n.notification_type === 'profile_comment') {
|
||||
handleNotificationClick(n);
|
||||
navigate(`/profile/${user?.username}`);
|
||||
} else if (n.notification_type === 'follow') {
|
||||
handleNotificationClick(n);
|
||||
navigate(`/profile/${n.actor_username}`);
|
||||
} else {
|
||||
handleNotificationClick(n);
|
||||
}
|
||||
@@ -540,6 +544,7 @@ const App: React.FC = () => {
|
||||
<strong>{n.actor_username}</strong> {
|
||||
n.notification_type === 'reply' ? `replied to your review on ${n.flavor_name}` :
|
||||
n.notification_type === 'mention' ? `mentioned you on ${n.flavor_name}` :
|
||||
n.notification_type === 'follow' ? t('community.notifFollow') :
|
||||
n.notification_type === 'profile_comment' ? `left a message on your guestbook` :
|
||||
n.notification_type === 'ticket_new' ? t('community.notifTicketNew') :
|
||||
user?.is_superuser ? t('community.notifTicketReplyAdmin') : t('community.notifTicketReply')
|
||||
@@ -696,6 +701,7 @@ const App: React.FC = () => {
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/settings" element={<Settings themeName={themeName} onThemeChange={handleThemeChange} />} />
|
||||
<Route path="/support" element={<Support />} />
|
||||
<Route path="/verify-email" element={<VerifyEmail />} />
|
||||
<Route path="/admin-panel" element={<AdminDashboard />} />
|
||||
<Route path="/admin-panel/user/:id" element={<AdminUserDetail />} />
|
||||
<Route path="/admin-panel/rating/:id" element={<AdminRatingDetail />} />
|
||||
|
||||
@@ -118,6 +118,20 @@ const resources = {
|
||||
"save": "Save",
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"auth": {
|
||||
"loginTitle": "Login",
|
||||
"signupTitle": "Signup",
|
||||
"verifyTitle": "Verify Account",
|
||||
"verifyDesc": "Enter the code sent to your email for <strong>{{username}}</strong>.",
|
||||
"resendButton": "Resend Verification Code",
|
||||
"resendSuccess": "A new verification code has been sent to your email.",
|
||||
"verifySuccess": "Account verified! You can now login.",
|
||||
"unverifiedError": "Account not verified. Please verify your account to login.",
|
||||
"loginFailed": "Login failed. Please check your credentials.",
|
||||
"signupSuccess": "Signup successful! Please enter the verification code sent to your email.",
|
||||
"forgotPassword": "Forgot Password?",
|
||||
"backToAuth": "Back to Signup/Login"
|
||||
},
|
||||
"community": {
|
||||
"title": "Community Activity",
|
||||
"subtitle": "Stay updated with your circle's latest ratings and discussions.",
|
||||
@@ -127,6 +141,7 @@ const resources = {
|
||||
"notifications": "Recent Notifications",
|
||||
"notifReply": "replied",
|
||||
"notifMention": "mentioned you",
|
||||
"notifFollow": "started following you",
|
||||
"notifTicketNew": "opened a new support ticket",
|
||||
"notifTicketReply": "replied to your support ticket",
|
||||
"notifTicketReplyAdmin": "replied to a support ticket",
|
||||
@@ -344,6 +359,20 @@ const resources = {
|
||||
"save": "Speichern",
|
||||
"cancel": "Abbrechen"
|
||||
},
|
||||
"auth": {
|
||||
"loginTitle": "Anmelden",
|
||||
"signupTitle": "Registrieren",
|
||||
"verifyTitle": "Konto verifizieren",
|
||||
"verifyDesc": "Gib den Code ein, der an deine E-Mail für <strong>{{username}}</strong> gesendet wurde.",
|
||||
"resendButton": "Verifizierungscode erneut senden",
|
||||
"resendSuccess": "Ein neuer Verifizierungscode wurde an deine E-Mail gesendet.",
|
||||
"verifySuccess": "Konto verifiziert! Du kannst dich jetzt anmelden.",
|
||||
"unverifiedError": "Konto nicht verifiziert. Bitte verifiziere dein Konto, um dich anzumelden.",
|
||||
"loginFailed": "Anmeldung fehlgeschlagen. Bitte überprüfe deine Zugangsdaten.",
|
||||
"signupSuccess": "Registrierung erfolgreich! Bitte gib den Verifizierungscode ein, der an deine E-Mail gesendet wurde.",
|
||||
"forgotPassword": "Passwort vergessen?",
|
||||
"backToAuth": "Zurück zur Anmeldung"
|
||||
},
|
||||
"community": {
|
||||
"title": "Community-Aktivität",
|
||||
"subtitle": "Bleib auf dem Laufenden mit den neuesten Bewertungen und Diskussionen deines Kreises.",
|
||||
@@ -353,6 +382,7 @@ const resources = {
|
||||
"notifications": "Neueste Benachrichtigungen",
|
||||
"notifReply": "hat geantwortet",
|
||||
"notifMention": "hat dich erwähnt",
|
||||
"notifFollow": "folgt dir jetzt",
|
||||
"notifTicketNew": "hat ein neues Support-Ticket eröffnet",
|
||||
"notifTicketReply": "hat auf dein Support-Ticket geantwortet",
|
||||
"notifTicketReplyAdmin": "hat auf ein Support-Ticket geantwortet",
|
||||
|
||||
@@ -70,7 +70,7 @@ interface Notification {
|
||||
id: number;
|
||||
actor_username: string;
|
||||
actor_avatar: string | null;
|
||||
notification_type: 'reply' | 'mention';
|
||||
notification_type: 'reply' | 'mention' | 'follow' | 'ticket_new' | 'ticket_reply' | 'profile_comment';
|
||||
rating: number | null;
|
||||
reply: number | null;
|
||||
is_read: boolean;
|
||||
@@ -385,22 +385,37 @@ const CommunityFeed: React.FC<CommunityFeedProps> = ({ adminMode }) => {
|
||||
{notifications.length === 0 ? (
|
||||
<Box sx={{ p: 3, textAlign: 'center' }}><Typography variant="body2" color="text.secondary">{t('community.noNotifications')}</Typography></Box>
|
||||
) : (
|
||||
notifications.map(n => (
|
||||
<ListItemButton key={n.id} onClick={() => n.flavor_id && navigate(`/flavor/${n.flavor_id}`)} sx={{ py: 1.5 }}>
|
||||
<ListItemAvatar sx={{ minWidth: 40 }}>
|
||||
<Avatar src={n.actor_avatar || undefined} sx={{ width: 30, height: 30, fontSize: '0.8rem' }}>{n.actor_username.charAt(0)}</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Typography variant="body2" sx={{ fontSize: '0.8rem' }}>
|
||||
<strong>{n.actor_username}</strong> {n.notification_type === 'reply' ? t('community.notifReply') : t('community.notifMention')}
|
||||
</Typography>
|
||||
}
|
||||
secondary={formatTimestamp(n.created_at)}
|
||||
secondaryTypographyProps={{ sx: { fontSize: '0.7rem' } }}
|
||||
/>
|
||||
</ListItemButton>
|
||||
))
|
||||
notifications.map(n => {
|
||||
const handleClick = () => {
|
||||
if (n.notification_type === 'follow') {
|
||||
navigate(`/profile/${n.actor_username}`);
|
||||
} else if (n.flavor_id) {
|
||||
navigate(`/flavor/${n.flavor_id}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ListItemButton key={n.id} onClick={handleClick} sx={{ py: 1.5 }}>
|
||||
<ListItemAvatar sx={{ minWidth: 40 }}>
|
||||
<Avatar src={n.actor_avatar || undefined} sx={{ width: 30, height: 30, fontSize: '0.8rem' }}>{n.actor_username.charAt(0)}</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Typography variant="body2" sx={{ fontSize: '0.8rem' }}>
|
||||
<strong>{n.actor_username}</strong> {
|
||||
n.notification_type === 'reply' ? t('community.notifReply') :
|
||||
n.notification_type === 'mention' ? t('community.notifMention') :
|
||||
n.notification_type === 'follow' ? t('community.notifFollow') :
|
||||
n.notification_type
|
||||
}
|
||||
</Typography>
|
||||
}
|
||||
secondary={formatTimestamp(n.created_at)}
|
||||
secondaryTypographyProps={{ sx: { fontSize: '0.7rem' } }}
|
||||
/>
|
||||
</ListItemButton>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</List>
|
||||
</Card>
|
||||
|
||||
@@ -4,11 +4,13 @@ import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import { Link } from 'react-router-dom';
|
||||
import api from '../api';
|
||||
import { useTitle } from '../hooks/useTitle';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface LoginProps {
|
||||
}
|
||||
|
||||
const Login: React.FC<LoginProps> = () => {
|
||||
const { t } = useTranslation();
|
||||
useTitle('Login');
|
||||
const [tab, setTab] = useState(0);
|
||||
const [username, setUsername] = useState('');
|
||||
@@ -16,6 +18,7 @@ const Login: React.FC<LoginProps> = () => {
|
||||
const [email, setEmail] = useState('');
|
||||
const [verificationCode, setVerificationCode] = useState('');
|
||||
const [isVerifying, setIsVerifying] = useState(false);
|
||||
const [showResend, setShowResend] = 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);
|
||||
@@ -31,7 +34,13 @@ const Login: React.FC<LoginProps> = () => {
|
||||
localStorage.setItem('token', res.data.token);
|
||||
window.location.href = '/';
|
||||
} catch (err: any) {
|
||||
setMessage({ type: 'error', text: 'Login failed. Please check your credentials or verify your account.' });
|
||||
const errorMsg = err.response?.data?.non_field_errors?.[0] || t('auth.loginFailed');
|
||||
if (errorMsg.toLowerCase().includes('inactive') || errorMsg.toLowerCase().includes('verify') || errorMsg.includes('disabled')) {
|
||||
setMessage({ type: 'error', text: t('auth.unverifiedError') });
|
||||
setIsVerifying(true);
|
||||
} else {
|
||||
setMessage({ type: 'error', text: errorMsg });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -41,7 +50,7 @@ const Login: React.FC<LoginProps> = () => {
|
||||
try {
|
||||
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.' });
|
||||
setMessage({ type: 'success', text: t('auth.signupSuccess') });
|
||||
} catch (err: any) {
|
||||
setMessage({ type: 'error', text: err.response?.data?.error || 'Signup failed' });
|
||||
}
|
||||
@@ -52,12 +61,24 @@ const Login: React.FC<LoginProps> = () => {
|
||||
setMessage(null);
|
||||
try {
|
||||
await api.post('users/verify_signup/', { username, code: verificationCode });
|
||||
setMessage({ type: 'success', text: 'Account verified! You can now login.' });
|
||||
setMessage({ type: 'success', text: t('auth.verifySuccess') });
|
||||
setIsVerifying(false);
|
||||
setTab(0);
|
||||
setPassword('');
|
||||
} catch (err: any) {
|
||||
setMessage({ type: 'error', text: err.response?.data?.error || 'Verification failed' });
|
||||
setShowResend(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResendCode = async () => {
|
||||
setMessage(null);
|
||||
try {
|
||||
await api.post('users/resend_verification/', { username });
|
||||
setMessage({ type: 'success', text: t('auth.resendSuccess') });
|
||||
setShowResend(false);
|
||||
} catch (err: any) {
|
||||
setMessage({ type: 'error', text: err.response?.data?.error || 'Failed to resend code' });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -93,20 +114,32 @@ const Login: React.FC<LoginProps> = () => {
|
||||
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>
|
||||
<Typography variant="h6" gutterBottom>{t('auth.verifyTitle')}</Typography>
|
||||
<Typography variant="body2" sx={{ mb: 2 }} dangerouslySetInnerHTML={{ __html: t('auth.verifyDesc', { username }) }} />
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Verification Code"
|
||||
margin="normal"
|
||||
value={verificationCode}
|
||||
onChange={(e) => setVerificationCode(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
<Button fullWidth variant="contained" type="submit" sx={{ mt: 2 }}>
|
||||
Verify
|
||||
</Button>
|
||||
<Button fullWidth onClick={() => setIsVerifying(false)} sx={{ mt: 1 }}>
|
||||
Back to Signup
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
variant={showResend ? "contained" : "outlined"}
|
||||
color={showResend ? "secondary" : "primary"}
|
||||
onClick={handleResendCode}
|
||||
sx={{ mt: 2, textTransform: 'none' }}
|
||||
>
|
||||
{t('auth.resendButton')}
|
||||
</Button>
|
||||
|
||||
<Button fullWidth onClick={() => { setIsVerifying(false); setMessage(null); }} sx={{ mt: 1 }}>
|
||||
{t('auth.backToAuth')}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
@@ -156,8 +189,8 @@ const Login: React.FC<LoginProps> = () => {
|
||||
return (
|
||||
<>
|
||||
<Tabs value={tab} onChange={(_, v) => { setTab(v); setMessage(null); }} sx={{ mb: 2 }}>
|
||||
<Tab label="Login" />
|
||||
<Tab label="Signup" />
|
||||
<Tab label={t('auth.loginTitle')} />
|
||||
<Tab label={t('auth.signupTitle')} />
|
||||
</Tabs>
|
||||
|
||||
{tab === 0 ? (
|
||||
@@ -178,10 +211,10 @@ const Login: React.FC<LoginProps> = () => {
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<Button fullWidth variant="contained" type="submit" sx={{ mt: 2 }}>
|
||||
Login
|
||||
{t('auth.loginTitle')}
|
||||
</Button>
|
||||
<Button fullWidth onClick={() => { setIsResetting(true); setMessage(null); }} sx={{ mt: 1, textTransform: 'none' }} size="small">
|
||||
Forgot Password?
|
||||
{t('auth.forgotPassword')}
|
||||
</Button>
|
||||
</form>
|
||||
) : (
|
||||
@@ -210,7 +243,7 @@ const Login: React.FC<LoginProps> = () => {
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<Button fullWidth variant="contained" type="submit" sx={{ mt: 2 }}>
|
||||
Signup
|
||||
{t('auth.signupTitle')}
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useLocation, useNavigate, Link } from 'react-router-dom';
|
||||
import { Container, Paper, Typography, Box, CircularProgress, Button, Alert } from '@mui/material';
|
||||
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
|
||||
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import api from '../api';
|
||||
import { useTitle } from '../hooks/useTitle';
|
||||
|
||||
const VerifyEmail: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');
|
||||
const [errorMsg, setErrorMsg] = useState('');
|
||||
|
||||
useTitle(t('auth.verifyTitle'));
|
||||
|
||||
useEffect(() => {
|
||||
const query = new URLSearchParams(location.search);
|
||||
const username = query.get('username');
|
||||
const code = query.get('code');
|
||||
|
||||
if (!username || !code) {
|
||||
setStatus('error');
|
||||
setErrorMsg('Invalid verification link.');
|
||||
return;
|
||||
}
|
||||
|
||||
const verify = async () => {
|
||||
try {
|
||||
await api.post('users/verify_signup/', { username, code });
|
||||
setStatus('success');
|
||||
// Optional: Auto redirect after 3 seconds
|
||||
setTimeout(() => {
|
||||
navigate('/login');
|
||||
}, 3000);
|
||||
} catch (err: any) {
|
||||
setStatus('error');
|
||||
setErrorMsg(err.response?.data?.error || 'Verification failed.');
|
||||
}
|
||||
};
|
||||
|
||||
verify();
|
||||
}, [location, navigate]);
|
||||
|
||||
return (
|
||||
<Container maxWidth="sm" sx={{ py: 12 }}>
|
||||
<Paper sx={{ p: 6, textAlign: 'center', borderRadius: 4 }}>
|
||||
{status === 'loading' && (
|
||||
<Box>
|
||||
<CircularProgress size={60} sx={{ mb: 4 }} />
|
||||
<Typography variant="h5" sx={{ fontWeight: 'bold' }}>
|
||||
Verifying your account...
|
||||
</Typography>
|
||||
<Typography color="text.secondary" sx={{ mt: 1 }}>
|
||||
Please wait while we confirm your email.
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{status === 'success' && (
|
||||
<Box>
|
||||
<CheckCircleOutlineIcon color="success" sx={{ fontSize: 80, mb: 2 }} />
|
||||
<Typography variant="h4" sx={{ fontWeight: 'bold', mb: 2 }}>
|
||||
{t('auth.verifySuccess')}
|
||||
</Typography>
|
||||
<Typography color="text.secondary" sx={{ mb: 4 }}>
|
||||
You will be redirected to the login page shortly.
|
||||
</Typography>
|
||||
<Button variant="contained" component={Link} to="/login" size="large" sx={{ borderRadius: 2 }}>
|
||||
Go to Login Now
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{status === 'error' && (
|
||||
<Box>
|
||||
<ErrorOutlineIcon color="error" sx={{ fontSize: 80, mb: 2 }} />
|
||||
<Typography variant="h4" sx={{ fontWeight: 'bold', mb: 2 }}>
|
||||
Verification Failed
|
||||
</Typography>
|
||||
<Alert severity="error" sx={{ mb: 4, textAlign: 'left' }}>
|
||||
{errorMsg}
|
||||
</Alert>
|
||||
<Typography color="text.secondary" sx={{ mb: 4 }}>
|
||||
The link might be expired or already used.
|
||||
</Typography>
|
||||
<Button variant="outlined" component={Link} to="/login" size="large" sx={{ borderRadius: 2 }}>
|
||||
Back to Login / Signup
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default VerifyEmail;
|
||||
Reference in new issue
Block a user