update saidebar layout build order crud

This commit is contained in:
m7amedez5511
2026-06-24 18:01:01 +03:00
parent db64b79fe3
commit a18ed59ac1
15 changed files with 2713 additions and 746 deletions

View File

@@ -1,116 +1,226 @@
"use client";
import Link from "next/link";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useState, useEffect, createContext, useContext } from "react";
import { getStoredUser } from "@/src/lib/auth";
import Logo from "@/src/utils/logo";
import BrandIcon from "@/src/utils/brandIcon";
//ــــــــــــــ Drawer Context
const DrawerCtx = createContext<{
open: boolean;
setOpen: (v: boolean) => void;
}>({ open: false, setOpen: () => {} });
export function useSidebarDrawer() {
return useContext(DrawerCtx);
}
export function SidebarProvider({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(false);
return (
<DrawerCtx.Provider value={{ open, setOpen }}>
{children}
</DrawerCtx.Provider>
);
}
/* ══════════════════════════════════════════
Nav items
══════════════════════════════════════════ */
const navSections = [
{
label: "الرئيسية",
items: [
{ href: "/dashboard", label: "Dashboard", icon: "ti-layout-dashboard", permission: "read-dashboard" },
{ href: "/dashboard/users", label: "Users", icon: "ti-users", permission: "read-user" },
{ href: "/dashboard", label: "Dashboard", icon: "ti-layout-dashboard", permission: "read-dashboard" },
{ href: "/dashboard/users", label: "Users", icon: "ti-users", permission: "read-user" },
],
},
{
label: "الأسطول",
items: [
{ href: "/dashboard/cars", label: "Cars", icon: "ti-car", permission: "read-car" },
{ href: "/dashboard/drivers", label: "Drivers", icon: "ti-steering-wheel",permission: "read-driver" },
{ href: "/dashboard/trips", label: "Trips", icon: "ti-route", permission: "read-trip" },
{ href: "/dashboard/cars", label: "Cars", icon: "ti-car", permission: "read-car" },
{ href: "/dashboard/drivers", label: "Drivers", icon: "ti-steering-wheel", permission: "read-driver" },
{ href: "/dashboard/trips", label: "Trips", icon: "ti-route", permission: "read-trip" },
],
},
{
label: "العمليات",
items: [
{ href: "/dashboard/orders", label: "Orders", icon: "ti-package", permission: "read-order" },
{ href: "/dashboard/clients", label: "Clients", icon: "ti-users-group", permission: "read-client" },
{ href: "/dashboard/branches", label: "Branches", icon: "ti-building", permission: "read-branch" },
{ href: "/dashboard/roles", label: "Roles", icon: "ti-shield", permission: "read-role" },
{ href: "/dashboard/role_permission", label: "Roles & Permission", icon: "ti-shield", permission: "manage-role-permissions" },
{ href: "/dashboard/audit", label: "Audit", icon: "ti-clipboard-list", permission: "read-audit" },
{ href: "/dashboard/orders", label: "Orders", icon: "ti-package", permission: "read-order" },
{ href: "/dashboard/clients", label: "Clients", icon: "ti-users-group", permission: "read-client" },
{ href: "/dashboard/branches", label: "Branches", icon: "ti-building", permission: "read-branch" },
{ href: "/dashboard/roles", label: "Roles", icon: "ti-shield", permission: "read-role" },
{ href: "/dashboard/role_permission", label: "Roles & Permission", icon: "ti-shield", permission: "manage-role-permissions" },
{ href: "/dashboard/audit", label: "Audit", icon: "ti-clipboard-list", permission: "read-audit" },
],
},
];
/* ══════════════════════════════════════════
Brand Icon
══════════════════════════════════════════ */
export function BrandIconButton({ onClick }: { onClick: () => void }) {
return (
<button
type="button"
onClick={onClick}
aria-label="فتح القائمة"
style={{
background: "transparent",
border: "1px solid var(--color-border)",
borderRadius: 8,
width: 36, height: 36,
padding: 2,
display: "flex", alignItems: "center", justifyContent: "center",
cursor: "pointer",
flexShrink: 0,
}}
>
<BrandIcon size={28} bg="#2563EB" />
</button>
);
}
const GRAD = "linear-gradient(175deg, #1E3A8A 0%, #1D4ED8 60%, #1565C0 100%)";
/* ══════════════════════════════════════════
Sidebar
⚠️ NO display property in any inline style here
globals.css controls ALL display with !important
══════════════════════════════════════════ */
export function Sidebar() {
const pathname = usePathname();
const user = getStoredUser();
const permissions = user?.permissions ?? navSections.flatMap(s => s.items.map(i => i.permission));
const { open, setOpen } = useSidebarDrawer();
useEffect(() => { setOpen(false); }, [pathname, setOpen]);
useEffect(() => {
document.body.style.overflow = open ? "hidden" : "";
return () => { document.body.style.overflow = ""; };
}, [open]);
return (
<>
{/*
Full sidebar — visible lg+.
No `left`/`right` is set anywhere: placement comes purely from
document order + `dir="rtl"` on <html>. A flex row in RTL lays
its first child against the inline-start edge, which is
physically the RIGHT of the screen. Because <Sidebar /> is
still the first child in DashboardLayout, it now renders on
the right automatically — no positional CSS to flip.
*/}
{/* ══ 1. Full sidebar — lg+ ══*/}
<aside
id="sb-full"
suppressHydrationWarning
className="hidden lg:flex lg:flex-col"
style={{
background: GRAD,
flexDirection: "column", /* NO display property */
width: "var(--sidebar-width)",
flexShrink: 0,
background: "linear-gradient(175deg, #1E3A8A 0%, #1D4ED8 60%, #1565C0 100%)",
minHeight: "100vh",
padding: "20px 12px",
}}
>
<SidebarContent
pathname={pathname}
permissions={permissions}
user={user}
compact={false}
/>
<SidebarContent pathname={pathname} permissions={permissions} user={user} compact={false} />
</aside>
{/*
Compact icon rail — md and below (replaces the old behavior of
hiding the sidebar entirely under `lg`). Fixed to the
inline-end... no: fixed to the RIGHT edge explicitly, because a
`position: fixed` element takes no part in flex flow and so
gets no automatic RTL placement from document order. This is
exactly the kind of element logical properties exist for:
`inset-inline-end: 0` means "right in RTL, left in LTR"
without a manual swap if direction ever toggles.
*/}
{/* ══ 2. Compact rail */}
<aside
id="sb-compact"
suppressHydrationWarning
className="hidden md:flex lg:hidden md:flex-col md:items-center"
aria-label="التنقل المصغر"
style={{
background: GRAD,
flexDirection: "column", /* NO display property */
alignItems: "center",
position: "fixed",
insetBlockStart: 0,
insetBlockEnd: 0,
insetInlineEnd: 0,
top: 0,
bottom: 0,
right: 0,
left: "auto",
width: "var(--sidebar-width-compact)",
background: "linear-gradient(175deg, #1E3A8A 0%, #1D4ED8 60%, #1565C0 100%)",
padding: "16px 0",
zIndex: 30,
overflowY: "auto",
}}
aria-label="التنقل المصغر"
>
<SidebarContent
pathname={pathname}
permissions={permissions}
user={user}
compact={true}
<SidebarContent pathname={pathname} permissions={permissions} user={user} compact={true} />
</aside>
{/* mobile drower*/}
{/* open drower*/}
{open && (
<div
onClick={() => setOpen(false)}
aria-hidden="true"
style={{
position: "fixed",
top: 0, left: 0, right: 0, bottom: 0,
background: "rgba(0,0,0,0.5)",
zIndex: 40,
}}
/>
)}
{/* Drawer panel */}
<aside
id="sb-drawer"
suppressHydrationWarning
aria-label="قائمة التنقل"
style={{
background: GRAD,
flexDirection: "column", /* NO display property */
position: "fixed",
top: 0,
bottom: 0,
right: 0,
left: "auto",
width: "var(--sidebar-width)",
padding: "20px 12px",
zIndex: 50,
overflowY: "auto",
/* transform بيخفي/يظهر الـ drawer على sm */
transform: open ? "translateX(0)" : "translateX(100%)",
transition: "transform 280ms cubic-bezier(0.4,0,0.2,1)",
}}
>
{/* زرار الإغلاق */}
<button
type="button"
onClick={() => setOpen(false)}
aria-label="إغلاق القائمة"
style={{
position: "absolute",
top: 14,
left: 12,
background: "rgba(255,255,255,0.12)",
border: "none",
borderRadius: 8,
width: 30, height: 30,
display: "flex", alignItems: "center", justifyContent: "center",
cursor: "pointer",
color: "#fff",
}}
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none"
stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
<path d="M18 6 6 18M6 6l12 12"/>
</svg>
</button>
<SidebarContent pathname={pathname} permissions={permissions} user={user} compact={false} />
</aside>
</>
);
}
/* ══════════════════════════════════════════
SidebarContent
══════════════════════════════════════════ */
function SidebarContent({
pathname,
permissions,
user,
compact,
pathname, permissions, user, compact,
}: {
pathname: string;
permissions: string[];
@@ -119,40 +229,28 @@ function SidebarContent({
}) {
return (
<>
{/* Logo */}
<div style={{ marginBottom: 28, display: "flex", justifyContent: compact ? "center" : "flex-start" }}>
<Logo white />
{compact ? <BrandIcon size={36} /> : <Logo white />}
</div>
{/* Nav sections */}
<nav
style={{ flex: 1, display: "flex", flexDirection: "column", gap: 0, width: "100%" }}
aria-label="Main navigation"
>
<nav style={{ flex: 1, display: "flex", flexDirection: "column", width: "100%" }}
aria-label="Main navigation">
{navSections.map((section) => (
<div key={section.label} style={{ marginBottom: 8 }}>
{!compact && (
<p style={{
fontSize: 9,
letterSpacing: "0.14em",
textTransform: "uppercase",
color: "rgba(255,255,255,0.45)",
padding: "8px 8px 4px",
fontWeight: 600,
textAlign: "start",
fontSize: 9, letterSpacing: "0.14em", textTransform: "uppercase",
color: "rgba(255,255,255,0.45)", padding: "8px 8px 4px",
fontWeight: 600, textAlign: "start", margin: 0,
}}>
{section.label}
</p>
)}
{section.items.map((item) => {
const allowed = item.permission === "read-dashboard"
|| permissions.includes(item.permission);
const allowed = item.permission === "read-dashboard" || permissions.includes(item.permission);
if (!allowed) return null;
const active = pathname === item.href
|| (item.href !== "/dashboard" && pathname.startsWith(item.href));
return (
<Link
key={item.href}
@@ -176,11 +274,8 @@ function SidebarContent({
textAlign: "start",
}}
>
<i
className={`ti ${item.icon}`}
aria-hidden="true"
style={{ fontSize: compact ? 18 : 16, flexShrink: 0 }}
/>
<i className={`ti ${item.icon}`} aria-hidden="true"
style={{ fontSize: compact ? 18 : 16, flexShrink: 0 }} />
{!compact && item.label}
</Link>
);
@@ -189,34 +284,23 @@ function SidebarContent({
))}
</nav>
{/* User badge — collapses to an avatar dot in compact mode */}
{compact ? (
<div
suppressHydrationWarning
title={user?.name ?? user?.userName ?? "—"}
style={{
width: 36,
height: 36,
borderRadius: "var(--radius-full)",
width: 36, height: 36, borderRadius: "var(--radius-full)",
background: "rgba(255,255,255,0.18)",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: 13,
fontWeight: 600,
color: "#fff",
marginTop: 8,
display: "flex", alignItems: "center", justifyContent: "center",
fontSize: 13, fontWeight: 600, color: "#fff", marginTop: 8,
}}
>
{(user?.name ?? user?.userName ?? "—").slice(0, 1)}
</div>
) : (
<div style={{
background: "rgba(255,255,255,0.12)",
borderRadius: 10,
padding: "10px 12px",
marginTop: 8,
width: "100%",
background: "rgba(255,255,255,0.12)", borderRadius: 10,
padding: "10px 12px", marginTop: 8, width: "100%",
}}>
<p suppressHydrationWarning style={{ fontSize: 13, fontWeight: 500, color: "#fff", margin: 0, textAlign: "start" }}>
{user?.name ?? user?.userName ?? "—"}

View File

@@ -2,9 +2,11 @@
import { useRouter } from "next/navigation";
import { clearAuth, getStoredUser } from "@/src/lib/auth";
import { useSidebarDrawer, BrandIconButton } from "./Sidebar";
export function Topbar() {
const router = useRouter();
const { setOpen } = useSidebarDrawer();
async function handleLogout() {
await clearAuth();
@@ -24,33 +26,27 @@ export function Topbar() {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
/*
`justify-content: space-between` and `display: flex` are
direction-agnostic — they already flip with `dir` for free.
The title block (first child) lands at the inline-start edge
(right, in RTL) and the actions cluster (second child) lands
at inline-end (left). That matches the brief's "primary
actions easily accessible" requirement: in Arabic dashboards
the page title reads first at the right, and the role badge
+ logout sit together at the natural reading end — unchanged
from before, just now correctly positioned by `dir` instead
of by accident.
*/
}}
>
{/* عنوان الصفحة */}
<div>
<p style={{ fontSize: 14, fontWeight: 600, color: "var(--color-text-primary)", margin: 0, textAlign: "start" }}>
لوحة التحكم
</p>
<p style={{ fontSize: 11, color: "var(--color-text-muted)", margin: 0, textAlign: "start" }}>
{/* English product label intentionally stays LTR-embedded so
"Operations dashboard" doesn't get its word order or
punctuation reversed by the surrounding RTL run. */}
<span className="ltr-embed">Operations dashboard</span>
</p>
</div>
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
{/* أكشنز */}
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
{/* زرار المنيو — sm فقط، globals.css بتتحكم في الـ display */}
<div id="topbar-menu-btn">
<BrandIconButton onClick={() => setOpen(true)} />
</div>
{/* الدور */}
<div
suppressHydrationWarning
style={{
@@ -65,14 +61,13 @@ export function Topbar() {
{user?.role ?? "—"}
</div>
{/* خروج */}
<button
type="button"
onClick={handleLogout}
style={{
fontSize: 12,
fontWeight: 500,
color: "#DC2626",
background: "#FEF2F2",
fontSize: 12, fontWeight: 500,
color: "#DC2626", background: "#FEF2F2",
border: "1px solid #FECACA",
borderRadius: "var(--radius-full)",
padding: "5px 14px",