From 672b41f04fd5210e931e68077643687c0b9bf7ae Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 06:34:10 +0000 Subject: [PATCH 01/11] Redesign: new light header, footer, and Home page Ports the Home page from the redesign prototype into components/sections/home/, with all copy in en.json (homePage). Adds Plus Jakarta Sans, the scoped redesign stylesheet (app/redesign.css), the new logo/favicon, and the handoff images and videos. Pages not yet ported keep their dark styling under the new header and footer. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QzNDQSmhjgKE63rwUnwcMq --- app/[locale]/layout.tsx | 21 +- app/[locale]/page.tsx | 53 +- app/icon.png | Bin 0 -> 133222 bytes app/redesign.css | 1439 +++++++++++++++++ components/layout/SiteFooter.tsx | 90 ++ components/layout/SiteHeader.tsx | 217 +++ components/sections/home/AiScams.tsx | 130 ++ components/sections/home/CampusChapters.tsx | 82 + components/sections/home/EqualFooting.tsx | 62 + components/sections/home/FollowLinks.tsx | 61 + components/sections/home/FounderStrikes.tsx | 86 + components/sections/home/FreeResources.tsx | 37 + components/sections/home/HomeCta.tsx | 40 + components/sections/home/HomeHero.tsx | 121 ++ .../sections/home/PhoneNotifications.tsx | 60 + components/sections/home/Pillars.tsx | 44 + .../sections/home/PriceIsRightPreview.tsx | 104 ++ components/sections/home/PublicKnowledge.tsx | 33 + components/sections/home/ResearchSources.tsx | 16 + components/sections/home/icons.tsx | 56 + .../sections/home/usePrefersReducedMotion.ts | 21 + lib/i18n/dictionaries/en.json | 315 +++- lib/i18n/dictionary.ts | 115 ++ lib/social.ts | 7 + public/images/about/berkeley-tabling-1.webp | Bin 0 -> 267998 bytes public/images/about/berkeley-tabling-2.webp | Bin 0 -> 88478 bytes public/images/about/img_4422_2.webp | Bin 0 -> 359024 bytes public/images/about/img_4423_2.webp | Bin 0 -> 379110 bytes public/images/about/img_4424_2.webp | Bin 0 -> 331218 bytes public/images/about/img_8103.webp | Bin 0 -> 151004 bytes public/images/about/img_8112.webp | Bin 0 -> 205156 bytes public/images/about/img_8120.webp | Bin 0 -> 322068 bytes public/images/about/img_8126.webp | Bin 0 -> 289100 bytes public/images/about/img_8130.webp | Bin 0 -> 372956 bytes public/images/brochure/cover.webp | Bin 0 -> 41068 bytes public/images/campus/campanile.webp | Bin 0 -> 38774 bytes public/images/campus/quarry-amphitheater.webp | Bin 0 -> 236886 bytes public/images/campus/royce-hall.webp | Bin 0 -> 57640 bytes public/images/cars/2015-ford-f150.webp | Bin 0 -> 45600 bytes public/images/cars/2018-honda-civic.webp | Bin 0 -> 29570 bytes public/images/cars/2020-toyota-camry.webp | Bin 0 -> 24460 bytes public/images/home/hero-berkeley-team.webp | Bin 0 -> 337176 bytes public/images/learn/engine-bay.webp | Bin 0 -> 301752 bytes public/images/logo-vindicated.png | Bin 0 -> 133222 bytes public/videos/ayat-ashraf.mp4 | Bin 0 -> 158592 bytes public/videos/brisa-gomez.mp4 | Bin 0 -> 129331 bytes public/videos/daniel.mp4 | Bin 0 -> 479736 bytes public/videos/daryen-romero.mp4 | Bin 0 -> 337814 bytes public/videos/evanceline-tang.mp4 | Bin 0 -> 116138 bytes public/videos/fernando-cedillo-hernandez.mp4 | Bin 0 -> 97888 bytes public/videos/fiona-wangsawidjaja.mp4 | Bin 0 -> 130651 bytes public/videos/founder-rana.mp4 | Bin 0 -> 1848361 bytes public/videos/halima-cherif-hminat.mp4 | Bin 0 -> 460435 bytes public/videos/jordan-acle.mp4 | Bin 0 -> 1037270 bytes public/videos/noirit-ghosh-choudhuri.mp4 | Bin 0 -> 337610 bytes public/videos/ray-gan.mp4 | Bin 0 -> 119613 bytes public/videos/rizwaan-bana.mp4 | Bin 0 -> 170141 bytes 57 files changed, 3185 insertions(+), 25 deletions(-) create mode 100644 app/icon.png create mode 100644 app/redesign.css create mode 100644 components/layout/SiteFooter.tsx create mode 100644 components/layout/SiteHeader.tsx create mode 100644 components/sections/home/AiScams.tsx create mode 100644 components/sections/home/CampusChapters.tsx create mode 100644 components/sections/home/EqualFooting.tsx create mode 100644 components/sections/home/FollowLinks.tsx create mode 100644 components/sections/home/FounderStrikes.tsx create mode 100644 components/sections/home/FreeResources.tsx create mode 100644 components/sections/home/HomeCta.tsx create mode 100644 components/sections/home/HomeHero.tsx create mode 100644 components/sections/home/PhoneNotifications.tsx create mode 100644 components/sections/home/Pillars.tsx create mode 100644 components/sections/home/PriceIsRightPreview.tsx create mode 100644 components/sections/home/PublicKnowledge.tsx create mode 100644 components/sections/home/ResearchSources.tsx create mode 100644 components/sections/home/icons.tsx create mode 100644 components/sections/home/usePrefersReducedMotion.ts create mode 100644 lib/social.ts create mode 100644 public/images/about/berkeley-tabling-1.webp create mode 100644 public/images/about/berkeley-tabling-2.webp create mode 100644 public/images/about/img_4422_2.webp create mode 100644 public/images/about/img_4423_2.webp create mode 100644 public/images/about/img_4424_2.webp create mode 100644 public/images/about/img_8103.webp create mode 100644 public/images/about/img_8112.webp create mode 100644 public/images/about/img_8120.webp create mode 100644 public/images/about/img_8126.webp create mode 100644 public/images/about/img_8130.webp create mode 100644 public/images/brochure/cover.webp create mode 100644 public/images/campus/campanile.webp create mode 100644 public/images/campus/quarry-amphitheater.webp create mode 100644 public/images/campus/royce-hall.webp create mode 100644 public/images/cars/2015-ford-f150.webp create mode 100644 public/images/cars/2018-honda-civic.webp create mode 100644 public/images/cars/2020-toyota-camry.webp create mode 100644 public/images/home/hero-berkeley-team.webp create mode 100644 public/images/learn/engine-bay.webp create mode 100644 public/images/logo-vindicated.png create mode 100644 public/videos/ayat-ashraf.mp4 create mode 100644 public/videos/brisa-gomez.mp4 create mode 100644 public/videos/daniel.mp4 create mode 100644 public/videos/daryen-romero.mp4 create mode 100644 public/videos/evanceline-tang.mp4 create mode 100644 public/videos/fernando-cedillo-hernandez.mp4 create mode 100644 public/videos/fiona-wangsawidjaja.mp4 create mode 100644 public/videos/founder-rana.mp4 create mode 100644 public/videos/halima-cherif-hminat.mp4 create mode 100644 public/videos/jordan-acle.mp4 create mode 100644 public/videos/noirit-ghosh-choudhuri.mp4 create mode 100644 public/videos/ray-gan.mp4 create mode 100644 public/videos/rizwaan-bana.mp4 diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx index 431fb95..25a08b8 100644 --- a/app/[locale]/layout.tsx +++ b/app/[locale]/layout.tsx @@ -1,10 +1,11 @@ import "../globals.css"; +import "../redesign.css"; import Script from "next/script"; -import { Lora, Figtree } from "next/font/google"; +import { Lora, Figtree, Plus_Jakarta_Sans } from "next/font/google"; import { notFound } from "next/navigation"; -import { Nav } from "@/components/layout/Nav"; +import { SiteHeader } from "@/components/layout/SiteHeader"; import { Starfield } from "@/components/ui/Starfield"; -import { Footer } from "@/components/layout/Footer"; +import { SiteFooter } from "@/components/layout/SiteFooter"; import { locales, isValidLocale, isRtl, type Locale } from "@/lib/i18n/config"; import { getDictionary } from "@/lib/i18n/get-dictionary"; import { getRootMetadata } from "@/lib/i18n/metadata"; @@ -25,6 +26,14 @@ const figtree = Figtree({ display: "swap", }); +// Redesign typeface (see app/redesign.css). +const jakarta = Plus_Jakarta_Sans({ + subsets: ["latin"], + variable: "--font-jakarta", + weight: ["400", "500", "600", "700", "800"], + display: "swap", +}); + // Pre-render all 10 locale routes at build time export function generateStaticParams() { return locales.map((locale) => ({ locale })); @@ -55,7 +64,7 @@ export default async function RootLayout({ + + + + + From f76a907b1eb6602d229265ab62a48268b6e880dd Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 07:06:34 +0000 Subject: [PATCH 10/11] Redesign: restore general contact form, remove replaced components - /contact redirected to a General Contact tab on /join that the new Get Involved design dropped; add it back as an "Or just say hello" section (same submitContactForm action) and point /contact and the footer's Get in Touch at /join#contact. - Delete the old Nav, Footer, LanguageSwitcher and home/about/team section components that nothing imports anymore, plus lib/constants and lib/i18n/home-dict. - RTL: anchor header dropdowns to the right. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QzNDQSmhjgKE63rwUnwcMq --- app/[locale]/contact/page.tsx | 2 +- app/[locale]/join/page.tsx | 9 + app/redesign-overrides.css | 4 + components/layout/Footer.tsx | 107 ---- components/layout/LanguageSwitcher.tsx | 95 --- components/layout/Nav.tsx | 563 ------------------ components/layout/SiteFooter.tsx | 2 +- components/sections/HomeCards.tsx | 1 - components/sections/HomeDashboard.tsx | 84 --- components/sections/HomeFounder.tsx | 1 - components/sections/HomeHero.tsx | 157 ----- components/sections/HomeQuote.tsx | 1 - components/sections/HomeSections.tsx | 137 ----- components/sections/PhotoCarousel.tsx | 93 --- components/sections/RedFlagField.tsx | 114 ---- components/sections/RoadScene.tsx | 84 --- components/sections/ScamNotebook.tsx | 138 ----- components/sections/StoryCarousel.tsx | 151 ----- components/sections/StudyCard.tsx | 52 -- components/sections/TeamDirectory.tsx | 284 --------- components/sections/WollstonecraftQuotes.tsx | 125 ---- components/sections/join/ContactForm.tsx | 99 +++ .../sections/research/PriceIsRightGame.tsx | 6 +- lib/constants.ts | 125 ---- lib/i18n/dictionaries/en.json | 24 + lib/i18n/home-dict.ts | 1 - 26 files changed, 143 insertions(+), 2316 deletions(-) delete mode 100644 components/layout/Footer.tsx delete mode 100644 components/layout/LanguageSwitcher.tsx delete mode 100644 components/layout/Nav.tsx delete mode 100644 components/sections/HomeCards.tsx delete mode 100644 components/sections/HomeDashboard.tsx delete mode 100644 components/sections/HomeFounder.tsx delete mode 100644 components/sections/HomeHero.tsx delete mode 100644 components/sections/HomeQuote.tsx delete mode 100644 components/sections/HomeSections.tsx delete mode 100644 components/sections/PhotoCarousel.tsx delete mode 100644 components/sections/RedFlagField.tsx delete mode 100644 components/sections/RoadScene.tsx delete mode 100644 components/sections/ScamNotebook.tsx delete mode 100644 components/sections/StoryCarousel.tsx delete mode 100644 components/sections/StudyCard.tsx delete mode 100644 components/sections/TeamDirectory.tsx delete mode 100644 components/sections/WollstonecraftQuotes.tsx create mode 100644 components/sections/join/ContactForm.tsx delete mode 100644 lib/constants.ts delete mode 100644 lib/i18n/home-dict.ts diff --git a/app/[locale]/contact/page.tsx b/app/[locale]/contact/page.tsx index bf5c96d..eb9006b 100644 --- a/app/[locale]/contact/page.tsx +++ b/app/[locale]/contact/page.tsx @@ -9,5 +9,5 @@ export default async function ContactRedirect({ params: Promise<{ locale: string }>; }) { const { locale } = await params; - redirect(localizeHref(locale as Locale, "/join")); + redirect(localizeHref(locale as Locale, "/join#contact")); } diff --git a/app/[locale]/join/page.tsx b/app/[locale]/join/page.tsx index 1d01be3..1ad38fd 100644 --- a/app/[locale]/join/page.tsx +++ b/app/[locale]/join/page.tsx @@ -1,6 +1,7 @@ import Link from "next/link"; import { PageHero } from "@/components/sections/shared/PageHero"; import { ApplyForm } from "@/components/sections/join/ApplyForm"; +import { ContactForm } from "@/components/sections/join/ContactForm"; import { getDictionary } from "@/lib/i18n/get-dictionary"; import type { Locale } from "@/lib/i18n/config"; @@ -72,6 +73,14 @@ export default async function JoinPage({ + +
+
+

{d.contact.title}

+

{d.contact.body}

+ +
+
); } diff --git a/app/redesign-overrides.css b/app/redesign-overrides.css index 2ae4bf6..9d40826 100644 --- a/app/redesign-overrides.css +++ b/app/redesign-overrides.css @@ -499,3 +499,7 @@ outline: 3px solid var(--purple); outline-offset: 4px; } +[dir="rtl"] .rd-hdr .drop { + left: auto; + right: 0; +} diff --git a/components/layout/Footer.tsx b/components/layout/Footer.tsx deleted file mode 100644 index a5bf12e..0000000 --- a/components/layout/Footer.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import Link from "next/link"; -import { footerNav } from "@/lib/constants"; -import { localizeHref, type Locale } from "@/lib/i18n/config"; -import type { SiteDictionary } from "@/lib/i18n/dictionary"; - -type FooterDict = Pick; - -// Maps a footer link's canonical href to its dictionary-covered nav -// label, where one exists. Links without an entry here (Team, -// LinkedIn, Instagram, "Get in Touch") keep their literal label from -// lib/constants.ts. -const DICT_KEY_BY_HREF: Record = { - "/": "home", - "/about": "about", - "/team": "team", - "/inspection": "inspection", - "/fraud": "fraud", - "/documents": "documents", - "/research": "research", - "/contact": "contact", - "/join": "getInvolved", -}; - -const FOOTER_HEADING_KEY: Record = { - Navigate: "navigate", - Resources: "resources", - Connect: "connect", -}; - -const FOOTER_LINK_KEY: Record = { - LinkedIn: "linkedIn", - Instagram: "instagram", - "Get in Touch": "getInTouch", -}; - -export function Footer({ locale, dict }: { locale: Locale; dict: FooterDict }) { - return ( -
-
-
- {/* Brand */} -
- - VINdicated - -

- {dict.footer.mission} -

-
- - {/* Nav columns */} - {footerNav.map((col) => ( -
-

- {dict.footer.columns[FOOTER_HEADING_KEY[col.heading]] ?? col.heading} -

-
    - {col.links.map((link) => { - const key = DICT_KEY_BY_HREF[link.href]; - const isExternal = "external" in link && link.external; - const footerLinkKey = FOOTER_LINK_KEY[link.label]; - const label = footerLinkKey - ? (dict.footer.links[footerLinkKey] ?? link.label) - : key - ? (dict.nav?.[key] ?? link.label) - : link.label; - return ( -
  • - - {label} - -
  • - ); - })} -
-
- ))} -
- -
-

- {dict.footer.copyright} -

-

- {dict.footer.mission} -

-
-
-
- ); -} diff --git a/components/layout/LanguageSwitcher.tsx b/components/layout/LanguageSwitcher.tsx deleted file mode 100644 index 9e54642..0000000 --- a/components/layout/LanguageSwitcher.tsx +++ /dev/null @@ -1,95 +0,0 @@ -"use client"; - -import { useState, useRef, useEffect } from "react"; -import { usePathname, useRouter } from "next/navigation"; -import { locales, localeLabels, type Locale } from "@/lib/i18n/config"; - -export function LanguageSwitcher({ - currentLocale, - label = "Change language", -}: { - currentLocale: Locale; - label?: string; -}) { - const [open, setOpen] = useState(false); - const [triggerHovered, setTriggerHovered] = useState(false); - const [hoveredLocale, setHoveredLocale] = useState(null); - const ref = useRef(null); - const pathname = usePathname(); - const router = useRouter(); - - useEffect(() => { - function onClickOutside(e: MouseEvent) { - if (ref.current && !ref.current.contains(e.target as Node)) { - setOpen(false); - } - } - document.addEventListener("mousedown", onClickOutside); - return () => document.removeEventListener("mousedown", onClickOutside); - }, []); - - function switchTo(locale: Locale) { - const segments = pathname.split("/"); - segments[1] = locale; - router.push(segments.join("/")); - setOpen(false); - } - - return ( -
- - - {open && ( -
- {locales.map((locale) => ( - - ))} -
- )} -
- ); -} diff --git a/components/layout/Nav.tsx b/components/layout/Nav.tsx deleted file mode 100644 index 60ee917..0000000 --- a/components/layout/Nav.tsx +++ /dev/null @@ -1,563 +0,0 @@ -"use client"; - -import { useState, useEffect, useRef, useCallback } from "react"; -import Image from "next/image"; -import Link from "next/link"; -import { usePathname } from "next/navigation"; -import { navLinks } from "@/lib/constants"; -import { localizeHref, type Locale } from "@/lib/i18n/config"; -import type { SiteDictionary } from "@/lib/i18n/dictionary"; -import { LanguageSwitcher } from "@/components/layout/LanguageSwitcher"; - -// Types -type NavLinkType = { - label: string; - href: string; - standalone?: boolean; - children?: readonly { - label: string; - href: string; - description?: string; - icon?: string; - }[]; -}; - -type NavDict = Pick; - -const DICT_KEY_BY_HREF: Record = { - "/": "home", - "/about": "about", - "/about#mission": "aboutMission", - "/about#story": "aboutFounder", - "/about#vindicated-from": "aboutProvides", - "/team": "team", - "/inspection": "inspection", - "/inspection#engine-diagram": "inspectionEngine", - "/inspection#what-to-inspect": "inspectionChecks", - "/inspection#where-to-get": "inspectionWhere", - "/inspection#dealer-locators": "inspectionDealership", - "/fraud": "fraud", - "/fraud#red-flags": "fraudRedFlags", - "/fraud#know-your-rights": "fraudRights", - "/fraud#after": "fraudAfter", - "/documents": "documents", - "/volunteer": "volunteer", - "/research": "research", - "/contact": "contact", - "/join": "getInvolved", -}; - -const DICT_KEY_BY_LABEL: Record = { - "Who We Are": "aboutWhoWeAre", - "What Is a PPI?": "inspectionPpi", - Overview: "fraudOverview", - "Learning Resources": "learningResources", -}; - -function labelFor(link: { label: string; href: string }, dict: NavDict) { - const key = DICT_KEY_BY_LABEL[link.label] ?? DICT_KEY_BY_HREF[link.href]; - return key ? (dict.nav[key] ?? link.label) : link.label; -} - -function expandLabel(dict: NavDict, label: string) { - return dict.ui.expandSection.replace("{label}", label); -} - -export function Nav({ locale, dict }: { locale: Locale; dict: NavDict }) { - const [scrolled, setScrolled] = useState(false); - const [menuOpen, setMenuOpen] = useState(false); - const [activeDropdown, setActiveDropdown] = useState(null); - const closeTimer = useRef | null>(null); - const pathname = usePathname(); - - const clearCloseTimer = () => { - if (closeTimer.current) { - clearTimeout(closeTimer.current); - closeTimer.current = null; - } - }; - - const closeMenu = useCallback(() => setMenuOpen(false), []); - - useEffect(() => { - const onScroll = () => setScrolled(window.scrollY > 50); - window.addEventListener("scroll", onScroll, { passive: true }); - return () => window.removeEventListener("scroll", onScroll); - }, []); - - // Close mobile menu on route change - useEffect(() => { - closeMenu(); - }, [pathname, closeMenu]); - - // Lock body scroll when menu is open - useEffect(() => { - if (menuOpen) { - document.body.style.overflow = "hidden"; - } else { - document.body.style.overflow = ""; - } - return () => { - document.body.style.overflow = ""; - }; - }, [menuOpen]); - - const isActive = (href: string) => { - const target = localizeHref(locale, href); - return href === "/" ? pathname === target : pathname.startsWith(target); - }; - - return ( - <> - - - {/* Mobile menu rendered outside