diff --git a/app/[locale]/about/page.tsx b/app/[locale]/about/page.tsx index 74de65c..2be9552 100644 --- a/app/[locale]/about/page.tsx +++ b/app/[locale]/about/page.tsx @@ -1,62 +1,14 @@ -import Image from "next/image"; -import { - PageHero, - FadeUp, - SectionTitle, -} from "@/components/ui"; -import { WollstonecraftQuotes } from "@/components/sections/WollstonecraftQuotes"; -import { - PhotoCarousel, - type CarouselPhoto, -} from "@/components/sections/PhotoCarousel"; +import { PageHero } from "@/components/sections/shared/PageHero"; +import { CtaBand, PeopleArt } from "@/components/sections/shared/CtaBand"; +import { ZoomSlideshow } from "@/components/sections/about/ZoomSlideshow"; +import { MissionPillars } from "@/components/sections/about/MissionPillars"; +import { FounderStory } from "@/components/sections/about/FounderStory"; +import { FounderVideo } from "@/components/sections/about/FounderVideo"; +import { QuoteBand } from "@/components/sections/about/QuoteBand"; +import { WhatWeProvide } from "@/components/sections/about/WhatWeProvide"; import { getDictionary } from "@/lib/i18n/get-dictionary"; import { getRouteMetadata } from "@/lib/i18n/metadata"; -import type { Locale } from "@/lib/i18n/config"; -import type { AboutTextSegment } from "@/lib/i18n/dictionary"; - -const berkeleyPhotos: CarouselPhoto[] = [ - { - src: "/berkeley-team-1.jpg", - alt: "VINdicated's UC Berkeley chapter team standing arm in arm in front of Doe Library and the Campanile", - position: "center 65%", - }, - { - src: "/berkeley-team-2.jpg", - alt: "VINdicated's UC Berkeley chapter team posing on the lawn by Doe Library", - position: "center 60%", - }, - { - src: "/berkeley-team-3.jpg", - alt: "VINdicated's UC Berkeley chapter team making playful poses beneath the Campanile", - position: "center 75%", - }, -]; - -const tablingPhotos: CarouselPhoto[] = [ - { - src: "/ucla-tabling-1.jpg", - alt: "Two VINdicated volunteers standing beside the UCLA VINdicated display board", - }, - { - src: "/ucla-tabling-3.jpg", - alt: "Students gathering around the VINdicated table at a busy UCLA activities fair", - }, - { - src: "/ucla-tabling-4.jpg", - alt: "A VINdicated volunteer sharing free car-buying resources with a student at UCLA", - }, -]; - -// Videos live in /public/videos. Add or remove entries here. -// poster = optional still image shown before the video plays. -const videos: { src: string; poster?: string; label: string }[] = [ - { - src: "/videos/video-1.mp4", - poster: "/videos/video-1-poster.jpg", - label: "VINdicated video filmed on the UC Berkeley campus", - }, - { src: "/videos/video-2.mp4", label: "VINdicated video 2" }, -]; +import { localizeHref, type Locale } from "@/lib/i18n/config"; export async function generateMetadata({ params, @@ -67,334 +19,40 @@ export async function generateMetadata({ return getRouteMetadata(locale, "about", "/about"); } -function Segments({ segments }: { segments: AboutTextSegment[] }) { - return ( - <> - {segments.map((seg, i) => - seg.bold ? ( - - {seg.text} - - ) : ( - {seg.text} - ), - )} - - ); -} - - export default async function AboutPage({ params, }: { params: Promise<{ locale: string }>; }) { const { locale } = await params; - const dict = await getDictionary(locale as Locale); - const d = dict.about; - - const pillars = d.mission.pillars; - const strikes = d.story.strikes; - const provideItems = d.provide.items; + const dict = (await getDictionary(locale as Locale)).aboutPage; + const href = (h: string) => localizeHref(locale as Locale, h); return ( - <> +
- {d.hero.titlePlain} -
- {d.hero.titleEm} - - } - titleStyle={ - { - fontSize: "clamp(2.4rem,4vw,4rem)", - maxWidth: "1100px", - } as React.CSSProperties - } - subtitle={d.hero.subtitle} + className="a-hero" + centered + title={dict.hero.title} + body={dict.hero.body} + > + +
+ + + + + + + } /> - - {/* Why We Exist */} - -
-
- - {d.whyWeExist.titleLine1}{" "} - {d.whyWeExist.titleEm} - {/* -
- {d.whyWeExist.titleLine2} - */} -
-

- Ayres, I. & Siegelman, P. (1995).{" "} - The American Economic Review, 85(3), 304-321. -

- -
- {[d.whyWeExist.para1, d.whyWeExist.para2, d.whyWeExist.para3].map( - (para, i) => ( -
-

- -

-
- ), - )} -
- -

- {(() => { - const text = d.whyWeExist.para4; - const words = text.split(" "); - const idx = words.findIndex( - (w) => w.toLowerCase().replace(/[^a-z']/gi, "") === "done", - ); - if (idx === -1) return text; - return ( - <> - {words.slice(0, idx).join(" ")} - {idx > 0 && " "} - - {words[idx]} - - {idx < words.length - 1 && " "} - {words.slice(idx + 1).join(" ")} - - ); - })()} -

-
-
-
- - {/* Mission */} - -
-
- - {d.mission.heading} {d.mission.headingEm} - -

- {d.mission.subheading} -

-
- VINdicated UC Berkeley chapter members posing on the lawn beneath the Campanile -
- {pillars.map(({ word, body }, i) => ( -
-

- {word} -

-

- {body} -

-
- ))} -
-
-
-
-
- - {/* Berkeley chapter */} - -
-
- -
-
-
- - {/* Founder story */} - -
-
- - {(() => { - const title = d.story.titleEm; - const words = title.split(" "); - const last = words.pop(); - const rest = words.join(" "); - return ( - <> - {rest} - {rest && " "} - {last} - - ); - })()} - -

- {d.story.subtitle} -

- - {/* Strikes, text only */} -
- {strikes.map(({ num, label, body }) => ( -
-

- Strike {num} -

-

- {label} -

- {(body ?? []).map((p, i) => ( -

- {p} -

- ))} -
- ))} -
- - {/* Tabling photo */} -
- VINdicated volunteers talking with a UCLA student at their tabling booth -
- - {/* Wollstonecraft quote carousel -
- -
- */} - - {/* Real-world presence */} -
- -
-
-
-
- - {/* Videos */} - -
-
1 ? "grid-cols-2 max-md:grid-cols-1" : "grid-cols-1" - }`} - > - {videos.map((v) => ( -
-
-
- - {/* What we provide */} - -
-
- - {d.provide.titlePlain} {d.provide.titleEm} - -

- {d.provide.subtitle} -

-
- {provideItems.map(({ title, body }, i) => ( -
-

- {title} -

-

{body}

-
- ))} -
-
-
-
- +
); } 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]/documents/page.tsx b/app/[locale]/documents/page.tsx index 964320f..1a223fc 100644 --- a/app/[locale]/documents/page.tsx +++ b/app/[locale]/documents/page.tsx @@ -1,1147 +1,151 @@ -"use client"; - -import { useState } from "react"; +import { Fragment } from "react"; import Image from "next/image"; -import { PageHero, FadeUp } from "@/components/ui"; - -// ── Types ───────────────────────────────────────────────────── -type PinColor = "red" | "gold" | "purple" | "green"; - -const pinColorMap: Record = { - red: { - bg: "var(--color-red)", - border: "rgba(214,59,59,0.4)", - hoverBg: "rgba(214,59,59,0.08)", - hlBorder: "var(--color-red)", - hlBg: "rgba(214,59,59,0.14)", - }, - gold: { - bg: "var(--color-gold)", - border: "rgba(201,168,76,0.4)", - hoverBg: "rgba(201,168,76,0.08)", - hlBorder: "var(--color-gold)", - hlBg: "rgba(201,168,76,0.12)", - }, - purple: { - bg: "var(--color-vivid)", - border: "rgba(149,51,165,0.5)", - hoverBg: "rgba(149,51,165,0.08)", - hlBorder: "var(--color-light)", - hlBg: "rgba(149,51,165,0.12)", - }, - green: { - bg: "#2d9e6b", - border: "rgba(45,158,107,0.4)", - hoverBg: "rgba(45,158,107,0.08)", - hlBorder: "#2d9e6b", - hlBg: "rgba(45,158,107,0.12)", - }, -}; - -// ── Document Exhibit ───────────────────────────────────────── -type PinData = { n: number; top: string; left: string; color: PinColor }; -type HlData = { - n: number; - top: string; - left: string; - w: string; - h: string; - color: PinColor; -}; -type CardData = { - badge: string; - badgeColor: PinColor; - title: React.ReactNode; - body: React.ReactNode; - verdict: React.ReactNode; - verdictColor: PinColor; -}; +import Link from "next/link"; +import { PageHero } from "@/components/sections/shared/PageHero"; +import { ScrollTour, type TourRect } from "@/components/sections/shared/ScrollTour"; +import { getDictionary } from "@/lib/i18n/get-dictionary"; +import type { DocumentsPageDict } from "@/lib/i18n/dictionary"; +import type { Locale } from "@/lib/i18n/config"; + +type Severity = "easy" | "watch" | "red" | "know"; + +// Spotlight rects on the Carfax screenshots (fractions of the image) and +// each stop's severity. Same order as documentsPage.page1/page2.items. +const PAGE1: (TourRect & { s: Severity })[] = [ + { x: 0.01, y: 0.2, w: 0.97, h: 0.07, s: "know" }, + { x: 0.19, y: 0.3, w: 0.4, h: 0.13, s: "easy" }, + { x: 0.19, y: 0.43, w: 0.4, h: 0.11, s: "know" }, + { x: 0.62, y: 0.26, w: 0.36, h: 0.32, s: "know" }, + { x: 0.01, y: 0.54, w: 0.97, h: 0.04, s: "red" }, + { x: 0.01, y: 0.63, w: 0.97, h: 0.17, s: "easy" }, +]; +const PAGE2: (TourRect & { s: Severity })[] = [ + { x: 0.01, y: 0.13, w: 0.97, h: 0.39, s: "easy" }, + { x: 0.01, y: 0.515, w: 0.97, h: 0.175, s: "know" }, + { x: 0.01, y: 0.7, w: 0.97, h: 0.13, s: "know" }, + { x: 0.01, y: 0.785, w: 0.97, h: 0.025, s: "watch" }, + { x: 0.01, y: 0.81, w: 0.97, h: 0.025, s: "watch" }, +]; +const GUIDE_TAGS: Severity[] = ["red", "watch", "know", "watch", "easy", "know"]; -function DocExhibit({ - image, - alt, - pins, - highlights, - cards, - navLabels, +function CarfaxTour({ + d, + page, + src, + rects, }: { - image: string; - alt: string; - pins: PinData[]; - highlights: HlData[]; - cards: Record; - navLabels: Record; + d: DocumentsPageDict; + page: DocumentsPageDict["page1"]; + src: string; + rects: (TourRect & { s: Severity })[]; }) { - const [active, setActive] = useState(null); - const card = active !== null ? cards[active] : null; - return ( -
- {/* Image */} -
-
- {alt} - - {/* Highlights */} - {highlights.map(({ n, top, left, w, h, color }) => { - const cfg = pinColorMap[color]; - return ( -
- ); - })} - - {/* Pins */} - {pins.map(({ n, top, left, color }) => { - const cfg = pinColorMap[color]; - return ( - - ); - })} -
-
- - {/* Panel */} -
- {/* Legend */} -
- {( - [ - ["red", "Critical caveat"], - ["gold", "Price impact"], - ["purple", "Know this"], - ["green", "Good sign"], - ] as [PinColor, string][] - ).map(([color, label]) => ( -
-
- {label} -
- ))} -
- - {/* Prompt or card */} - {!card ? ( -
- - ← - - - ↑ - -

- Click any numbered -
- pin on the document -

-
- ) : ( -
-
-

- {card.badge} -

-

- {card.title} -

-
- {card.body} -
-
-

- Bottom Line -

-

{card.verdict}

-
-
- {/* Nav */} -
- {pins.map(({ n, color }) => ( - - ))} -
-
- )} -
-
+ ( + + + {i + 1} · {d.severity[rects[i].s]} + +

{it.title}

+

{it.body}

+ {it.say &&
{it.say}
} +
+ ))} + /> ); } -// ── Buyers Guide Accordion ─────────────────────────────────── -const bgItems = [ - { - tag: "Most Important", - tagColor: "red" as PinColor, - title: '"AS IS, NO DEALER WARRANTY"', - body: ( - <> -

- This is the most important box on the form. If the "As Is" box is - checked, the dealer is telling you:{" "} - - if anything breaks after you drive off the lot, that is entirely - your problem. - {" "} - No refunds. No repairs. No obligation. -

- - ), - flag: { - color: "red" as PinColor, - text: 'What to do: If a car is sold "As Is," price that risk into your offer. Budget for potential repairs. And get an independent inspection before you buy. It is the only protection you have.', - }, - }, - { - tag: "Know This", - tagColor: "gold" as PinColor, - title: '"DEALER WARRANTY" Full or Limited', - body: ( - <> -

- If the Dealer Warranty box is checked, the dealer is promising to - cover certain repairs for a limited time.{" "} - Full Warranty means 100%.{" "} - Limited Warranty means a percentage, so read those - numbers carefully. -

- - ), - flag: { - color: "gold" as PinColor, - text: "Always ask for the warranty in writing. The form says what systems are covered and for how long. Verbal warranties are nearly impossible to enforce.", - }, - }, - { - tag: "Know This", - tagColor: "purple" as PinColor, - title: '"MANUFACTURER\'S WARRANTY STILL APPLIES"', - body: ( - <> -

- This means the original factory warranty has not yet expired. Some of - the car components are still covered by the manufacturer, not the - dealer. This is genuinely valuable. -

- - ), - flag: { - color: "purple" as PinColor, - text: "Look up the warranty terms for that specific make and model. A car still under manufacturer warranty is worth more.", - }, - }, - { - tag: "Watch This", - tagColor: "gold" as PinColor, - title: '"SERVICE CONTRACT" Available for Extra Charge', - body: ( - <> -

- A service contract (extended warranty) is optional and not free. The - word "available" means they want to sell it to you, but it - doesn't necessarily mean you need - it. -

- - ), - flag: { - color: "gold" as PinColor, - text: "If you want extended coverage: Compare the the dealer price to third-party providers. The the dealer version is almost always more expensive for the same coverage.", - }, - }, - { - tag: "Protects You", - tagColor: "green" as PinColor, - title: '"SPOKEN PROMISES ARE DIFFICULT TO ENFORCE"', - body: ( - <> -

- This line is federal law telling you directly:{" "} - do not trust verbal promises. Any promise a dealer - makes that is not written on the contract does not legally exist. -

- - ), - flag: { - color: "green" as PinColor, - text: "The rule: Any promise not written on the contract does not exist. If they won't write it down, they don't intend to keep it.", - }, - }, - { - tag: "Know This", - tagColor: "purple" as PinColor, - title: '"ASK THE DEALER IF YOUR MECHANIC CAN INSPECT"', - body: ( - <> -

- This line is also federal law. You do not need to ask permission. You - have the right. California law additionally requires dealers to post a - sign inside the dealership stating this. -

- - ), - flag: { - color: "green" as PinColor, - text: "Always get the inspection. A $100–$200 inspection before purchase is the cheapest insurance you can buy.", - }, - }, -]; - -function BuyersGuideAccordion() { - const [open, setOpen] = useState(null); - - return ( -
- {bgItems.map((item, i) => { - const cfg = pinColorMap[item.tagColor]; - return ( -
- - {open === i && ( -
-
- {item.body} -
-
-

- What To Do -

-

{item.flag.text}

-
-
- )} -
- ); - })} -
- ); -} +export default async function DocumentsPage({ + params, +}: { + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + const d = (await getDictionary(locale as Locale)).documentsPage; -// ── Page ───────────────────────────────────────────────────── -export default function DocumentsPage() { return ( - <> - - Real documents. -
- Decoded. - - } - subtitle="A Carfax decoded pin by pin, plus the FTC Buyers Guide explained in plain English, i.e., the two documents every used car buyer needs to understand." - /> +
+ +
+ + {d.hero.ctaPrimary} + + + {d.hero.ctaSecondary} + +
+
- {/* Carfax Page 1 */} - -
-
-

- How to Read a Carfax -
- Page One -

-

- Every Carfax leads with the same structure: mileage and VIN, - accident history, reliability forecast, and recent service. This - walkthrough explains what each section means and how each finding - affects what you should pay. -

+
+
+
+

{d.page1.title}

+

{d.page1.body}

- - 195,555 Miles:{" "}
- What High Mileage Actually Means - - ), - body: ( - <> -

- Under 100k: Low mileage, most systems - within lifespan. -
- 100k–150k: Sweet spot. Depreciation - happened, systems still good. -
- 150k–200k: High, but a well-maintained - Honda can reach 250k–300k+. -

-

- This car averaged 10,257 mi/yr under Owner 3, well below - the 15,000/yr average. The 2013 Accord also uses a{" "} - timing chain, not a belt, removing one - major high-mileage expense. -

- - ), - verdict: ( - <> - Price range: At 195k miles, a 2013 Accord - EX-L in good condition should be roughly $7,000–$10,000 - depending on condition. - - ), - verdictColor: "gold", - }, - 2: { - badge: "02: Good Sign", - badgeColor: "green", - title: ( - <> - No Accidents Reported:{" "}
- A Real Green Flag, With One Caveat - - ), - body: ( - <> -

- No insurance claims, police reports, or rental damage - reports across all three owners. For a 13-year-old car at - 195k miles, this is genuinely significant. -

-

- A clean accident history supports $1,000–$2,000 in premium - vs. comparable cars with reported accidents. -

- - ), - verdict: ( - <> - The caveat: Carfax estimates up to 1 in 5 - accidents goes unreported. "No accidents reported" ≠ "no - accidents ever." A physical inspection catches unreported - damage. - - ), - verdictColor: "green", - }, - 3: { - badge: "03: Reliability", - badgeColor: "purple", - title: ( - <> - Good Reliability Forecast:{" "}
- What "Good" Means at 195k Miles - - ), - body: ( - <> -

- Top 50% vs. similar vehicles at similar mileage, average - repair cost $350/year. For a 13-year-old car at 195k, that - is solid. -

-

- Budget for ongoing repairs: spark plugs ($150–200), - accessory belt ($150–300), CV axles ($300–500), eventually - timing chain ($800–1,200). -

- - ), - verdict: ( - <> - Budget: $500–$1,000/year for maintenance at - this mileage. Factor it into your offer as honest financial - planning. - - ), - verdictColor: "purple", - }, - 4: { - badge: "04: Summary Stats", - badgeColor: "purple", - title: ( - <> - 17 Records, 3 Owners, -
- Personal Use, California - - ), - body: ( - <> -

- 17 service records means maintenance was - done at shops that report to Carfax. -
- Personal vehicle across all three owners - is better than commercial or fleet use. -
- California ownership means no rust from - northeastern salt-belt states. -

- - ), - verdict: ( - <> - All-California ownership is a genuine - positive. It extends mechanical lifespan meaningfully. - - ), - verdictColor: "purple", - }, - 5: { - badge: "05: Critical Caveat", - badgeColor: "red", - title: ( - <> - Carfax Own Disclaimer:{" "}
- This Report Is Not Enough Alone - - ), - body: ( - <> -

- The small text says: "Other information about this - vehicle, including problems, may not have been reported to - CARFAX. Use this report as one important tool, along with - a vehicle inspection and test drive." -

-

- - Carfax is telling you directly: do not rely only on this - report. - -

- - ), - verdict: ( - <> - Non-negotiable: A clean Carfax is necessary - but not sufficient. Always pair it with an independent - physical inspection. - - ), - verdictColor: "red", - }, - 6: { - badge: "06: Good Sign", - badgeColor: "green", - title: ( - <> - Oil Changed August 2025:{" "}
- What Recent Service Signals - - ), - body: ( - <> -

- The current owner maintained this car right up to nearly - 200,000 miles. A car serviced regularly to the point of - sale suggests the owner wasn't hiding a developing - problem. -

- - ), - verdict: ( - <> - What to ask for: Request physical receipts, - not just the Carfax summary. Gaps in the record should be - explained by the current seller. - - ), - verdictColor: "green", - }, - }} - /> -
- + +
+
- {/* Carfax Page 2 */} - -
-
-

- How to Read a Carfax -
- Page Two -

-

- The second page is where the serious history lives: title brands, - the additional history grid, and the ownership table. These - sections can reveal problems that do not appear anywhere else. -

+
+
+
+

{d.page2.title}

+

{d.page2.body}

- - The Checks That Matter Most -
- Any Flag Here Changes Everything - - ), - body: ( - <> -

- Total Loss: Declared worth less than - repair costs. These vehicles are worth 30–50% less. -
- Structural/Frame Damage: Affects handling - and crash safety permanently. -
- Airbag Deployment: Indicates a serious - collision. Verify replacement airbag quality. -
- Odometer Rollback: Federal crime. Walk - away immediately. -

- - ), - verdict: ( - <> - Any flag in this grid is a serious - negotiating point or a deal-ender. "Total Loss" or - "Structural Damage" should make you question the asking - price by 30%+ or walk away. - - ), - verdictColor: "green", - }, - 2: { - badge: "02: Title History", - badgeColor: "purple", - title: ( - <> - Title Brands and What They Mean -
- Carfax Strongest Guarantee - - ), - body: ( - <> -

- Salvage: Total loss. Legally cannot be - resold without rebuild and inspection. -
- Rebuilt/Reconstructed: Was salvage, was - repaired. Typically 20–40% below clean title. -
- Flood/Fire/Hail: Flood cars are - especially problematic because corrosion of electrical systems - can appear years later. -
- Lemon Law Buyback: Must be disclosed. - Legal to resell but represents known defect history. -

- - ), - verdict: ( - <> - Carfax buyback guarantee covers - DMV-reported brands. It does not cover unreported damage, - which is why the physical inspection remains essential. - - ), - verdictColor: "purple", - }, - 3: { - badge: "03: Ownership Count and Length", - badgeColor: "purple", - title: ( - <> - Reading the Ownership Table -
- Tenure Matters More Than Count - - ), - body: ( - <> -

- Long tenures (3+ years each): Owners who - kept the car for years typically cared for it. -
- Short tenures (under 12 months): Always - ask what the current seller knows about why. -
- Many owners in quick succession: That car - has a story. It might be fine, or it might explain why no - one kept it. -

- - ), - verdict: ( - <> - The ideal profile: One or two long-term - owners who drove average or below-average annual mileage. - - ), - verdictColor: "purple", - }, - 4: { - badge: "04: Short Ownership Tenures", - badgeColor: "gold", - title: ( - <> - When Someone Only Kept It a Few Months -
- How to Investigate Without Assuming - - ), - body: ( - <> -

- A short tenure does not automatically mean a problem, but - it deserves an explanation. Check: Did they do multiple - repairs during that tenure? Where did they buy it from? - Does the mileage add up to normal driving? -

- - ), - verdict: ( - <> - What to say: "I noticed the previous owner - only kept this car for [X] months. Do you have any - information about why they sold?" Their answer, or their - hesitation, is data. - - ), - verdictColor: "gold", - }, - 5: { - badge: "05: Calculating Mileage Yourself", - badgeColor: "gold", - title: ( - <> - The Math the Carfax Sometimes Will not Do -
- Always Run These Numbers Yourself - - ), - body: ( - <> -

- Subtract starting odometer from ending odometer to get - total miles per owner. Divide by tenure in months, - multiply by 12 to get annual rate. Compare to the - 12,000–15,000 mile industry average. -

- - ), - verdict: ( - <> - Why this matters: Annual mileage rate - reveals how the car was actually used, which is more informative - than total mileage alone. - - ), - verdictColor: "gold", - }, - }} - /> -
- + +
+
- {/* Buyers Guide */} - -
-
-

- The Sticker in Every Used Car Window -
- What It Actually Means -

-

- Federal law requires this form to be displayed in the window of - every used car for sale at a dealership. Most buyers glance at it - and move on. Here is what every section actually means. -

+
+
+
+

{d.guide.title}

+

{d.guide.body}

- -
-
- FTC Buyers Guide -
-
-
-

- Click any section below to expand it -

-
- +
+ {d.guide.imageAlt} +
+ {d.guide.items.map((it, i) => ( +
+ + + {it.tag} + {it.title} + + +
+ {it.body} +
{it.say}
+
+
+ ))}
- - {/* Takeaway strip */} -
- {[ - { - color: "var(--color-gold)", - label: "The Most Important Line", - title: "\u201CKeep this form.\u201D", - body: "The Buyers Guide is part of the contract. Keep your copy. If the dealer warranty promises do not match what they told you, this is your evidence.", - }, - { - color: "var(--color-red)", - label: "Federal Violation", - title: "Removing the sticker before sale is illegal.", - body: "16 C.F.R. 455 prohibits removing the Buyers Guide before consumer purchase. If it\u2019s not in the window, that\u2019s a federal violation you can report to the FTC.", - }, - { - color: "var(--color-light)", - label: "Check These Two Things", - title: "VIN + Warranty Status", - body: "Cross-check the VIN on the Buyers Guide with the dashboard VIN and the title. Note exactly which warranty box is checked before any conversation about price.", - }, - ].map(({ color, label, title, body }) => ( -
-

- {label} -

-
-

{title}

-

- {body} -

-
+
+ {d.guide.keys.map((k) => ( +
+

{k.kicker}

+

{k.title}

+

{k.body}

))}
-
- - +
+
+
); } diff --git a/app/[locale]/fraud/page.tsx b/app/[locale]/fraud/page.tsx index a816eac..2d37881 100644 --- a/app/[locale]/fraud/page.tsx +++ b/app/[locale]/fraud/page.tsx @@ -1,669 +1,216 @@ -"use client"; - -import { useState, useEffect } from "react"; +import { Fragment } from "react"; import Image from "next/image"; -import { - PageHero, - FadeUp, - SectionTitle, - Button, -} from "@/components/ui"; -import { ScamNotebook } from "@/components/sections/ScamNotebook"; -import { RedFlagField } from "@/components/sections/RedFlagField"; - -// ── Pink Slip hotspots ─────────────────────────────────────── -// One object per field: position, content, and styling all live -// together, so nothing can drift out of sync with anything else. -const hotspots = [ - { - id: "title", - label: "Certificate of Title", - top: "4%", left: "27%", w: "44%", h: "5%", - danger: false, - title: ( - <> - California -
- Certificate of Title - - ), - body: "This is the \u201cpink slip,\u201d issued by the California DMV and the only legal proof that someone owns a vehicle. When you buy a car, the seller signs the back and hands it to you. You take it to the DMV to transfer it into your name. Without this step, the car is not legally yours, no matter what you paid.", - verdict: "If a seller says you don't need the pink slip, that they'll \u201cmail it later,\u201d or that it's \u201clost\u201d: do not complete the purchase.", - }, - { - id: "owner", - label: "Registered Owner", - top: "25%", left: "8%", w: "48%", h: "13%", - danger: false, - title: ( - <> - Who Legally -
- Owns This Car - - ), - body: "The name here must match the person selling you the car. Red flags: name doesn't match the seller's ID, the registered owner is an unknown business, or the seller claims to be selling \u201con behalf of\u201d someone without documentation.", - verdict: "Always ask for the seller's government-issued ID. The name must match the title exactly.", - }, - { - id: "history", - label: "Vehicle History", - top: "6.5%", left: "61%", w: "34%", h: "5%", - danger: true, - title: ( - <> - Vehicle History -
- Check This First. - - ), - body: "This box records DMV flags: salvage designation, lemon law buyback, flood damage, or prior theft recovery. A \u201csalvage\u201d title means the car was declared a total loss by an insurer. Both salvage and rebuilt titles dramatically affect value.", - verdict: "If you see any text in the Vehicle History box, research exactly what it means before proceeding.", - }, - { - id: "vin", - label: "Vehicle ID Number", - top: "12.5%", left: "8%", w: "47%", h: "5%", - danger: false, - title: ( - <> - The VIN Must -
- Match Exactly. - - ), - body: "The VIN is a unique 17-character code assigned at manufacture. Physically locate it on the car (dashboard and door jamb) and verify it matches this document character by character. A mismatch could indicate VIN cloning, where a stolen car takes the identity of a legitimate one.", - verdict: "Enter the VIN at vehiclehistory.gov (free) or Carfax to check accident history and title status before anything else.", - }, - { - id: "signature", - label: "Signature Section", - top: "47%", left: "8%", w: "88%", h: "7%", - danger: true, - title: ( - <> - The Seller Must Sign -
- In Front of You. - - ), - body: "This is where the registered owner legally releases their interest in the vehicle. This must be signed in front of you at the time of sale, not pre-signed, not signed later, not by someone other than the registered owner. A pre-signed title means someone already released ownership; you don't know to whom, or when.", - verdict: "Never accept: a pre-signed title, a blank signature section, or a signature from anyone other than the registered owner of record.", - }, - { - id: "odometer", - label: "Odometer Disclosure", - top: "56%", left: "8%", w: "88%", h: "10%", - danger: true, - title: ( - <> - Mileage Fraud -
- Is a Federal Crime. - - ), - body: "Federal law requires the seller to disclose actual mileage at time of sale. Odometer rollback is a federal offense under the Motor Vehicle Information and Cost Savings Act. Verify the reading here matches the car's physical odometer, then cross-check against the vehicle history report.", - verdict: "If you suspect fraud: document the discrepancy with photos, do not complete the purchase, and report to NHTSA at nhtsa.gov.", - }, +import Link from "next/link"; +import { PageHero } from "@/components/sections/shared/PageHero"; +import { CtaBand } from "@/components/sections/shared/CtaBand"; +import { ScrollTour, type TourRect } from "@/components/sections/shared/ScrollTour"; +import { FlagsChatTour } from "@/components/sections/fraud/FlagsChatTour"; +import { YoyoScene } from "@/components/sections/fraud/YoyoScene"; +import { FourSquare } from "@/components/sections/fraud/FourSquare"; +import { FeeReceipt } from "@/components/sections/fraud/FeeReceipt"; +import { AprCalculator } from "@/components/sections/fraud/AprCalculator"; +import { FlipCards } from "@/components/sections/fraud/FlipCards"; +import { getDictionary } from "@/lib/i18n/get-dictionary"; +import { localizeHref, type Locale } from "@/lib/i18n/config"; +import { caveat } from "@/lib/fonts"; + +// Pink slip boxes, as fractions of public/pink-slip.png. `danger` boxes +// get a red tag. Same order as fraudPage.pinkSlip.items. +const SLIP: (TourRect & { danger: boolean })[] = [ + { x: 0.27, y: 0.04, w: 0.44, h: 0.05, danger: false }, + { x: 0.08, y: 0.25, w: 0.48, h: 0.13, danger: false }, + { x: 0.61, y: 0.065, w: 0.34, h: 0.05, danger: true }, + { x: 0.08, y: 0.125, w: 0.47, h: 0.05, danger: false }, + { x: 0.08, y: 0.47, w: 0.88, h: 0.07, danger: true }, + { x: 0.08, y: 0.56, w: 0.88, h: 0.1, danger: true }, ]; -// ── Signature draw-on animation ────────────────────────────── -function SignatureDraw() { - const [drawn, setDrawn] = useState(false); - - useEffect(() => { - const raf = requestAnimationFrame(() => setDrawn(true)); - return () => cancelAnimationFrame(raf); - }, []); - +type LawCard = { tag: string; title: string; body: string; say: string }; +function Law({ card, className }: { card: LawCard; className?: string }) { return ( - - - - ); -} - -// ── Odometer flip-up animation ─────────────────────────────── -function OdometerFlip() { - const [display, setDisplay] = useState("000000"); - - useEffect(() => { - let raf: number; - const start = performance.now(); - const duration = 1100; - const target = 150000; - - function tick(now: number) { - const t = Math.min((now - start) / duration, 1); - const eased = 1 - Math.pow(1 - t, 3); - setDisplay(Math.round(eased * target).toString().padStart(6, "0")); - if (t < 1) raf = requestAnimationFrame(tick); - } - raf = requestAnimationFrame(tick); - return () => cancelAnimationFrame(raf); - }, []); - - return ( -
- - {display} - +
+ {card.tag} +

{card.title}

+

{card.body}

+
{card.say}
); } -function PinkSlipExplainer() { - const [activeId, setActiveId] = useState(null); - const [playKey, setPlayKey] = useState(0); - const active = hotspots.find((h) => h.id === activeId) || null; - - function selectHotspot(id: string) { - setActiveId((cur) => { - const next = cur === id ? null : id; - if (next) setPlayKey((k) => k + 1); - return next; - }); - } +export default async function FraudPage({ + params, +}: { + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + const d = (await getDictionary(locale as Locale)).fraudPage; + const href = (h: string) => localizeHref(locale as Locale, h); return ( -
- {/* Document image, directly clickable */} -
- California Certificate of Title +
+ +
+ + {d.hero.ctaPrimary} + + + {d.hero.ctaSecondary} + +
+
- {/* Clickable regions, with a subtle idle outline so they read as interactive even before you click */} - {hotspots.map((hl) => ( - - ))}
+ - {active === null ? ( -

- Click directly on the document, or pick a field from the list above. -

- ) : ( -
-

{active.title}

-

- {active.body} -

-

- {active.verdict} -

+
+
+
+

{d.flags.title}

+

{d.flags.body}

- )} -
-
- ); -} - -const redFlags = [ - { - trigger: '"Buy Today or Lose It": Artificial Urgency', - body: ( -
-

- One of the oldest tactics in the playbook. The salesperson implies - another buyer is coming in tonight, the deal expires at close of - business, or the price goes up tomorrow. None of this is usually true; the car has been sitting on the lot for weeks. -

-

- It's designed to stop you from getting an independent inspection, - sleeping on the decision, or comparing prices elsewhere. -

-

- Your response: “I'll need 48 hours to have - the car independently inspected.” A legitimate dealer - accommodates this. A bad one won't, which is your answer. -

-
- ), - }, - { - trigger: "The Four-Square Method: Payment Confusion", - body: ( -
-

- A widely documented dealer tactic where negotiations are split into - four boxes: purchase price, trade-in value, down payment, and monthly - payment. By shuffling focus between boxes, dealers can make a bad deal - look good. -

-

- A $400/month payment sounds reasonable until you realize it's - over 84 months with a 20% APR. The four-square hides that math on - purpose. -

-

- Your response: Negotiate the out-the-door price only. - Don't discuss monthly payments until the total price is locked in - writing. -

-
- ), - }, - { - trigger: 'Spot Delivery Scam: "Yo-Yo" Financing', - body: ( -
-

- You drive off the lot thinking the deal is done. Days or weeks later, - the dealer calls to say financing “fell through” and you - need to return the car or sign a new contract at a higher interest - rate. By then, you've already traded in your old car and - emotionally moved on. -

-

- Your response: Before you drive off, confirm in - writing that financing is fully approved, not conditional. If - they can't confirm it, don't take the car home yet. -

-
- ), - }, - { - trigger: "The Finance Office Ambush: Hidden Arbitration Clauses", - body: ( -
-

- After you've agreed on a price, you're handed a stack of - paperwork in the finance office. Buried inside is often a mandatory - arbitration clause, a provision that strips your right to sue - the dealership in court if something goes wrong. -

-

- Arbitration clauses are legal and common, but they're rarely - explained. Studies have shown that consumers win in arbitration at far - lower rates than in court. -

-

- What to do: Read every document before signing. If - you see “arbitration,” ask what it means and whether you can - opt out. Some contracts allow you to opt out within 30 days of - signing. -

-
- ), - }, - { - trigger: "Post-Sale Document Pressure: The Second Signing", - body: ( -
-

- After the sale is complete, some dealers call buyers back in claiming - a form was missed or a “small correction” is needed. What - follows is a second round of signing, often done quickly and without a - chance to read what's being signed. -

-

- Your response: Never sign anything post-sale under - time pressure. Ask them to mail or email it so you can review it - first. You are under no legal obligation to sign new documents after a - completed sale. -

-
- ), - }, - { - trigger: "Add-Ons & Upsells: The Finance Office Markup", - body: ( -
-

- The finance office is where dealers make a significant portion of - their profit, not on the car itself, but on add-ons presented as - standard. Common examples: -

-
    -
  • - Nitrogen tire inflation ($200–$400): Air - is already 78% nitrogen. Widely considered a scam. -
  • -
  • - VIN etching ($200–$400): You can buy a - kit and do this yourself for under $25. -
  • -
  • - Paint/fabric protection ($300–$1,000): - Usually a basic wax application at extreme markup. -
  • -
  • - GAP insurance (heavily marked up): - Legitimate, but available much cheaper through your own insurer. -
  • -
-

- Everything in the finance office is optional. California law (AB68) - requires each item to be listed separately with its individual cost. -

-
- ), - }, -]; - -type KyrLaw = { - tag: string; - title: string; - body: string; - action: string; - actionColor: string; -}; - -const federalLaws: KyrLaw[] = [ - { - tag: "Truth in Lending Act (TILA)", - title: "Your APR must be disclosed before you sign. No exceptions.", - body: "The dealer is required by federal law to show you your Annual Percentage Rate, the total finance charge, and the total cost of the loan before you sign anything. On a $15,000 loan, the difference between 7% APR and 30% APR is over $8,000.", - action: - "\u201CShow me the APR, finance charge, and total of payments right now, before I sign.\u201D", - actionColor: "var(--color-gold)", - }, - { - tag: "FTC Used Car Rule", - title: "Every used car must have a Buyers Guide in the window.", - body: "By law, every used car on a dealer lot must display a Buyers Guide sticker disclosing whether it has a warranty, what the warranty covers, or that it\u2019s sold \u201Cas is.\u201D The FTC can fine dealers up to $51,744 per missing sticker.", - action: - "No sticker visible, or a dealer who removes it before you can read it, is a federal violation. Document it.", - actionColor: "var(--color-red)", - }, - { - tag: "Motor Vehicle Information Act", - title: "Odometer rollback is a federal felony.", - body: "The dealer must provide a written odometer disclosure at the time of sale. Cross-reference the number on the contract with the physical odometer, the Carfax report, and any service records. All three must match.", - action: - "If they don\u2019t match: Stop the transaction. Photograph everything. Report to the FTC at reportfraud.ftc.gov.", - actionColor: "var(--color-red)", - }, - { - tag: "Fair Credit Reporting Act", - title: - "If they pull your credit, you\u2019re entitled to see exactly what they found.", - body: "Dealers must give you a written disclosure showing your exact credit score, which bureau provided it, and how it affected your loan terms. Know your number before you walk in.", - action: - "Before you go: Pull your free report at annualcreditreport.com. Walk in knowing your number.", - actionColor: "var(--color-gold)", - }, -]; - -const californiaLaws: KyrLaw[] = [ - { - tag: "AB68: Cancellation", - title: - "You can undo the purchase within 2 days, but you have to buy this right at signing.", - body: "California does not have an automatic cooling-off period. However, every licensed dealer must offer you a 2-day contract cancellation option on used cars priced $40,000 or less. It costs $75 to 1% of the purchase price plus a restocking fee if used, but it gives you a legal exit.", - action: - "\u201CI\u2019d like to purchase the 2-day contract cancellation option.\u201D", - actionColor: "var(--color-accent)", - }, - { - tag: "AB68: Interest Rates", - title: - "Interest rate markups are capped. The dealer can\u2019t charge whatever they want.", - body: "California law caps dealer interest rate markups: max 2% on loans over 60 months, max 2.5% on shorter loans. Getting pre-approved at a bank or credit union before you walk in removes this leverage entirely.", - action: - "Before you go: Get pre-approved. Walk in with a rate. They have to beat it.", - actionColor: "var(--color-gold)", - }, - { - tag: "AB68: Add-Ons", - title: - "Every add-on must be listed separately with its own price. No bundling.", - body: "Any financed item must appear as a separate line item. The dealer must show your monthly payment with and without each optional item. One big bundled number presented as \u201Cthe payment\u201D is a California law violation.", - action: - "\u201CIt\u2019s all just one package,\u201D that\u2019s not legal. Ask for itemized line items, in writing.", - actionColor: "var(--color-red)", - }, - { - tag: "Prohibited Practice", - title: "They cannot require an add-on as a condition of financing.", - body: "No dealer can require you to purchase GAP insurance, an extended warranty, paint protection, or any other add-on as a condition of getting your loan approved. If a finance manager implies you \u201Chave to\u201D take an add-on to qualify, that\u2019s a lie, and it\u2019s illegal.", - action: - "\u201CPlease show me in writing where it says this is required to get financing.\u201D Watch what happens next.", - actionColor: "var(--color-red)", - }, -]; - -function LawList({ laws }: { laws: KyrLaw[] }) { - return ( -
- {laws.map((law) => ( -
-

- {law.tag} -

-

- {law.title} -

-

- {law.body} -

-

- {law.action} -

+
- ))} -
- ); -} - -export default function FraudPage() { - return ( - <> - - Know What -
- They're Doing. - - } - subtitle="Real tactics used against buyers. Named, explained, and countered, because knowing you're being scammed is the first step to walking away." - /> + - {/* Pink Slip Explainer */} - -
- - This document is your -
- only proof you own the car. -
-

- A California Certificate of Title, called the “pink - slip,” is the legal document that proves vehicle - ownership. Without it in your name, the car is not legally yours, no - matter what you paid. Click directly on the document to learn what - each section means. -

- -
-
+ - {/* Red Flags */} - -
- - Car Purchasing Red Flags. - - -
-
+
+
+
+

{d.fourSquare.title}

+

{d.fourSquare.body}

+
+ +
+
- {/* Know Your Rights */} - -
- - The law is already on your side. - -

- Most buyers don't know what dealers are legally required to do. - Here's what the law actually says, in plain English - and exactly what to say when they try to ignore it. -

+
+
+
+

{d.receipt.title}

+

{d.receipt.body}

+
+ +
+
- {/* Federal */} -

- Federal Protections -

- +
+
+
+

{d.apr.title}

+

{d.apr.body}

+
+ +
+
- {/* California */} -

- California Protections -

- +
+
+
+

{d.law.title}

+

{d.law.body}

+
+

{d.law.federalTitle}

+
+ {d.law.federal.map((c) => ( + + ))} +
+
+ +
+

{d.law.californiaTitle}

+
+ {d.law.californiaNotice.title} + {d.law.californiaNotice.body}{" "} + + {d.law.californiaNotice.link} + +
+
+ {d.law.california.map((c) => ( + + ))} +
+
+
-
-

- You walked in knowing nothing. Now you know everything. -

-
- - +
+
+
+

{d.after.title}

+

{d.after.body}

+
+ +
+
+ +
+
+
+
+ {d.brochure.coverAlt} +
+
+ {d.brochure.tag} +

{d.brochure.title}

+

{d.brochure.body}

+
+
-
- - - {/* After a scam */} - -
- - What to Do After a Scam. - - -
-
- +
+
+ +
); } diff --git a/app/[locale]/inspection/page.tsx b/app/[locale]/inspection/page.tsx index 23032cc..510f9b3 100644 --- a/app/[locale]/inspection/page.tsx +++ b/app/[locale]/inspection/page.tsx @@ -1,1196 +1,136 @@ -"use client"; - -import { useState, useRef, useEffect } from "react"; -import Image from "next/image"; import Link from "next/link"; -import { - PageHero, - FadeUp, - SectionTitle, - Accordion, - Pullquote, - CardGrid, - InfoBox, -} from "@/components/ui"; - -// ── Engine diagram pins ────────────────────────────────────── -type PinColor = "green" | "gold" | "red" | "purple"; - -const enginePins: { - id: number; - top: string; - left: string; - color: PinColor; - title: string; -}[] = [ - { id: 1, top: "20%", left: "66%", color: "purple", title: "Engine Cover" }, - { id: 2, top: "55%", left: "27%", color: "gold", title: "Oil Filler Cap" }, - { - id: 3, - top: "31%", - left: "20%", - color: "green", - title: "Coolant Reservoir", - }, - { id: 4, top: "76%", left: "25%", color: "gold", title: "Serpentine Belt" }, - { id: 5, top: "85%", left: "48%", color: "green", title: "Radiator & Fan" }, - { id: 6, top: "18%", left: "34%", color: "gold", title: "Heat Shielding" }, - { - id: 7, - top: "78%", - left: "82%", - color: "purple", - title: "Air Filter Housing", - }, - { id: 8, top: "33%", left: "86%", color: "red", title: "Battery" }, - { - id: 9, - top: "65%", - left: "10%", - color: "green", - title: "Power Steering Fluid", - }, - { id: 10, top: "64%", left: "88%", color: "red", title: "Fuse Box" }, -]; - -const colorMap: Record = { - green: { - bg: "#1a7a4a", - badge: "rgba(26,122,74,0.2)", - text: "#4ddb94", - label: "Easy to Check", - }, - gold: { - bg: "#8a6010", - badge: "rgba(201,168,76,0.15)", - text: "var(--color-gold)", - label: "Watch Carefully", - }, - red: { - bg: "#8a1a1a", - badge: "rgba(214,59,59,0.15)", - text: "#ff7070", - label: "Serious Red Flag", - }, - purple: { - bg: "var(--color-vivid)", - badge: "rgba(90,48,105,0.2)", - text: "var(--color-accent)", - label: "Need to Know", - }, -}; - -type CardContent = { - subtitle: string; - what: string; - steps: string[]; - flag: string; - flagColor: PinColor; - flag2?: string; -}; - -const engineCards: Record = { - 1: { - subtitle: "The plastic cover that hides the engine below", - what: "The engine cover is a plastic panel that sits on top of the engine. It's mostly cosmetic; it makes the engine bay look clean and reduces noise.", - steps: [ - "Check if the cover is cracked or warped. Minor cosmetic damage is fine; significant damage suggests rough handling.", - "Look at whether it sits evenly. A cover that doesn't sit flush can mean lots of DIY repairs.", - "Ask the mechanic to remove it and look at what's underneath.", - ], - flag: "A clean, intact engine cover on a high-mileage car is actually a good sign; it means the seller cared about presentation.", - flagColor: "gold", - }, - 2: { - subtitle: "Open this: what's inside tells you a lot", - what: "The oil filler cap is the round cap on top of the engine where you pour new oil in. It takes two seconds to open and what you see inside tells you things the dipstick alone cannot.", - steps: [ - "Twist it counterclockwise and lift off. Look at the underside of the cap and inside the opening.", - "Look for any white or cream-colored residue, froth, or buildup.", - "Smell inside briefly; it should smell like oil, nothing chemical or sweet.", - ], - flag: "Serious red flag: White or creamy residue that looks like mayonnaise is an emulsion of oil and coolant, caused by a blown head gasket. This repair can exceed $3,000. Walk away or negotiate hard.", - flagColor: "red", - flag2: - "Also watch for: Thick sludge like tar. This means the engine went extended periods without oil changes. A sludged engine has shortened lifespan.", - }, - 3: { - subtitle: "Never open when hot. Read it cold.", - what: "The coolant reservoir is a translucent plastic tank that holds the engine's coolant. Most modern cars have an overflow reservoir you can read without opening anything.", - steps: [ - "Look at the exterior of the tank; it's translucent so you can see the level without opening it.", - "Check that the level is between MIN and MAX markers.", - "Look at the color through the tank walls. Coolant should be bright green, orange, pink, or blue.", - "Only open the cap if the engine is completely cold; hot coolant under pressure can cause serious burns.", - ], - flag: "Red flags: Brown or rusty coolant means neglect. Milky or foamy coolant means oil has mixed in: head gasket failure, a $1,500 to $3,000+ repair.", - flagColor: "red", - }, - 4: { - subtitle: "One belt runs almost everything; know what to look for", - what: "The serpentine belt drives the alternator, power steering pump, AC compressor, and water pump. If it breaks while driving, you lose all of those systems simultaneously.", - steps: [ - "With the engine OFF, locate the belt, a wide, flat ribbed belt running between multiple pulleys.", - "Look at the ribbed side. Fine cracks between the ribs are the first sign of aging.", - "Check the smooth back side. A glazed, shiny appearance means the belt is slipping.", - "Ask your mechanic to assess remaining life during the inspection.", - ], - flag: "Price impact: Serpentine belt replacement costs $75 to $200 parts and labor. If it looks worn, use it as a negotiating point, not a deal-breaker.", - flagColor: "gold", - }, - 5: { - subtitle: "The system that keeps the engine from overheating", - what: "The radiator is the large flat heat-exchanger at the front of the engine bay. Hot coolant from the engine flows through it and air passing through the fins cools it down before it recirculates.", - steps: [ - "With the engine cold, look at the front of the radiator for bent fins, cracks, or signs of leakage (dried green, orange, or pink residue).", - "Check around the base of the radiator and hoses for wet spots or dried coolant stains.", - "Start the engine and let it warm up. The fan should turn on after a few minutes at idle.", - "Check the temperature gauge during your test drive; it should stay in the normal range.", - ], - flag: "Red flag: Any coolant leak is a repair item before purchase. A cracked radiator replacement runs $300 to $900. Overheating on the test drive is a serious warning.", - flagColor: "red", - }, - 6: { - subtitle: "The foil-like material protecting wires and hoses from heat", - what: "Heat shielding is a reflective foil or wrapped material that protects fuel lines, wiring harnesses, and rubber hoses from the extreme heat generated by the exhaust manifold and engine block.", - steps: [ - "Look for shielding that's loose, hanging, or completely missing in sections.", - "Check nearby rubber hoses and wiring for heat damage: cracking, melting, or discoloration.", - "Ask your mechanic to check if the shielding is intact and if any heat-sensitive components show wear.", - ], - flag: "Missing heat shielding isn't an immediate breakdown, but damaged wiring from heat exposure can cause intermittent electrical problems that are expensive and hard to diagnose.", - flagColor: "gold", - }, - 7: { - subtitle: - "Where the engine breathes, and what a dirty filter signals", - what: "The air filter housing is the black plastic box that the air intake hose connects to. Inside is the air filter, a paper or foam element that keeps dust and debris out of the engine.", - steps: [ - "Find the large black plastic box, usually connected to a wide intake hose going toward the engine.", - "If you can open it (usually 2-4 clips), look at the filter inside. Hold it up to the light.", - "A clean filter is light gray or white. A dirty filter is dark gray or black.", - ], - flag: 'A severely clogged filter on a car with "full service history" is a red flag: either the service records are incomplete or the seller is exaggerating. Air filters cost $15-30 and should be replaced every 15,000-30,000 miles.', - flagColor: "gold", - }, - 8: { - subtitle: "Corrosion tells the story. So does the date.", - what: "The 12-volt battery powers the starter motor, all electronics, and keeps the car's computers alive when the engine is off. Most car batteries last 3-5 years. Replacement is $100-$250.", - steps: [ - "Find the battery, a large rectangular box, usually in a corner of the engine bay.", - "Look for a date sticker on the top or side; most batteries are stamped with month/year of manufacture.", - "Look at the terminals. Check for corrosion (white, blue, or green fuzzy buildup).", - "Look at the battery case: check for bulging, cracking, or leaking.", - ], - flag: "Red flags: Heavy corrosion on terminals, a date that's 4+ years old, or a bulging battery case. A visibly old battery is a $150-$250 legitimate ask off the price.", - flagColor: "red", - }, - 9: { - subtitle: "Easy to check, tells you if the steering system is healthy", - what: "The power steering reservoir is a small translucent tank that holds the hydraulic fluid that makes turning the steering wheel easy. Many newer cars have electric power steering and won't have this; that's normal.", - steps: [ - "Find the reservoir; it's usually small and labeled with a steering wheel icon.", - "Check the fluid level through the translucent side; it should be between MIN and MAX.", - "Look at the color. New fluid is clear to light yellow. Dark brown or black means it's overdue.", - "During the test drive, listen for a whining noise when turning.", - ], - flag: "Low fluid without a visible leak means the system is consuming fluid, indicating a slow leak somewhere. Whining on turns + low fluid = power steering pump issue. That's a $200-$600 repair.", - flagColor: "gold", - }, - 10: { - subtitle: "The box that keeps the car's electricity safe", - what: "The fuse box holds tiny safety pieces called fuses that protect every electrical circuit in the car. If something goes wrong electrically, a fuse blows to stop damage, like a safety switch.", - steps: [ - "Ask to open the fuse box lid. Look for any fuses that are visibly blown; the metal strip inside will be broken.", - "Look for mismatched fuses. A 30-amp fuse in a slot rated for 10-amp means someone bypassed an electrical problem. This is dangerous.", - "Check for burn marks, dark spots, melting, or a burnt smell inside the box.", - "Look for dirt, water stains, or rust inside. The inside should be completely clean and dry.", - "Check if wires look messy, taped, or spliced together. Clean, factory wiring is normal.", - ], - flag: "Red flag: Blown fuses, mismatched fuses, burn marks, or messy wiring mean existing electrical problems. Electrical issues on used cars are among the hardest and most expensive to diagnose.", - flagColor: "red", - }, -}; - -// Shared "bright bar on the left" callout, matching the Important/InfoBox -// treatment used elsewhere on the site: rounded card, small bold colored -// label, plain white body text. -function FlagCallout({ - label, - text, - color, +import { PageHero } from "@/components/sections/shared/PageHero"; +import { Rich } from "@/components/sections/shared/Rich"; +import { QuoteBand } from "@/components/sections/about/QuoteBand"; +import { EngineInspector } from "@/components/sections/inspection/EngineInspector"; +import { PpiDrive } from "@/components/sections/inspection/PpiDrive"; +import { getDictionary } from "@/lib/i18n/get-dictionary"; +import type { Locale } from "@/lib/i18n/config"; + +const DEALERS = [ + ["Toyota", "https://www.toyota.com/dealers/"], + ["Honda", "https://automobiles.honda.com/tools/dealer-locator"], + ["Ford", "https://ford.com/dealerships"], + ["Nissan", "https://nissanusa.com/dealer-locator.html"], + ["Chevrolet", "https://chevrolet.com/dealer-locator"], + ["Hyundai", "https://hyundaiusa.com/us/en/dealer-locator"], + ["Mazda", "https://mazdausa.com/shopping-tools/dealer-locator"], + ["Volkswagen", "https://vw.com/find-a-dealer"], +] as const; + +export default async function InspectionPage({ + params, }: { - label: string; - text: string; - color: string; + params: Promise<{ locale: string }>; }) { - return ( -
-

- {label} -

-

{text}

-
- ); -} - -function EngineDiagram() { - const [active, setActive] = useState(null); - const card = active ? engineCards[active] : null; - const pin = active ? enginePins.find((p) => p.id === active) : null; - const scrollRef = useRef(null); - const imageWrapRef = useRef(null); - const [panelHeight, setPanelHeight] = useState( - undefined - ); - - useEffect(() => { - scrollRef.current?.scrollTo({ top: 0 }); - }, [active]); - - // Keep the detail panel capped to the engine image's rendered height - // (which is set by aspect-ratio, not content) so long descriptions - // scroll inside the panel instead of stretching the image taller. - useEffect(() => { - const el = imageWrapRef.current; - if (!el) return; - const mq = window.matchMedia("(min-width: 1024px)"); - const update = () => setPanelHeight(mq.matches ? el.clientHeight : undefined); - update(); - const ro = new ResizeObserver(update); - ro.observe(el); - mq.addEventListener("change", update); - return () => { - ro.disconnect(); - mq.removeEventListener("change", update); - }; - }, []); + const { locale } = await params; + const d = (await getDictionary(locale as Locale)).inspectionPage; return ( -
-
-
-

- What to Actually Look For -

-

- Click any numbered pin on the engine to learn what that component - does, how to inspect it yourself, and what condition it should be in. -

-
-
- -
- {/* Image + pins */} -
- Car engine bay - {enginePins.map((p) => ( - - ))} +
+ +
+ + {d.hero.ctaPrimary} + + + {d.hero.ctaSecondary} +
+
- {/* Panel */} -
- {/* Legend */} -
- {Object.entries(colorMap).map(([key, val]) => ( - - - {val.label} - +
+
+

{d.what.title}

+

+ +

+

{d.what.notTitle}

+
+ {d.what.nots.map((n) => ( +
+ {n.title} + {n.body} +
))}
- - {/* Content */} - {!active ? ( -
- 🔍 -

- Click any numbered pin -
- on the engine image -

-

- 10 components to inspect -

-
- ) : card && pin ? ( -
-
-

- {pin.title} -

-

- {card.subtitle} -

-
- -
-
-

- What it is -

-

- {card.what} -

-
-
-

- How to check it -

-
    - {card.steps.map((step, i) => ( -
  1. - - {i + 1} - - {step} -
  2. - ))} -
-
- - {card.flag2 && ( - - )} -
- - {/* Nav dots */} -
- {enginePins.map((p) => ( - - ))} -
-
- ) : null}
-
-
- ); -} - -// ── Winding Road: "How to Schedule a PPI" ──────────────────── -type RoadItem = { strong: string; text: string }; - -function WindingRoad({ items }: { items: RoadItem[] }) { - const [active, setActive] = useState(null); - const [hovered, setHovered] = useState(null); - const [visited, setVisited] = useState>(new Set()); - - // Snakes left-to-right, then right-to-left on the next row, so the - // road stays short and wide instead of one long vertical scroll. - const [cols, setCols] = useState(1); - useEffect(() => { - const mq = window.matchMedia("(min-width: 768px)"); - const update = () => setCols(mq.matches ? 3 : 1); - update(); - mq.addEventListener("change", update); - return () => mq.removeEventListener("change", update); - }, []); - - // Measured in pixels so the popup card can be clamped to the actual - // rendered width instead of the viewport — on narrow phones the road - // container is much narrower than 100vw, and a viewport-based max - // width let the card spill past the container's edges. - const containerRef = useRef(null); - const [containerWidth, setContainerWidth] = useState(0); - useEffect(() => { - const el = containerRef.current; - if (!el) return; - const ro = new ResizeObserver(([entry]) => { - setContainerWidth(entry.contentRect.width); - }); - ro.observe(el); - return () => ro.disconnect(); - }, []); - - // Fades/scales the popup in on open rather than snapping into view. - const [entered, setEntered] = useState(false); - - const width = cols === 1 ? 400 : 1040; - const stepY = cols === 1 ? 230 : 170; - const topPad = 90; - const bottomPad = 90; - const rows = Math.ceil(items.length / cols); - const height = topPad + (rows - 1) * stepY + bottomPad; - const colWidth = width / cols; - const amp = 70; - - const stops = items.map((_, i) => { - const row = Math.floor(i / cols); - const idxInRow = i % cols; - const col = row % 2 === 0 ? idxInRow : cols - 1 - idxInRow; - return { - x: - cols === 1 - ? width / 2 - amp * Math.sin(i * 1.15 + 0.4) - : colWidth * (col + 0.5), - y: topPad + row * stepY, - }; - }); - - // Bows each segment perpendicular to its direction (up/down for - // horizontal stretches, left/right for the vertical connectors), - // alternating sign so the road reads as one continuous curvy line. - const bulge = 34; - let pathD = `M ${stops[0].x} ${stops[0].y}`; - for (let i = 1; i < stops.length; i++) { - const p0 = stops[i - 1]; - const p1 = stops[i]; - const dx = p1.x - p0.x; - const dy = p1.y - p0.y; - const horizontal = Math.abs(dx) >= Math.abs(dy); - const sign = i % 2 === 0 ? -1 : 1; - const c1x = horizontal ? p0.x + dx * 0.33 : p0.x + sign * bulge; - const c1y = horizontal ? p0.y + sign * bulge : p0.y + dy * 0.33; - const c2x = horizontal ? p0.x + dx * 0.66 : p1.x + sign * bulge; - const c2y = horizontal ? p1.y + sign * bulge : p0.y + dy * 0.66; - pathD += ` C ${c1x} ${c1y}, ${c2x} ${c2y}, ${p1.x} ${p1.y}`; - } + - const shown = hovered !== null ? hovered : active; - const shownItem = shown !== null ? items[shown] : null; - const shownStop = shown !== null ? stops[shown] : null; - const shownRow = shown !== null ? Math.floor(shown / cols) : 0; - const flipBelow = shownRow === 0; - - // Clamp the popup card to the measured container so it can never - // spill past the road's own edges, regardless of viewport width. - const sidePad = 12; - const scale = containerWidth ? containerWidth / width : 0; - const popupWidthPx = containerWidth - ? Math.min(300, containerWidth - sidePad * 2) - : 300; - const popupLeftPx = - shownStop && containerWidth - ? Math.min( - Math.max(shownStop.x * scale - popupWidthPx / 2, sidePad), - containerWidth - popupWidthPx - sidePad - ) - : 0; - - useEffect(() => { - if (shown === null) return; - setEntered(false); - const id = requestAnimationFrame(() => setEntered(true)); - return () => cancelAnimationFrame(id); - }, [shown]); - - const openStop = (i: number) => { - setActive((cur) => (cur === i ? null : i)); - setVisited((prev) => new Set(prev).add(i)); - }; - - const dismiss = () => { - setActive(null); - setHovered(null); - }; - - return ( -
-
- - {/* Road surface */} - - {/* Lane dashes */} - - - - {/* Stops — idle/active treatment matches the pink-slip hotspots: - dashed border at rest, colored border + tinted fill + glow when active */} - {items.map((item, i) => { - const s = stops[i]; - const isActive = active === i; - const isShown = shown === i; - const isVisited = visited.has(i); - return ( - - ); - })} - - {/* Explanation card: shows on hover as a transient preview, and - stays pinned after a click until dismissed. Flips below the - stop for the top row so it never overlaps the button. */} - {shownItem && shownStop && ( - <> - {active !== null && ( -
- )} -
- {active === shown && ( - - )} -

- {shownItem.strong} -

-

- {shownItem.text} -

-
- - )} -
-
- ); -} - -const inspectionAccordion = [ - { - trigger: "Engine: The Heart of the Car", - body: ( -
    -
  • - Oil leaks: Is oil dripping from the engine? - Small leaks become big repairs. -
  • -
  • - Coolant leaks: Overheating can destroy an - engine quickly and expensively. -
  • -
  • - Timing belt/chain: If this snaps while driving, - your engine could be totaled. -
  • -
  • - Check engine light codes: Even if the light - isn't on, a mechanic can pull stored codes. -
  • -
  • - Compression test: Reveals internal engine wear - that visual inspection can't catch. -
  • -
- ), - }, - { - trigger: "Transmission: Often the Most Expensive Fix", - body: ( -
-

- Transmission repairs can cost $3,000-$8,000. An inspector checks - for: -

-
    -
  • Smooth gear shifts (automatics and manuals)
  • -
  • - Fluid condition and color (should be clear pink or red, not brown or - burnt) -
  • -
  • Slipping between gears under acceleration
  • -
  • Unusual sounds during gear changes
  • -
-
- ), - }, - { - trigger: "Brakes: Non-Negotiable Safety", - body: ( -
    -
  • - Brake pad thickness: Under 3mm means immediate - replacement. -
  • -
  • - Rotor condition: Scored, cracked, or warped - rotors affect stopping distance. -
  • -
  • - Brake fluid: Old fluid absorbs moisture and - reduces braking performance. -
  • -
  • - ABS function: Anti-lock braking systems require - specific diagnostic tools to verify. -
  • -
- ), - }, - { - trigger: "Suspension & Steering", - body: ( -
-

- Suspension issues directly affect handling and safety at highway - speeds, and are one of the most commonly missed problems in - seller-provided inspections. -

-
    -
  • Ball joints, tie rods, and control arms
  • -
  • - CV axle boots (watch for cracking or grease spray on surrounding - surfaces) -
  • -
  • Shock absorbers and struts
  • -
  • Steering rack condition
  • -
-
- ), - }, - { - trigger: "Body & Frame: Accident History", - body: ( -
-

- A Carfax report helps but isn't the whole picture. A physical - inspection can catch: -

-
    -
  • Uneven panel gaps (signs of bodywork after an accident)
  • -
  • Mismatched paint (inconsistent sheen = repainted panel)
  • -
  • Frame damage or previous straightening
  • -
  • - Undercarriage rust, especially in previously-northeastern cars -
  • -
-

- Pro tip: Get a Carfax AND a physical inspection. They - catch different things. -

-
- ), - }, -]; - -const brands = [ - { - name: "Toyota", - logo: "/logo-toyota.png", - url: "https://www.toyota.com/dealers/", - }, - { - name: "Honda", - logo: "/logo-honda.png", - url: "https://automobiles.honda.com/tools/dealer-locator", - }, - { name: "Ford", logo: "/logo-ford.png", url: "https://ford.com/dealerships" }, - { - name: "Nissan", - logo: "/logo-nissan.png", - url: "https://nissanusa.com/dealer-locator.html", - }, - { - name: "Chevrolet", - logo: "/logo-chevrolet.png", - url: "https://chevrolet.com/dealer-locator", - }, - { - name: "Hyundai", - logo: "/logo-hyundai.png", - url: "https://hyundaiusa.com/us/en/dealer-locator", - }, - { - name: "Mazda", - logo: "/logo-mazda.png", - url: "https://mazdausa.com/shopping-tools/dealer-locator", - }, - { - name: "Volkswagen", - logo: "/logo-vw.png", - url: "https://vw.com/find-a-dealer", - }, -]; - -export default function InspectionPage() { - return ( - <> - - Don't Buy a Car -
- Without This. - - } - titleStyle={ - { fontSize: "clamp(2.6rem,4.2vw,4.6rem)" } as React.CSSProperties - } - subtitle="A pre-purchase inspection (PPI) is the single most important thing you can do before buying a used car. It costs $100-200 and can save you thousands." - /> +
+
+
+

{d.engine.title}

+

{d.engine.body}

+
+ +
+
- {/* What is a PPI */} - -
-
-
-
- - What Is a
- Pre-Purchase Inspection? -
-

- A PPI is when you pay an independent mechanic, - not the seller, not the dealership, to inspect a - car before you buy it. -

-

- A good PPI costs $100-$200 and is the - single most powerful tool you have before signing anything. -

-
-
-

- A PPI is not the same as -

-
- {[ - { term: "The seller\u2019s inspection", def: "they\u2019re trying to sell you the car." }, - { term: "Your own visual check", def: "you\u2019re not a trained mechanic." }, - { term: "A test drive", def: "you can\u2019t see what\u2019s happening under the hood." }, - ].map(({ term, def }, i) => ( -
- - {i + 1} - -
-

- {term} -

-

- {def} -

-
-
+
+
+
+

{d.where.title}

+
+
+ {d.where.cards.map((c, i) => ( +
+ {i === 1 && {d.where.recommended}} +

{c.title}

+ {c.paras.map((p, k) => ( +

+ +

))}
-
+ ))}
+

{d.where.dealersTitle}

+

{d.where.dealersBody}

+
+ {DEALERS.map(([name, url]) => ( + + {name} + + ))}
-
- - - {/* Engine Diagram */} - -
- -
-
- - {/* Where to get one */} - -
-
- Where to Get a PPI - -

- Lemon Squad (lemonsquad.com): - Mobile service, 110-point inspection. Cost: $150-250. -

-

- CARCHEX (carchex.com): Mobile or - shop-based, detailed report. Cost: $100-200. -

- - ), - }, - { - num: "Manufacturer Network", - title: "Manufacturer Dealerships", - recommended: true, - body: ( - <> -

- If you're buying a used Honda from a Mitsubishi - dealership,{" "} - - take it to a Honda dealership for the PPI. - -

-

- They know that brand inside and out, have the right - diagnostic equipment, and have zero incentive to lie. - Cost: usually $100-150. +

+
+ +
+
+

{d.checks.title}

+

{d.checks.body}

+
+ {d.checks.items.map((item, i) => ( +
+ {item.title} +
+ {item.intro} +
    + {item.points.map((pt, k) => ( +
  • + +
  • + ))} +
+ {item.outro.length > 0 && ( +

+

- - ), - }, - { - num: "If You Have It", - title: "AAA Membership", - body: "Many AAA locations offer free or discounted PPIs for members. Check your local AAA auto service center.", - }, - { - num: "If You Trust Them", - title: "Trusted Local Mechanics", - body: 'If you have a mechanic you trust, ask if they do PPIs. Red flags: they refuse to put findings in writing, they rush through it, or they tell you "everything looks fine" without checking anything.', - }, - ]} - /> -
-
-
- - {/* Dealer locators */} - -
-
- - Find a Manufacturer Dealership - -

- Buying a used Honda from a Mitsubishi dealer? Take it to a Honda - dealership for your PPI. They know that brand, have the right tools, - and have zero incentive to lie. -

-
- {brands.map((brand) => ( - -
- {brand.name} + )}
-

- {brand.name} -

- + ))}
-
-
-
- - {/* What inspectors check */} - -
-
- - Why You're Paying for Your Safety - -

- A good inspector goes through the car systematically. Here's - what they're checking, and why each category matters. -

- -
-
-
+
+
- {/* Pullquote */} - -
-
- - Your salesperson is not your friend, your partner, or - someone looking out for your best interests. - They're paid on commission. The more - you pay, the more they make. This doesn't mean - they're evil. It just means you need to{" "} - protect yourself. - - } - cite="Rana Darwich, VINdicated Founder" - /> -
-
-
+
+ +
- {/* How to schedule */} - -
-
- How to Schedule a PPI -

- Follow the road, stop by stop. Tap a stop to read what to do there. -

- - - If a seller stalls on producing service records or promises - they'll "get them later," schedule the inspection - before you commit emotionally.{" "} - - Schedule the inspection before you're attached, not - after. - - -
-
-
- + +
); } diff --git a/app/[locale]/join/page.tsx b/app/[locale]/join/page.tsx index 61e67f7..1ad38fd 100644 --- a/app/[locale]/join/page.tsx +++ b/app/[locale]/join/page.tsx @@ -1,510 +1,86 @@ -"use client"; - -import { useState } from "react"; -import Image from "next/image"; -import { PageHero, FadeUp, SectionTitle, Button } from "@/components/ui"; -import { submitVolunteerApplication } from "@/app/actions/submitVolunteer"; -import { submitContactForm } from "@/app/actions/email"; - -const CHAPTERS = [ - { - value: "ucla", - label: "UCLA Chapter", - school: "University of California, Los Angeles", - lead: "Rana Darwich, Founder & Executive Director", - contact: "getvindicated@outlook.com", - focus: "Built the interactive dealership risk map and leads VINdicated's core consumer research and data infrastructure.", - mascot: "/ucla-mascot.png", - }, - { - value: "ucberkeley", - label: "UC Berkeley Chapter", - school: "University of California, Berkeley", - lead: "Halima Cherif Hminat, Research & Outreach Lead · Ameerah Zafar, Data Engineer", - contact: "halimacherif@berkeley.edu · azzafar@berkeley.edu", - focus: "Currently building out its membership. Focused on consumer education and outreach.", - mascot: "/berkeley-mascot.png", - }, - { - value: "ucsc", - label: "UC Santa Cruz Chapter", - school: "University of California, Santa Cruz", - lead: "Ashwin Vinod, President · Gundeep Sambee, Outreach Lead", - contact: "asvinod@ucsc.edu · gsambee@ucsc.edu", - mascot: "/ucsc-mascot.png", - focus: "Focused on algorithmic discrimination and AI bias in automotive retail, bringing a technical lens to consumer protection.", - }, -]; - -const ROLES = [ - { value: "research-analyst", label: "Research Analyst", min_hours: 5 }, - { value: "data-engineer", label: "Data Engineer / Analyst", min_hours: 5 }, - { value: "software-engineer", label: "Software Engineer", min_hours: 5 }, - { value: "outreach", label: "Outreach & Communications", min_hours: 3 }, - { value: "legal-research", label: "Legal Research", min_hours: 3 }, - { value: "design", label: "Graphic Design / Illustration", min_hours: 3 }, -]; - -const TOPICS = [ - { value: "volunteer", label: "Volunteering with VINdicated" }, - { value: "research", label: "Participating in Research" }, - { value: "story", label: "Sharing My Story" }, - { value: "press", label: "Press & Media" }, - { value: "partnership", label: "Partnership or Collaboration" }, - { value: "grant", label: "Grants & Funding" }, - { value: "general", label: "General Inquiry" }, -]; - -const SIDE_INFO = [ - { - cat: "Volunteer", - body: "We have teams for tech, content, design, and operations. If you want to contribute skills to a mission that matters, we'd love to have you.", - }, - { - cat: "Research Participants", - body: "Our correspondence audit studies need testers. Participation is remote, anonymous, and directly contributes to publishable research on automotive discrimination.", - }, - { - cat: "Share Your Story", - body: "Have you experienced discriminatory treatment at a dealership or from a private seller? Your story may contribute to our research or help us better serve our community.", - }, - { - cat: "Press & Media", - body: "For media inquiries about VINdicated or our research, reach out directly. Our founder is available for comment on automotive consumer discrimination.", - }, -]; - -// Borderless, filled-box style for form fields (no underline). -const fieldCls = - "w-full bg-white/5 rounded-lg px-4 py-3 outline-none text-white text-[1rem] transition-colors focus:bg-white/10"; - -type Tab = "contact" | "volunteer"; -type FormState = "idle" | "submitting" | "success" | "error"; - -export default function JoinPage() { - const [tab, setTab] = useState("volunteer"); +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"; + +// Minimum weekly hours per role, same order as joinPage.roles.items. +const ROLE_HOURS = [5, 5, 5, 3, 3, 3]; + +export default async function JoinPage({ + params, +}: { + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + const d = (await getDictionary(locale as Locale)).joinPage; return ( - <> - Get Involved.} - subtitle="Volunteer with a chapter, share your story, participate in our research, or just say hello, pick what fits below." - /> - - -
-
-

- Our Chapters. -

- -
- {CHAPTERS.map((ch) => ( -
- {ch.mascot && ( - - )} -

{ch.label}

-

{ch.school}

-

{ch.focus}

-

- Leads: {ch.lead} -

-

- Contact: {ch.contact} -

+
+ +
+ + {d.hero.ctaPrimary} + + + {d.hero.ctaSecondary} + +
+
+ +
+
+
+

{d.chapters.title}

+
+
+ {d.chapters.items.map((c) => ( +
+ {c.tag} +

{c.name}

+
{c.school}
+

{c.focus}

+
+ {d.chapters.leadsLabel} {c.leads} +
))}
- - {/* Tab toggle */} -
- - -
- - {tab === "volunteer" ? : }
-
- - - -
-
-

- Questions? Email us at{" "} - - getvindicated@outlook.com - -

-
-
-
- - ); -} - -// ── Volunteer application panel ────────────────────────────── -function VolunteerPanel() { - const [state, setState] = useState("idle"); - const [message, setMessage] = useState(""); - - async function handleSubmit(e: React.FormEvent) { - e.preventDefault(); - setState("submitting"); - const form = e.currentTarget; - const data = new FormData(form); - - const resumeFile = data.get("resume") as File | null; - let resume: { filename: string; base64: string } | undefined; - - if (resumeFile && resumeFile.size > 0) { - if (resumeFile.size > 5 * 1024 * 1024) { - setState("error"); - setMessage("Your resume file is too large. Please upload a file under 5MB."); - return; - } - const base64 = await new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = () => resolve((reader.result as string).split(",")[1]); - reader.onerror = () => reject(new Error("Could not read file")); - reader.readAsDataURL(resumeFile); - }).catch(() => undefined); - if (base64) { - resume = { filename: resumeFile.name, base64 }; - } - } - - try { - const result = await submitVolunteerApplication({ - name: data.get("name") as string, - email: data.get("email") as string, - phone: data.get("phone") as string, - chapter: data.get("chapter") as string, - role: data.get("role") as string, - hours: Number(data.get("hours")), - location: data.get("location") as string, - background: data.get("background") as string, - why: data.get("why") as string, - student: data.get("student") as string, - resume, - }); - if (result.success) { - setState("success"); - setMessage(result.message); - form.reset(); - } else { - setState("error"); - setMessage(result.message); - } - } catch { - setState("error"); - setMessage("Something went wrong. Please try again or email us directly at getvindicated@outlook.com."); - } - } - - if (state === "success") { - return ( -
- - Application received. - -

{message}

-
- ); - } - - return ( -
-
-

- What we're looking for. -

-
- {ROLES.map((role) => ( -
-

{role.label}

-

{role.min_hours}+ hours per week

-
- ))} +
+ +
+
+
+

{d.roles.title}

+

{d.roles.body}

+
+
    + {d.roles.items.map((r, i) => ( +
  • + {r} + {d.roles.hours.replace("{n}", String(ROLE_HOURS[i]))} +
  • + ))} +
-

- All positions are remote and unpaid. VINdicated is a volunteer-run nonprofit. -

-
- -
- {state === "error" && ( -

- {message} -

- )} - -
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- -
- {["Yes", "No"].map((opt) => ( - - ))} -
-
- -
- - -
- -
- -