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
103 changes: 103 additions & 0 deletions src/components/auth/AccountModal.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
.account-modal__backdrop {
align-items: center;
background: rgba(15, 23, 42, 0.6);
display: flex;
inset: 0;
justify-content: center;
padding: 1rem;
position: fixed;
z-index: 50;
}

.account-modal {
background: #f8fafc;
border-radius: 1rem;
box-shadow: 0 20px 50px rgba(15, 23, 42, 0.15);
max-width: 420px;
padding: 1.5rem;
width: 100%;
}

.account-modal__header {
align-items: center;
display: flex;
justify-content: space-between;
margin-bottom: 1.5rem;
}

.account-modal__header h2 {
font-size: 1.25rem;
margin: 0;
}

.account-modal__close {
background: transparent;
border: none;
color: #475569;
cursor: pointer;
font-size: 1.5rem;
line-height: 1;
}

.account-modal__form {
display: grid;
gap: 1rem;
}

.account-modal__field {
color: #0f172a;
display: grid;
font-size: 0.95rem;
gap: 0.35rem;
}

.account-modal__field input {
border: 1px solid #cbd5f5;
border-radius: 0.75rem;
padding: 0.65rem 0.85rem;
}

.account-modal__error {
color: #dc2626;
font-size: 0.875rem;
margin: 0;
}

.account-modal__actions {
display: grid;
gap: 0.75rem;
}

.account-modal__primary,
.account-modal__secondary {
border: none;
border-radius: 999px;
cursor: pointer;
font-weight: 600;
padding: 0.65rem 1.25rem;
transition: filter 0.2s ease-in-out, transform 0.2s ease-in-out;
}

.account-modal__primary {
background: linear-gradient(135deg, #38bdf8, #0ea5e9);
color: #0f172a;
}

.account-modal__secondary {
background: transparent;
border: 1px solid #0ea5e9;
color: #0ea5e9;
}

.account-modal__primary:hover,
.account-modal__secondary:hover {
filter: brightness(0.95);
transform: translateY(-1px);
}

.account-modal__primary:disabled,
.account-modal__secondary:disabled {
cursor: not-allowed;
filter: grayscale(0.3);
opacity: 0.7;
}
87 changes: 87 additions & 0 deletions src/components/auth/AccountModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { FormEvent, useState } from 'react';
import { useAuth } from '../../context/AuthContext';
import './AccountModal.css';

export const AccountModal = () => {
const { isAccountModalOpen, closeAccountModal, login } = useAuth();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);

if (!isAccountModalOpen) {
return null;
}

const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
setIsSubmitting(true);
setError(null);

try {
await login({ email, password });
} catch (err) {
setError(err instanceof Error ? err.message : 'حدث خطأ غير متوقع');
} finally {
setIsSubmitting(false);
}
};

return (
<div className="account-modal__backdrop" role="dialog" aria-modal="true">
<div className="account-modal">
<header className="account-modal__header">
<h2>تسجيل الدخول</h2>
<button type="button" className="account-modal__close" onClick={closeAccountModal} aria-label="إغلاق">
×
</button>
</header>

<form className="account-modal__form" onSubmit={handleSubmit}>
<label className="account-modal__field">
البريد الإلكتروني
<input
type="email"
required
value={email}
onChange={(event) => setEmail(event.target.value)}
placeholder="name@example.com"
/>
</label>

<label className="account-modal__field">
كلمة المرور
<input
type="password"
required
value={password}
onChange={(event) => setPassword(event.target.value)}
placeholder="••••••••"
/>
</label>

{error && <p className="account-modal__error">{error}</p>}

<div className="account-modal__actions">
<button type="submit" className="account-modal__primary" disabled={isSubmitting}>
{isSubmitting ? 'جاري الدخول…' : 'تسجيل الدخول'}
</button>
<button
type="button"
className="account-modal__secondary"
onClick={() => {
closeAccountModal();
if (typeof window !== 'undefined') {
window.location.href = '/signup';
}
}}
disabled={isSubmitting}
>
مستخدم جديد
</button>
</div>
</form>
</div>
</div>
);
};
42 changes: 42 additions & 0 deletions src/components/layout/Navbar.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
.navbar {
align-items: center;
background: #0f172a;
color: #f8fafc;
display: flex;
justify-content: space-between;
padding: 0.75rem 1rem;
}

.navbar__brand {
font-size: 1.125rem;
font-weight: 600;
}

.navbar__account-button {
background-color: #38bdf8;
border: none;
border-radius: 9999px;
color: #0f172a;
cursor: pointer;
font-size: 1rem;
font-weight: 600;
padding: 0.5rem 1.25rem;
transition: background-color 0.2s ease-in-out, transform 0.2s ease-in-out;
}

.navbar__account-button:hover,
.navbar__account-button:focus {
background-color: #0ea5e9;
transform: translateY(-1px);
}

@media (max-width: 640px) {
.navbar {
flex-direction: column;
gap: 0.75rem;
}

.navbar__account-button {
width: 100%;
}
}
26 changes: 26 additions & 0 deletions src/components/layout/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useAuth } from '../../context/AuthContext';
import './Navbar.css';

export const Navbar = () => {
const { isAuthenticated, openAccountModal } = useAuth();

const handleAccountClick = () => {
if (!isAuthenticated) {
openAccountModal();
return;
}

if (typeof window !== 'undefined') {
window.location.href = '/client/dashboard';
}
};

return (
<nav className="navbar">
<div className="navbar__brand">WebApp</div>
<button type="button" className="navbar__account-button" onClick={handleAccountClick}>
حسابي
</button>
</nav>
);
};
69 changes: 69 additions & 0 deletions src/context/AuthContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { createContext, ReactNode, useCallback, useContext, useMemo, useState } from 'react';

type AuthContextValue = {
isAuthenticated: boolean;
isAccountModalOpen: boolean;
openAccountModal: () => void;
closeAccountModal: () => void;
login: (credentials: { email: string; password: string }) => Promise<void>;
logout: () => void;
};

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

interface AuthProviderProps {
children: ReactNode;
onLoginSuccess?: () => void;
}

export const AuthProvider = ({ children, onLoginSuccess }: AuthProviderProps) => {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [isAccountModalOpen, setIsAccountModalOpen] = useState(false);

const openAccountModal = useCallback(() => setIsAccountModalOpen(true), []);
const closeAccountModal = useCallback(() => setIsAccountModalOpen(false), []);

const login = useCallback(
async (_credentials: { email: string; password: string }) => {
// TODO: replace with real API integration once backend is ready.
await new Promise((resolve) => setTimeout(resolve, 300));
setIsAuthenticated(true);
setIsAccountModalOpen(false);

if (onLoginSuccess) {
onLoginSuccess();
} else if (typeof window !== 'undefined') {
window.location.href = '/client/dashboard';
}
},
[onLoginSuccess]
);

const logout = useCallback(() => {
setIsAuthenticated(false);
}, []);

const value = useMemo(
() => ({
isAuthenticated,
isAccountModalOpen,
openAccountModal,
closeAccountModal,
login,
logout,
}),
[isAuthenticated, isAccountModalOpen, openAccountModal, closeAccountModal, login, logout]
);

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

export const useAuth = () => {
const context = useContext(AuthContext);

if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}

return context;
};