diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..6958625 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,499 @@ +# Slash.sa Logistics Frontend — Architecture Refactor + +## 1. Current Issues Analysis + +### 1.1 File Structure Problems +| Issue | Location | Impact | +|---|---|---| +| Components mixed into `src/Components/` with PascalCase subdirectories, but `app/components/` also exists | Both directories | Confusing, inconsistent import paths | +| Empty component files (`admen.tsx`, `driver.tsx`, `tripe.tsx`, etc.) | `src/Components/` | Dead weight, misleading tree | +| API logic scattered (`src/lib/api.ts`, direct `fetch()` calls in components) | Multiple files | Hard to mock/test, duplicated base URL logic | +| No barrel (`index.ts`) exports anywhere | All directories | Verbose imports everywhere | +| `helperFun.ts` mixes session management, formatting utilities, and order-status config | `src/utils/` | Violates single-responsibility principle | +| Proxy route handler in `app/api/proxy/` but also `NEXT_PUBLIC_API_BASE_URL` pointing to the same origin | Both | Redundant, confusing config | + +### 1.2 Design Inconsistencies +| Issue | Example | +|---|---| +| Dark `slate-950` dashboard theme vs. light `slate-50` login/register theme — no shared token layer | `app/login/page.tsx` vs `app/dashboard/page.tsx` | +| Three different spinner implementations (inline SVG in `spinner.tsx`, div spinner in `NewOrder/page.tsx`, none elsewhere) | Multiple files | +| `Plus Jakarta Sans` loaded via `@import` in `globals.css` but Geist fonts loaded via `next/font` in `layout.tsx` — double font loading | `app/layout.tsx`, `app/globals.css` | +| Tailwind v4 used (`@import "tailwindcss"`) but `tailwind.config.js` targets v3 API | Config mismatch | +| RTL direction set per-page inconsistently (`dir="rtl"` on `
` or `
`) | All pages | +| No shared color CSS variables consumed by Tailwind — raw hex strings in JSX | Throughout | + +### 1.3 API / State Issues +| Issue | Location | +|---|---| +| `API_BASE` read from `process.env.NEXT_PUBLIC_API_BASE_URL!` with `!` assertion and a `console.log` | `client.tsx`, `order.tsx`, `clientAdress.tsx` | +| Auth token cookie set manually with string concatenation | `src/lib/auth.ts` | +| No centralised error-boundary or query-state helper | Everywhere | +| `requestJson` retries 3× with exponential back-off but components also retry inline | `api.ts` + components | + +--- + +## 2. Proposed Folder Structure + +``` +/ +├── app/ +│ ├── (admin)/ # Route group — requires auth + admin role +│ │ ├── dashboard/page.tsx +│ │ ├── users/page.tsx +│ │ ├── cars/page.tsx +│ │ ├── drivers/page.tsx +│ │ ├── clients/page.tsx +│ │ ├── orders/page.tsx +│ │ ├── trips/page.tsx +│ │ ├── branches/page.tsx +│ │ ├── roles/page.tsx +│ │ ├── audit/page.tsx +│ │ └── layout.tsx # DashboardLayout (Sidebar + Topbar) +│ │ +│ ├── (client)/ # Route group — public client portal +│ │ ├── NewOrder/page.tsx +│ │ ├── client/page.tsx +│ │ └── register/page.tsx +│ │ +│ ├── (auth)/ # Route group — unauthenticated +│ │ ├── login/page.tsx +│ │ └── forgot-password/page.tsx +│ │ +│ ├── forbidden/page.tsx +│ ├── api/ +│ │ └── proxy/[...path]/route.ts +│ ├── globals.css +│ └── layout.tsx # Root layout (fonts, metadata) +│ +├── components/ # Shared UI primitives +│ ├── ui/ +│ │ ├── Spinner.tsx ✅ Single canonical spinner +│ │ ├── Alert.tsx ✅ Typed alert banner +│ │ ├── Button.tsx ✅ PrimaryBtn + variants +│ │ ├── Input.tsx ✅ Labelled input with error +│ │ └── index.ts barrel +│ │ +│ ├── layout/ +│ │ ├── Sidebar.tsx +│ │ ├── Topbar.tsx +│ │ ├── Navbar.tsx +│ │ └── index.ts +│ │ +│ ├── auth/ +│ │ └── LoginForm.tsx +│ │ +│ ├── client/ +│ │ ├── RegisterStep.tsx +│ │ ├── AddressStep.tsx +│ │ └── OrderDashboard.tsx (was DashboardStep) +│ │ +│ └── dashboard/ +│ ├── SummaryCard.tsx +│ ├── ActiveTripCard.tsx +│ └── AlertsCard.tsx +│ +├── services/ # All network calls — no UI imports +│ ├── api.ts base requestJson helper +│ ├── auth.service.ts loginUser, logout helpers +│ ├── dashboard.service.ts getDashboardSummary +│ ├── client.service.ts getClientOrders, postClientAddress … +│ └── index.ts barrel +│ +├── lib/ # Non-UI utilities +│ ├── auth.ts token storage, cookie helpers +│ ├── session.ts (extracted from helperFun.ts) +│ ├── formatters.ts fmtDate, fmtAmount +│ ├── order-status.ts statusColor, statusLabel, OrderStatus type +│ └── index.ts +│ +├── hooks/ # Custom React hooks +│ ├── useAuth.ts +│ ├── useDashboardSummary.ts +│ └── useClientOrders.ts +│ +├── styles/ +│ └── tokens.css CSS custom properties (design tokens) +│ +├── types/ +│ ├── auth.ts +│ ├── dashboard.ts +│ └── client.ts +│ +└── public/ +``` + +--- + +## 3. Design System + +### 3.1 Color Tokens (`styles/tokens.css`) + +```css +/* ── Brand ─────────────────────────────── */ +--color-brand-600: #1A73E8; +--color-brand-700: #1557B0; +--color-brand-50: #EBF3FF; + +/* ── Semantic ───────────────────────────── */ +--color-success: #34A853; +--color-warning: #F59E0B; +--color-danger: #E53935; +--color-info: #1A73E8; + +/* ── Surface (light) ────────────────────── */ +--color-surface: #FFFFFF; +--color-surface-muted: #F9FAFB; +--color-border: #E5E7EB; + +/* ── Surface (dark — admin dashboard) ───── */ +--color-surface-dark: #0F172A; /* slate-950 */ +--color-surface-dark-card: #1E293B; /* slate-800 */ +--color-border-dark: rgba(255,255,255,0.1); + +/* ── Text ───────────────────────────────── */ +--color-text-primary: #111827; +--color-text-muted: #6B7280; +--color-text-hint: #9CA3AF; +--color-text-inverse: #FFFFFF; + +/* ── Font ───────────────────────────────── */ +--font-sans: 'Plus Jakarta Sans', sans-serif; +--font-mono: 'IBM Plex Mono', monospace; + +/* ── Radius ─────────────────────────────── */ +--radius-sm: 7px; +--radius-md: 10px; +--radius-lg: 14px; +--radius-xl: 20px; +--radius-full: 9999px; + +/* ── Sidebar width ──────────────────────── */ +--sidebar-width: 280px; +``` + +These are imported once in `app/globals.css` and referenced via Tailwind's `theme.extend` for utility access. + +### 3.2 Typography + +| Role | Font | Weight | Size | +|---|---|---|---| +| Page heading | Plus Jakarta Sans | 700 | 28–38px | +| Section title | Plus Jakarta Sans | 600 | 18–22px | +| Body | Plus Jakarta Sans | 400 | 13–14px | +| Label/caps | Plus Jakarta Sans | 700 | 10–11px | +| Code/mono | IBM Plex Mono | 400/500 | 12–13px | + +Load **once** via `next/font/google` in `app/layout.tsx`. Remove `@import url(...)` from `globals.css`. + +--- + +## 4. Key Component Implementations + +### 4.1 Canonical Spinner (`components/ui/Spinner.tsx`) + +```tsx +import { cn } from "@/lib/utils"; + +interface SpinnerProps { + size?: "sm" | "md" | "lg"; + className?: string; +} + +const sizes = { sm: "h-4 w-4", md: "h-6 w-6", lg: "h-10 w-10" }; + +export function Spinner({ size = "md", className }: SpinnerProps) { + return ( + + + + + ); +} +``` + +**Usage:** ``, ``, `` + +### 4.2 Alert (`components/ui/Alert.tsx`) + +```tsx +interface AlertProps { + type?: "error" | "info" | "success" | "warning"; + message: string; + onClose?: () => void; +} + +const config = { + error: { bg: "bg-red-50 border-red-200 text-red-700", icon: "⚠" }, + info: { bg: "bg-blue-50 border-blue-200 text-blue-700", icon: "ℹ" }, + success: { bg: "bg-emerald-50 border-emerald-200 text-emerald-700", icon: "✓" }, + warning: { bg: "bg-amber-50 border-amber-200 text-amber-700", icon: "!" }, +}; + +export function Alert({ type = "error", message, onClose }: AlertProps) { + const { bg, icon } = config[type]; + return ( +
+ + {message} + {onClose && ( + + )} +
+ ); +} +``` + +### 4.3 Button (`components/ui/Button.tsx`) + +```tsx +import { Spinner } from "./Spinner"; +import { cn } from "@/lib/utils"; + +interface ButtonProps extends React.ButtonHTMLAttributes { + loading?: boolean; + variant?: "primary" | "secondary" | "ghost" | "danger"; + size?: "sm" | "md" | "lg"; +} + +const variants = { + primary: "bg-[var(--color-brand-600)] hover:bg-[var(--color-brand-700)] text-white shadow-sm", + secondary: "bg-white border border-[var(--color-border)] text-[var(--color-text-primary)] hover:bg-[var(--color-surface-muted)]", + ghost: "text-[var(--color-text-muted)] hover:bg-[var(--color-surface-muted)]", + danger: "bg-red-600 hover:bg-red-700 text-white", +}; + +const sizes = { + sm: "h-8 px-3 text-[12px]", + md: "h-10 px-4 text-[13px]", + lg: "h-11 px-6 text-[14px]", +}; + +export function Button({ loading, variant = "primary", size = "md", children, className, disabled, ...rest }: ButtonProps) { + return ( + + ); +} +``` + +### 4.4 Input (`components/ui/Input.tsx`) + +```tsx +interface InputProps extends React.InputHTMLAttributes { + label: string; + error?: string; + hint?: string; +} + +export function Input({ label, error, hint, id, className, ...rest }: InputProps) { + const inputId = id ?? label.toLowerCase().replace(/\s+/g, "-"); + return ( +
+ + + {error && ( + + )} + {hint && !error && ( +

+ {hint} +

+ )} +
+ ); +} +``` + +--- + +## 5. Services Layer + +### 5.1 `services/api.ts` (base) + +```ts +// Single source of truth for all HTTP calls. +// Never import React or UI code here. + +const API_BASE = + typeof window === "undefined" + ? process.env.INTERNAL_API_BASE_URL ?? "" + : "/api/proxy"; + +export class ApiError extends Error { + constructor(public status: number, message: string) { + super(message); + this.name = "ApiError"; + } +} + +export async function request( + path: string, + init: RequestInit = {}, + token?: string | null, +): Promise { + 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 res = await fetch(`${API_BASE}/${path.replace(/^\//, "")}`, { + ...init, + headers, + cache: "no-store", + }); + + if (!res.ok) { + const json = await res.json().catch(() => null); + throw new ApiError(res.status, json?.message ?? `HTTP ${res.status}`); + } + return res.json() as Promise; +} +``` + +### 5.2 `services/dashboard.service.ts` + +```ts +import { request } from "./api"; +import type { DashboardSummary } from "@/types/dashboard"; + +export const dashboardService = { + getSummary: (token: string) => + request<{ success: boolean; data: DashboardSummary }>("dashboard/summary", {}, token), +}; +``` + +### 5.3 `services/auth.service.ts` + +```ts +import { request } from "./api"; +import type { LoginResponse } from "@/types/auth"; + +export const authService = { + login: (identity: string, password: string) => + request("auth/login", { + method: "POST", + body: JSON.stringify({ identity, password }), + }), +}; +``` + +--- + +## 6. Custom Hooks + +### `hooks/useDashboardSummary.ts` + +```ts +"use client"; +import { useEffect, useState } from "react"; +import { dashboardService } from "@/services/dashboard.service"; +import { getStoredToken } from "@/lib/auth"; +import type { DashboardSummary } from "@/types/dashboard"; + +export function useDashboardSummary() { + const [data, setData] = useState(null); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const token = getStoredToken(); + if (!token) { setLoading(false); return; } + dashboardService.getSummary(token) + .then(r => setData(r.data)) + .catch(e => setError(e.message)) + .finally(() => setLoading(false)); + }, []); + + return { data, error, loading }; +} +``` + +--- + +## 7. Accessibility Checklist + +| Area | Implementation | +|---|---| +| **Focus management** | All interactive elements reachable via Tab; custom components include `focus:ring-2` | +| **ARIA roles** | `role="alert"` on ``, `role="status"` + `aria-label="Loading"` on `` | +| **Form labels** | `` always renders a `