PageSourceSearch

https://vintageproject.io/app.jsx

js vintageproject.io collected 2026-10-04 23:47:03 UTC 15,772 bytes, 325 lines download raw bytes

1const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
2  "palette": "Sand",
3  "type": "Archivo + Spectral"
4}/*EDITMODE-END*/;
5
6const PALETTES = {
7  "Sand":       { bg: "#e9e0d2", panel: "#f3ece0", ink: "#1c1813", soft: "#766d5d", line: "rgba(28,24,19,.13)" },
8  "Cool paper": { bg: "#eceef0", panel: "#f6f7f8", ink: "#15181c", soft: "#697079", line: "rgba(21,24,28,.12)" },
9  "Ivory":      { bg: "#f4efe3", panel: "#fbf8f0", ink: "#211d15", soft: "#7a7363", line: "rgba(33,29,21,.12)" },
10  "Slate":      { bg: "#17181a", panel: "#202225", ink: "#ededea", soft: "#9a9a95", line: "rgba(237,237,234,.14)" },
11};
12const TYPESETS = {
13  "Archivo + Spectral": { display: "'Archivo','Helvetica Neue',Arial,sans-serif", body: "'Spectral',Georgia,serif" },
14  "All Archivo":        { display: "'Archivo','Helvetica Neue',Arial,sans-serif", body: "'Archivo','Helvetica Neue',Arial,sans-serif" },
15  "Archivo + Newsreader": { display: "'Archivo','Helvetica Neue',Arial,sans-serif", body: "'Newsreader',Georgia,serif" },
16};
17
18function applyTheme(t) {
19  const p = PALETTES[t.palette] || PALETTES["Sand"];
20  const ty = TYPESETS[t.type] || TYPESETS["Archivo + Spectral"];
21  const r = document.documentElement.style;
22  r.setProperty("--cv-bg", p.bg);
23  r.setProperty("--cv-panel", p.panel);
24  r.setProperty("--cv-ink", p.ink);
25  r.setProperty("--cv-soft", p.soft);
26  r.setProperty("--cv-line", p.line);
27  r.setProperty("--font-display", ty.display);
28  r.setProperty("--font-body", ty.body);
29  document.documentElement.dataset.palette = t.palette === "Slate" ? "dark" : "light";
30}
31
32/* Curated marquee sets for the home carousels */
33const ICONIC_FUNDS = ["usv-2004", "bench-7", "seq-8", "thrive-2", "rb-1", "mk-1"];
34const ICONIC_PEOPLE = [
35  "seq-valentine", "seq-moritz", "bench-gurley", "kp-doerr", "ff-thiel", "ff-rabois",
36  "ff-stephens", "usv-wilson", "thrive-kushner", "mk-hsu", "mk-rannala", "fr-kopelman", "rb-malka",
37];
38
39/* One-line teasers leading with the notable investments / returns that made each fund iconic */
40const ICONIC_FUND_TEASERS = {
41  "usv-2004": "Twitter, Zynga, Etsy & Tumblr — ~13.9x on a $125M fund, Twitter alone ~8x.",
42  "bench-7": "Led Uber's 2011 Series A — one of the greatest venture returns ever recorded.",
43  "seq-8": "Sequoia's 1998 vintage — the Google bet (~$12M → billions), plus PayPal.",
44  "thrive-2": "Instagram in 2011 — ~2x in 72 hours — as the concentration thesis took shape.",
45  "rb-1": "Ribbit's debut — Coinbase, Credit Karma & the seed of a fintech dynasty.",
46  "mk-1": "Honey returned ~$280M (23x the fund); ServiceTitan IPO'd at ~$9B — 43–53x net.",
47};
48
49/* One-sentence teasers for what made each iconic person iconic */
50const ICONIC_PEOPLE_TEASERS = {
51  "seq-valentine": "The father of Silicon Valley venture — backed Atari, Apple, Oracle & Cisco.",
52  "seq-moritz": "Wrote the early checks into Google, Yahoo & PayPal.",
53  "bench-gurley": "Led Benchmark's Uber bet and defined the internet-marketplace playbook.",
54  "kp-doerr": "Backed Google and Amazon; brought OKRs to the Valley.",
55  "ff-thiel": "First outside investor in Facebook; co-founded PayPal and Palantir.",
56  "ff-rabois": "The operator-investor behind Square, Affirm, Ramp & Faire.",
57  "ff-stephens": "Co-founded Anduril and turned American Dynamism into a thesis.",
58  "usv-wilson": "Built USV on a daily blog; early in Twitter, Etsy & Coinbase.",
59  "thrive-kushner": "Turned a $5M first fund into Thrive; led OpenAI's record round.",
60  "mk-hsu": "Ex-eBay & AT&T operator who built Mucker's LA-first micro-VC.",
61  "mk-rannala": "Coined the 'Blue Collar VC' and rode Honey to a 23x fund.",
62  "fr-kopelman": "Invented institutional seed; wrote First Round's first check into Uber.",
63  "rb-malka": "Fintech operator-investor behind Coinbase, Nubank & Robinhood.",
64};
65
66/* A firm-swatch used in group headers and the home collage */
67function FirmSwatch({ firm }) {
68  return (
69    <span className="dirgroup__swatch" style={{ background: firm.bg, color: firm.ink }}>
70      {firm.logoSrc ? <img src={firm.logoSrc} alt="" /> : firm.name.split(" ").map((w) => w[0]).slice(0, 2).join("")}
71    </span>
72  );
73}
74
75/* Directory view — every fund, grouped by firm */
76function FundsDirectory({ nav, search, setSearch }) {
77  return (
78    <main className="home">
79      <TopBar search={search} setSearch={setSearch} onMenu={nav.menu} onHome={nav.home} nav={nav} />
80      <div className="pagenav" style={{ padding: "0 44px" }}>
81        <Pill variant="ghost" onClick={nav.back}><span style={{ marginRight: 8 }}>←</span> Back</Pill>
82      </div>
83      <div className="dirhead">
84        <h1 className="dirhead__title">Every fund</h1>
85        <p className="dirhead__sub">All {window.FUNDS.length} funds across the directory, grouped by firm and ordered by vintage.</p>
86      </div>
87      {window.FIRMS.map((firm) => {
88        const funds = window.fundsForFirm(firm.id).sort((a, b) => (a.vintage || 0) - (b.vintage || 0));
89        if (!funds.length) return null;
90        return (
91          <section className="dirgroup" key={firm.id}>
92            <div className="dirgroup__head">
93              <FirmSwatch firm={firm} />
94              <button className="dirgroup__name" style={{ background: "none", border: "none", cursor: "pointer", padding: 0 }} onClick={() => nav.firm(firm.id)}>{firm.name}</button>
95              <span className="dirgroup__count">{funds.length} funds</span>
96            </div>
97            <DirectoryGrid>
98              {funds.map((fd) => (
99                <Tile key={fd.id} title={fd.name} tag={String(fd.vintage)}
100                  teaser={fd.strategy ? (fd.strategy.length > 116 ? fd.strategy.slice(0, 113).replace(/\s+\S*$/, "") + "…" : fd.strategy) : ""}
101                  bg={firm.bg} ink={firm.ink} accent={firm.accent} onClick={() => nav.fund(fd.id)} />
102              ))}
103            </DirectoryGrid>
104          </section>
105        );
106      })}
107      <SiteFooter />
108    </main>
109  );
110}
111
112/* Directory view — every person, grouped by firm */
113function PeopleDirectory({ nav, search, setSearch }) {
114  return (
115    <main className="home">
116      <TopBar search={search} setSearch={setSearch} onMenu={nav.menu} onHome={nav.home} nav={nav} />
117      <div className="pagenav" style={{ padding: "0 44px" }}>
118        <Pill variant="ghost" onClick={nav.back}><span style={{ marginRight: 8 }}>←</span> Back</Pill>
119      </div>
120      <div className="dirhead">
121        <h1 className="dirhead__title">Every person</h1>
122        <p className="dirhead__sub">All {window.PEOPLE.length} investors profiled across the directory, grouped by firm.</p>
123      </div>
124      {window.FIRMS.map((firm) => {
125        const people = window.peopleForFirm(firm.id);
126        if (!people.length) return null;
127        return (
128          <section className="dirgroup" key={firm.id}>
129            <div className="dirgroup__head">
130              <FirmSwatch firm={firm} />
131              <button className="dirgroup__name" style={{ background: "none", border: "none", cursor: "pointer", padding: 0 }} onClick={() => nav.firm(firm.id)}>{firm.name}</button>
132              <span className="dirgroup__count">{people.length} people</span>
133            </div>
134            <DirectoryGrid>
135              {people.map((p) => (
136                <Tile key={p.id} variant="person" initials={initialsOf(p.name)} fullName={p.noPhoto ? p.name : null} title={p.name} tag={p.name}
137                  teaser={<React.Fragment><span className="tile__teaserrole">{p.role}</span><br />{p.tenure}</React.Fragment>}
138                  bg={p.noPhoto ? "#111111" : firm.bg} ink={p.noPhoto ? "#ffffff" : firm.ink} accent={firm.accent}
139                  logoId={p.noPhoto ? undefined : "dir-headshot-" + p.id} src={p.noPhoto ? undefined : p.headshotSrc} onClick={() => nav.person(p.id)} />
140              ))}
141            </DirectoryGrid>
142          </section>
143        );
144      })}
145      <SiteFooter />
146    </main>
147  );
148}
149
150/* ===== URL routing (real paths, e.g. /firm/sequoia) =====
151   Paths are shareable and SEO-indexable. The host must rewrite all routes to
152   index.html (vercel.json / serve.json included in this folder). */
153function viewToPath(v) {
154  if (v.kind === "home") return "/";
155  if (v.kind === "funds") return "/funds";
156  if (v.kind === "people") return "/people";
157  return "/" + v.kind + "/" + v.id;
158}
159function pathToView(path) {
160  const p = (path || "/").replace(/\/+$/, "") || "/";
161  if (p === "/" || p.endsWith("index.html") || p.endsWith(".html")) return { kind: "home" };
162  if (p === "/funds") return { kind: "funds" };
163  if (p === "/people") return { kind: "people" };
164  const m = p.match(/^\/(firm|fund|person)\/([\w-]+)$/);
165  if (m) {
166    const lookup = { firm: window.getFirm, fund: window.getFund, person: window.getPerson }[m[1]];
167    if (lookup(m[2])) return { kind: m[1], id: m[2] };
168  }
169  return { kind: "home" };
170}
171/* Build the logical ancestry for a deep-linked view so the in-app Back works. */
172function stackForView(v) {
173  const home = { kind: "home" };
174  if (v.kind === "home") return [home];
175  if (v.kind === "funds" || v.kind === "people") return [home, v];
176  if (v.kind === "firm") return [home, v];
177  const rec = v.kind === "fund" ? window.getFund(v.id) : window.getPerson(v.id);
178  return [home, { kind: "firm", id: rec.firmId }, v];
179}
180function viewTitle(v) {
181  const base = "The Vintage Project";
182  if (v.kind === "firm") return window.getFirm(v.id).name + " · " + base;
183  if (v.kind === "fund") { const f = window.getFund(v.id); return f.name + " · " + window.getFirm(f.firmId).name + " · " + base; }
184  if (v.kind === "person") { const p = window.getPerson(v.id); return p.name + " · " + window.getFirm(p.firmId).name + " · " + base; }
185  if (v.kind === "funds") return "Every fund · " + base;
186  if (v.kind === "people") return "Every person · " + base;
187  return base + " | Every investment firm has a story.";
188}
189
190function App() {
191  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
192  const [stack, setStack] = React.useState(() => stackForView(pathToView(window.location.pathname)));
193  const [search, setSearch] = React.useState("");
194  const [menuOpen, setMenuOpen] = React.useState(false);
195
196  React.useEffect(() => { applyTheme(t); }, [t.palette, t.type]);
197
198  const view = stack[stack.length - 1];
199
200  /* Keep the address bar + tab title in sync with the current view. */
201  React.useEffect(() => {
202    const path = viewToPath(view);
203    if (window.location.pathname !== path && window.location.protocol !== "file:") {
204      window.history.pushState({}, "", path);
205    }
206    document.title = viewTitle(view);
207  }, [view]);
208
209  /* Browser back/forward → rebuild the view from the URL. */
210  React.useEffect(() => {
211    const onPop = () => { setStack(stackForView(pathToView(window.location.pathname))); window.scrollTo(0, 0); };
212    window.addEventListener("popstate", onPop);
213    return () => window.removeEventListener("popstate", onPop);
214  }, []);
215
216  const go = (v) => { setStack((s) => [...s, v]); window.scrollTo(0, 0); };
217  const nav = {
218    home: () => { setStack([{ kind: "home" }]); window.scrollTo(0, 0); },
219    back: () => setStack((s) => (s.length > 1 ? s.slice(0, -1) : s)),
220    firm: (id) => go({ kind: "firm", id }),
221    fund: (id) => go({ kind: "fund", id }),
222    person: (id) => go({ kind: "person", id }),
223    funds: () => go({ kind: "funds" }),
224    people: () => go({ kind: "people" }),
225    menu: () => setMenuOpen(true),
226  };
227
228  const filtered = React.useMemo(() => {
229    const q = search.trim().toLowerCase();
230    if (!q) return window.FIRMS;
231    return window.FIRMS.filter((f) => (f.name + " " + f.teaser + " " + f.blurb).toLowerCase().includes(q));
232  }, [search]);
233
234  return (
235    <div className="app">
236      {view.kind === "home" && (
237        <main className="home">
238          <TopBar search={search} setSearch={setSearch} onMenu={nav.menu} onHome={nav.home} nav={nav} />
239
240          {search.trim() ? (
241            filtered.length === 0 ? (
242              <div className="empty">No firms match “{search}”.</div>
243            ) : (
244              <DirectoryGrid>
245                {filtered.map((f) => (
246                  <Tile key={f.id} title={f.name} tag={f.name} teaser={f.teaser} logoId={"logo-" + f.id}
247                    src={f.logoSrc} bg={f.bg} ink={f.ink} accent={f.accent} onClick={() => nav.firm(f.id)} />
248                ))}
249              </DirectoryGrid>
250            )
251          ) : (
252            <React.Fragment>
253              {/* 1 — Firm directory, above the fold, no header */}
254              <DirectoryGrid>
255                {window.FIRMS.map((f) => (
256                  <Tile key={f.id} title={f.name} tag={f.name} teaser={f.teaser} logoId={"logo-" + f.id}
257                    src={f.logoSrc} bg={f.bg} ink={f.ink} accent={f.accent} onClick={() => nav.firm(f.id)} />
258                ))}
259              </DirectoryGrid>
260
261              {/* 2 — Every firm has a story */}
262              <section className="storyband">
263                <h1 className="storyband__title">Every investment firm has a story.</h1>
264                <p className="storyband__sub">We research the funds, people, and decisions that have created the most iconic firms in the world.</p>
265                <div className="storyband__cta">
266                  <ArrowPill onClick={() => window.scrollTo({ top: 0, behavior: "smooth" })}>Explore The Vintage Project</ArrowPill>
267                </div>
268              </section>
269
270              {/* 3 — Iconic funds */}
271              <Carousel title="Iconic funds" sub="The vintages that rewrote what a single fund could return." onMore={nav.funds}>
272                {ICONIC_FUNDS.map((id) => {
273                  const fd = window.getFund(id); if (!fd) return null;
274                  return <FundCard key={id} fund={fd} firm={window.getFirm(fd.firmId)} teaser={ICONIC_FUND_TEASERS[id]} onClick={() => nav.fund(id)} />;
275                })}
276              </Carousel>
277
278              {/* 4 — Iconic people */}
279              <Carousel title="Iconic people" sub="The investors who defined their firms and eras." onMore={nav.people}>
280                {ICONIC_PEOPLE.map((id) => {
281                  const p = window.getPerson(id); if (!p) return null;
282                  return <PersonCard key={id} person={p} firm={window.getFirm(p.firmId)} teaser={ICONIC_PEOPLE_TEASERS[id]} onClick={() => nav.person(id)} />;
283                })}
284              </Carousel>
285            </React.Fragment>
286          )}
287
288          <SiteFooter />
289        </main>
290      )}
291
292      {view.kind === "funds" && <FundsDirectory nav={nav} search={search} setSearch={setSearch} />}
293      {view.kind === "people" && <PeopleDirectory nav={nav} search={search} setSearch={setSearch} />}
294
295      {view.kind === "firm" && (
296        <FirmView firm={window.getFirm(view.id)} nav={nav} search={search} setSearch={setSearch} />
297      )}
298      {view.kind === "fund" && (
299        <FundView fund={window.getFund(view.id)} nav={nav} search={search} setSearch={setSearch} />
300      )}
301      {view.kind === "person" && (
302        <PersonView person={window.getPerson(view.id)} nav={nav} search={search} setSearch={setSearch} />
303      )}
304
305      <MenuOverlay open={menuOpen} onClose={() => setMenuOpen(false)} onHome={nav.home} />
306
307      <NewsletterPopup key={view.kind + (view.id || "")} />
308
309      {view.kind === "home" && <WelcomePopup />}
310
311      <TweaksPanel>
312        <TweakSection label="Palette" />
313        <TweakRadio label="Canvas" value={t.palette}
314          options={["Sand", "Cool paper", "Ivory", "Slate"]}
315          onChange={(v) => setTweak("palette", v)} />
316        <TweakSection label="Typography" />
317        <TweakRadio label="Type set" value={t.type}
318          options={["Archivo + Spectral", "All Archivo", "Archivo + Newsreader"]}
319          onChange={(v) => setTweak("type", v)} />
320      </TweaksPanel>
321    </div>
322  );
323}
324
325ReactDOM.createRoot(document.getElementById("root")).render(<App />);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.