Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
7 changes: 7 additions & 0 deletions next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true
};

module.exports = nextConfig;
22 changes: 22 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "booking-web",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "13.5.6",
"react": "18.2.0",
"react-dom": "18.2.0"
},
"devDependencies": {
"@types/node": "20.5.9",
"@types/react": "18.2.21",
"@types/react-dom": "18.2.7",
"typescript": "5.2.2"
}
}
176 changes: 176 additions & 0 deletions src/components/AccountModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { FormEvent, useEffect, useState } from 'react';
import { useAuth } from '../hooks/useAuth';
import { useBookingFlow } from '../hooks/useBookingFlow';

export function AccountModal() {
const { login } = useAuth();
const {
isAccountModalOpen,
closeAccountModal,
openBookingFlow,
setAuthNotification,
authNotification
} = useBookingFlow();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState<string | null>(null);
const [isSubmitting, setSubmitting] = useState(false);
const [showReset, setShowReset] = useState(false);
const [resetEmail, setResetEmail] = useState('');
const [isResetting, setResetting] = useState(false);

useEffect(() => {
if (!isAccountModalOpen) {
setShowReset(false);
setError(null);
setResetEmail('');
setSubmitting(false);
setResetting(false);
setAuthNotification(null);
}
}, [isAccountModalOpen, setAuthNotification]);

if (!isAccountModalOpen) {
return null;
}

const handleLogin = async (event: FormEvent) => {
event.preventDefault();
setError(null);
setSubmitting(true);

try {
await login(email, password);
setAuthNotification(null);
closeAccountModal();
openBookingFlow();
Comment on lines +37 to +46
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Ensure booking modal opens after successful login

The login handler calls openBookingFlow() immediately after await login and closing the account modal, but the callback reference it uses was created before authentication state flipped to true. Because openBookingFlow is memoized on isAuthenticated, this invocation still sees false and executes the unauthenticated branch, reopening the account modal and never showing the booking modal even though the credentials were accepted. Users will think login failed unless they close the modal and click the CTA again. Trigger the booking modal only after the auth context re-renders (e.g., via an effect that watches isAuthenticated or by setting the booking modal state directly on success).

Useful? React with 👍 / 👎.

} catch (loginError) {
setError(loginError instanceof Error ? loginError.message : 'حدث خطأ غير متوقع.');
} finally {
setSubmitting(false);
}
};

const handleResetPassword = async (event: FormEvent) => {
event.preventDefault();
setError(null);
setResetting(true);

try {
const response = await fetch('/api/auth/reset-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ email: resetEmail })
});

if (!response.ok) {
const body = await response.json().catch(() => ({}));
throw new Error(body.error ?? 'تعذّر إرسال رسالة استرجاع كلمة المرور.');
}

setAuthNotification('تم إرسال رسالة استرجاع كلمة المرور إلى بريدك الإلكتروني.');
setShowReset(false);
setResetEmail('');
} catch (resetError) {
setError(resetError instanceof Error ? resetError.message : 'تعذّر إرسال الرسالة.');
} finally {
setResetting(false);
}
};

return (
<div className="modal-backdrop" role="dialog" aria-modal="true">
<div className="modal">
{showReset ? (
<>
<h2>استعادة كلمة المرور</h2>
<p>أدخل بريدك الإلكتروني وسنرسل لك رابط إعادة التعيين.</p>
<form onSubmit={handleResetPassword}>
<div className="input-group">
<label htmlFor="reset-email">البريد الإلكتروني</label>
<input
id="reset-email"
type="email"
value={resetEmail}
onChange={(event) => setResetEmail(event.target.value)}
required
/>
</div>
{error && <div className="alert" role="alert">{error}</div>}
<div className="modal-actions">
<button className="secondary-button" type="button" onClick={() => setShowReset(false)}>
رجوع
</button>
<button type="submit" disabled={isResetting}>
{isResetting ? 'جاري الإرسال...' : 'أرسل الرابط'}
</button>
</div>
</form>
</>
) : (
<>
<h2>تسجيل الدخول</h2>
{authNotification && (
<div className="alert" role="status">
{authNotification}
</div>
)}
<form onSubmit={handleLogin}>
<div className="input-group">
<label htmlFor="login-email">البريد الإلكتروني</label>
<input
id="login-email"
type="email"
value={email}
onChange={(event) => setEmail(event.target.value)}
required
/>
</div>
<div className="input-group">
<label htmlFor="login-password">كلمة المرور</label>
<input
id="login-password"
type="password"
value={password}
onChange={(event) => setPassword(event.target.value)}
required
/>
</div>
{error && <div className="alert" role="alert">{error}</div>}
<div className="modal-actions">
<button className="secondary-button" type="button" onClick={closeAccountModal}>
إغلاق
</button>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'جاري الدخول...' : 'دخول'}
</button>
</div>
</form>
<div style={{ marginTop: '1rem', textAlign: 'center' }}>
<button
type="button"
style={{
background: 'none',
border: 'none',
color: '#3358f4',
fontSize: '0.95rem',
cursor: 'pointer'
}}
onClick={() => {
setError(null);
setAuthNotification(null);
setShowReset(true);
setResetEmail(email);
}}
>
نسيت الرقم السري؟
</button>
</div>
</>
)}
</div>
</div>
);
}
53 changes: 53 additions & 0 deletions src/components/BookingModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { FormEvent, useState } from 'react';
import { useAuth } from '../hooks/useAuth';
import { useBookingFlow } from '../hooks/useBookingFlow';

export function BookingModal() {
const { user } = useAuth();
const { isBookingModalOpen, closeBookingModal } = useBookingFlow();
const [details, setDetails] = useState('');
const [isSubmitting, setSubmitting] = useState(false);
const [confirmation, setConfirmation] = useState<string | null>(null);

if (!isBookingModalOpen) {
return null;
}

const handleSubmit = async (event: FormEvent) => {
event.preventDefault();
setSubmitting(true);
await new Promise((resolve) => setTimeout(resolve, 400));
setConfirmation('تم استلام حجزك وسيتم التواصل معك قريباً.');
setSubmitting(false);
};

return (
<div className="modal-backdrop" role="dialog" aria-modal="true">
<div className="modal">
<h2>نموذج الحجز</h2>
<p>مرحباً {user?.name ?? 'بالعميل'}! أخبرنا بالمزيد عن طلبك.</p>
<form onSubmit={handleSubmit}>
<div className="input-group">
<label htmlFor="booking-details">تفاصيل الخدمة</label>
<textarea
id="booking-details"
style={{ minHeight: '120px', padding: '0.75rem', borderRadius: '8px', border: '1px solid #d0d0d0' }}
value={details}
onChange={(event) => setDetails(event.target.value)}
required
/>
</div>
{confirmation && <div className="alert" role="status">{confirmation}</div>}
<div className="modal-actions">
<button className="secondary-button" type="button" onClick={closeBookingModal}>
إغلاق
</button>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'جاري الإرسال...' : 'إرسال الطلب'}
</button>
</div>
</form>
</div>
</div>
);
}
58 changes: 58 additions & 0 deletions src/context/AuthContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { createContext, ReactNode, useCallback, useContext, useMemo, useState } from 'react';

export interface AuthUser {
id: string;
email: string;
name?: string;
}

interface AuthContextValue {
user: AuthUser | null;
isAuthenticated: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
}

const AuthContext = createContext<AuthContextValue | undefined>(undefined);

export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<AuthUser | null>(null);

const login = useCallback(async (email: string, password: string) => {
if (!email || !password) {
throw new Error('يجب إدخال البريد الإلكتروني وكلمة المرور.');
}

await new Promise((resolve) => setTimeout(resolve, 300));

setUser({
id: 'demo-user',
email,
name: email.split('@')[0] ?? 'عميل'
});
}, []);

const logout = useCallback(() => {
setUser(null);
}, []);

const value = useMemo(
() => ({
user,
isAuthenticated: Boolean(user),
login,
logout
}),
[login, logout, user]
);

return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

export function useAuthContext() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuthContext must be used within an AuthProvider');
}
return context;
}
Loading