update laye out and update home page also create footer page
This commit is contained in:
16
app/components/layout/ConditionalNavbar.tsx
Normal file
16
app/components/layout/ConditionalNavbar.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
import { getStoredUser } from "@/src/lib/auth";
|
||||
import Navbar from "./Navbar";
|
||||
|
||||
export default function ConditionalNavbar() {
|
||||
const pathname = usePathname();
|
||||
const user = getStoredUser();
|
||||
|
||||
// لو المستخدم مسجل دخول، أو الصفحة الحالية جوه الداشبورد، منظهرش الناف بار العام
|
||||
const isDashboardRoute = pathname?.startsWith("/dashboard");
|
||||
if (user || isDashboardRoute) return null;
|
||||
|
||||
return <Navbar />;
|
||||
}
|
||||
13
app/components/layout/Footer.tsx
Normal file
13
app/components/layout/Footer.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import Logo from "@/src/utils/logo";
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="bg-[#0F172A] text-white">
|
||||
<div className="mx-auto flex w-full max-w-7xl flex-col gap-4 px-6 py-6 text-[13px] lg:flex-row lg:items-center lg:justify-between lg:px-8">
|
||||
<Logo white={true} />
|
||||
<div className="text-center text-white/40">
|
||||
© 2026 Slash.sa. جميع الحقوق محفوظة.
|
||||
</div>
|
||||
</div>
|
||||
</footer>)
|
||||
}
|
||||
@@ -176,6 +176,7 @@ export function Sidebar() {
|
||||
flexShrink: 0,
|
||||
minHeight: "100vh",
|
||||
padding: "20px 12px",
|
||||
|
||||
}}
|
||||
>
|
||||
<SidebarContent
|
||||
|
||||
@@ -28,7 +28,6 @@ export function Topbar() {
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
{/* عنوان الصفحة */}
|
||||
<div>
|
||||
<p style={{ fontSize: 14, fontWeight: 600, color: "var(--color-text-primary)", margin: 0, textAlign: "start" }}>
|
||||
لوحة التحكم
|
||||
@@ -38,15 +37,21 @@ export function Topbar() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* أكشنز */}
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
||||
|
||||
{/* زرار المنيو — sm فقط، globals.css بتتحكم في الـ display */}
|
||||
<div id="topbar-menu-btn">
|
||||
<BrandIconButton onClick={() => setOpen(true)} />
|
||||
</div>
|
||||
|
||||
{/* الدور */}
|
||||
{/* زرار "حسابي" — بيروح لصفحة البروفايل، مع hover بيغير اللون ويعمل scale بسيط */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.push("/dashboard/profile")}
|
||||
className="group flex items-center gap-1.5 rounded-full border border-slate-200 bg-slate-50 px-3 py-[5px] text-xs font-medium text-slate-600 transition-all duration-150 hover:scale-105 hover:border-blue-200 hover:bg-blue-50 hover:text-blue-600"
|
||||
>
|
||||
<i className="ti ti-user text-[14px]" aria-hidden="true" />
|
||||
حسابي
|
||||
</button>
|
||||
|
||||
<div
|
||||
suppressHydrationWarning
|
||||
style={{
|
||||
@@ -61,7 +66,6 @@ export function Topbar() {
|
||||
{user?.role ?? "—"}
|
||||
</div>
|
||||
|
||||
{/* خروج */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleLogout}
|
||||
|
||||
4
app/components/layout/index.ts
Normal file
4
app/components/layout/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { default } from "./Navbar";
|
||||
export { Sidebar } from "./Sidebar";
|
||||
export { Footer } from "./Footer";
|
||||
export { Topbar } from "./Topbar";
|
||||
62
app/dashboard/profile/page.tsx
Normal file
62
app/dashboard/profile/page.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
"use client";
|
||||
|
||||
import { useCurrentUser } from "@/src/hooks/useCurrentUser";
|
||||
|
||||
export default function ProfilePage() {
|
||||
const { user, loading, error } = useCurrentUser();
|
||||
|
||||
if (loading) {
|
||||
// حالة تحميل بسيطة تتماشى مع سبينر الصور اللي عملتها قبل كده في الصفحات التانية
|
||||
return (
|
||||
<div className="flex min-h-[60vh] items-center justify-center">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-2 border-blue-600 border-t-transparent" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !user) {
|
||||
return (
|
||||
<div className="mx-auto mt-10 max-w-md rounded-lg bg-red-50 p-4 text-center text-sm text-red-600">
|
||||
{error ?? "تعذر عرض بيانات الحساب."}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const fields: { label: string; value: string }[] = [
|
||||
{ label: "الاسم", value: user.name },
|
||||
{ label: "اسم المستخدم", value: user.userName ?? "—" },
|
||||
{ label: "البريد الإلكتروني", value: user.email ?? "—" },
|
||||
{ label: "رقم الهاتف", value: user.phone },
|
||||
{ label: "الحالة", value: user.isActive ? "نشط" : "غير نشط" },
|
||||
{ label: "تاريخ الإنشاء", value: new Date(user.createdAt).toLocaleDateString("ar-SA") },
|
||||
{ label: "آخر تحديث", value: new Date(user.updatedAt).toLocaleDateString("ar-SA") },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-lg px-4 py-10" dir="rtl">
|
||||
<div className="rounded-2xl bg-white p-6 shadow-md">
|
||||
<div className="mb-6 flex flex-col items-center gap-3">
|
||||
<img
|
||||
src={user.photo ?? "/images/avatar-placeholder.png"}
|
||||
alt={user.name}
|
||||
className="h-20 w-20 rounded-full object-cover ring-2 ring-blue-100"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).src = "/images/avatar-placeholder.png";
|
||||
}}
|
||||
/>
|
||||
<h1 className="text-base font-semibold text-slate-900">{user.name}</h1>
|
||||
</div>
|
||||
|
||||
{/* عرض البيانات بشكل read-only بدون أي فورم أو زرار تعديل */}
|
||||
<dl className="divide-y divide-slate-100">
|
||||
{fields.map((f) => (
|
||||
<div key={f.label} className="flex items-center justify-between py-2.5 text-[13px]">
|
||||
<dt className="text-slate-500">{f.label}</dt>
|
||||
<dd className="font-medium text-slate-800">{f.value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -10,8 +10,7 @@ import {
|
||||
ClipboardList,
|
||||
FileBarChart2,
|
||||
} from "lucide-react";
|
||||
import Navbar from "@/app/components/layout/Navbar";
|
||||
import Logo from "@/src/utils/logo";
|
||||
|
||||
|
||||
type Tone = "blue" | "emerald" | "amber";
|
||||
|
||||
@@ -81,7 +80,7 @@ const featureGroups: { icon: typeof Car; tone: Tone; title: string; text: string
|
||||
export default function FeaturesPage() {
|
||||
return (
|
||||
<main className="min-h-screen bg-[#ECEEF2] text-slate-900" dir="rtl">
|
||||
<Navbar />
|
||||
|
||||
|
||||
<section className="bg-[linear-gradient(145deg,#0D47A1_0%,#1A73E8_45%,#1565C0_100%)] text-white">
|
||||
<div className="mx-auto max-w-7xl px-6 py-14 lg:px-8">
|
||||
@@ -143,14 +142,6 @@ export default function FeaturesPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer className="bg-[#0F172A] text-white">
|
||||
<div className="mx-auto flex w-full max-w-7xl flex-col gap-4 px-6 py-6 text-[13px] lg:flex-row lg:items-center lg:justify-between lg:px-8">
|
||||
<Logo white={true} />
|
||||
<div className="text-center text-white/40">
|
||||
© 2026 Slash.sa. جميع الحقوق محفوظة.
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -3,8 +3,7 @@
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import Navbar from "@/app/components/layout/Navbar";
|
||||
import Logo from "@/src/utils/logo";
|
||||
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
@@ -42,7 +41,7 @@ export default function FaqPage() {
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-[#ECEEF2] text-slate-900" dir="rtl">
|
||||
<Navbar />
|
||||
|
||||
|
||||
<section className="bg-[linear-gradient(145deg,#0D47A1_0%,#1A73E8_45%,#1565C0_100%)] text-white">
|
||||
<div className="mx-auto max-w-7xl px-6 py-14 lg:px-8">
|
||||
@@ -105,14 +104,7 @@ export default function FaqPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer className="bg-[#0F172A] text-white">
|
||||
<div className="mx-auto flex w-full max-w-7xl flex-col gap-4 px-6 py-6 text-[13px] lg:flex-row lg:items-center lg:justify-between lg:px-8">
|
||||
<Logo white={true} />
|
||||
<div className="text-center text-white/40">
|
||||
© 2026 Slash.sa. جميع الحقوق محفوظة.
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -106,7 +106,7 @@ const roles = [
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<main className="min-h-screen bg-[#ECEEF2] text-slate-900" dir="rtl">
|
||||
<Navbar />
|
||||
|
||||
|
||||
{/* Hero */}
|
||||
<section className="relative overflow-hidden bg-[linear-gradient(145deg,#0D47A1_0%,#1A73E8_45%,#1565C0_100%)] text-white">
|
||||
@@ -284,19 +284,7 @@ export default function HomePage() {
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<footer className="bg-[#0F172A] text-white">
|
||||
<div className="mx-auto flex w-full max-w-7xl flex-col gap-4 px-6 py-6 text-[13px] lg:flex-row lg:items-center lg:justify-between lg:px-8">
|
||||
<Logo white={true} />
|
||||
<div className="text-center text-white/40">
|
||||
© 2026 Slash.sa. جميع الحقوق محفوظة.
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-white/40">
|
||||
<Link href="/prices" className="hover:text-white/70">الأسعار</Link>
|
||||
<Link href="/frequently_asked_questions" className="hover:text-white/70">الأسئلة الشائعة</Link>
|
||||
<Link href="/how_work" className="hover:text-white/70">كيف يعمل</Link>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -33,7 +33,7 @@ const steps = [
|
||||
export default function HowWorkPage() {
|
||||
return (
|
||||
<main className="min-h-screen bg-[#ECEEF2] text-slate-900" dir="rtl">
|
||||
<Navbar />
|
||||
|
||||
|
||||
<section className="bg-[linear-gradient(145deg,#0D47A1_0%,#1A73E8_45%,#1565C0_100%)] text-white">
|
||||
<div className="mx-auto max-w-7xl px-6 py-14 lg:px-8">
|
||||
@@ -95,14 +95,7 @@ export default function HowWorkPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer className="bg-[#0F172A] text-white">
|
||||
<div className="mx-auto flex w-full max-w-7xl flex-col gap-4 px-6 py-6 text-[13px] lg:flex-row lg:items-center lg:justify-between lg:px-8">
|
||||
<Logo white={true} />
|
||||
<div className="text-center text-white/40">
|
||||
© 2026 Slash.sa. جميع الحقوق محفوظة.
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,29 +1,9 @@
|
||||
import type { Metadata } from "next";
|
||||
import "@tabler/icons-webfont/dist/tabler-icons.min.css";
|
||||
import "./globals.css";
|
||||
import ConditionalNavbar from "./components/layout/ConditionalNavbar";
|
||||
|
||||
/**
|
||||
* NOT SHOWN IN THE ORIGINAL FILE SET.
|
||||
* This is the one file every RTL conversion ultimately hinges on:
|
||||
* `dir="rtl"` and `lang="ar"` belong on the root <html> element so
|
||||
* (a) the browser's native bidi algorithm kicks in everywhere,
|
||||
* (b) Tailwind v4's `rtl:`/`ltr:` variants have something to key off,
|
||||
* (c) form controls, scrollbars, and native widgets mirror correctly.
|
||||
*
|
||||
* If your real app/layout.tsx differs (providers, fonts, etc.), keep
|
||||
* everything else as-is and only add the import below.
|
||||
*
|
||||
* ICON FONT: the project already uses Tabler Icons class names
|
||||
* everywhere (ti ti-layout-dashboard, ti ti-car, ti ti-route, ...)
|
||||
* but no stylesheet that defines those classes was ever loaded, so
|
||||
* every icon in the sidebar was silently rendering as nothing.
|
||||
* Self-hosted via `npm install @tabler/icons-webfont` rather than a
|
||||
* CDN <link> — no external request at runtime, works behind
|
||||
* corporate proxies / restrictive network policies, and Next bundles
|
||||
* + fingerprints the CSS and font files automatically. Run
|
||||
* `npm install @tabler/icons-webfont` if it isn't already a
|
||||
* dependency, then this import resolves on its own.
|
||||
*/
|
||||
import { Footer } from "./components/layout";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "لوحة التحكم",
|
||||
@@ -36,8 +16,14 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
// ⚠️ اتصلح: الـ Navbar والـ footer كانوا برة <html>/<body> وده HTML غير صالح
|
||||
<html lang="ar" dir="rtl">
|
||||
<body className="app-shell">{children}</body>
|
||||
<body className="app-shell">
|
||||
{/* الناف بار بيظهر بس لو مفيش يوزر مسجل دخول */}
|
||||
<ConditionalNavbar />
|
||||
{children}
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -49,7 +49,7 @@ const plans = [
|
||||
export default function PricesPage() {
|
||||
return (
|
||||
<main className="min-h-screen bg-[#ECEEF2] text-slate-900" dir="rtl">
|
||||
<Navbar />
|
||||
|
||||
|
||||
<section className="bg-[linear-gradient(145deg,#0D47A1_0%,#1A73E8_45%,#1565C0_100%)] text-white">
|
||||
<div className="mx-auto max-w-7xl px-6 py-14 text-center lg:px-8">
|
||||
@@ -121,14 +121,7 @@ export default function PricesPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer className="bg-[#0F172A] text-white">
|
||||
<div className="mx-auto flex w-full max-w-7xl flex-col gap-4 px-6 py-6 text-[13px] lg:flex-row lg:items-center lg:justify-between lg:px-8">
|
||||
<Logo white={true} />
|
||||
<div className="text-center text-white/40">
|
||||
© 2026 Slash.sa. جميع الحقوق محفوظة.
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</main>
|
||||
);
|
||||
}
|
||||
34
src/hooks/useCurrentUser.ts
Normal file
34
src/hooks/useCurrentUser.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { getStoredToken } from "@/src/lib/auth";
|
||||
import { userService, extractMeUser } from "@/src/services/user.service";
|
||||
import type { UserMe } from "@/src/types/user";
|
||||
|
||||
export function useCurrentUser() {
|
||||
const [user, setUser] = useState<UserMe | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const token = getStoredToken();
|
||||
const res = await userService.getMe(token);
|
||||
const u = extractMeUser(res.data ?? res);
|
||||
if (!u) throw new Error("لا توجد بيانات");
|
||||
setUser(u as UserMe);
|
||||
} catch {
|
||||
setError("تعذر تحميل بيانات الحساب. حاول مرة أخرى.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, [load]);
|
||||
|
||||
return { user, loading, error, reload: load };
|
||||
}
|
||||
@@ -4,7 +4,15 @@ import type { ApiListResponse, User, UserFormData, UserResponse } from "@/src/ty
|
||||
import type { Role } from "@/src/types/role";
|
||||
import type { Branch } from "@/src/types/branch";
|
||||
|
||||
|
||||
export interface MeApiResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
responseAt: string;
|
||||
data: {
|
||||
data: User[] | User;
|
||||
meta?: { total: number; page: number; limit: number; totalPages: number };
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**Building a query string to fetch users with search and pagination*/
|
||||
@@ -49,7 +57,14 @@ export const userService = {
|
||||
branch: { name: string };
|
||||
};
|
||||
}>(`users/${id}`, token),
|
||||
getMe: (token: string | null) =>
|
||||
get<MeApiResponse>("users/me", token),
|
||||
};
|
||||
export function extractMeUser(res: MeApiResponse): User | null {
|
||||
const d = res.data?.data;
|
||||
if (Array.isArray(d)) return d[0] ?? null;
|
||||
return d ?? null;
|
||||
}
|
||||
|
||||
/** Converting the form data into a payload suitable for the API */
|
||||
function buildPayload(
|
||||
|
||||
@@ -112,3 +112,9 @@ export interface ApiErrorResponse {
|
||||
details: { field: string; code: string }[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface UserMe extends User {
|
||||
photo: string | null;
|
||||
isDeleted: boolean;
|
||||
updatedAt: string;
|
||||
}
|
||||
Reference in New Issue
Block a user