finshing architecture for project now we have all layers to build profitionl project app,components ,hooks ,lib,services,styel ,types

This commit is contained in:
m7amedez5511
2026-06-08 23:05:19 +03:00
parent 70853958f5
commit 53c03e9867
45 changed files with 1408 additions and 78 deletions

103
lib/api.ts Normal file
View File

@@ -0,0 +1,103 @@
const DEFAULT_BASE_URL = "/api/proxy";
export const API_BASE_URL =
process.env.NEXT_PUBLIC_API_BASE_URL || DEFAULT_BASE_URL;
function normalizePath(path: string) {
return path.startsWith("/") ? path : `/${path}`;
}
// ─── API REQUESTS ─────────────────────────────
export async function requestJson<T>(
path: string,
init: RequestInit = {},
): Promise<T> {
const token =
typeof window !== "undefined" ? localStorage.getItem("auth_token") : null;
const headers = new Headers(init.headers || {});
if (!headers.has("Content-Type") && !(init.body instanceof FormData)) {
headers.set("Content-Type", "application/json");
}
if (token) {
headers.set("Authorization", `Bearer ${token}`);
}
const endpoint = `${API_BASE_URL}${normalizePath(path)}`;
let attempt = 0;
const maxAttempts = 3;
while (attempt < maxAttempts) {
try {
const response = await fetch(endpoint, {
...init,
headers,
cache: "no-store",
method: init.method || "GET",
});
if (!response.ok) {
// try to parse error message from response, fallback to status text
const json = await response.json().catch(() => null);
const message =
json?.message ||
json?.error ||
`Request failed with status ${response.status}`;
throw new Error(message);
}
return (await response.json()) as T;
} catch (error) {
attempt += 1;
if (attempt >= maxAttempts) throw error;
// exponential back-off: 600ms, 1200ms
await new Promise((resolve) =>
setTimeout(resolve, 2 ** attempt * 300),
);
}
}
throw new Error("Request failed after retries.");
}
// ─── Types ───────────────────────────────────
export type DashboardSummaryResponse = {
success: boolean;
message: string;
data: {
stats: {
clients: number;
orders: number;
trips: number;
cars: number;
drivers: number;
};
alerts: {
expiringCars: Array<Record<string, unknown>>;
expiringDrivers: Array<Record<string, unknown>>;
upcomingMaint: Array<Record<string, unknown>>;
};
activeTrips: Array<{
id: string;
tripNumber: string;
title: string;
progress: number;
}>;
accountSecurity: {
lastLogin: string | null;
requestMeta: Record<string, unknown> | null;
};
};
};
// ─── API calls ───────────────────────────────
export async function getDashboardSummary() {
return requestJson<DashboardSummaryResponse>("/dashboard/summary");
}
export async function getHealth() {
return requestJson<{ status: string; message: string }>("/health");
}

138
lib/auth.ts Normal file
View File

@@ -0,0 +1,138 @@
import { requestJson } from "./api";
const AUTH_TOKEN_KEY = "auth_token";
const AUTH_USER_KEY = "auth_user";
const AUTH_TOKEN_COOKIE = "auth_token"; // cookie name must match what the server expects (e.g., in middleware)
// ─── Cookie helpers ───────────────────────────
function setTokenCookie(token: string) {
if (typeof document === "undefined") return;
const exp = new Date(Date.now() + 7 * 864e5).toUTCString(); // 7 أيام
document.cookie = `${AUTH_TOKEN_COOKIE}=${encodeURIComponent(token)}; expires=${exp}; path=/; SameSite=Lax`;
}
function deleteTokenCookie() {
if (typeof document === "undefined") return;
document.cookie = `${AUTH_TOKEN_COOKIE}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`;
}
// ─── Types ───────────────────────────────────
export interface AuthUser {
id?: string;
name?: string;
email?: string;
role?: string;
permissions?: string[];
roleId?: string;
userName?: string;
phone?: string;
[key: string]: unknown;
}
export interface LoginResponse {
token: string;
user: AuthUser;
}
// ─── Storage ─────────────────────────────────
export function getStoredToken(): string | null {
if (typeof window === "undefined") return null;
return localStorage.getItem(AUTH_TOKEN_KEY);
}
export function getStoredUser(): AuthUser | null {
if (typeof window === "undefined") return null;
const raw = localStorage.getItem(AUTH_USER_KEY);
if (!raw) return null;
try {
return JSON.parse(raw) as AuthUser;
} catch {
return null;
}
}
export function saveAuth(token: string, user: AuthUser) {
if (typeof window === "undefined") return;
//save in localStorage for client-side access
localStorage.setItem(AUTH_TOKEN_KEY, token);
localStorage.setItem(AUTH_USER_KEY, JSON.stringify(user));
// save in cookie for server-side access (e.g., in middleware)
setTokenCookie(token);
}
export function clearAuth() {
if (typeof window === "undefined") return;
localStorage.removeItem(AUTH_TOKEN_KEY);
localStorage.removeItem(AUTH_USER_KEY);
deleteTokenCookie();
}
// ─── Blocked Roles ────────────────────────────
const BLOCKED_ROLES = ["driver", "سائق"] as const;
type RawLoginPayload = {
success?: boolean;
message?: string;
data?: { token?: string; user?: AuthUser };
token?: string;
user?: AuthUser;
};
type RawUser = {
role?: {
name?: string;
permissions?: Array<{ permission?: { slug?: string } }>;
} | string;
};
// ─── Login ────────────────────────────────────
export async function loginUser(
identity: string,
password: string,
): Promise<LoginResponse> {
const payload = await requestJson<RawLoginPayload>("/auth/login", {
method: "POST",
body: JSON.stringify({ identity, password }),
});
console.log("Login response payload:", payload);
const token = payload.data?.token ?? payload.token;
const user = payload.data?.user ?? payload.user;
if (!token || !user) {
throw new Error("اسم المستخدم أو كلمة المرور غير صحيحة.");
}
// destracture role from user and determine roleName
const rawRole = (user as unknown as RawUser).role;
const roleName: string | undefined =
typeof rawRole === "string"
? rawRole
: typeof rawRole === "object" && rawRole !== null
? rawRole.name
: undefined;
// reject if role is in blocked list
if (roleName && (BLOCKED_ROLES as readonly string[]).includes(roleName)) {
throw new Error("غير مصرح لك بالوصول إلى هذه اللوحة.");
}
//fetch permissions
const rolePermissions =
typeof rawRole === "object" && rawRole !== null
? rawRole.permissions ?? []
: [];
const permissions = Array.isArray(rolePermissions)
? rolePermissions
.map((entry) => entry?.permission?.slug)
.filter((slug): slug is string => Boolean(slug))
: [];
const fullUser: AuthUser = { ...user, role: roleName, permissions };
// save auth data in both localStorage and cookie for client and server access
saveAuth(token, fullUser);
return { token, user: fullUser };
}

20
lib/formatters.ts Normal file
View File

@@ -0,0 +1,20 @@
/**
* Format an ISO date string as a localised Arabic date.
* @example fmtDate("2026-06-08T10:00:00Z") → "٨ يونيو ٢٠٢٦"
*/
export function fmtDate(iso: string): string {
return new Date(iso).toLocaleDateString("ar-SA", {
year: "numeric",
month: "short",
day: "numeric",
});
}
/**
* Format a numeric amount as Saudi Riyals.
* @example fmtAmount(1234.5) → "١٬٢٣٤٫٥٠ ر.س"
*/
export function fmtAmount(n: number): string {
return `${n.toFixed(2)} ر.س`;
}

28
lib/order-status.ts Normal file
View File

@@ -0,0 +1,28 @@
// lib/order-status.ts
// Order status display config — no React, no network.
import type { OrderStatus } from "../types/client";
export type { OrderStatus };
/** Tailwind class string for a status badge */
export function statusColor(status: OrderStatus): string {
const map: Record<OrderStatus, string> = {
CREATED: "bg-blue-50 text-blue-700 border-blue-200",
IN_TRANSIT: "bg-amber-50 text-amber-700 border-amber-200",
DELIVERED: "bg-emerald-50 text-emerald-700 border-emerald-200",
CANCELLED: "bg-red-50 text-red-600 border-red-200",
};
return map[status];
}
/** Arabic label for a status */
export function statusLabel(status: OrderStatus): string {
const map: Record<OrderStatus, string> = {
CREATED: "تم الإنشاء",
IN_TRANSIT: "قيد التوصيل",
DELIVERED: "تم التسليم",
CANCELLED: "ملغي",
};
return map[status];
}

53
lib/session.ts Normal file
View File

@@ -0,0 +1,53 @@
export const SESSION_KEY = "lf_client";
export const SESSION_COOKIE = "lf_client_id";
export interface ClientSession {
id: string;
name: string;
email: string;
phone: string;
}
// ── Cookie helpers ────────────────────────────────────
export function getCookie(name: string): string | null {
if (typeof document === "undefined") return null;
const match = document.cookie.match(new RegExp(`(^| )${name}=([^;]+)`));
return match ? decodeURIComponent(match[2]) : null;
}
export function setCookie(name: string, value: string, days = 30) {
const exp = new Date(Date.now() + days * 864e5).toUTCString();
document.cookie = `${name}=${encodeURIComponent(value)}; expires=${exp}; path=/; SameSite=Lax`;
}
export function deleteCookie(name: string) {
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`;
}
// ── Session read / write ──────────────────────────────
export function loadSession(): ClientSession | null {
try {
const raw = typeof localStorage !== "undefined"
? localStorage.getItem(SESSION_KEY)
: null;
if (raw) return JSON.parse(raw) as ClientSession;
} catch { /* ignore parse errors */ }
const id = getCookie(SESSION_COOKIE);
if (id) return { id, name: "", email: "", phone: "" };
return null;
}
export function saveSession(s: ClientSession) {
if (typeof localStorage !== "undefined") {
localStorage.setItem(SESSION_KEY, JSON.stringify(s));
}
setCookie(SESSION_COOKIE, s.id);
}
export function clearSession() {
if (typeof localStorage !== "undefined") {
localStorage.removeItem(SESSION_KEY);
}
deleteCookie(SESSION_COOKIE);
}

11
lib/utils.ts Normal file
View File

@@ -0,0 +1,11 @@
// Minimal class-name merger used by UI components.
// Install `clsx` and `tailwind-merge` for production:
// npm i clsx tailwind-merge
// Then replace the body with:
// import { clsx } from "clsx"; import { twMerge } from "tailwind-merge";
// export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
export function cn(...classes: (string | undefined | null | false)[]): string {
return classes.filter(Boolean).join(" ");
}