Skip to content

Repository files navigation

yo

A satirical Stanford-flavored personality quiz and the Fizz-like social feed built on top of it. 23 in-group campus archetypes (The Ghost, YC Dropout, Lake Lag Swimmer, …), one ~3-minute quiz, a cinematic 3-2-1 reveal, then an anonymous typed feed with a shareable QR card. Built for CS278 (Social Computing).

The quiz is the acquisition surface; the feed is the product. The bet: personality typing is a low-stakes coordinating object ("what's your type?" beats "what's your major?" at orientation), the feed gives that conversation somewhere to live, and the QR makes the offline→online conversion measurable.

Live: https://cs278-proj.vercel.app (Vercel · Supabase backend) Spec version: 1.8.0 · Wire shapes: PersonalityType.v=1 · QuizResult.v=1 · QuizEvent.v=1 · Post.v=1 · Comment.v=1 · Profile.v=1 · Account.v=1 Source of truth for content: stanford_personality_guide_ordered.pdf

Where to look

If you're reading this cold and want to find something fast:

You want to know… Read
What the product is, who it's for, what's in scope, how to amend the spec, and the changelog docs/PRODUCT.md
Stack, repo layout, layer rules, all service interfaces, scoring algorithm, telemetry pipeline, URL encoding, file-by-file inventory, local→cloud migration, social layer + accounts (§ 21), Supabase backend + RLS + deploy (§ 22) docs/ARCHITECTURE.md
Brand mark, palette, typography (Anton + Fraunces + Inter), glyph & iconography system, components, screen flows, microcopy guidelines docs/DESIGN.md
What changed when and why (append-only dev trace, every commit's intent) LOG.md
The original implementation plan (proposal that became this code) ~/.claude/plans/for-this-project-for-binary-boot.md
What a personality type's content actually says (verbatim source) data/types/group1.ts and data/types/group2.ts
The scoring math (pure function, fully tested) core/scoring/impl/HeuristicScorer.ts + tests/core/scoring.test.ts
The feed ranking algorithm (cold-start recommender, pure, tested) core/feed/impl/HeuristicFeedRanker.ts + tests/core/feed.test.ts + ARCHITECTURE.md § 21
The social domain (posts + titles, comments, votes, profiles, all v:1) core/domain/social.ts
Accounts: handle + password sign-in (no email; server-only Account, scrypt, lazy password) core/domain/account.ts, core/account/, app/signin/page.tsx
The telemetry event schema (Zod, wire-stable) core/domain/events.ts
The DI container that wires services core/container.ts

Run locally

npm install
npm run dev      # → http://localhost:3000

No env vars needed for local dev. By default everything runs locally with zero external services: the social feed persists to a gitignored .data/social.json and self-seeds in-character posts + reply threads on first visit to /feed (delete .data/ to reset).

Supabase mode (what the live site runs): set NEXT_PUBLIC_IDENTITY=supabase + IDENTITY=supabase plus the three Supabase keys in .env.local (template in .env.example), and the same UI runs on Supabase Postgres + Auth with Row Level Security — real sessions, server-validated identity, the feed gated in the database. Flip the flag back to anon for the instant local fallback. Full design in docs/ARCHITECTURE.md § 22; the local→cloud map is § 16.

The flow to click through: / → take the quiz → locked result (glass-gradient teaser) → Claim my badge (handle only, no email) → badge reveal → See my full result or Enter the feed → post (title + body + topic typeahead) / vote / share a post / reply to any reply in a thread / open a channel / share your QR card (/badge). Returning users Sign in by handle (/signin): an unknown handle routes to the quiz, a handle with no password yet sets one, a handle with a password verifies it.

Local-dev conveniences: /demo?to=<surface> drops straight into any surface against a persistent demo account (@yodemo, password yoyoyo123) that is seeded server-side and survives a .data reset, so every claimed-only page works without re-claiming. Surfaces: result, claim, feed, compose, badge, post, profile (e.g. /demo?to=feed, /demo?to=post); add &type=<id> to pick the hero type. Every demo link uses router.replace, so the browser Back button never bounces off it.

npx tsc --noEmit       # type-check (must be clean)
npx vitest run         # 13 tests: scoring + feed ranker (must pass)
npx tsx scripts/verify-social.ts   # Playwright phone-viewport walkthrough of the whole flow

Tech stack (one line)

Next.js 16 (App Router) + TypeScript + Tailwind v4 + Framer Motion + Zod + Vitest + qrcode.react + nanoid. Passwords hashed with node:crypto scrypt. Anton + Fraunces + Inter via next/font/google.

Layer rules (the discipline)

  • core/ — pure domain logic. No React/Next/browser imports. Portable.
  • data/ — static content (the 23 type catalog). Validated at boot.
  • app/ + components/ + hooks/ — the only place React lives. Components are presentational; state and service calls live in hooks or page components.

Every external concern (catalog, scoring, telemetry, identity, results, flags, item selection) sits behind an interface in core/<domain>/<Name>.ts with a concrete impl under core/<domain>/impl/. The DI container (core/container.ts) is the single point that wires them.

Spec amendment

Any change to product surface, design, or wire shapes follows the rules in docs/PRODUCT.md § Spec versioning. Bump the version, add a changelog entry with What / Why / Migration, update the relevant doc, log it in LOG.md. tsc and vitest must pass.

Status

v1.8.0 — QR/referral growth funnel + feed perf. Shared badges now point at a self-hosted dynamic-QR redirect hop (/r/<handle>) instead of straight at the landing: the hop logs the scan server-side (via after(), off the redirect's critical path, so it stays seamless) — capturing even scans that bounce before the page loads — then 302s on to /?ref=…&via=…&sid=…. A server-issued sid stitches that exact scan to the later quiz_start and signup_complete, so the full scan → quiz → signup funnel (by channel qr/link, by viewer new-vs-returning-player, by device) is reconstructable. Every funnel event lands in two real-time places: the durable Supabase events table (queried by scripts/referral-funnel.ts — conversion rates, K-factor, top-referrer leaderboard) and Vercel Web Analytics custom events (referral_scan, signup_complete, … via @vercel/analytics/server on the hop + @vercel/analytics on the client) for the live dashboard. Identity on persisted events is server-derived from the session (never the client-sent userId), closing the telemetry spoof. Separately, the feed got a stale-while-revalidate cache so returning to it (or a channel) paints instantly instead of flashing "Loading the feed…", and scripts/seed-today.ts added a fresh finals-season batch of in-character posts/accounts. Additive QuizEvent fields only (v=1 unchanged); tsc clean, 24/24 vitest, build clean, hop live-smoked. See docs/ARCHITECTURE.md § 22.10.

v1.7.3 — result-flow UX polish + analytics. The signup teaser's coarse blur is replaced with elegant "locked collectible" tiles (lock glyph, redacted name bars, real match % shown to bait curiosity); the "Results are in." curtain plays exactly once per result (keyed by sessionId, decided synchronously at mount — no replay on back-nav/reload); returning from "Share your card" reliably shows the full result (SupabaseAuthIdentity no longer drops a just-claimed handle when a later auth event fires before updateUser propagates); in-app "Take the quiz" CTAs route through the full branded splash via /?intro=1 (superseding 1.7.2's skip-to-/quiz); and Vercel Web Analytics (@vercel/analytics/next, cookieless/no-PII) is wired in app/layout.tsx — implemented directly in place of the bot PR. No wire-shape changes; 24/24 vitest, build clean, browser-smoked. See docs/ARCHITECTURE.md § 22.9.

v1.7.2 — Supabase-cutover bug fixes (identity layer). Five user-facing regressions, four of them one root cause: in Supabase mode signInAnonymously() gives every visitor a userId on load, so the old claimed = !!userId predicate treated brand-new anonymous users as players — fully revealing results instead of teasing, hiding the signup gate, offering the feed CTA, and spinning /badge forever. Now claimed keys off the handle (the real "became a player" signal in both backends), which also restores the feed/profile/post gate for logged-out visitors. Separately, SupabaseAuthIdentity.current() now returns the per-tab sessionId synchronously (fixes the quiz landing on "No result here" when finished before the auth round-trip), and in-app "Take the quiz" CTAs route straight to /quiz (no landing-splash flash). Also fixed sharing: every share control (result/badge/post) now copies through one reliable lib/clipboard.ts helper (so "Link copied" is truthful), and the result-link token was slimmed ~5KB → ~3.5KB (dropped telemetry-only answer fields + rounded scores) so messaging apps stop truncating it — the recipient can actually open the link. No wire-shape changes, no SQL. Guarded by tests/core/{identity,result}.test.ts (11 new); 24/24 vitest, tsc clean. See docs/ARCHITECTURE.md § 22.9 and docs/PRODUCT.md § 11.

v1.7.1 — security hardening. A full audit of the cutover (auth/session, authorization, RLS, bugs) → fix → live-verify pass: ownership-bound set-password (/api/account/secure, closes handle squatting), the local-store account routes 404'd in Supabase mode, atomic score/comment_count via DB triggers (kills a lost-update race + a count-zeroing bug), dropped the exploitable posts/profiles UPDATE policies (no direct PostgREST write that bypasses the server), server-side retake-cooldown + authoritative timestamps, correct HTTP status codes, clamped query params, and the missing Supabase branch on GET /api/posts/[id]. Each verified live. Deferred (flagged): rate limiting + telemetry user_id spoofing. See docs/ARCHITECTURE.md § 22.8 and docs/PRODUCT.md § 11.

v1.7.0 — Supabase backend cutover + Vercel deploy. The whole social backend (identity, profiles, posts, comments, votes, accounts) now runs flag-gated on Supabase Postgres + Supabase Auth with Row Level Security, and the app is live at https://cs278-proj.vercel.app. Identity is a real RLS-scoped session (anonymous sign-in on load, upgrade-on-claim, password sign-in for returning users — no real email, synthetic <handle>@yo.app); every social API route derives the caller from the session cookie and writes as that user, never the request body, so impersonation is closed at the database. The full local dataset was migrated (74 profiles / 123 posts / 289 comments) and the RLS model proven (anon feed read = 0, cross-user insert BLOCKED) plus browser-E2E'd end-to-end over HTTPS. The local JSON store stays the default and an instant fallback — flip both IDENTITY vars to anon. See docs/ARCHITECTURE.md § 22 and LOG.md for the trace. Below the cutover, the product is unchanged from —

v1.6.0 — handle + password accounts, threaded replies. On top of the working quiz and the v1.4–1.5 social layer (Fizz-like feed ranked by the cold-start HeuristicFeedRanker, posting with titles / comments / voting / per-post sharing, profiles, QR growth loop, topic typeahead): the account model is now handle + password with no email and no explicit signup — finishing the quiz claims a handle, and the password is set lazily on first sign-in (server-only Account, scrypt, timingSafeEqual). A /signin page branches by handle status (unknown → quiz, no password → set one, has password → verify); post threads support replying to any reply (nested comments); the post card's vote / reply / share controls are clear buttons; and a persistent demo account makes every surface testable. We now collect zero PII — the handle is the only identifier and it is already public. All running locally with zero external services; every seam stays interface-compatible with its named Supabase successor. Real auth + a production database + the LearnedFeedRanker, plus the queued post-feature bundles (trust & safety, reactions, content engine), are the next steps. See docs/PRODUCT.md § 11 changelog and LOG.md for the full trace.

About

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages