# Architecture — Said Yaqub Sadat Personal Portfolio

## 1. Architecture decision

The specification (§10.1) requires one documented approach. This delivery implements
**option (A): a React single page application that consumes a Laravel REST API**.

```
                 browser
                    │
        ┌───────────┴────────────┐
        │                        │
  React SPA (Vite)          Laravel 13 API
  frontend/                 backend/
  static assets             JSON REST API
  :5173 (dev)               :8000 (dev)
        │                        │
        │  HTTPS + JSON          │  Eloquent
        │  Bearer token for      ▼
        │  admin routes     MySQL 8 (portfolio_db)
        ▼
```

Why option (A):

* The React application is deployed as static files (GitHub Pages / any static host),
  the Laravel application runs on a PHP host. They can be scaled and released
  independently.
* Authentication is handled with **Laravel Sanctum personal access tokens** sent as
  `Authorization: Bearer <token>`. Because the dashboard is token based (not cookie
  based), CSRF token handling is not required for the API, and the API never mixes
  cookie and token assumptions. The API middleware stack is stateless (`api` group),
  the SPA is served from a different origin, and CORS is configured explicitly for the
  frontend origin.
* Business authorization decisions are always taken on the server. The client only
  hides UI that the token is not allowed to use.

## 2. Backend (Laravel 13, PHP 8.3)

```
backend/
├── app/
│   ├── Http/
│   │   ├── Controllers/
│   │   │   ├── CvController.php              # /cv download + /sitemap.xml
│   │   │   └── Api/
│   │   │       ├── PublicApiController.php   # public content + contact
│   │   │       ├── AdminAuthController.php   # login / logout / me
│   │   │       ├── AdminDashboardController.php
│   │   │       ├── AdminProjectController.php
│   │   │       ├── AdminSkillController.php
│   │   │       ├── AdminExperienceController.php
│   │   │       ├── AdminEducationController.php
│   │   │       ├── AdminCertificateController.php
│   │   │       ├── AdminMessageController.php
│   │   │       ├── AdminSettingController.php
│   │   │       ├── AdminProfileController.php   # own account + password
│   │   │       ├── AdminUserController.php       # account management
│   │   │       ├── AdminUploadController.php
│   │   │       └── AdminActivityLogController.php
│   │   └── Middleware/EnsureUserIsAdmin.php  # server-side authorization
│   ├── Models/                              # Eloquent models + relationships
│   └── Providers/AppServiceProvider.php
├── config/cors.php                          # allowed origins from FRONTEND_URL
├── config/portfolio.php                     # admin seed credentials from env
├── database/migrations/                     # every schema change
├── database/seeders/                        # verified content from Cv.pdf
├── routes/api.php                           # public + admin route groups
├── routes/web.php                           # /, /cv, /sitemap.xml
└── tests/Feature/                           # 82 automated tests
```

### Request lifecycle

1. Request enters the `api` middleware group (JSON, CORS, throttling where configured).
2. Public routes are executed directly; they only ever read **published** content.
3. Admin routes pass `auth:sanctum` (token → user) and then `admin`
   (`EnsureUserIsAdmin`, 403 when `is_admin` is false).
4. Controllers validate input with Laravel's validator, then write through Eloquent
   models with explicit `$fillable` lists (no mass assignment of unknown keys).
5. Responses use one envelope: `{"data": ..., "success": true}` for reads,
   `{"data": ..., "message": ..., "success": true}` for writes, and
   `{"message": ..., "errors": {...}}` with 422 for validation failures.

### Models and relationships

| Model | Relations | Notes |
|---|---|---|
| `User` | owns personal access tokens | `is_admin` flag gates the dashboard |
| `Project` | hasMany `ProjectImage` | `scopePublished()`, `getRouteKeyName() = 'slug'` |
| `ProjectImage` | belongsTo `Project` | caption, alt text, sort order |
| `Skill`, `Experience`, `Education`, `Certificate` | — | ordered by `sort_order` |
| `Message` | — | status workflow `new → read → replied → archived` |
| `ActivityLog` | belongsTo `User` | administrator audit trail |
| `Setting` | — | key/value site configuration, cached |

### Route model binding split

`Project::getRouteKeyName()` returns `slug` so the public detail URL is
`/api/projects/{slug}`. Dashboard routes bind explicitly to the primary key
(`/api/admin/projects/{project:id}`) so the admin UI can keep using numeric ids.

## 3. Frontend (React 19 + TypeScript + Vite + Tailwind CSS)

```
frontend/
├── src/
│   ├── api/
│   │   ├── client.ts        # axios instance, base URL, auth interceptor, storageUrl()/cvUrl()
│   │   └── index.ts         # typed endpoint wrappers (public + admin)
│   ├── components/          # Seo, ScrollToTop, PageHero, States, Modal, Navbar, Footer,
│   │                        # Slideshow, ProjectCard, ProjectQuickView, ProfilePhoto, Logo
│   ├── hooks/              # useSiteBrand (logo + name shared by the whole chrome)
│   ├── layouts/             # PublicLayout (skip link) and AdminLayout (sidebar)
│   ├── pages/public/        # Home, About, Skills, Projects, ProjectDetail, Experience,
│   │                        # Education, Certificates, Cv, Contact, Privacy, NotFound
│   ├── pages/admin/         # AdminLogin, Dashboard, Projects, Skills, Experiences,
│   │                        # Education, Certificates, Messages, Profile, Users, Settings
│   ├── types/index.ts       # shared API types
│   ├── App.tsx              # route table (public + admin)
│   └── index.css            # design tokens and base styles
├── public/                  # favicon.svg, og-image.png, profile-placeholder.png,
│                            # slides/*.svg, robots.txt, sitemap.xml
├── tailwind.config.js       # brand colour tokens
└── vite.config.ts           # dev proxy for /api and /storage
```

### Data flow

* Every page fetches through `src/api/index.ts`; components never call axios directly.
* `apiClient` adds `Authorization: Bearer …` for any URL containing `/admin/`.
  A 401 response clears the stored token and redirects to `/admin/login`.
* The API base URL is `import.meta.env.VITE_API_URL ?? '/api'`. In development the Vite
  dev server proxies `/api` and `/storage` to `http://127.0.0.1:8000`, so the
  browser sees a single origin and no CORS preflight is needed while developing.
  `/cv` is deliberately **not** proxied: the SPA owns that path for its CV page, so
  the file itself is downloaded through the API alias `/api/cv`.
* `storageUrl(path)` and `cvUrl()` build absolute URLs for files that live on the
  Laravel public disk. `cvUrl()` returns `<api-root>/api/cv`, which resolves to the
  API host in production and to the proxied API in development.
* The navigation bar and the footer read the profile through `getProfileOnce()`, a
  module-level cache that keeps the single `/api/profile` request for the session.
  The profile photo therefore reaches the shared chrome without every component
  fetching on its own.

### Interactive components

| Component | Responsibility |
|---|---|
| `Slideshow` | Cross-fading image carousel used on the home page and for project galleries. Auto-advance pauses on hover and focus, previous/next buttons and dot indicators are keyboard operable, and `prefers-reduced-motion` disables the automatic rotation |
| `ProjectCard` | Renders one project. Without an `onOpen` handler it is a link to the case study; with one it becomes a button that opens the quick view |
| `ProjectQuickView` | Dialog with the category, role, technologies, problem and results of a project plus a link to the full case study |
| `ProfilePhoto` | Renders the uploaded photo, falls back to `public/profile-placeholder.png` and finally to a monogram, so no broken image can appear |

### Design system

| Token | Value | Usage |
|---|---|---|
| Deep Navy | `#0B1B32` | headings, dark surfaces, footer |
| Primary Blue | `#087FE5` | buttons, links, active states |
| Blue Dark | `#0666B8` | hover state |
| White | `#FFFFFF` | page background, cards |
| Body text | `#1F2937` | paragraphs |
| Muted text | `#64748B` | metadata, helper text |
| Pale blue | `#BFDBFE` | soft accents, gradients |
| Success / Error | `#16A34A` / `#DC2626` | validation and status feedback |

Tokens live in `tailwind.config.js` (`brand.*`) and in CSS custom properties in
`src/index.css`. The Inter typeface is loaded from the Google Fonts CDN.

### Accessibility decisions

* One `h1` per page, ordered headings, semantic `header`/`nav`/`main`/`footer`.
* Skip-to-content link as the first focusable element of the public layout.
* All inputs have a `<label>`; errors are rendered next to the field and referenced
  with `aria-describedby`.
* The modal dialog uses `role="dialog"`, `aria-modal="true"`, closes on `Escape` and
  returns focus to the trigger.
* `focus-visible` outlines are defined globally; `prefers-reduced-motion` disables
  transitions and animations.

## 4. Content and data flow

```
Cv.pdf ──(manual)──► seeders ──► MySQL ──► Laravel API ──► React pages
                                    ▲
                              Admin dashboard writes
```

* Seeders only contain content that is verifiable in `Cv.pdf`
  (`ProjectSeeder`, `ExperienceSeeder`, `EducationSeeder`, `CertificateSeeder`,
  `SkillSeeder`, `SettingsSeeder`).
* Uploads are stored on the `public` disk (`storage/app/public`), the database keeps
  only the path — binary files are never stored in MySQL.
* `Setting::get()` caches values; after re-seeding run `php artisan cache:clear`.

## 5. Deployment topology

| Component | Local | Production |
|---|---|---|
| SPA | Vite dev server on :5173 | static files (e.g. GitHub Pages) or the web server root |
| API | `php artisan serve --port=8000` | Nginx/Apache with the document root at `backend/public` |
| Database | MySQL `portfolio_db` on 127.0.0.1:3306 | MySQL 8 with a dedicated user |
| Media | `backend/storage/app/public` | same directory, served through `/storage` symlink |

The backend redirects `/` to `FRONTEND_URL`, so the API host can be used as a single
entry point during development.

## 6. Known trade-offs

* Project categories and technologies are strings (category) and a JSON array
  (`tech_stack`) instead of normalised tables. This keeps the admin UI simple; a
  normalised `project_categories` / `project_technologies` pair would be the next step
  if categories need their own descriptions or ordering.
* The dashboard keeps its token in `localStorage`. This is acceptable for a
  single-administrator site; a stricter deployment would prefer a short-lived token
  plus refresh, or a server-side session with CSRF.
* No automated browser tests — the suite covers the API and the publication rules.
