feat: add customer authentication and order dashboard

This commit is contained in:
2026-09-16 09:06:48 +03:30
parent f49ef11c9c
commit 7c55b3916c
31 changed files with 2193 additions and 53 deletions
+85
View File
@@ -0,0 +1,85 @@
'use client';
import {useEffect, useState} from 'react';
import {Alert, App, Button, Form, Input, Modal, Segmented, Typography} from 'antd';
import {LockOutlined, MobileOutlined, SafetyCertificateOutlined} from '@ant-design/icons';
import {useAuth} from '@/app/context/AuthContext';
import {useLanguage} from '@/app/context/LanguageContext';
import {accountCopy} from '@/data/account';
type LoginMode = 'otp' | 'password';
export default function AuthModal() {
const auth = useAuth();
const {message} = App.useApp();
const {locale} = useLanguage();
const copy = accountCopy[locale];
const [mode, setMode] = useState<LoginMode>('otp');
const [step, setStep] = useState<'phone' | 'code'>('phone');
const [phone, setPhone] = useState('09123456789');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
useEffect(() => {
if (auth.loginOpen) {
setMode('otp');
setStep('phone');
setError('');
}
}, [auth.loginOpen]);
const submit = async (values: {phone?: string; code?: string; password?: string}) => {
setLoading(true);
setError('');
try {
const currentPhone = (values.phone || phone).replace(/\s/g, '');
if (mode === 'otp' && step === 'phone') {
await auth.sendOtp(currentPhone);
setPhone(currentPhone);
setStep('code');
message.success(copy.otpSent);
} else if (mode === 'otp') {
await auth.verifyOtp(currentPhone, values.code || '');
} else {
await auth.loginPassword(currentPhone, values.password || '');
}
} catch (caught) {
setError(caught instanceof Error && caught.message === 'INVALID_PHONE' ? copy.invalidPhone : copy.invalidCredentials);
} finally {
setLoading(false);
}
};
return (
<Modal open={auth.loginOpen} onCancel={auth.closeLogin} footer={null} centered title={copy.login} destroyOnClose>
<Segmented<LoginMode> block value={mode} onChange={value => {setMode(value); setStep('phone'); setError('');}}
options={[{label: copy.otpLogin, value: 'otp'}, {label: copy.passwordLogin, value: 'password'}]}/>
<Alert className="mt-5" type="info" showIcon message={copy.demoTitle}
description={<div><div>{copy.demoOtp}</div><div>{copy.demoPassword}</div></div>}/>
{error && <Alert className="mt-4" type="error" showIcon message={error}/>}
<Form layout="vertical" className="mt-5" onFinish={submit} initialValues={{phone: '09123456789', code: '12345', password: '123456'}}>
{(mode === 'password' || step === 'phone') && (
<Form.Item name="phone" label={copy.phone} rules={[{required: true, pattern: /^09\d{9}$/, message: copy.invalidPhone}]}>
<Input dir="ltr" size="large" maxLength={11} prefix={<MobileOutlined/>}/>
</Form.Item>
)}
{mode === 'password' && (
<Form.Item name="password" label={copy.password} rules={[{required: true}]}>
<Input.Password dir="ltr" size="large" prefix={<LockOutlined/>}/>
</Form.Item>
)}
{mode === 'otp' && step === 'code' && (
<>
<Typography.Paragraph type="secondary">{copy.phone}: <span dir="ltr">{phone}</span></Typography.Paragraph>
<Form.Item name="code" label={copy.otp} rules={[{required: true}]}>
<Input dir="ltr" size="large" maxLength={5} prefix={<SafetyCertificateOutlined/>}/>
</Form.Item>
</>
)}
<Button htmlType="submit" type="primary" size="large" block loading={loading}>
{mode === 'otp' ? (step === 'phone' ? copy.sendOtp : copy.verifyAndLogin) : copy.loginButton}
</Button>
</Form>
</Modal>
);
}
+19
View File
@@ -0,0 +1,19 @@
'use client';
import type {MouseEvent, ReactNode} from 'react';
import Link from 'next/link';
import {useRouter} from 'next/navigation';
import {useAuth} from '@/app/context/AuthContext';
export default function ProtectedLink({href, children, className, onClick}: {href: string; children: ReactNode; className?: string; onClick?: () => void}) {
const auth = useAuth();
const router = useRouter();
const handleClick = (event: MouseEvent<HTMLAnchorElement>) => {
onClick?.();
if (!auth.isAuthenticated) {
event.preventDefault();
auth.openLogin(() => router.push(href));
}
};
return <Link href={href} className={className} onClick={handleClick}>{children}</Link>;
}
+36
View File
@@ -0,0 +1,36 @@
'use client';
import type {ReactNode} from 'react';
import {useEffect, useRef} from 'react';
import {Button, Skeleton} from 'antd';
import {LockOutlined} from '@ant-design/icons';
import {useAuth} from '@/app/context/AuthContext';
import {useLanguage} from '@/app/context/LanguageContext';
import {accountCopy} from '@/data/account';
export default function ProtectedPage({children}: {children: ReactNode}) {
const auth = useAuth();
const {locale} = useLanguage();
const copy = accountCopy[locale];
const prompted = useRef(false);
useEffect(() => {
if (auth.ready && !auth.isAuthenticated && !prompted.current) {
prompted.current = true;
auth.openLogin();
}
}, [auth]);
if (!auth.ready) return <main className="max-w-5xl mx-auto min-h-[70vh] px-5 pt-32"><Skeleton active/></main>;
if (!auth.isAuthenticated) return (
<main className="max-w-3xl mx-auto min-h-[70vh] px-5 pt-32 pb-20 text-center">
<div className="rounded-3xl border border-subtle bg-surface p-10">
<LockOutlined className="text-4xl text-brand-400"/>
<h1 className="text-2xl font-extrabold text-primary mt-5">{copy.protectedTitle}</h1>
<p className="text-secondary mt-3 mb-7">{copy.protectedDescription}</p>
<Button type="primary" size="large" onClick={() => auth.openLogin()}>{copy.openLogin}</Button>
</div>
</main>
);
return children;
}