Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/[locale]/about/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { CtaBand } from "@/components/sections/shared/CtaBand";
import { ZoomSlideshow } from "@/components/sections/about/ZoomSlideshow";
import { PhotoGrid } from "@/components/sections/about/PhotoGrid";
import { MissionPillars } from "@/components/sections/about/MissionPillars";
import { FounderStory } from "@/components/sections/about/FounderStory";
import { FounderVideo } from "@/components/sections/about/FounderVideo";
import { AiScams } from "@/components/sections/about/AiScams";
import { QuoteBand } from "@/components/sections/about/QuoteBand";
import { WhatWeProvide } from "@/components/sections/about/WhatWeProvide";
Expand Down Expand Up @@ -43,6 +45,8 @@ export default async function AboutPage({
<ZoomSlideshow dict={dict.zoom} />
<PhotoGrid dict={dict.photos} />
<MissionPillars dict={dict.mission} />
<FounderStory dict={dict.story} />
<FounderVideo dict={dict.video} />
<AiScams dict={dict.ai} rightsHref={href("/fraud#law")} />
<QuoteBand text={dict.quote.text} cite={dict.quote.cite} />
<WhatWeProvide dict={dict.provide} projectsHref={href("/research")} />
Expand Down
5 changes: 5 additions & 0 deletions app/[locale]/inspection/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Link from "next/link";
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 { PpiSteps } from "@/components/sections/inspection/PpiSteps";
import { getDictionary } from "@/lib/i18n/get-dictionary";
Expand Down Expand Up @@ -125,6 +126,10 @@ export default async function InspectionPage({
</div>
</section>

<div className="quote-pad">
<QuoteBand text={d.quote.text} cite={d.quote.cite} />
</div>

<PpiSteps dict={d.schedule} />
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion app/[locale]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export default async function HomePage({

const ppi = href("/inspection");
const fraud = href("/fraud");
const story = href("/about");
const story = href("/about#story");

return (
<div className="rd rd-page">
Expand Down
2 changes: 1 addition & 1 deletion app/[locale]/volunteer/page.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const CHAPTERS = [
value: "ucberkeley",
label: "UC Berkeley Chapter",
school: "University of California, Berkeley",
lead: "Noirit Ghosh Choudhuri, Internal Vice President · Halima Cherif Hminat, External Vice President",
lead: "Rana Darwich, President · Noirit Ghosh Choudhuri, Internal Vice President · Halima Cherif Hminat, External Vice President",
contact: "noiritgc@gmail.com",
focus: "Data and outreach hub. Leads the interactive dealership risk map, contributes to data engineering on the audit study, and runs community-facing education and outreach initiatives.",
},
Expand Down
6 changes: 6 additions & 0 deletions app/redesign-overrides.css
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,12 @@
margin-left: auto;
margin-right: auto;
}
.rd .fv-section {
padding-top: 88px;
}
.rd .pull {
margin-inline: 0;
}
.rd figure.quote-band {
margin: 0;
}
Expand Down
39 changes: 39 additions & 0 deletions app/redesign.css
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ html:has(.rd-page){scroll-behavior:smooth;scroll-padding-top:calc(env(safe-area-
@media (max-width:900px){
.rd .story{grid-template-columns:1fr;gap:28px}
}
.rd .pull{margin-top:24px;border-left:4px solid var(--purple);padding:6px 0 6px 22px;font-size:1.2rem;font-weight:700;line-height:1.55}
.rd .pull cite{display:block;font-style:normal;font-weight:600;font-size:1rem;margin-top:10px;color:var(--purple-deep)}
.rd [hidden]{display:none!important}
.rd .plan{display:grid;grid-template-columns:.9fr 1.1fr;gap:64px;align-items:start}
@media (max-width:900px){
Expand Down Expand Up @@ -286,6 +288,25 @@ html:has(.rd-page){scroll-behavior:smooth;scroll-padding-top:calc(env(safe-area-
.rd .pillar-lg{padding:8px 4px}
.rd .pillar-lg h3{font-size:1.7rem;margin-bottom:10px}
.rd .pillar-lg p{font-size:1.08rem;line-height:1.7}
.rd .founder{display:grid;grid-template-columns:.9fr 1.1fr;gap:64px;align-items:start}
@media (max-width:900px){
.rd .founder{grid-template-columns:1fr;gap:36px}
}
.rd .founder-side{position:sticky;top:120px}
@media (max-width:900px){
.rd .founder-side{position:static}
}
.rd .founder-side .lede{margin-top:18px}
.rd .strikes{display:flex;flex-direction:column;gap:20px}
.rd .strike-card{background:var(--bg);border-radius:24px;padding:32px;opacity:.35;transform:translateY(24px);transition:opacity .6s,transform .6s}
.rd .strike-card.in{opacity:1;transform:none}
@media (prefers-reduced-motion:reduce){
.rd .strike-card{opacity:1;transform:none;transition:none}
}
.rd .strike-top{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:14px}
.rd .strike-n{font-size:2.6rem;font-weight:800;color:var(--purple);letter-spacing:-.04em;line-height:1}
.rd .strike-card h3{font-size:1.5rem;margin-bottom:10px}
.rd .strike-card p{font-size:1.05rem;line-height:1.75}
.rd .provide4{display:grid;grid-template-columns:repeat(4,1fr);gap:20px}
@media (max-width:1000px){
.rd .provide4{grid-template-columns:1fr 1fr}
Expand Down Expand Up @@ -761,6 +782,24 @@ html:has(.rd-page){scroll-behavior:smooth;scroll-padding-top:calc(env(safe-area-
.rd .hot-ring{animation:none}
}
.rd .tour-card{opacity:.4}
.rd .fv{display:grid;grid-template-columns:1.35fr .65fr;gap:48px;align-items:center}
@media (max-width:900px){
.rd .fv{grid-template-columns:1fr;gap:24px}
}
.rd .fv-player{position:relative;border-radius:28px;overflow:hidden;background:var(--ink);box-shadow:0 30px 60px -28px rgba(42,15,49,.5);aspect-ratio:16/9}
.rd .fv-player video{display:block;width:100%;height:100%;object-fit:cover}
.rd .fv-play{position:absolute;inset:0;border:0;padding:0;cursor:pointer;background:linear-gradient(180deg,transparent 55%,rgba(42,15,49,.55));display:grid;place-items:center;color:#fff}
.rd .fv-btn{width:88px;height:88px;border-radius:50%;background:var(--purple);display:grid;place-items:center;box-shadow:0 0 0 10px rgba(255,255,255,.28);transition:transform .2s,box-shadow .2s}
.rd .fv-btn svg{margin-left:4px}
.rd .fv-play:hover .fv-btn{transform:scale(1.07);box-shadow:0 0 0 14px rgba(255,255,255,.32)}
.rd .fv-len{position:absolute;right:18px;bottom:16px;background:rgba(42,15,49,.7);border-radius:999px;padding:5px 12px;font-weight:700;font-size:.9rem}
.rd .fv-player.playing .fv-play{display:none}
.rd .fv-text .lede{margin-top:16px}
.rd .fv-who{margin-top:24px;padding-left:16px;border-left:4px solid var(--purple);line-height:1.5}
.rd .fv-who b{display:block;font-size:1.1rem}
@media (prefers-reduced-motion:reduce){
.rd .fv-btn{transition:none}
}
.rd .avatar-play{position:relative;border:0;padding:0;background:none;cursor:pointer;align-self:flex-start;border-radius:24px}
.rd .avatar-play .avatar-img{transition:transform .2s}
@keyframes rd-ring2{0%{transform:scale(.96);opacity:.9}100%{transform:scale(1.12);opacity:0}}
Expand Down
1 change: 1 addition & 0 deletions components/layout/SiteFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export function SiteFooter({
</div>
<div className="fbot">
<span>{footer.rights}</span>
<span>{footer.founded}</span>
</div>
</div>
</footer>
Expand Down
1 change: 1 addition & 0 deletions components/layout/SiteHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const NAV: NavItem[] = [
children: [
{ key: "aboutWhoWeAre", href: "/about" },
{ key: "aboutMission", href: "/about#mission" },
{ key: "aboutFounder", href: "/about#story" },
{ key: "aboutProvides", href: "/about#provide" },
{ key: "projects", href: "/research" },
],
Expand Down
47 changes: 47 additions & 0 deletions components/sections/about/FounderStory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"use client";

import type { AboutPageDict } from "@/lib/i18n/dictionary";
import { useInView } from "../shared/useInView";

// Sticky intro on the left, strike cards that fade in as they scroll past.
export function FounderStory({ dict }: { dict: AboutPageDict["story"] }) {
return (
<section className="band" id="story">
<div className="wrap founder">
<div className="founder-side">
<h2 className="h2">{dict.title}</h2>
<p className="lede">{dict.body}</p>
<blockquote className="pull">
{dict.quote}
<cite>{dict.quoteCite}</cite>
</blockquote>
</div>
<div className="strikes">
{dict.strikes.map((s) => (
<StrikeCard key={s.n} strike={s} />
))}
</div>
</div>
</section>
);
}

function StrikeCard({
strike,
}: {
strike: AboutPageDict["story"]["strikes"][number];
}) {
const [ref, inView] = useInView<HTMLElement>(0.35);
return (
<article ref={ref} className={`strike-card${inView ? " in" : ""}`}>
<div className="strike-top">
<span className="strike-n" aria-hidden="true">
{strike.n}
</span>
<span className="tag">{strike.tag}</span>
</div>
<h3>{strike.title}</h3>
<p>{strike.body}</p>
</article>
);
}
58 changes: 58 additions & 0 deletions components/sections/about/FounderVideo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"use client";

import { useRef, useState } from "react";
import type { AboutPageDict } from "@/lib/i18n/dictionary";

export function FounderVideo({ dict }: { dict: AboutPageDict["video"] }) {
const videoRef = useRef<HTMLVideoElement>(null);
const [playing, setPlaying] = useState(false);

function play() {
const v = videoRef.current;
if (!v) return;
setPlaying(true);
v.play();
v.focus();
}

return (
<section className="pt0 fv-section" id="founder-video">
<div className="wrap fv">
<div className={`fv-player${playing ? " playing" : ""}`}>
<video
ref={videoRef}
preload="metadata"
playsInline
controls={playing}
poster="/images/about/founder-rana-poster.webp"
aria-label={dict.label}
onEnded={() => {
setPlaying(false);
videoRef.current?.load();
}}
>
<source src="/videos/founder-rana.mp4" type="video/mp4" />
</video>
<button type="button" className="fv-play" aria-label={dict.play} onClick={play}>
<span className="fv-btn">
<svg width="30" height="30" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M8 5v14l11-7z" />
</svg>
</span>
<span className="fv-len" aria-hidden="true">
{dict.length}
</span>
</button>
</div>
<div className="fv-text">
<h2 className="h2">{dict.title}</h2>
<p className="lede">{dict.body}</p>
<p className="fv-who">
<b>{dict.name}</b>
{dict.role}
</p>
</div>
</div>
</section>
);
}
28 changes: 28 additions & 0 deletions components/sections/shared/useInView.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"use client";

import { useEffect, useRef, useState } from "react";

// Flips to true the first time the element scrolls into view, then stays
// true. Used for one-shot reveal animations.
export function useInView<T extends Element>(threshold = 0.35) {
const ref = useRef<T>(null);
const [inView, setInView] = useState(false);

useEffect(() => {
const el = ref.current;
if (!el || inView) return;
const io = new IntersectionObserver(
(entries) => {
if (entries.some((e) => e.isIntersecting)) {
setInView(true);
io.disconnect();
}
},
{ threshold },
);
io.observe(el);
return () => io.disconnect();
}, [threshold, inView]);

return [ref, inView] as const;
}
60 changes: 58 additions & 2 deletions lib/i18n/dictionaries/ar-EG.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"about": "عننا",
"aboutWhoWeAre": "إحنا مين",
"aboutMission": "مهمتنا",
"aboutFounder": "من المؤسسة",
"aboutProvides": "إحنا بنقدم إيه",
"team": "الفريق",
"inspection": "دليل الفحص",
Expand Down Expand Up @@ -34,7 +35,7 @@
"footer": {
"tagline": "نتعلّم. نتمكّن. ننصف حقنا.",
"subtagline": "تثقيف مجاني عن السيارات للستات والمهاجرين وأول مرة بيشتروا عربية.",
"copyright": "© 2026 VINdicated. جميع الحقوق محفوظة.",
"copyright": "© 2026 VINdicated. جميع الحقوق محفوظة. أسستها رنا درويش.",
"mission": "منظمة غير هادفة للربح ملتزمة بالمساواة بين الجنسين في تجارة السيارات.",
"columns": {
"navigate": "تصفّح",
Expand All @@ -58,7 +59,7 @@
},
"about": {
"title": "عننا",
"description": "اعرفي أكتر عن مهمتنا في تعليم وتمكين وإنصاف مشتريين العربيات.",
"description": "رنا درويش أسست VINdicated بعد ما عاشت بنفسها تمييز في معارض العربيات. اعرفي أكتر عن مهمتنا في تعليم وتمكين وإنصاف مشتريين العربيات.",
"imageAlt": "VINdicated: عننا"
},
"team": {
Expand Down Expand Up @@ -153,6 +154,14 @@
"cite": "Mary Wollstonecraft",
"source": "A Vindication of the Rights of Woman (1792)"
},
"founder": {
"titleLine1": "اتأسست في سن 19.",
"titleEm": "من الاحتياج.",
"body1": "تلات مواقف في تلات معارض مختلفة ورّوني نفس الحاجة: السيستم متصمم يلخبطك، يستعجلك، ويكسب من اللي إنتي مش عارفاه.",
"body2Prefix": "VINdicated موجودة لأن ",
"body2Bold": "كنت أتمنى تكون موجودة عشاني.",
"cta": "اقري قصة رنا كاملة"
},
"meta": {
"title": "نتعلّم. نتمكّن. ننصف حقنا.",
"description": "VINdicated منظمة مجانية غير هادفة للربح بتساعد مشتريين العربيات يتعاملوا مع المعارض من غير ما حد يستغلهم. أدلة مجانية، أبحاث، وموارد عن حقوقك."
Expand Down Expand Up @@ -229,6 +238,49 @@
}
]
},
"story": {
"titleEm": "ننصف حقنا من إيه؟",
"subtitle": "تلات مواقف. تلات ظروف مختلفة تماماً. ونفس السيستم في كل مرة.",
"strikes": [
{
"num": "1",
"label": "نصب الملكية",
"body": [
"وأنا عندي 18 سنة، كنت بحاول أشتري عربية من والد صاحبي. خلاني أستنى تلات شهور وهو طول الوقت بيوعدني إنه هيبيعهالي.",
"لما الوقت جه أخيراً، حاول يقولّي إني مش محتاجة ورقة الملكية. كان بيحاول ينصب عليّا. من غير ما الورقة تبقى باسمك، العربية مش ملكك قانونياً، مهما كنتي دفعتي فلوس."
]
},
{
"num": "2",
"label": "Facebook Marketplace",
"body": [
"حاولت أشتري عربية من Facebook Marketplace. الكلام بدأ عادي، عدد الكيلومترات، السعر، إمتى أقدر أروح أشوفها.",
"بس هو عرف إني بنت، نغمته اتغيرت خالص. قالّي، \"اخرسي يا ...\"، وعمل لي حظر."
]
},
{
"num": "3",
"label": "South Coast Mitsubishi",
"body": [
"روحت لوحدي أجرب العربية. لما رجعت مع أختي عشان نشتريها، فجأة لقيت مكتوب: \"اشتريها النهاردة وإلا هتضيع منك\".",
"بعدين دفعت عشان أعمل فحص مستقل. لقوا مشاكل. وبعدين دوني عقد تمويل بفايدة سنوية 30%. أنا عارفة الصفقة الوحشة شكلها إيه. ما مضيتش."
]
}
],
"pullquote": {
"quote": "أنا اشتكيت من إيدي. ولما المدير العام اتصل يعتذر، قلتله: أنا مش قابلة اعتذارك. أنا مستنية إن سواء بنت عندها 19 سنة أو راجل عنده 50 سنة، تعاملوا مع الكل باحترام.",
"attribution": "رنا درويش، مؤسسة VINdicated"
},
"bio": {
"label": "عن المؤسسة",
"body": "رنا درويش أسست VINdicated وهي عندها 19 سنة، بعد ما كادت توقع بند تحكيم استغلالي في معرض عربيات.",
"closingLine": "دلوقتي، جيلك."
},
"wollstonecraft": {
"label": "Mary Wollstonecraft، 1792",
"quote": "قوّوا عقل الست بتوسيعه، وهيخلص زمن الطاعة العمياء."
}
},
"provide": {
"titlePlain": "مجاني. متاح للكل.",
"titleEm": "من غير أي شروط.",
Expand Down Expand Up @@ -264,6 +316,10 @@
},
"sectionTitle": "اتعرفي على الفريق",
"members": {
"Rana Darwich": {
"position": "المؤسسة",
"bio": "رنا أسست VINdicated وهي عندها 19 سنة، بعد ما مصطلح من قانون التجارة نجّاها من إنها توقع عقد بيع استغلالي في معرض عربيات. اللي بدأ كأزمة شخصية، بقى مهمة: إنها تكسر الأنظمة اللي بتستغل المستهلكين اللي بيدخلوا معرض العربيات لوحدهم من غير دعم. هي بتقود مبادرات البحث والتوجه الاستراتيجي لـVINdicated، إيمانًا منها إن معرفة العربيات لازم تكون معرفة عامة."
},
"Rizwaan Bana": {
"position": "استراتيجي",
"bio": "رضوان بانا بيدرس علوم الكمبيوتر في UCLA. هو مسؤول البرمجيات في VINdicated، وهناك بيبني أدوات بتساعد المستهلكين يتعاملوا مع عملية شراء العربية من غير أي عرقلة. رضوان شغوف باستخدام التكنولوجيا لعمل تغيير إيجابي وجعل المعلومات أسهل وصولاً للجميع."
Expand Down
Loading