From ccc6412d485dcf8a9964a7eb40a394651e6b8c48 Mon Sep 17 00:00:00 2001 From: nasmans <114198613+nasmans@users.noreply.github.com> Date: Wed, 29 Oct 2025 19:40:42 +0100 Subject: [PATCH] Add account navbar and auth modal components --- src/components/auth/AccountModal.css | 103 +++++++++++++++++++++++++++ src/components/auth/AccountModal.tsx | 87 ++++++++++++++++++++++ src/components/layout/Navbar.css | 42 +++++++++++ src/components/layout/Navbar.tsx | 26 +++++++ src/context/AuthContext.tsx | 69 ++++++++++++++++++ 5 files changed, 327 insertions(+) create mode 100644 src/components/auth/AccountModal.css create mode 100644 src/components/auth/AccountModal.tsx create mode 100644 src/components/layout/Navbar.css create mode 100644 src/components/layout/Navbar.tsx create mode 100644 src/context/AuthContext.tsx diff --git a/src/components/auth/AccountModal.css b/src/components/auth/AccountModal.css new file mode 100644 index 0000000..3e8c591 --- /dev/null +++ b/src/components/auth/AccountModal.css @@ -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; +} diff --git a/src/components/auth/AccountModal.tsx b/src/components/auth/AccountModal.tsx new file mode 100644 index 0000000..2533edd --- /dev/null +++ b/src/components/auth/AccountModal.tsx @@ -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(null); + + if (!isAccountModalOpen) { + return null; + } + + const handleSubmit = async (event: FormEvent) => { + event.preventDefault(); + setIsSubmitting(true); + setError(null); + + try { + await login({ email, password }); + } catch (err) { + setError(err instanceof Error ? err.message : 'حدث خطأ غير متوقع'); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
+
+
+

تسجيل الدخول

+ +
+ +
+ + + + + {error &&

{error}

} + +
+ + +
+
+
+
+ ); +}; diff --git a/src/components/layout/Navbar.css b/src/components/layout/Navbar.css new file mode 100644 index 0000000..138d349 --- /dev/null +++ b/src/components/layout/Navbar.css @@ -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%; + } +} diff --git a/src/components/layout/Navbar.tsx b/src/components/layout/Navbar.tsx new file mode 100644 index 0000000..50619b1 --- /dev/null +++ b/src/components/layout/Navbar.tsx @@ -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 ( + + ); +}; diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx new file mode 100644 index 0000000..db56abc --- /dev/null +++ b/src/context/AuthContext.tsx @@ -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; + logout: () => void; +}; + +const AuthContext = createContext(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 {children}; +}; + +export const useAuth = () => { + const context = useContext(AuthContext); + + if (!context) { + throw new Error('useAuth must be used within an AuthProvider'); + } + + return context; +};