diff --git a/app/[locale]/about/page.tsx b/app/[locale]/about/page.tsx
index f7bc620..560706c 100644
--- a/app/[locale]/about/page.tsx
+++ b/app/[locale]/about/page.tsx
@@ -1,4 +1,3 @@
-import type { Metadata } from "next";
import Image from "next/image";
import {
PageHero,
@@ -6,25 +5,16 @@ import {
Divider,
SectionTitle,
} from "@/components/ui";
+import { getRouteMetadata } from "@/lib/i18n/metadata";
-export const metadata: Metadata = {
- title: "About Us",
- description:
- "VINdicated was founded by Rana Darwich after experiencing dealership discrimination firsthand. Learn about our mission to educate, empower, and vindicate car buyers.",
- openGraph: {
- title: "About Us",
- description:
- "VINdicated was founded by Rana Darwich after experiencing dealership discrimination firsthand. Learn about our mission to educate, empower, and vindicate car buyers.",
- url: "/about",
- type: "website",
- images: [{ url: "/illus-woman-dealership.png", alt: "VINdicated: About Us" }],
- },
- twitter: {
- title: "About Us",
- description:
- "VINdicated was founded by Rana Darwich after experiencing dealership discrimination firsthand. Learn about our mission to educate, empower, and vindicate car buyers.",
- },
-};
+export async function generateMetadata({
+ params,
+}: {
+ params: Promise<{ locale: string }>;
+}) {
+ const { locale } = await params;
+ return getRouteMetadata(locale, "about", "/about");
+}
export default function AboutPage() {
return (
diff --git a/app/[locale]/contact/layout.tsx b/app/[locale]/contact/layout.tsx
index c56f64b..c14286d 100644
--- a/app/[locale]/contact/layout.tsx
+++ b/app/[locale]/contact/layout.tsx
@@ -1,23 +1,13 @@
-import type { Metadata } from "next";
+import { getRouteMetadata } from "@/lib/i18n/metadata";
-export const metadata: Metadata = {
- title: "Contact",
- description:
- "Get in touch with VINdicated: volunteer with our team, share your story, participate in research, or partner with us.",
- openGraph: {
- title: "Contact",
- description:
- "Get in touch with VINdicated: volunteer with our team, share your story, participate in research, or partner with us.",
- url: "/contact",
- type: "website",
- images: [{ url: "/illus-woman-dealership.png", alt: "VINdicated: Contact" }],
- },
- twitter: {
- title: "Contact",
- description:
- "Get in touch with VINdicated: volunteer with our team, share your story, participate in research, or partner with us.",
- },
-};
+export async function generateMetadata({
+ params,
+}: {
+ params: Promise<{ locale: string }>;
+}) {
+ const { locale } = await params;
+ return getRouteMetadata(locale, "contact", "/contact");
+}
export default function ContactLayout({
children,
diff --git a/app/[locale]/documents/layout.tsx b/app/[locale]/documents/layout.tsx
index 9f8644e..7c4d3ad 100644
--- a/app/[locale]/documents/layout.tsx
+++ b/app/[locale]/documents/layout.tsx
@@ -1,23 +1,13 @@
-import type { Metadata } from "next";
+import { getRouteMetadata } from "@/lib/i18n/metadata";
-export const metadata: Metadata = {
- title: "Documents Decoded",
- description:
- "Interactive guides to understanding car buying documents: the pink slip, Carfax reports, financing contracts, and more. Know what you're signing before you sign it.",
- openGraph: {
- title: "Documents Decoded",
- description:
- "Interactive guides to understanding car buying documents: the pink slip, Carfax reports, financing contracts, and more. Know what you're signing before you sign it.",
- url: "/documents",
- type: "website",
- images: [{ url: "/illus-woman-dealership.png", alt: "VINdicated: Documents Decoded" }],
- },
- twitter: {
- title: "Documents Decoded",
- description:
- "Interactive guides to understanding car buying documents: the pink slip, Carfax reports, financing contracts, and more. Know what you're signing before you sign it.",
- },
-};
+export async function generateMetadata({
+ params,
+}: {
+ params: Promise<{ locale: string }>;
+}) {
+ const { locale } = await params;
+ return getRouteMetadata(locale, "documents", "/documents");
+}
export default function DocumentsLayout({
children,
diff --git a/app/[locale]/documents/page.tsx b/app/[locale]/documents/page.tsx
index 5b0f0a5..b9f596b 100644
--- a/app/[locale]/documents/page.tsx
+++ b/app/[locale]/documents/page.tsx
@@ -337,7 +337,8 @@ const bgItems = [
<>
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
+ word "available" means they want to sell it to you, but it
+ doesn't necessarily mean you need
it.
>
diff --git a/app/[locale]/fraud/layout.tsx b/app/[locale]/fraud/layout.tsx
index ec20f78..fcf4087 100644
--- a/app/[locale]/fraud/layout.tsx
+++ b/app/[locale]/fraud/layout.tsx
@@ -1,23 +1,13 @@
-import type { Metadata } from "next";
+import { getRouteMetadata } from "@/lib/i18n/metadata";
-export const metadata: Metadata = {
- title: "Fraud Prevention",
- description:
- "Learn to recognize and counter common dealer scams. Know the red flags, your legal rights under California and federal law, and what to do if you've been scammed.",
- openGraph: {
- title: "Fraud Prevention",
- description:
- "Learn to recognize and counter common dealer scams. Know the red flags, your legal rights under California and federal law, and what to do if you've been scammed.",
- url: "/fraud",
- type: "website",
- images: [{ url: "/illus-woman-dealership.png", alt: "VINdicated: Fraud Prevention" }],
- },
- twitter: {
- title: "Fraud Prevention",
- description:
- "Learn to recognize and counter common dealer scams. Know the red flags, your legal rights under California and federal law, and what to do if you've been scammed.",
- },
-};
+export async function generateMetadata({
+ params,
+}: {
+ params: Promise<{ locale: string }>;
+}) {
+ const { locale } = await params;
+ return getRouteMetadata(locale, "fraud", "/fraud");
+}
export default function FraudLayout({
children,
diff --git a/app/[locale]/inspection/layout.tsx b/app/[locale]/inspection/layout.tsx
index 2651731..4e75fc5 100644
--- a/app/[locale]/inspection/layout.tsx
+++ b/app/[locale]/inspection/layout.tsx
@@ -1,23 +1,13 @@
-import type { Metadata } from "next";
+import { getRouteMetadata } from "@/lib/i18n/metadata";
-export const metadata: Metadata = {
- title: "Inspection Guide",
- description:
- "A step-by-step guide to inspecting a used car before you buy. Know what to look for under the hood, on the lot, and in the paperwork.",
- openGraph: {
- title: "Inspection Guide",
- description:
- "A step-by-step guide to inspecting a used car before you buy. Know what to look for under the hood, on the lot, and in the paperwork.",
- url: "/inspection",
- type: "website",
- images: [{ url: "/illus-woman-dealership.png", alt: "VINdicated: Inspection Guide" }],
- },
- twitter: {
- title: "Inspection Guide",
- description:
- "A step-by-step guide to inspecting a used car before you buy. Know what to look for under the hood, on the lot, and in the paperwork.",
- },
-};
+export async function generateMetadata({
+ params,
+}: {
+ params: Promise<{ locale: string }>;
+}) {
+ const { locale } = await params;
+ return getRouteMetadata(locale, "inspection", "/inspection");
+}
export default function InspectionLayout({
children,
diff --git a/app/[locale]/inspection/page.tsx b/app/[locale]/inspection/page.tsx
index 72eea0d..0f25e04 100644
--- a/app/[locale]/inspection/page.tsx
+++ b/app/[locale]/inspection/page.tsx
@@ -633,11 +633,11 @@ const inspectionAccordion = [
Check engine light codes: Even if the light
- isn't on, a mechanic can pull stored codes.
+ isn't on, a mechanic can pull stored codes.
Compression test: Reveals internal engine wear
- that visual inspection can't catch.
+ that visual inspection can't catch.
- A Carfax report helps but isn't the whole picture. A physical
+ A Carfax report helps but isn't the whole picture. A physical
inspection can catch:
@@ -777,7 +777,7 @@ export default function InspectionPage() {
kicker=""
title={
<>
- Don't Buy a Car
+ Don't Buy a Car
Without This.
>
@@ -890,7 +890,7 @@ export default function InspectionPage() {
body: (
<>
- If you're buying a used Honda from a Mitsubishi
+ If you're buying a used Honda from a Mitsubishi
dealership,{" "}
take it to a Honda dealership for the PPI.
@@ -994,11 +994,11 @@ export default function InspectionPage() {
{ fontSize: "clamp(2.4rem,5vw,4rem)" } as React.CSSProperties
}
>
- Why You're Paying for Your Safety
+ 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.
+ A good inspector goes through the car systematically. Here's
+ what they're checking, and why each category matters.
@@ -1059,10 +1059,10 @@ export default function InspectionPage() {
If a seller stalls on producing service records or promises
- they'll "get them later," schedule the inspection
+ they'll "get them later," schedule the inspection
before you commit emotionally.{" "}
- Schedule the inspection before you're attached, not
+ Schedule the inspection before you're attached, not
after.
diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx
index 8c16155..7aa35f7 100644
--- a/app/[locale]/layout.tsx
+++ b/app/[locale]/layout.tsx
@@ -1,4 +1,3 @@
-import type { Metadata } from "next";
import "../globals.css";
import { Lora, Figtree } from "next/font/google";
import { notFound } from "next/navigation";
@@ -6,6 +5,7 @@ import { Nav } from "@/components/layout/Nav";
import { Footer } from "@/components/layout/Footer";
import { locales, isValidLocale, isRtl, type Locale } from "@/lib/i18n/config";
import { getDictionary } from "@/lib/i18n/get-dictionary";
+import { getRootMetadata } from "@/lib/i18n/metadata";
// import { CarCursor } from "@/components/ui/CarCursor";
const lora = Lora({
@@ -28,44 +28,14 @@ export function generateStaticParams() {
return locales.map((locale) => ({ locale }));
}
-export const metadata: Metadata = {
- metadataBase: new URL(
- process.env.NEXT_PUBLIC_SITE_URL ?? "https://getvindicated.org"
- ),
- title: {
- default: "VINdicated | Educate. Empower. Vindicate.",
- template: "%s | VINdicated",
- },
- description:
- "VINdicated is a nonprofit built on the belief that car knowledge should be public knowledge. We break down the systems that allow consumer discrimination to thrive.",
- openGraph: {
- type: "website",
- siteName: "VINdicated",
- title: {
- default: "VINdicated | Educate. Empower. Vindicate.",
- template: "%s | VINdicated",
- },
- description:
- "VINdicated is a nonprofit built on the belief that car knowledge should be public knowledge. We break down the systems that allow consumer discrimination to thrive.",
- url: "/",
- images: [
- {
- url: "/illus-woman-dealership.png",
- alt: "VINdicated",
- },
- ],
- },
- twitter: {
- card: "summary_large_image",
- title: {
- default: "VINdicated | Educate. Empower. Vindicate.",
- template: "%s | VINdicated",
- },
- description:
- "VINdicated is a nonprofit built on the belief that car knowledge should be public knowledge. We break down the systems that allow consumer discrimination to thrive.",
- images: ["/illus-woman-dealership.png"],
- },
-};
+export async function generateMetadata({
+ params,
+}: {
+ params: Promise<{ locale: string }>;
+}) {
+ const { locale } = await params;
+ return getRootMetadata(locale);
+}
export default async function RootLayout({
children,
diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx
index 8b3fe26..63a71c9 100644
--- a/app/[locale]/page.tsx
+++ b/app/[locale]/page.tsx
@@ -6,7 +6,7 @@ import { HomeQuote } from "@/components/sections/HomeQuote";
import { HomeFounder } from "@/components/sections/HomeFounder";
import { Divider } from "@/components/ui";
import { getDictionary } from "@/lib/i18n/get-dictionary";
-import type { Locale } from "@/lib/i18n/config";
+import { localizedPathnames, localizeHref, type Locale } from "@/lib/i18n/config";
import type { HomeDict } from "@/lib/i18n/home-dict";
export async function generateMetadata({
@@ -23,16 +23,23 @@ export async function generateMetadata({
return {
title,
+ description,
+ alternates: {
+ canonical: localizeHref(locale as Locale, "/"),
+ languages: localizedPathnames("/"),
+ },
openGraph: {
title,
description,
- url: "/",
+ url: localizeHref(locale as Locale, "/"),
type: "website",
+ locale,
images: [{ url: "/preview.webp", alt: "VINdicated" }],
},
twitter: {
title,
description,
+ images: ["/preview.webp"],
},
};
}
diff --git a/app/[locale]/research/page.tsx b/app/[locale]/research/page.tsx
index 1b5dfdd..a6a0087 100644
--- a/app/[locale]/research/page.tsx
+++ b/app/[locale]/research/page.tsx
@@ -1,4 +1,3 @@
-import type { Metadata } from "next";
import {
PageHero,
FadeUp,
@@ -7,25 +6,16 @@ import {
Button,
} from "@/components/ui";
import { StudyCard } from "@/components/sections/StudyCard";
+import { getRouteMetadata } from "@/lib/i18n/metadata";
-export const metadata: Metadata = {
- title: "Research",
- description:
- "VINdicated conducts correspondence audit studies documenting gender and racial pricing disparities at car dealerships. Read our findings and methodology.",
- openGraph: {
- title: "Research",
- description:
- "VINdicated conducts correspondence audit studies documenting gender and racial pricing disparities at car dealerships. Read our findings and methodology.",
- url: "/research",
- type: "website",
- images: [{ url: "/preview.webp", alt: "VINdicated: Research" }],
- },
- twitter: {
- title: "Research",
- description:
- "VINdicated conducts correspondence audit studies documenting gender and racial pricing disparities at car dealerships. Read our findings and methodology.",
- },
-};
+export async function generateMetadata({
+ params,
+}: {
+ params: Promise<{ locale: string }>;
+}) {
+ const { locale } = await params;
+ return getRouteMetadata(locale, "research", "/research", "/preview.webp");
+}
const stats = [
{
diff --git a/app/[locale]/rights/page.tsx b/app/[locale]/rights/page.tsx
index f1ca3b1..cffddd6 100644
--- a/app/[locale]/rights/page.tsx
+++ b/app/[locale]/rights/page.tsx
@@ -1,4 +1,3 @@
-import type { Metadata } from "next";
import {
PageHero,
FadeUp,
@@ -8,30 +7,16 @@ import {
Button,
} from "@/components/ui";
import { RightsPhoneDemo } from "@/components/sections/RightsPhoneDemo";
+import { getRouteMetadata } from "@/lib/i18n/metadata";
-export const metadata: Metadata = {
- title: "Know Your Rights",
- description:
- "Know your legal rights as a car buyer. Federal and California laws protect you from discrimination and deceptive practices at dealerships.",
- openGraph: {
- title: "Know Your Rights",
- description:
- "Know your legal rights as a car buyer. Federal and California laws protect you from discrimination and deceptive practices at dealerships.",
- url: "/rights",
- type: "website",
- images: [
- {
- url: "/preview.webp",
- alt: "VINdicated: Know Your Rights",
- },
- ],
- },
- twitter: {
- title: "Know Your Rights",
- description:
- "Know your legal rights as a car buyer. Federal and California laws protect you from discrimination and deceptive practices at dealerships.",
- },
-};
+export async function generateMetadata({
+ params,
+}: {
+ params: Promise<{ locale: string }>;
+}) {
+ const { locale } = await params;
+ return getRouteMetadata(locale, "rights", "/rights", "/preview.webp");
+}
export default function RightsPage() {
return (
diff --git a/app/[locale]/team/page.tsx b/app/[locale]/team/page.tsx
index 4072d9a..88d9bd7 100644
--- a/app/[locale]/team/page.tsx
+++ b/app/[locale]/team/page.tsx
@@ -1,4 +1,3 @@
-import type { Metadata } from "next";
import Image from "next/image";
import {
PageHero,
@@ -6,27 +5,16 @@ import {
Divider,
SectionTitle,
} from "@/components/ui";
+import { getRouteMetadata } from "@/lib/i18n/metadata";
-export const metadata: Metadata = {
- title: "Our Team",
- description:
- "Meet the people behind VINdicated: advocates, researchers, and students fighting dealership discrimination.",
- openGraph: {
- title: "Our Team",
- description:
- "Meet the people behind VINdicated: advocates, researchers, and students fighting dealership discrimination.",
- url: "/team",
- type: "website",
- images: [
- { url: "/preview.webp", alt: "VINdicated: Our Team" },
-],
- },
- twitter: {
- title: "Our Team",
- description:
- "Meet the people behind VINdicated: advocates, researchers, and students fighting dealership discrimination.",
- },
-};
+export async function generateMetadata({
+ params,
+}: {
+ params: Promise<{ locale: string }>;
+}) {
+ const { locale } = await params;
+ return getRouteMetadata(locale, "team", "/team", "/preview.webp");
+}
type Social = {
platform: "linkedin" | "github" | "instagram" | "website";
diff --git a/app/[locale]/volunteer/layout.tsx b/app/[locale]/volunteer/layout.tsx
new file mode 100644
index 0000000..6989b7c
--- /dev/null
+++ b/app/[locale]/volunteer/layout.tsx
@@ -0,0 +1,18 @@
+import { getRouteMetadata } from "@/lib/i18n/metadata";
+
+export async function generateMetadata({
+ params,
+}: {
+ params: Promise<{ locale: string }>;
+}) {
+ const { locale } = await params;
+ return getRouteMetadata(locale, "volunteer", "/volunteer", "/preview.webp");
+}
+
+export default function VolunteerLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return children;
+}
diff --git a/app/[locale]/volunteer/page.tsx b/app/[locale]/volunteer/page.tsx
index 6dfe0bc..f97e921 100644
--- a/app/[locale]/volunteer/page.tsx
+++ b/app/[locale]/volunteer/page.tsx
@@ -1,13 +1,298 @@
-import { redirect } from "next/navigation";
-import { localizeHref, type Locale } from "@/lib/i18n/config";
-
-// Merged into /join (tab: "Apply to Volunteer"). Kept as a redirect so
-// existing links/bookmarks to /volunteer don't break.
-export default async function VolunteerRedirect({
- params,
-}: {
- params: Promise<{ locale: string }>;
-}) {
- const { locale } = await params;
- redirect(localizeHref(locale as Locale, "/join"));
+"use client";
+
+import { useState } from "react";
+import { PageHero, FadeUp, Divider } from "@/components/ui";
+import { submitVolunteerApplication } from "@/app/actions/submitVolunteer";
+
+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.",
+ },
+ {
+ 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.",
+ },
+ {
+ 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",
+ 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 },
+];
+
+type FormState = "idle" | "submitting" | "success" | "error";
+
+export default function VolunteerPage() {
+ 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);
+ try {
+ const result = await submitVolunteerApplication({
+ name: data.get("name") as string,
+ email: data.get("email") 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,
+ });
+ if (result.success) {
+ setState("success");
+ setMessage(result.message);
+ } 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.>}
+ subtitle="We'll be in touch within a few days. Check your inbox for a confirmation email."
+ />
+
+
+
{message}
+
+
+ >
+ );
+ }
+
+ return (
+ <>
+ Join VINdicated.>}
+ subtitle="We're a student-led research nonprofit with chapters at UCLA, UC Berkeley, and UC Santa Cruz. Find your chapter and apply below."
+ />
+
+
+
+
+
+ Our Chapters.
+
+
+
+ {CHAPTERS.map((ch) => (
+
+
{ch.label}
+
{ch.school}
+
{ch.focus}
+
+ Leads: {ch.lead}
+
+
+ Contact: {ch.contact}
+
+
+ ))}
+
+
+
+
+
+
+ What we're looking for.
+
+
+ {ROLES.map((role) => (
+
+
{role.label}
+
{role.min_hours}+ hours per week
+
+ ))}
+
+
+ All positions are remote and unpaid. VINdicated is a volunteer-run nonprofit.
+