update user page and logo to be link also update profile page

This commit is contained in:
m7amedez5511
2026-08-17 16:24:58 +03:00
parent 17dc53fcfe
commit 55b03ad6f4
12 changed files with 453 additions and 211 deletions

View File

@@ -1,56 +0,0 @@
"use client";
import { useEffect, useState } from "react";
import type { Notification } from "@/src/hooks/useUser";
interface ToastProps {
notification: Notification | null;
}
export function Toast({ notification }: ToastProps) {
const [visible, setVisible] = useState(false);
useEffect(() => {
if (notification) {
setVisible(true);
} else {
// dismiss after 250ms to allow exit animation
const t = setTimeout(() => setVisible(false), 300);
return () => clearTimeout(t);
}
}, [notification]);
if (!visible && !notification) return null;
const isSuccess = notification?.type === "success";
return (
<div
role="status" aria-live="polite" aria-atomic="true"
style={{
position: "fixed", bottom: 24, left: "50%",
transform: `translateX(-50%) translateY(${notification ? "0" : "16px"})`,
zIndex: 9999,
transition: "transform 250ms ease, opacity 250ms ease",
opacity: notification ? 1 : 0,
pointerEvents: "none",
}}
>
<div style={{
display: "flex", alignItems: "center", gap: 10,
padding: "0.75rem 1.25rem",
borderRadius: "var(--radius-full)",
background: isSuccess ? "#065F46" : "#7F1D1D",
color: "#FFFFFF",
fontSize: 13, fontWeight: 600,
boxShadow: "0 8px 32px rgba(0,0,0,0.25)",
maxWidth: "90vw", whiteSpace: "nowrap",
fontFamily: "var(--font-sans)",
}}>
{/* icon */}
<span style={{ fontSize: 16 }}>{isSuccess ? "✓" : "⚠"}</span>
<span>{notification?.message}</span>
</div>
</div>
);
}

View File

@@ -1,11 +1,11 @@
"use client";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import type { Resolver } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import { Alert, Button, Input, Select } from "../UI";
import { createUserSchema, updateUserSchema } from "@/src/validations/user.validator";
import { useEditFormSync } from "@/src/hooks/useEditFormSync"; // CHANGE: added hook — fixes roleId/branchId not pre-selecting on edit
import type { Branch } from "@/src/types/branch";
import type { Role } from "@/src/types/role";
import type { User, UserFormData } from "@/src/types/user";
@@ -30,11 +30,6 @@ export function UserFormModal({ editUser, roles, branches, onClose, onSubmit }:
const resolver: Resolver<UserFormData> = async (values, context, options) => {
const schema = isNew ? createUserSchema : updateUserSchema;
const data = !isNew && !values.password ? { ...values, password: undefined } : values;
// Cast both the schema argument and yupResolver's own return value — same
// fix already applied in TripFormModal/DriverFormModal/CarFormModal:
// passing an explicit <UserFormData> generic here forces a structural
// comparison between yup's inferred optional-field shape and
// UserFormData that fails the same way it did for those forms.
return (yupResolver(schema as any) as any)(data as UserFormData, context, options);
};
@@ -56,25 +51,26 @@ export function UserFormModal({ editUser, roles, branches, onClose, onSubmit }:
},
});
// roles/branches are passed in as props rather than fetched inside this
// modal (unlike Trip/Driver/Car), but the same timing issue applies if the
// parent still has them loading when the modal first mounts: defaultValues
// are applied before the matching <option> exists, so the <select>
// silently falls back to "". Reapply the saved id once each list actually
// contains it.
// CHANGE: replaced the two manual useEffect blocks with useEditFormSync —
// same behavior (re-apply saved id once its option list contains it),
// reused across Driver/Car/Trip forms that have the same bug pattern.
useEditFormSync(setValue, "roleId", editUser?.role?.id, roles);
useEditFormSync(setValue, "branchId", editUser?.branch?.id, branches);
// CHANGE: surface an inline error if the saved role/branch no longer
// exists in the fetched list, instead of silently falling back to the
// placeholder with no explanation.
useEffect(() => {
const savedRoleId = editUser?.role?.id;
if (savedRoleId && roles.some(r => r.id === savedRoleId)) {
setValue("roleId", savedRoleId);
if (editUser?.role?.id && roles.length && !roles.some(r => r.id === editUser.role!.id)) {
setError("roleId", { message: "الدور المحفوظ لم يعد متاحًا، الرجاء اختيار دور آخر" });
}
}, [roles, editUser, setValue]);
}, [roles, editUser, setError]);
useEffect(() => {
const savedBranchId = editUser?.branch?.id;
if (savedBranchId && branches.some(b => b.id === savedBranchId)) {
setValue("branchId", savedBranchId);
if (editUser?.branch?.id && branches.length && !branches.some(b => b.id === editUser.branch!.id)) {
setError("branchId", { message: "الفرع المحفوظ لم يعد متاحًا، الرجاء اختيار فرع آخر" });
}
}, [branches, editUser, setValue]);
}, [branches, editUser, setError]);
const submitHandler = async (data: UserFormData) => {
const payload: Partial<UserFormData> = { ...data };
@@ -136,7 +132,17 @@ export function UserFormModal({ editUser, roles, branches, onClose, onSubmit }:
</div>
{/* body */}
<form onSubmit={handleSubmit(submitHandler)} noValidate style={{ padding: "1.5rem", display: "flex", flexDirection: "column", gap: "1rem" }}>
{/* CHANGE: added suppressHydrationWarning — form elements are the
most common target for browser-extension-injected attributes
(e.g. bis_skin_checked from Bitdefender), which caused the
hydration mismatch error on Save. See ExtensionAttributeCleanup
for the global fix; this is a scoped safety net. */}
<form
onSubmit={handleSubmit(submitHandler)}
noValidate
suppressHydrationWarning
style={{ padding: "1.5rem", display: "flex", flexDirection: "column", gap: "1rem" }}
>
{errors.name?.type === "manual" && (
<Alert type="error" message={errors.name.message ?? ""} onClose={() => {}} />
)}

View File

@@ -6,7 +6,10 @@ import { yupResolver } from "@hookform/resolvers/yup";
import { Alert, Button, Input } from "@/src/Components/UI";
import { useAuth } from "@/src/hooks/useAuth";
import Logo from "@/src/utils/logo";
import { loginSchema, type LoginFormData } from "@/src/validations/auth.validator";
import {
loginSchema,
type LoginFormData,
} from "@/src/validations/auth.validator";
export function LoginForm() {
const { login, loading, error, clearError } = useAuth();
@@ -25,47 +28,212 @@ export function LoginForm() {
};
return (
<main style={{ minHeight: "100vh", background: "var(--color-surface-muted)", color: "var(--color-text-primary)" }}>
<section style={{ minHeight: "100vh", width: "100%", display: "grid", gridTemplateColumns: "1fr 1fr" }}>
<aside style={{ position: "relative", overflow: "hidden", background: "linear-gradient(135deg, #0f172a 0%, #2563EB 50%, #3b82f6 100%)", color: "#FFF", padding: "2.5rem 3rem", display: "flex", flexDirection: "column", justifyContent: "space-between" }}>
<div aria-hidden="true" style={{ position: "absolute", inset: 0, opacity: 0.28, backgroundImage: "radial-gradient(circle, rgba(255,255,255,0.18) 1px, transparent 1px)", backgroundSize: "40px 40px" }} />
<div style={{ position: "relative", zIndex: 1, display: "flex", flexDirection: "column", justifyContent: "space-between", height: "100%" }}>
<main
style={{
minHeight: "100vh",
background: "var(--color-surface-muted)",
color: "var(--color-text-primary)",
}}
>
<section
style={{
minHeight: "100vh",
width: "100%",
display: "grid",
gridTemplateColumns: "1fr 1fr",
}}
>
<aside
style={{
position: "relative",
overflow: "hidden",
background:
"linear-gradient(135deg, #0f172a 0%, #2563EB 50%, #3b82f6 100%)",
color: "#FFF",
padding: "2.5rem 3rem",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
}}
>
<div
aria-hidden="true"
style={{
position: "absolute",
inset: 0,
opacity: 0.28,
backgroundImage:
"radial-gradient(circle, rgba(255,255,255,0.18) 1px, transparent 1px)",
backgroundSize: "40px 40px",
}}
/>
<div
style={{
position: "relative",
zIndex: 1,
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
height: "100%",
}}
>
<div>
<Logo white={true} />
<h1 style={{ marginTop: "2.5rem", maxWidth: "28rem", fontSize: "2.25rem", fontWeight: 700, lineHeight: 1.2, letterSpacing: "-0.02em" }}>
<Logo white={true} href="/" />
<h1
style={{
marginTop: "2.5rem",
maxWidth: "28rem",
fontSize: "2.25rem",
fontWeight: 700,
lineHeight: 1.2,
letterSpacing: "-0.02em",
}}
>
اللوجستيات ببساطة، والتسليم بثقة.
</h1>
<p style={{ marginTop: "1rem", maxWidth: "28rem", fontSize: "0.95rem", lineHeight: 1.8, color: "rgba(255,255,255,0.75)" }}>
راقب عمليات التسليم، وقم بتنظيم السائقين، وحافظ على رؤية كل طلب من خلال تسجيل دخول آمن واحد.
<p
style={{
marginTop: "1rem",
maxWidth: "28rem",
fontSize: "0.95rem",
lineHeight: 1.8,
color: "rgba(255,255,255,0.75)",
}}
>
راقب عمليات التسليم، وقم بتنظيم السائقين، وحافظ على رؤية كل طلب
من خلال تسجيل دخول آمن واحد.
</p>
<div style={{ marginTop: "2rem", display: "flex", flexDirection: "column", gap: "0.75rem" }}>
<div
style={{
marginTop: "2rem",
display: "flex",
flexDirection: "column",
gap: "0.75rem",
}}
>
{[
["التنسيق اللحظي", "تتبع كل طلب من الاستلام إلى التسليم في مكان واحد."],
["وصول آمن", "الجلسات المعتمدة على الصلاحيات تبقي مسارات العميل والإدارة منفصلة."],
["عمليات سريعة", "استخدم لوحة الإدارة لمراجعة الحالة والتنبيهات وحركة الرحلات."],
[
"التنسيق اللحظي",
"تتبع كل طلب من الاستلام إلى التسليم في مكان واحد.",
],
[
"وصول آمن",
"الجلسات المعتمدة على الصلاحيات تبقي مسارات العميل والإدارة منفصلة.",
],
[
"عمليات سريعة",
"استخدم لوحة الإدارة لمراجعة الحالة والتنبيهات وحركة الرحلات.",
],
].map(([title, desc]) => (
<article key={title} style={{ display: "flex", alignItems: "flex-start", gap: "0.75rem", borderRadius: "0.75rem", border: "1px solid rgba(255,255,255,0.15)", background: "rgba(255,255,255,0.1)", padding: "0.85rem" }}>
<div aria-hidden="true" style={{ marginTop: "0.125rem", flexShrink: 0, width: "2.25rem", height: "2.25rem", borderRadius: "0.6rem", background: "rgba(255,255,255,0.15)", display: "flex", alignItems: "center", justifyContent: "center", fontSize: "0.95rem" }}>
<article
key={title}
style={{
display: "flex",
alignItems: "flex-start",
gap: "0.75rem",
borderRadius: "0.75rem",
border: "1px solid rgba(255,255,255,0.15)",
background: "rgba(255,255,255,0.1)",
padding: "0.85rem",
}}
>
<div
aria-hidden="true"
style={{
marginTop: "0.125rem",
flexShrink: 0,
width: "2.25rem",
height: "2.25rem",
borderRadius: "0.6rem",
background: "rgba(255,255,255,0.15)",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "0.95rem",
}}
>
</div>
<div>
<h2 style={{ fontSize: "0.9rem", fontWeight: 600, margin: 0 }}>{title}</h2>
<p style={{ marginTop: "0.25rem", fontSize: "0.8rem", color: "rgba(255,255,255,0.8)" }}>{desc}</p>
<h2
style={{
fontSize: "0.9rem",
fontWeight: 600,
margin: 0,
}}
>
{title}
</h2>
<p
style={{
marginTop: "0.25rem",
fontSize: "0.8rem",
color: "rgba(255,255,255,0.8)",
}}
>
{desc}
</p>
</div>
</article>
))}
</div>
</div>
<p style={{ fontSize: "0.75rem", color: "rgba(255,255,255,0.5)" }}>مصمم لفرق العمليات التي تحتاج إلى الوضوح والسرعة والثقة.</p>
<p style={{ fontSize: "0.75rem", color: "rgba(255,255,255,0.5)" }}>
مصمم لفرق العمليات التي تحتاج إلى الوضوح والسرعة والثقة.
</p>
</div>
</aside>
<section style={{ display: "flex", alignItems: "center", justifyContent: "center", background: "var(--color-surface)", padding: "2.5rem 1.5rem" }}>
<form onSubmit={handleSubmit(onSubmit)} style={{ width: "100%", maxWidth: "26rem", borderRadius: "0.9rem", border: "1px solid var(--color-border)", background: "#FFF", padding: "2rem", boxShadow: "var(--shadow-card)" }} noValidate>
<p style={{ fontSize: "1.75rem", fontWeight: 700, color: "var(--color-text-primary)", margin: 0 }}>مرحبًا بعودتك</p>
<p style={{ marginTop: "0.35rem", fontSize: "0.9rem", color: "var(--color-text-muted)" }}>سجل الدخول لإدارة عمليات التسليم</p>
<section
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "var(--color-surface)",
padding: "2.5rem 1.5rem",
}}
>
<form
onSubmit={handleSubmit(onSubmit)}
style={{
width: "100%",
maxWidth: "26rem",
borderRadius: "0.9rem",
border: "1px solid var(--color-border)",
background: "#FFF",
padding: "2rem",
boxShadow: "var(--shadow-card)",
}}
noValidate
>
<p
style={{
fontSize: "1.75rem",
fontWeight: 700,
color: "var(--color-text-primary)",
margin: 0,
}}
>
مرحبًا بعودتك
</p>
<p
style={{
marginTop: "0.35rem",
fontSize: "0.9rem",
color: "var(--color-text-muted)",
}}
>
سجل الدخول لإدارة عمليات التسليم
</p>
<div style={{ marginTop: "1.5rem", display: "flex", flexDirection: "column", gap: "1rem" }}>
<div
style={{
marginTop: "1.5rem",
display: "flex",
flexDirection: "column",
gap: "1rem",
}}
>
<Input
label="البريد الإلكتروني أو رقم الهاتف أو اسم المستخدم"
autoComplete="username"
@@ -84,8 +252,23 @@ export function LoginForm() {
/>
</div>
<div style={{ marginTop: "0.75rem", display: "flex", justifyContent: "flex-end" }}>
<Link href="/forgot-password" style={{ fontSize: "0.8rem", color: "var(--color-brand-600)", textDecoration: "none" }}>
<div
style={{
marginTop: "0.75rem",
display: "flex",
justifyContent: "flex-end",
}}
>
<Link
href="/forgot-password"
style={{
fontSize: "0.8rem",
color: "var(--color-brand-600)",
textDecoration: "none",
pointerEvents: "none",
opacity: 0.5,
}}
>
هل نسيت كلمة المرور؟
</Link>
</div>
@@ -96,13 +279,35 @@ export function LoginForm() {
</div>
)}
<Button type="submit" loading={loading} fullWidth className="mt-6 h-11">
<Button
type="submit"
loading={loading}
fullWidth
className="mt-6 h-11"
>
{loading ? "جاري تسجيل الدخول…" : "تسجيل الدخول"}
</Button>
<p style={{ marginTop: "1rem", textAlign: "center", fontSize: "0.85rem", color: "var(--color-text-muted)" }}>
<p
style={{
marginTop: "1rem",
textAlign: "center",
fontSize: "0.85rem",
color: "var(--color-text-muted)",
}}
>
ليس لديك حساب؟{" "}
<Link href="/register" style={{ color: "var(--color-brand-600)", textDecoration: "none" }}>
<Link
href="/register"
style={{
color: "var(--color-brand-600)",
textDecoration: "none",
pointerEvents: "none",
opacity: 0.5,
}}
aria-disabled="true"
tabIndex={-1}
>
إنشاء حساب
</Link>
</p>

View File

@@ -0,0 +1,22 @@
"use client";
import { useEffect } from "react";
import type { UseFormSetValue, FieldValues, Path } from "react-hook-form";
// Re-applies a saved id to a select field once its option list has
// finished loading — covers the case where the edit modal mounts
// before roles/branches finish fetching from the API.
export function useEditFormSync<T extends FieldValues>(
setValue: UseFormSetValue<T>,
fieldName: Path<T>,
savedId: string | undefined,
options: { id: string }[],
) {
useEffect(() => {
if (savedId && options.some(o => o.id === savedId)) {
// shouldDirty: false — this is a programmatic sync, not a user edit
setValue(fieldName, savedId as never, { shouldDirty: false });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [options, savedId, setValue]);
}

View File

@@ -1,33 +1,47 @@
import { FC } from "react";
import Link from "next/link";
interface LogoProps {
white?: boolean;
href?: string;
}
const Logo: FC<LogoProps> = ({ white = false }) => (
<div className="flex items-center gap-2">
<div style={{
width: 32, height: 32,
background: white ? "rgba(255,255,255,0.15)" : "#2563EB",
borderRadius: 8,
display: "flex", alignItems: "center", justifyContent: "center",
}}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none"
stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M5 17H3a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v3"/>
<rect x="9" y="11" width="14" height="10" rx="2"/>
<circle cx="12" cy="21" r="1"/>
<circle cx="20" cy="21" r="1"/>
</svg>
const Logo: FC<LogoProps> = ({ white = false, href }) => {
const content = (
<div className="flex items-center gap-2">
<div style={{
width: 32, height: 32,
background: white ? "rgba(255,255,255,0.15)" : "#2563EB",
borderRadius: 8,
display: "flex", alignItems: "center", justifyContent: "center",
}}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none"
stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M5 17H3a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v3"/>
<rect x="9" y="11" width="14" height="10" rx="2"/>
<circle cx="12" cy="21" r="1"/>
<circle cx="20" cy="21" r="1"/>
</svg>
</div>
<span style={{
fontWeight: 700,
fontSize: 15,
color: white ? "#FFFFFF" : "#0F172A",
}}>
Slash.sa
</span>
</div>
<span style={{
fontWeight: 700,
fontSize: 15,
color: white ? "#FFFFFF" : "#0F172A",
}}>
Slash.sa
</span>
</div>
);
);
if (href) {
return (
<Link href={href} style={{ textDecoration: "none" }}>
{content}
</Link>
);
}
return content;
};
export default Logo;