# Work Progress — Tafsin Naeem Fashion Website

Tracks completion against `Fashion_Photographer_Website_UI_Prompts.md`. Each entry is added when a phase is finished and verified.

---

## Phase 0 — Project Setup ✅ Done (2026-07-30)

- Laravel 12 project created (via `composer create-project laravel/laravel:^12.0`), Breeze installed with `--stack=vue --dark`, giving Inertia.js + Vue 3 (Composition API, `<script setup>`).
- Auth scaffolding (login/register/etc.) kept intact and functional; `GuestLayout.vue` restyled off the default gray/blue Breeze look onto the new `cream`/`ink` palette. Auth pages (Login/Register/Profile forms) still use Breeze's original component styling — full restyle deferred to the future admin-panel phase, not required for the public site.
- Removed the default `Pages/Welcome.vue` (replaced by `Pages/Public/Home.vue`).
- Installed: `@vueuse/core`, `vue3-carousel`, `vue-easy-lightbox`, `aos`, `lucide-vue-next`, `@fontsource/cormorant-garamond`, `@fontsource/inter`.
- Fonts loaded via `@fontsource` imports in `resources/css/app.css` (no CDN): Cormorant Garamond (serif, headings) + Inter (sans, body).
- Tailwind (`tailwind.config.js`) extended with:
  - Colors: `ink` (near-black scale), `cream` (off-white scale), `gold` (accent scale, base `#C9A961`).
  - `fontFamily.serif` / `fontFamily.sans` mapped to the two fonts above.
  - `maxWidth.container` (1440px) and a `section` / `section-sm` spacing scale.
- Folder structure created:
  - `resources/js/Layouts/PublicLayout.vue` (bare wrapper for now — NavBar/Footer wired in Phase 1)
  - `resources/js/Components/Public/` (contains `SeoHead.vue` so far)
  - `resources/js/Pages/Public/{Home,About,Gallery,Video,Store,Contact}.vue` (placeholder content only)
  - `resources/images/placeholder/` (empty, ready for Phase 2+ placeholder images)
- Routes added in `routes/web.php`: `/`, `/about`, `/gallery`, `/video`, `/store`, `/contact` — all named, all rendering their `Public/*` Inertia page.
- `SeoHead.vue` built using `@inertiajs/vue3`'s `<Head>`, accepts `title` / `description` / `ogImage` props, used on all 6 placeholder pages.
- Verified: `npm run build` succeeds with no errors; booted `php artisan serve` and curled all 6 routes → all return HTTP 200 with no server errors logged.

**Not done yet (intentionally, per spec):** no real page content — that starts in Phase 1 (shared layout components) and Phase 2 (Home page).

---

## Phase 1 — Design System & Shared Layout Components ✅ Done (2026-07-30)

- `NavBar.vue`: fixed/sticky, transparent on load, transitions to solid `cream` background past 40px scroll (via `@vueuse/core`'s `useWindowScroll`). Serif text logo left, uppercase nav links right with underline-on-hover reveal. "Store" link has a small "Soon" badge. Mobile: hamburger (lucide `Menu`/`X`) opens a full-screen dark overlay with large centered serif links and a fade transition.
- `Footer.vue`: dark `ink-900` background, `cream` text. 4-column grid — brand blurb + social icons (Instagram/YouTube/Facebook via `lucide-vue-next`, placeholder `#` links), quick links (repeats nav), contact snippet, newsletter signup (local `ref` only, no backend yet — shows a "Thanks — you're on the list" message on submit). Bottom bar with copyright + credit line.
- `PageHero.vue`: reusable, props `title` / `subtitle` / `backgroundImage`, dark gradient overlay for legibility, serif title with `data-aos="fade-up"` entrance.
- `SectionHeading.vue`: reusable, props `eyebrow` / `title` / `align` (`left`|`center`), thin decorative underline accent, AOS fade-up.
- `Button.vue`: reusable, variants `primary` / `outline` / `ghost`; renders as an Inertia `<Link>` when given an `href` prop, otherwise a `<button>`. Subtle scale/underline hover states.
- `PublicLayout.vue` wired: `NavBar` + `<slot />` + `Footer`, initializes AOS on mount (respects `prefers-reduced-motion` via AOS's `disable` option) and scrolls to top + refreshes AOS on every Inertia `navigate` event.
- Verified: `npm run build` succeeds with no errors; all 6 public routes still return HTTP 200 after wiring the new layout.

**Note:** placeholder page bodies (Phase 0's "coming in Phase N" text) don't yet use `PageHero`, so the transparent navbar currently has nothing dark to sit over on first load — this resolves itself once Phase 2/3 add real hero sections.

## Phase 2 — Home Page ✅ Done (2026-07-30)

- Generated a full local placeholder-image set (`public/images/placeholder/*.svg`, gradient tiles with a small unobtrusive corner watermark, no CDN dependency): `hero.svg`, `about-teaser.svg`, `about-portrait.svg`, `instagram-1..6.svg`, `bts-1..5.svg`, `gallery-1..20.svg` (20 items spread across Editorial/Runway/Campaign/Portrait/Lookbook with varied aspect ratios, shared by both Home's preview and the future Phase 4 full gallery).
- Built `Pages/Public/Home.vue` with its own custom hero (not `PageHero`) — full-viewport background image with a scroll-linked parallax offset (`@vueuse/core`'s `useWindowScroll` + `usePreferredReducedMotion`, parallax disabled when reduced-motion is preferred), serif name + tagline, animated scroll-down chevron.
- Sections built in spec order: intro pull-quote → asymmetric gallery preview (8 items, hover zoom + caption overlay, "View Full Gallery →" to `/gallery`) → About teaser (split layout, photo/text sides) → Video highlight (YouTube thumbnail via `img.youtube.com/vi/{id}/maxresdefault.jpg` pattern, placeholder ID, links through to `/video`) → Instagram-style strip (6 squares + "Follow on Instagram" CTA) → full-width dark Contact CTA banner.
- All sections responsive (mobile stacks split layouts), staggered AOS reveal on grids.
- **Bug caught during visual QA and fixed:** the first version of `hero.svg` had "Tafsin Naeem" baked into the image as large centered text, which visually collided with the real `<h1>` overlaid on top. Regenerated all placeholder SVGs to use a small, low-opacity corner watermark instead of centered text, so no future overlay/caption can collide with baked-in image text.
- Verified via a real Playwright-driven Chromium session (no `chromium-cli` on this Windows box, so installed `playwright` + `chromium` into the scratchpad and drove it directly): zero console/page errors, all 6 routes 200, hero renders cleanly, gallery/about/video/instagram/CTA sections all render as expected in a real viewport screenshot. (Note: a `fullPage` stitched screenshot showed two artifacts — Instagram images appearing "missing" and faint footer text ghosting at the very top — both confirmed to be Playwright full-page-capture quirks from the fixed navbar + native `loading="lazy"` images, not real bugs; a plain viewport screenshot at scroll-top is clean.)

## Phase 3 — About Page ✅ Done (2026-07-30)

- Built `Pages/Public/About.vue`: `PageHero` (title "About", subtitle, `about-portrait.svg` background) → two-column bio (portrait image + serif pull-quote + biography copy) → Specialties grid (5 items — Editorial/Runway/Campaign/Portrait/Lookbook — via `lucide-vue-next` icons `Newspaper`/`Footprints`/`Megaphone`/`UserRound`/`BookOpen`, 5-col desktop down to 2-col mobile) → Behind the Scenes horizontal-scroll strip (5 `bts-*.svg` placeholders) → Clients/Brands logo strip (6 grayscale-to-color-on-hover placeholder tiles) → closing dark CTA to `/contact`.
- Same design language as Home (serif headings, `SectionHeading`/`Button` reused, AOS fade-up throughout), fully responsive.
- Verified with a Playwright screenshot of `/about`: zero console errors, all sections render correctly, no layout breakage.

## Phase 4 — Gallery Page ✅ Done (2026-07-30)

- Built `Pages/Public/Gallery.vue`: `PageHero` → filter bar (`All`/`Editorial`/`Runway`/`Campaign`/`Portrait`/`Lookbook` pills, gold underline on active, client-side filtering, cross-fades the whole grid via `<Transition mode="out-in">` keyed on the active filter — no page reload) → masonry grid via CSS `columns` (1/2/3/4 responsive, `break-inside-avoid`, no masonry JS library) → "Load More" (client-side, reveals 8 more of the filtered set at a time).
- Dataset: `photos` prop defaults to a 20-item dummy array (`{ id, src, category, title }`, matching the 20 `gallery-*.svg` placeholders generated in Phase 2, 4 per category) — accepting `photos` as a prop means swapping in real data from a Laravel controller later is a one-line change (`<Gallery :photos="..." />`), matching the spec's requirement.
- Hover: zoom + dark overlay with category/title caption (same pattern as Home's gallery preview). Click opens `vue-easy-lightbox` with prev/next nav, zoom controls, caption, and close button. Images use `loading="lazy"`.
- **Bug caught during testing and fixed:** `PageHero`'s default `backgroundImage` prop still pointed at a non-existent `/images/placeholder/hero.jpg` (leftover from before I switched all placeholders to `.svg`) — every page using `PageHero` without an explicit image (Gallery, and later Video/Store/Contact) was 404ing on that background and logging a console error. Fixed the default to `hero.svg` and gave Gallery its own explicit hero image.
- Verified functionally with a scripted Playwright session (not just a visual screenshot): filtering by category returns the correct count, Load More correctly expands 8→16 items, the lightbox opens with working prev/next/close, and zero console errors after the `PageHero` fix.

## Phase 5 — Video Page ✅ Done (2026-07-30)

- Built `Pages/Public/Video.vue`: `PageHero` → responsive grid of 6 video cards (3-col desktop / 1-col mobile) → dark "More on YouTube" CTA banner with a `Youtube` icon and a "Subscribe on YouTube" button linking to a placeholder channel URL.
- Each card: YouTube thumbnail via the `img.youtube.com/vi/{id}/maxresdefault.jpg` pattern with a play-button overlay; clicking swaps the thumbnail for an inline embedded `<iframe>` player in place (approach (a) from the spec, not a modal), title + description below.
- Dataset: local `videos` ref, 6 items shaped `{ id, youtubeId, title, description }` — easy to move into the database/admin panel later.
- **Bug caught during testing and fixed:** 2 of the 6 placeholder YouTube IDs I picked from memory didn't actually have a `maxresdefault.jpg` (some videos only have lower-res thumbnails), causing silent 404s on those two cards. Verified all 6 IDs with `curl` before finalizing and swapped the two bad ones for confirmed-working IDs.
- **Second bug caught and fixed (affects every page, not just Video):** `Button.vue`'s `href` mode always rendered through Inertia's `<Link>`, but Inertia only skips its own client-side interception when the anchor has a non-`_self` `target` — so external links (Home/Video's "Follow on Instagram" / "Subscribe on YouTube" buttons pointing at `https://...`) would have fired a broken cross-origin Inertia visit instead of actually navigating. Fixed `Button.vue` to render plain `<a target="_blank" rel="noopener noreferrer">` for `http(s)://` hrefs, and a real (non-Inertia) inert anchor for placeholder `#` hrefs, keeping `<Link>` only for genuine internal routes.
- Verified via scripted Playwright: clicking a thumbnail correctly swaps in a working `<iframe>`, zero console/network errors across the whole page after both fixes.

## Phase 6 — Store (Coming Soon) Page ✅ Done (2026-07-30)

- Built `Pages/Public/Store.vue`: skips `PageHero` for a custom full-height (`min-h-[85vh]`) centered layout, keeps `PublicLayout`'s `NavBar`/`Footer` so it doesn't feel disconnected from the rest of the site.
- Background: a tasteful animated dark gradient (`ink-900`→`ink-800`→`ink-900`, panning via a Tailwind `gradient-pan` keyframe added to `tailwind.config.js`, `motion-safe:` prefixed so it's automatically disabled under `prefers-reduced-motion`) rather than a placeholder photo — avoids a fake "broken image" look.
- Large serif "Store — Coming Soon" heading, supporting copy (prints/presets/merchandise), and an email signup form ("Notify Me") — UI-only per spec: a local `ref` simulates the submit (with a brief "Sending…" state) and swaps in a success message; a `TODO` comment marks where a real `useForm(...).post('/store/notify')` call would go once the backend exists. Gentle `data-aos="fade"` entrance on load.
- Verified via scripted Playwright: filling the email and clicking "Notify Me" correctly shows the success message, zero console errors.

## Phase 7 — Contact Page ✅ Done (2026-07-30)

- Backend: `ContactController@store` (new), validates `name`/`email`/`subject`/`message` (required, valid email, max lengths) and flashes a success message back — no email sending yet, marked with a `TODO` comment for Laravel Mail. Route `POST /contact` (`contact.store`) added in `routes/web.php`. `HandleInertiaRequests` now shares a `flash.success` prop so the frontend can read it.
- Built `Pages/Public/Contact.vue`: `PageHero` → two-column layout — left: form (Name, Email, Shoot-Type dropdown, Message) built with Inertia's `useForm()`, native HTML5 required/email validation, `form.processing` loading state on submit, server-side validation errors shown per-field via `form.errors`, and the flashed success message rendered above the form; right: contact info block (email/phone/studio address/business hours/social icons) plus an embedded Google Map (`maps.google.com/maps?q=...&output=embed` — no API key needed, `grayscale` filter to match the site's palette). Below both columns: a 4-question FAQ accordion using the CSS `grid-template-rows: 0fr → 1fr` technique for a smooth height animation (no JS height measuring).
- **Significant infrastructure bug found and fixed (affects the whole app, not just Contact):** `.env` had `SESSION_DRIVER=database`, but the fresh Laravel 12 skeleton never actually generated a `sessions` table migration (confirmed no such file anywhere, and `php artisan session:table` refuses to generate one due to an upstream Laravel quirk — its "does a migration already exist" check unconditionally treats the presence of the squashed `..._create_users_table.php` file as "yes"). Despite that, a `sessions` table already existed in `database.sqlite` (likely auto-created by an early framework bootstrap step) — so session writes were succeeding, but the flash-message read-back was legitimately failing for a different, more interesting reason (see below), which made this look like a session-table problem at first. Chased it down with direct DB inspection, a manual `Store`-level session test, and finally request/response-level `Log::info` tracing in the controller and middleware.
- **Root cause of the "success message not showing" symptom — it wasn't actually broken:** my first two test scripts read `document.getElementById('app').dataset.page` to check the flash prop, but Inertia only sets that attribute on the very first server-rendered page load — client-side visits (like the POST→redirect→GET after a form submit) update Inertia's in-memory/reactive page state, not that static DOM attribute. So my "no flash after submit" reading was a false alarm from a flawed test method, not a real bug. Verified this by waiting for the actual rendered `<p>{{ successMessage }}</p>` text in the DOM instead (the real signal the user would see) — it appears correctly.
- Verified via scripted Playwright: FAQ expands/collapses on click, the "Name" field is enforced as required, and after a full valid submission the real success message renders in the DOM with zero console errors.

## Phase 8 — Final Polish Pass ✅ Done (2026-07-30)

- **Architecture fix enabling proper page transitions:** every page previously self-wrapped with `<PublicLayout>...</PublicLayout>` in its own template, which meant Inertia destroyed and recreated the entire layout (including `NavBar`/`Footer`) on *every* navigation — no persistent element for a `<Transition>` to animate against. Refactored all 6 public pages + the new `Error.vue` to Inertia's persistent-layout pattern (`defineOptions({ layout: PublicLayout })`, single root `<div>` per page instead of the `<PublicLayout>` wrapper). `PublicLayout` now mounts once and stays mounted across navigations; `NavBar`/`Footer` no longer remount on every page change.
- **Global page-transition effect:** `PublicLayout.vue` now wraps `<slot />` in `<Transition name="page" mode="out-in">` (short 200ms opacity fade, matching the site's "tasteful, fast" animation rule), which only actually works correctly because of the persistent-layout fix above. Disabled under `prefers-reduced-motion` via a media query.
- **Loading progress bar:** Inertia's built-in `progress` option (already present from Breeze) — recolored from the default gray to the site's gold accent (`#C9A961`).
- **Bug fixed — double-suffixed page titles:** `app.js` had Breeze's default `title: (title) => \`${title} - ${appName}\`` callback, but every page's `SeoHead` component *also* builds its own full title (`"Home — Tafsin Naeem"`). Together these would have produced `"Home — Tafsin Naeem - Tafsin Naeem"`. Removed the app.js-level title callback so each page's own title is used as-is. Also fixed `APP_NAME` (was still the Laravel default) in `.env`/`.env.example`.
- **Images:** audited every `<img>` — all have `alt` text, `loading="eager"` only on above-the-fold hero images and `loading="lazy"` everywhere else, and every image sits inside an `aspect-*`-constrained or fixed-size grid container so nothing causes layout shift while loading.
- **Code cleanliness:** audited every Public component/page for unused imports — none found. Component naming is consistent (`PascalCase` files, `Public/` namespace for all shared/public-site components).
- **Favicon + meta tags:** added `<link rel="icon">` to `app.blade.php` (favicon already existed at `public/favicon.ico`), removed the leftover Bunny Fonts CDN `<link>` tags (no longer needed since Phase 0 already self-hosts fonts via `@fontsource`). Generated a real branded default OG image (`public/images/og-default.png`, 1200×630) since `SeoHead.vue`'s default `ogImage` prop pointed at a file that never existed — every page already sets its own `title`/`description` via `SeoHead`.
- **`prefers-reduced-motion` audit:** confirmed respected everywhere it matters — AOS is disabled globally via its `disable` option (Phase 1), the Home hero parallax is disabled (Phase 2, `usePreferredReducedMotion`), the Store page's animated gradient uses `motion-safe:`, the scroll-down chevron bounce uses `motion-safe:`, and the new page-transition fade is disabled via a `prefers-reduced-motion` media query.
- **Custom 404 page:** built `Pages/Error.vue` matching the site's design (serif status code, message, "Back to Home" button, full `NavBar`/`Footer`), and wired it in `bootstrap/app.php` via `$exceptions->respond(...)` to render for 404/403/500/503 outside local/testing environments. Verified by temporarily booting a server with `APP_ENV=production APP_DEBUG=false` (without touching the real `.env`) and hitting a nonexistent route — confirmed HTTP 404 status and correct rendering.
- **Bug found and fixed during 404 testing:** the Error page has a plain light `cream` background with no hero image behind it, and `NavBar` was fully transparent (cream-on-cream text) until scrolled — completely invisible on any light-background page with no hero. Fixed by adding a subtle always-on dark gradient scrim (`from-ink-900/40 to-transparent`, ~96px tall) behind the nav bar in its non-scrolled state, so nav text stays legible on both photo heroes and flat light pages. Verified visually before/after on both the Home hero (no visual regression) and the 404 page (now legible).
- **Responsive audit:** scripted a check across all 6 public pages at 375/768/1024/1440px — zero horizontal overflow on any page at any width. Spot-checked mobile (375px) screenshots of Home, About, Gallery, and Contact — masonry collapses to 1 column, filter pills wrap cleanly, two-column layouts (About bio, Contact form/info) stack correctly, no cramped spacing.
- **Final full-site sweep:** scripted Playwright pass over all 6 public pages + `/login` + `/register` — zero console errors, zero failed requests (the one non-2xx response, a 301 on the Google Maps embed URL, is Maps' own normal redirect behavior, not a bug — the map still renders correctly).

### Summary of what was fixed this phase
1. Persistent-layout refactor (enables real page transitions, stops NavBar/Footer remounting every navigation).
2. Page-transition fade + gold progress bar.
3. Double-suffixed page `<title>` bug.
4. Missing/broken `PageHero` default background image (`hero.jpg` → `hero.svg`, caught in Phase 4 but worth restating here as part of the overall image audit).
5. Missing OG default image (generated one).
6. Invisible NavBar on light-background pages with no hero (caught via the new 404 page).
7. Custom on-brand 404 page + proper Laravel/Inertia exception wiring.

### Still needs real content before this goes live
- All photography/video is placeholder (generated SVG gradient tiles + a handful of public YouTube IDs standing in for real behind-the-scenes videos) — swap everything under `public/images/placeholder/` and the `youtubeId`s in `Video.vue`/`Home.vue`.
- All copy (bio, FAQ answers, client names, business hours, address) is placeholder text — needs the real photographer's voice/details.
- Contact details (email, phone, Gulshan/Dhaka address, social links) are placeholders — the Google Map embed on `/contact` currently points at a generic "Gulshan, Dhaka" query, not a real studio address.
- Social links (Instagram/YouTube/Facebook icons in the Footer and Contact page) are all `href="#"` placeholders.
- Contact form sends no email yet (flags success but doesn't notify anyone) — `TODO` left in `ContactController` for Laravel Mail once real inbox details exist.
- Store notify form is UI-only (no backend endpoint wired) — `TODO` left in `Store.vue`.
- No Inertia SSR — OG meta tags render correctly for real browsers, but a non-JS social-media crawler (Facebook/Twitter/LinkedIn preview bots) won't see them without setting up SSR separately; worth doing before heavy social sharing is expected.
- Auth pages (Login/Register/Profile) still carry Breeze's default styling, not the site's editorial design — intentionally deferred, since a future admin panel phase will need real auth UI anyway.
