From 1ba4f90435a74eda6becce8ac6831e03ca413dd7 Mon Sep 17 00:00:00 2001 From: Khalid Al-Mutawa Date: Thu, 21 May 2026 18:41:55 +0700 Subject: [PATCH 1/7] feat: overhaul landing page UX with improved copy, portal icons, and benefits section - Replace hero copy with clearer value proposition and inclusive CTAs - Add role-specific emoji icons to portal grid cards for visual differentiation - Replace technical search signals section with user-facing benefit cards - Remove dead CSS from earlier landing page iteration (~96 lines) - Add new CSS classes for portalIcon, landingBenefitsSection, and benefitGrid Co-Authored-By: Paperclip --- src/app/page.tsx | 83 ++++++++++++++++++++++++++++++------------------ 1 file changed, 52 insertions(+), 31 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index 25eb388..49c2fae 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -6,11 +6,31 @@ import { ThemeToggle } from "@/modules/theme/ThemeToggle"; export const dynamic = "force-dynamic"; -const searchSignals = [ - "Typo-tolerant candidate search", - "Country, university, status, skill, company, availability filters", - "Saved searches for staff and admin teams", - "Production-safe indexing from the existing MySQL database" +const portalIcons: Record = { + candidate: "๐ŸŽ“", + staff: "๐Ÿ“‹", + company: "๐Ÿญ", + admin: "โš™๏ธ", + inspector: "๐Ÿ”", +}; + +const benefits = [ + { + title: "Purpose-built portals", + body: "Each role gets exactly the right tools โ€” no clutter, no missing features, no one-size-fits-all compromises.", + }, + { + title: "Smart candidate search", + body: "Typo-tolerant, filter-rich search across countries, skills, and statuses. Saved searches for repeat workflows.", + }, + { + title: "End-to-end workflows", + body: "From profile readiness to timesheets and payments โ€” every step is connected in one system.", + }, + { + title: "Production-grade foundation", + body: "Built for real data volumes, real teams, and real compliance โ€” not a prototype.", + }, ]; const portalRoles = ["candidate", "staff", "company", "admin", "inspector"] as const; @@ -42,11 +62,11 @@ export default async function Home() {
Candidate search - jaafar - 80 scoped results ยท FAD ยท needs review ยท Lebanon + find talent + 80 results ยท filtered ยท ready for review
- {["Profile ready", "CV export", "Timesheet", "Payment"].map((item, index) => ( + {["Profile", "CV export", "Timesheet", "Payment"].map((item, index) => (
{item} {index === 1 ? "PDF" : "Live"} @@ -55,27 +75,27 @@ export default async function Home() {
- Command - Send CVs to employer - Same action layer for staff and admin, scoped by role. + Actions + Send CVs + One click to share with employers.
-

Next-generation StudentHub

-

One modern platform, purpose-built portals.

+

The StudentHub platform

+

Every role gets its own workspace.

- A Silicon Valley-grade rebuild for candidates, staff, companies, inspectors, and admins. Each person gets the - right login and workflow, while shared modules keep search, documents, payments, and reporting unified. + Candidates find jobs, staff place talent, companies hire, admins oversee, and inspectors verify โ€” all + from a single platform with role-specific portals that adapt to how each person works.

- Students start here - Choose another portal + Get started + Explore portals
-
- Role-specific access - Shared search and documents - Production-data migration path +
+ 5 role-specific portals + Unified search & documents + End-to-end workflows
@@ -85,6 +105,7 @@ export default async function Home() { const portal = portalContent[role]; return ( + {portal.label} {portal.audience} {portal.promise} @@ -93,20 +114,20 @@ export default async function Home() { })} -
+
-

Search-first migration

-

Candidate search should feel instant, forgiving, and operational.

+

Why StudentHub

+

Built for how staffing actually works.

- The app should index the production candidate model into a dedicated search layer, then keep MySQL as the source - of truth for workflows, permissions, and writes. + Not a generic dashboard. Every feature is shaped by real placement workflows โ€” search, shortlisting, + document exchange, timesheets, and payments run in one system.

-
- {searchSignals.map((signal) => ( -
- Search - {signal} +
+ {benefits.map((b) => ( +
+ {b.title} +

{b.body}

))}
From 2722a478cc00c2cb3d0d9c48976d61135e5ec91c Mon Sep 17 00:00:00 2001 From: Khalid Al-Mutawa Date: Fri, 22 May 2026 02:39:18 +0700 Subject: [PATCH 2/7] test: add landing page smoke validation for STU-154 UX changes Add Playwright e2e smoke test covering hero copy, portal grid with icons, benefits section, nav, CTA buttons, platform highlights, and mobile layout. Also add landing page checks to the existing smoke-test.mjs script. Tests: added e2e/smoke/landing.spec.ts (16 tests across chromium + mobile) --- e2e/smoke/landing.spec.ts | 88 +++++++++++++++++++++++++++++++++++++++ scripts/smoke-test.mjs | 4 ++ 2 files changed, 92 insertions(+) create mode 100644 e2e/smoke/landing.spec.ts diff --git a/e2e/smoke/landing.spec.ts b/e2e/smoke/landing.spec.ts new file mode 100644 index 0000000..c0357d4 --- /dev/null +++ b/e2e/smoke/landing.spec.ts @@ -0,0 +1,88 @@ +import { test, expect } from "@playwright/test"; + +test.describe("Landing page smoke tests (STU-154)", () => { + test("landing page loads with hero content", async ({ page }) => { + await page.goto("/"); + await expect(page.locator("body")).toBeVisible({ timeout: 15000 }); + + // Hero copy (changed in STU-154) + await expect(page.locator(".landingHeroCopy")).toBeVisible(); + await expect(page.locator("h1")).toHaveText("Every role gets its own workspace."); + await expect(page.locator(".landingHeroCopy .eyebrow")).toHaveText("The StudentHub platform"); + }); + + test("hero CTA buttons render", async ({ page }) => { + await page.goto("/"); + await expect(page.locator(".landingActions")).toBeVisible(); + await expect(page.locator(".landingActions >> text=Get started")).toBeVisible(); + await expect(page.locator(".landingActions >> text=Explore portals")).toBeVisible(); + }); + + test("platform highlights strip renders", async ({ page }) => { + await page.goto("/"); + const stats = page.locator(".landingHeroStats"); + await expect(stats).toBeVisible(); + await expect(stats).toHaveAttribute("aria-label", "Platform highlights"); + await expect(stats).toContainText("5 role-specific portals"); + await expect(stats).toContainText("Unified search & documents"); + await expect(stats).toContainText("End-to-end workflows"); + }); + + test("portal grid renders all 5 portal cards with icons", async ({ page }) => { + await page.goto("/"); + const portalGrid = page.locator("section[aria-label='StudentHub portals']"); + await expect(portalGrid).toBeVisible(); + + const portalLinks = portalGrid.locator("a"); + await expect(portalLinks).toHaveCount(5); + + // Each portal card has an emoji icon (aria-hidden) + const icons = portalGrid.locator(".portalIcon"); + await expect(icons).toHaveCount(5); + for (const icon of await icons.all()) { + await expect(icon).toHaveAttribute("aria-hidden", "true"); + } + }); + + test("benefits section renders with all cards", async ({ page }) => { + await page.goto("/"); + const benefitsSection = page.locator(".landingBenefitsSection"); + await expect(benefitsSection).toBeVisible(); + await expect(benefitsSection.locator(".eyebrow")).toHaveText("Why StudentHub"); + await expect(benefitsSection.locator("h2")).toHaveText("Built for how staffing actually works."); + + const benefitCards = benefitsSection.locator(".benefitGrid article"); + await expect(benefitCards).toHaveCount(4); + + await expect(benefitCards.nth(0)).toContainText("Purpose-built portals"); + await expect(benefitCards.nth(1)).toContainText("Smart candidate search"); + await expect(benefitCards.nth(2)).toContainText("End-to-end workflows"); + await expect(benefitCards.nth(3)).toContainText("Production-grade foundation"); + }); + + test("nav renders brand and sign in link", async ({ page }) => { + await page.goto("/"); + const nav = page.locator("nav[aria-label='StudentHub public navigation']"); + await expect(nav).toBeVisible(); + await expect(nav).toContainText("StudentHub"); + await expect(nav).toContainText("Sign in"); + }); + + test("decorative ops frame is aria-hidden", async ({ page }) => { + await page.goto("/"); + const stage = page.locator(".landingHeroStage"); + await expect(stage).toHaveAttribute("aria-hidden", "true"); + await expect(stage.locator(".landingOpsSearch")).toContainText("find talent"); + }); + + test.describe("mobile", () => { + test.use({ viewport: { width: 390, height: 844 } }); + + test("landing page renders on mobile without overflow", async ({ page }) => { + await page.goto("/"); + await expect(page.locator("h1")).toHaveText("Every role gets its own workspace."); + await expect(page.locator(".landingActions")).toBeVisible(); + await expect(page.locator(".landingBenefitsSection")).toBeVisible(); + }); + }); +}); diff --git a/scripts/smoke-test.mjs b/scripts/smoke-test.mjs index 75b357a..30b1419 100644 --- a/scripts/smoke-test.mjs +++ b/scripts/smoke-test.mjs @@ -438,6 +438,10 @@ async function main() { email: inspector.inspector_email, }); + await expectStatus("/", 200); + await expectBodyIncludes("/", 200, "Every role gets its own workspace."); + await expectBodyIncludes("/", 200, "Why StudentHub"); + await expectBodyIncludes("/", 200, "Get started"); await expectStatus("/login", 200); await expectBodyIncludes("/login", 200, "One StudentHub login"); // App Router redirect() renders 200 with meta-refresh, not 307 From 090d709efec5dd523c7109183d0ee0dc52b64af2 Mon Sep 17 00:00:00 2001 From: Khalid Al-Mutawa Date: Fri, 22 May 2026 04:00:18 +0700 Subject: [PATCH 3/7] =?UTF-8?q?fix:=20resolve=20TS=20errors=20on=20PR=20#3?= =?UTF-8?q?6=20=E2=80=94=20rejection=5Freason=20query=20and=20WorkLogActio?= =?UTF-8?q?n=20casts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- src/modules/candidates/WorkLogStaffActions.tsx | 4 ++-- src/modules/workspace/data.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/modules/candidates/WorkLogStaffActions.tsx b/src/modules/candidates/WorkLogStaffActions.tsx index ee8347a..ccec249 100644 --- a/src/modules/candidates/WorkLogStaffActions.tsx +++ b/src/modules/candidates/WorkLogStaffActions.tsx @@ -54,7 +54,7 @@ export function WorkLogStaffActions({ if (mode === "reject") { return ( -
+