# Requirements — Said Yaqub Sadat Personal Portfolio

Source specification: `Said_Yaqub_Sadat_Portfolio_Complete_Documentation.docx`
(§2 Goals, §5 Information architecture, §8 Portfolio, §17 Functional requirements, §18 Acceptance criteria).

## 1. Purpose

A public portfolio website that presents Said Yaqub Sadat's verified professional
history (projects, skills, employment, education, certificates), offers a working CV
download and a validated contact form, and is managed through a protected
administrator dashboard.

## 2. Users and access

| Role | Access | Authentication |
|---|---|---|
| Public visitor | Published projects, skills, experience, education, certificates, CV download, contact form | None |
| Administrator | Dashboard, CRUD for all content, media uploads, messages, site settings, activity log | Laravel Sanctum personal access token (Bearer) |

Authorization is enforced on the server for every admin route
(`auth:sanctum` + `App\Http\Middleware\EnsureUserIsAdmin`). The React router guard is a
convenience only and never the security boundary.

## 3. Functional requirements

Status legend: **Done** = implemented and exercised by an automated test or a manual
end-to-end check; **Partial** = implemented but narrower than the specification text;
**Not done** = deliberately out of scope for this delivery.

| ID | Requirement | Priority | Status | Where |
|---|---|---|---|---|
| FR-01 | Responsive public pages and navigation | Must | Done | `frontend/src/pages/public/*`, `Navbar`, `Footer`, `PublicLayout` |
| FR-02 | Database-backed project listing and detail pages | Must | Done | `PublicApiController::projects/project`, `pages/public/Projects.tsx`, `ProjectDetail.tsx` |
| FR-03 | Admin authentication and authorization | Must | Done | `AdminAuthController`, `EnsureUserIsAdmin`, `AdminProtectedRoute` |
| FR-04 | CRUD for projects, skills, experience, education, certificates | Must | Done | `Admin*Controller` classes, `pages/admin/Admin*.tsx` |
| FR-05 | CV download | Must | Done | `GET /cv` (`CvController::download`), CV page (linked from the footer) and `GET /api/cv` |
| FR-06 | Validated contact form and message inbox | Must | Done | `PublicApiController::contact`, `AdminMessageController`, `pages/admin/AdminMessages.tsx` |
| FR-07 | Secure image uploads and media management | Must | Partial | `AdminUploadController` (type/size limits, generated names, path sanitising). A standalone media-library screen is not built; uploads are attached from the project and settings screens |
| FR-08 | SEO metadata, sitemap, and social previews | Must | Done | `components/Seo.tsx`, `GET /sitemap.xml`, `public/robots.txt`, `public/og-image.png` |
| FR-09 | Search and project category filtering | Should | Done | `GET /api/projects?search=&category=&tech=&featured=`, `pages/public/Projects.tsx` (URL-driven state) |
| FR-10 | Draft preview and publication workflow | Should | Partial | Draft/archived/published states with server-side publication control are implemented; an in-dashboard preview page for unpublished projects is not |
| FR-11 | Dark/light theme | Should | Not done | The site ships a single light theme using the specified palette. A dark theme was not requested as "must" and is not part of this delivery |
| FR-12 | Technical blog | Optional | Not done | Optional in the specification; no blog tables, routes or pages were built |
| FR-13 | Multiple languages | Optional | Not done | Optional in the specification; the site is English only (CV language list is displayed as data) |
| FR-14 | Privacy-conscious analytics | Optional | Not done | Optional in the specification; no analytics or tracking scripts are embedded |

### 3.1 Project content requirements (§8)

Implemented per project: title, unique slug, short description, full description,
category, status, role, technologies, start/end date, featured flag, sort order,
cover image, multiple screenshots with caption and alt text, optional live/repository
links, and the case-study sections *problem*, *solution*, *challenges*, *results*.

Publication states are `draft`, `published`, `archived`; only `published` projects are
returned by the public API (verified by `ProjectPublicationTest`).

Categories are free-text values seeded from the CV
(`LMS & Education Systems`, `MIS & Databases`, `Web Applications`). The specification
lists a `project_categories` table with a many-to-many mapping; the implementation keeps
a single category string plus a `tech_stack` JSON list, which is documented in
[database.md](./database.md#5-specification-mapping).

### 3.2 Contact form requirements (§13)

| Requirement | Implementation |
|---|---|
| Fields | name, email, subject, message |
| Server-side validation | name ≤120, valid email ≤180, subject ≤150, message 10–2000 characters |
| Spam mitigation | hidden honeypot field `website` (`nullable|max:0`) |
| Rate limiting | 5 requests per minute per IP on `POST /api/contact` |
| Safe persistence | Stored in `messages` before any notification is attempted |
| Honest delivery reporting | Response contains `mail_sent` (boolean) and `success`; a stored message is never presented as a delivered email |
| Access restriction | Message endpoints live in the admin-only route group |

### 3.3 CV and media requirements (§14)

* The CV is stored on the `public` disk under `cv/` and exposed through the stable
  route `GET /cv`; the SPA downloads it through the API alias `GET /api/cv` (the
  `cvUrl()` helper), so a single-origin deployment can keep the SPA page at `/cv`.
* Image uploads accept `jpeg, png, jpg, gif, svg, webp` up to 5 MB; the CV upload
  accepts PDF up to 10 MB; the profile photo accepts `jpeg, png, jpg, webp` up to
  5 MB and is stored under `profile/`.
* Original file names are slugged and a timestamp is appended, so the stored name is
  never the client supplied one.
* Deletion only accepts paths inside the managed folders `projects/`, `certificates/`,
  `cv/` and `profile/`; traversal segments are rejected.
* The profile photo is optional: the `photo` setting may be empty, in which case the
  site renders the branded placeholder `frontend/public/profile-placeholder.png`.

## 4. Non-functional requirements

| Area | Requirement | Status |
|---|---|---|
| SEO | Unique title/description per public page, canonical URL, Open Graph tags, sitemap XML, robots.txt, clean URLs | Done |
| Accessibility | Semantic landmarks and headings, labelled form controls, keyboard-operable navigation and dialogs, visible focus, skip link, `prefers-reduced-motion` support, status not conveyed by colour alone | Done |
| Performance | Targets: LCP ≤ 2.5 s, INP ≤ 200 ms, CLS ≤ 0.1 (75th percentile) | Targets are **not measured** on a deployed site in this delivery. The build produces a 132 kB gzipped JS bundle and a 7 kB gzipped CSS bundle, images are lazy-loaded and non-critical media is deferred. Measurement requires a deployed origin and a real-user or lab run |
| Security | Server-side authorization, input validation, rate limiting, upload restrictions, path traversal protection, hashed passwords, tokens never in URLs, secrets outside source control, debug off in production | Done |
| Maintainability | Migrations for all schema changes, explicit fillable policies, shared API client, design tokens, documented API | Done |

## 5. Acceptance criteria (§18) and verification

| Criterion | How it is verified |
|---|---|
| Navigation | Manual pass over every public route and CTA; `frontend/src/App.tsx` route table |
| Responsive UI | Manual review at mobile/tablet/desktop widths in the browser |
| Authentication | `AdminAuthenticationTest` — anonymous requests rejected (401), non-admin token rejected (403) |
| Authorization | `AdminAuthenticationTest`, `AdminCrudTest` — all writes are admin-only and server-checked |
| CRUD | `AdminCrudTest` — projects, certificates, skills, messages, settings |
| Publication | `ProjectPublicationTest` — draft/archived invisible publicly, list and categories clean |
| Uploads | `FileUploadTest` — non-image rejected, oversized rejected, path traversal rejected, valid upload stored |
| Contact | `ContactMessageTest` — valid stored, invalid rejected, honeypot rejected, rate limit returns 429 |
| CV | `CvDownloadTest` — PDF served with download headers, missing file returns 404, settings path honoured |
| Database | Migrations run cleanly on MySQL; relationships verified through model factories and `RefreshDatabase` |
| SEO | Per-page `<Seo />` metadata, `GET /sitemap.xml` (tested), `public/robots.txt` |
| Accessibility | Manual keyboard/contrast review; see [testing.md](./testing.md#6-manual-checks) |
| Deployment | Documented procedure in [deployment.md](./deployment.md); **not executed on a real production host** |

## 6. Out of scope for this delivery

* Technical blog, multi-language UI, privacy analytics (optional items FR-12 – FR-14).
* Dark/light theme switch (FR-11, "should").
* Automated browser (end-to-end) test suite — the repository contains API-level feature
  tests only.
* Performance measurement against a deployed domain.
