<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Relish / Engineering Log</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.23.2/babel.min.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#f8fafc;min-height:100vh}
textarea,input,select{font-family:inherit}
::-webkit-scrollbar{width:6px} ::-webkit-scrollbar-track{background:#f1f1f1} ::-webkit-scrollbar-thumb{background:#ccc;border-radius:3px}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const {useState,useRef} = React;
const G="#1a6b3a",GL="#e8f5ee";
const PAGES=["All","Partner Portal","Homepage","Case Studies","Creator Landing Page","Contact / Signup","Find Campaigns","Local Campaigns","Map","Dashboard","Campaign Manager","Discover"];
const PHASES=["All","Phase 1","Phase 2","Phase 3"];
const STATUSES=["Backlog","In Progress","Done"];
const PHASE_COLORS={
"Phase 1":{bg:"#eff6ff",text:"#1d4ed8",border:"#bfdbfe"},
"Phase 2":{bg:"#f5f3ff",text:"#7c3aed",border:"#ddd6fe"},
"Phase 3":{bg:"#f0fdf4",text:"#16a34a",border:"#bbf7d0"}
};
const STATUS_COLORS={
Backlog: {bg:"#f8fafc",text:"#64748b",border:"#e2e8f0"},
"In Progress":{bg:"#eff6ff",text:"#2563eb",border:"#bfdbfe"},
Done: {bg:"#f0fdf4",text:"#16a34a",border:"#bbf7d0"}
};
const ALL_REQUESTS=[
// HOMEPAGE
{id:1,reqNum:"R001",page:"Homepage",feature:"Tile/card motif reinforcement",priority:"Phase 1",status:"Backlog",notes:"From recording #1. Current hero lacks the tile/card motif that defines Relish's brand language. Reference Hummingbirds card grids for inspiration, then differentiate.",images:[]},
{id:2,reqNum:"R002",page:"Homepage",feature:"Card text: black font on white",priority:"Phase 1",status:"Backlog",notes:"From recording #1. Bottom homepage cards use light/inverted text that reduces legibility. All cards on white background should use near-black (#1a1a2e). Quick token/CSS fix.",images:[]},
{id:3,reqNum:"R003",page:"Homepage",feature:"Product differentiators section",priority:"Phase 1",status:"Backlog",notes:"From recording #1. Need a section explaining why Relish is different — 'fully managed', 'hyper-local creators', 'foot traffic + sales focus'. Compare against Hummingbirds. Copy TBD — Michael to draft.",images:[]},
{id:4,reqNum:"R004",page:"Homepage",feature:"Creator showcase grid",priority:"Phase 1",status:"Backlog",notes:"From recording #1. Visual section showcasing real Relish creators — split 'everyday' vs 'hyperlocal'. Each tile: follower count, city tag, content sample. Source from creator database.",images:[]},
{id:5,reqNum:"R005",page:"Homepage",feature:"Case study teasers on homepage",priority:"Phase 1",status:"Backlog",notes:"From recording #1. Surface 2–3 top case study cards on the homepage. Each card: brand name, result headline (e.g. '6× engagement'), link to full case study.",images:[]},
{id:6,reqNum:"R006",page:"Homepage",feature:"Map section showing active markets",priority:"Phase 1",status:"Backlog",notes:"From recording #1. Show a map with city pins for active markets. Each pin labeled with number of active campaigns in that city.",images:[]},
{id:7,reqNum:"R007",page:"Homepage",feature:"Dual CTA hero (brands + creators)",priority:"Phase 1",status:"Backlog",notes:"From recording #1. Current hero has single brand CTA. Need split CTA — 'Run a Campaign' for brands + 'Join as a Creator' for creators.",images:[]},
{id:45,reqNum:"R045",page:"Homepage",feature:"Footer: two Instagram links",priority:"Phase 1",status:"Backlog",notes:"From recording #1. Footer should link to both @tryrelish (brand) and @localrelishers (creator-focused). These are separate audiences.",images:[]},
{id:46,reqNum:"R046",page:"Homepage",feature:"Background tile depth effect",priority:"Phase 1",status:"Backlog",notes:"From recording #1. Hero section should have a subtle tile/grid pattern in the background for depth. Use CSS background-pattern or subtle SVG tile motif.",images:[]},
{id:71,reqNum:"R071",page:"Homepage",feature:"👋 Try me — Fix footer copyright year",priority:"Phase 1",status:"Backlog",notes:"Quick fix — footer still shows 2024. Update to 2026. Test task for Golfie to verify the engineering log is working.",images:[]},
// CASE STUDIES
{id:8,reqNum:"R008",page:"Case Studies",feature:"Filter by city",priority:"Phase 1",status:"Backlog",notes:"From recording #1. Brands want to find results in their own city. Add city filter — requires adding 'city' metadata field to CMS schema.",images:[]},
{id:9,reqNum:"R009",page:"Case Studies",feature:"Filter by brand type",priority:"Phase 1",status:"Backlog",notes:"From recording #1. Add brand-type filter (Restaurant / CPG / Retail) to case studies.",images:[]},
{id:10,reqNum:"R010",page:"Case Studies",feature:"Improved card visual distinction",priority:"Phase 1",status:"Backlog",notes:"From recording #1. Cards feel flat and samey. Increase min-height, improve shadow/border, more visual breathing room.",images:[]},
{id:47,reqNum:"R047",page:"Case Studies",feature:"Results metrics on cards",priority:"Phase 1",status:"Backlog",notes:"From recording #1. Each card should show a headline result metric — e.g. '6× engagement', '2,400 visits'. Add metrics field to CMS.",images:[]},
{id:48,reqNum:"R048",page:"Case Studies",feature:"Carousel or animation on desktop",priority:"Phase 1",status:"Backlog",notes:"From recording #1. On desktop, case studies grid could animate as a carousel. Prototype in Figma first.",images:[]},
{id:49,reqNum:"R049",page:"Case Studies",feature:"Mobile card height fix",priority:"Phase 1",status:"Backlog",notes:"From recording #1. Cards collapse to inconsistent heights on mobile. Fix to be uniform and properly padded across all mobile screen sizes.",images:[]},
{id:72,reqNum:"R072",page:"Case Studies",feature:"Section bridge / visual dividers",priority:"Phase 1",status:"Backlog",notes:"From recording #1. Add thin horizontal dividers between major sections for clear visual breaks and improved scannability on scroll.",images:[]},
// CREATOR LANDING PAGE
{id:11,reqNum:"R011",page:"Creator Landing Page",feature:"Build dedicated creator marketing page",priority:"Phase 1",status:"Backlog",notes:"From recording #1. No standalone page for creators yet. Build from scratch — what is a Relisher, how it works, earnings/perks, apply CTA.",images:[]},
{id:12,reqNum:"R012",page:"Creator Landing Page",feature:"Fix creator portal link",priority:"Phase 1",status:"Backlog",notes:"From recording #1. Nav link to creator portal is broken or routes wrong. Quick fix — update href to correct creator portal URL.",images:[]},
{id:50,reqNum:"R050",page:"Creator Landing Page",feature:"Earnings/perk transparency section",priority:"Phase 1",status:"Backlog",notes:"From recording #1. Creators need to understand what they'll get before applying — free meals, discounts, paid deals, etc.",images:[]},
// CONTACT/SIGNUP
{id:13,reqNum:"R013",page:"Contact / Signup",feature:"Clarify creator vs brand contact flow",priority:"Phase 1",status:"Backlog",notes:"From recording #1. Contact page doesn't differentiate brand vs creator flows. Add helper copy and routing logic so each visitor type lands in the right flow.",images:[]},
// FIND CAMPAIGNS
{id:14,reqNum:"R014",page:"Find Campaigns",feature:"Location filter",priority:"Phase 1",status:"Backlog",notes:"From recording #3. Creators need to filter campaigns by city or region. Build location filter and wire to campaign API.",images:[]},
{id:15,reqNum:"R015",page:"Find Campaigns",feature:"Brand type filter",priority:"Phase 1",status:"Backlog",notes:"From recording #3. Add brand-type filter (restaurant / CPG / retail) so creators can find preferred campaign types.",images:[]},
{id:51,reqNum:"R051",page:"Find Campaigns",feature:"Total campaign count display",priority:"Phase 1",status:"Backlog",notes:"From recording #3. Add count badge at top (e.g. '143 campaigns available'). Reinforces platform scale.",images:[]},
{id:52,reqNum:"R052",page:"Find Campaigns",feature:"Animated/carousel campaign card images",priority:"Phase 1",status:"Backlog",notes:"From recording #3. Campaign cards could use subtle image carousel to show multiple images per campaign.",images:[]},
{id:53,reqNum:"R053",page:"Find Campaigns",feature:"Mobile discovery layout improvements",priority:"Phase 1",status:"Backlog",notes:"From recording #3. Top banner takes too much vertical space on mobile. Compress banner, test side-scroll card layout.",images:[]},
// LOCAL CAMPAIGNS
{id:16,reqNum:"R016",page:"Local Campaigns",feature:"Auto-select top 6 states",priority:"Phase 1",status:"Backlog",notes:"From recording #3. 'Top Destinations' section should dynamically surface 6 states with most active campaigns.",images:[]},
{id:54,reqNum:"R054",page:"Local Campaigns",feature:"Big map discovery at top",priority:"Phase 1",status:"Backlog",notes:"From transcript. Large interactive map at the top of the page as primary navigation surface.",images:[]},
{id:55,reqNum:"R055",page:"Local Campaigns",feature:"Prevent duplicate state cards",priority:"Phase 1",status:"Backlog",notes:"From transcript. Same state sometimes appears twice due to deduplication bug. Add deduplication logic to state-level grouping.",images:[]},
{id:56,reqNum:"R056",page:"Local Campaigns",feature:"Compress mobile breadcrumb banner",priority:"Phase 1",status:"Backlog",notes:"From transcript. Breadcrumb banner takes excessive vertical space on mobile. Compress or hide on small screens.",images:[]},
{id:73,reqNum:"R073",page:"Local Campaigns",feature:"Rename to 'Top Destinations'",priority:"Phase 1",status:"Backlog",notes:"From transcript. Replace 'Featured US States & Other Markets' with 'Top Destinations' — more inviting.",images:[]},
// MAP
{id:17,reqNum:"R017",page:"Map",feature:"CPG: show all retail location dots",priority:"Phase 1",status:"Backlog",notes:"From recording #3. CPG Digital campaigns should show every retail store as a small dot, not just brand HQ.",images:[]},
{id:18,reqNum:"R018",page:"Map",feature:"Restaurant/retail: larger distinct markers",priority:"Phase 1",status:"Backlog",notes:"From recording #3. Restaurant and retail locations need larger, more distinct markers vs small CPG store dots.",images:[]},
{id:57,reqNum:"R057",page:"Map",feature:"CMS-editable map featured locations",priority:"Phase 1",status:"Backlog",notes:"From recording #3. Internal team should update featured market pins via CMS without dev work.",images:[]},
// DASHBOARD
{id:19,reqNum:"R019",page:"Dashboard",feature:"Fix card click hierarchy (campaigns vs chart)",priority:"Phase 2",status:"Backlog",notes:"From recording #2. Clicking campaign count card opens trend chart — surprises users expecting campaign list.",images:[]},
{id:20,reqNum:"R020",page:"Dashboard",feature:"Add reach, impressions & engagement KPIs",priority:"Phase 2",status:"Backlog",notes:"From recording #2. Surface network-wide reach, impressions, engagement per brand and campaign.",images:[]},
{id:21,reqNum:"R021",page:"Dashboard",feature:"Units sold / weekly sales / stores selling",priority:"Phase 2",status:"Backlog",notes:"From recording #2. CPG brands need units sold, weekly sales velocity, stores actively selling.",images:[]},
{id:22,reqNum:"R022",page:"Dashboard",feature:"Account-level success metrics section",priority:"Phase 2",status:"Backlog",notes:"From recording #2. Per-brand section: application velocity, scheduling speed, post reach, creators deployed.",images:[]},
{id:23,reqNum:"R023",page:"Dashboard",feature:"Configurable weekly metrics by account manager",priority:"Phase 2",status:"Backlog",notes:"From recording #2. AMs need admin interface to submit weekly metrics per brand.",images:[]},
{id:24,reqNum:"R024",page:"Dashboard",feature:"Creator sourcing metrics",priority:"Phase 2",status:"Backlog",notes:"From recording #2. Pull Mailchimp data: emails sent, open rate, click rate, DM count, reply rate per brand.",images:[]},
{id:25,reqNum:"R025",page:"Dashboard",feature:"Fix 'needs scheduling' metric (cumulative)",priority:"Phase 2",status:"Backlog",notes:"From recording #2. 'Needs scheduling' shows point-in-time daily count that drops to zero even when backlogged.",images:[]},
{id:26,reqNum:"R026",page:"Dashboard",feature:"AI brand summary agent",priority:"Phase 2",status:"Backlog",notes:"From recording #2. Auto-generate one-paragraph brand summary: what they do, active cities, creator count, what customers are saying.",images:[]},
{id:27,reqNum:"R027",page:"Dashboard",feature:"Holiday/seasonal campaign nudge",priority:"Phase 2",status:"Backlog",notes:"From recording #2. Track when brand last launched a campaign and surface nudge card when inactive.",images:[]},
{id:28,reqNum:"R028",page:"Dashboard",feature:"Aggregate post comments for brand sentiment",priority:"Phase 2",status:"Backlog",notes:"From recording #2. Pull creator post comments, run AI summarization for brand sentiment.",images:[]},
{id:58,reqNum:"R058",page:"Dashboard",feature:"Screenshot/content preview in post cards",priority:"Phase 2",status:"Backlog",notes:"From recording #2. Post cards should show thumbnail preview of creator's actual content.",images:[]},
{id:59,reqNum:"R059",page:"Dashboard",feature:"Trend charts: comparative time context",priority:"Phase 2",status:"Backlog",notes:"From recording #2. Charts should show delta — 'up 12 from last month'.",images:[]},
{id:60,reqNum:"R060",page:"Dashboard",feature:"Improve mobile dashboard layout",priority:"Phase 2",status:"Backlog",notes:"From recording #2. Dashboard not optimized for mobile — cards overlap, charts unreadable.",images:[]},
{id:61,reqNum:"R061",page:"Dashboard",feature:"Pagination for appointments and posts",priority:"Phase 2",status:"Backlog",notes:"From recording #2. Appointments and recent posts widgets show fixed short list. Add pagination/load more.",images:[]},
{id:62,reqNum:"R062",page:"Dashboard",feature:"Application velocity signal",priority:"Phase 2",status:"Backlog",notes:"From recording #2. Surface 'applications per day' velocity metric per campaign.",images:[]},
// CAMPAIGN MANAGER
{id:29,reqNum:"R029",page:"Campaign Manager",feature:"Default sort by created date",priority:"Phase 2",status:"Backlog",notes:"From recording #3. Campaign list should default to newest first.",images:[]},
{id:30,reqNum:"R030",page:"Campaign Manager",feature:"Show cumulative scheduled creators",priority:"Phase 2",status:"Backlog",notes:"From recording #3. 'Scheduled' metric should show all-time cumulative total, not just currently in queue.",images:[]},
{id:31,reqNum:"R031",page:"Campaign Manager",feature:"Average days-to-schedule metric",priority:"Phase 2",status:"Backlog",notes:"From recording #3. Show avg days from application to scheduled visit, plus platform-wide benchmark comparison.",images:[]},
{id:32,reqNum:"R032",page:"Campaign Manager",feature:"Stale creator flag (>14 days)",priority:"Phase 2",status:"Backlog",notes:"From recording #3. Flag creator cards sitting in scheduling/completed >14 days with visual indicator.",images:[]},
{id:33,reqNum:"R033",page:"Campaign Manager",feature:"Sales team notification on campaign submit",priority:"Phase 2",status:"Backlog",notes:"From recording #3. Auto-notify sales team via Slack/email when a brand submits a new campaign.",images:[]},
{id:34,reqNum:"R034",page:"Campaign Manager",feature:"Prominent 'Create Campaign' button",priority:"Phase 2",status:"Backlog",notes:"From recording #3. Create Campaign CTA is easy to miss. Redesign to be bold, exciting.",images:[]},
{id:35,reqNum:"R035",page:"Campaign Manager",feature:"Clarify pipeline stage definitions",priority:"Phase 2",status:"Backlog",notes:"From recording #3. Stage labels like 'Assessing', 'Scheduling' are not self-explanatory.",images:[]},
{id:36,reqNum:"R036",page:"Campaign Manager",feature:"Visualize stages as a flow/conveyor belt",priority:"Phase 2",status:"Backlog",notes:"From recording #3. Pipeline should feel like a directional conveyor belt — Assessing → Scheduling → Completed → Posted.",images:[]},
{id:37,reqNum:"R037",page:"Campaign Manager",feature:"AI one-sentence creator summary",priority:"Phase 2",status:"Backlog",notes:"From recording #3. Each creator card needs AI-generated one-sentence summary.",images:[]},
{id:38,reqNum:"R038",page:"Campaign Manager",feature:"Creator location & engagement metrics on card",priority:"Phase 2",status:"Backlog",notes:"From recording #3. Creator cards missing: city/location, average engagement rate, follower count.",images:[]},
{id:39,reqNum:"R039",page:"Campaign Manager",feature:"Campaign title AI suggestions",priority:"Phase 2",status:"Backlog",notes:"From recording #3. Title field should suggest AI-generated options based on brand context + past high-performing titles.",images:[]},
{id:40,reqNum:"R040",page:"Campaign Manager",feature:"Pre-fill campaign brief from past campaigns",priority:"Phase 2",status:"Backlog",notes:"From recording #3. Brief and perks fields should auto-populate from past high-reach campaigns for that brand.",images:[]},
{id:63,reqNum:"R063",page:"Campaign Manager",feature:"Break create campaign into stepped sections",priority:"Phase 2",status:"Backlog",notes:"From recording #3. Creation form is one long page. Break into steps — Brief / Times / Locations.",images:[]},
{id:64,reqNum:"R064",page:"Campaign Manager",feature:"Add location/city to campaign card title",priority:"Phase 2",status:"Backlog",notes:"From recording #3. Campaign list cards don't show targeted location.",images:[]},
{id:65,reqNum:"R065",page:"Campaign Manager",feature:"Bring campaign banner image into card",priority:"Phase 2",status:"Backlog",notes:"From recording #3. Show campaign banner image on list cards. Makes list much more visually scannable.",images:[]},
{id:66,reqNum:"R066",page:"Campaign Manager",feature:"Allow editing of auto-generated campaign URL",priority:"Phase 2",status:"Backlog",notes:"From recording #3. Campaign URLs are auto-generated and not editable. Brands want custom URLs.",images:[]},
{id:67,reqNum:"R067",page:"Campaign Manager",feature:"Remove redundant 'Scheduled' pill in scheduling view",priority:"Phase 2",status:"Backlog",notes:"From recording #3. 'Scheduled' pill in scheduling view is redundant.",images:[]},
{id:68,reqNum:"R068",page:"Campaign Manager",feature:"Table view option for creator cards",priority:"Phase 2",status:"Backlog",notes:"From recording #3. AMs managing large creator lists need compact table view.",images:[]},
{id:74,reqNum:"R074",page:"Campaign Manager",feature:"Campaign notes: less aggressive styling",priority:"Phase 2",status:"Backlog",notes:"From recording #3. Campaign note component is too visually dominant. Restyle to subdued/muted appearance.",images:[]},
{id:75,reqNum:"R075",page:"Campaign Manager",feature:"Improve 'no content yet' posted state",priority:"Phase 2",status:"Backlog",notes:"From recording #3. Empty state for the 'Posted' tab is bare and unhelpful.",images:[]},
// DISCOVER
{id:41,reqNum:"R041",page:"Discover",feature:"Rename 'Find Campaigns' to 'Discover'",priority:"Phase 2",status:"Backlog",notes:"From transcript. 'Find Campaigns' is too narrow — page is evolving into broader local discovery experience.",images:[]},
{id:42,reqNum:"R042",page:"Discover",feature:"Partner business listings on Discover map",priority:"Phase 2",status:"Backlog",notes:"From transcript. Show InKind restaurants, Shopline retail, and other partners on the Discover map.",images:[]},
{id:43,reqNum:"R043",page:"Discover",feature:"Relish campaigns prominently differentiated from partners",priority:"Phase 2",status:"Backlog",notes:"From transcript. Relish paid campaigns must dominate visual hierarchy. Partner pins secondary.",images:[]},
{id:44,reqNum:"R044",page:"Discover",feature:"City-level discovery context and 'What's happening'",priority:"Phase 2",status:"Backlog",notes:"From transcript. Each city view should have editorial blurb: what's trending, which brands are active.",images:[]},
{id:69,reqNum:"R069",page:"Discover",feature:"Local community events and volunteering",priority:"Phase 2",status:"Backlog",notes:"From transcript. Discover should include local community events and volunteering.",images:[]},
{id:70,reqNum:"R070",page:"Discover",feature:"Discount/perk mechanics clearly explained",priority:"Phase 2",status:"Backlog",notes:"From transcript. Partner perks need clearer explanation.",images:[]},
// PARTNER PORTAL
{id:103,reqNum:"R-PP03",page:"Partner Portal",feature:"Nav — Links (Story, Investors)",priority:"Phase 3",status:"Backlog",epic:"Partner Portal — Navigation",notes:"Two nav links right of divider. 14px, 500 weight, #444, 6px/12px padding, 20px border radius. Hover: #f4f4f4. Active: #f0f0f0, color #1a1a2e. 'Story' → Story section. 'Investors 🔒' → gated section.",images:[]},
{id:104,reqNum:"R-PP04",page:"Partner Portal",feature:"Hero — Welcome pill badge",priority:"Phase 3",status:"Backlog",epic:"Partner Portal — Hero Section",notes:"Rounded pill above headline. Border: 1.5px solid #1a7c5a. Text: #1a7c5a, 13px 500 weight. Padding: 5px 14px. Border-radius: 20px. Text: 'Welcome to the Relish Partner Portal'",images:[]},
{id:105,reqNum:"R-PP05",page:"Partner Portal",feature:"Hero — Headline",priority:"Phase 3",status:"Backlog",epic:"Partner Portal — Hero Section",notes:"clamp(32px, 5vw, 52px), 800 weight, line-height 1.1, color #0f1f1a, letter-spacing -0.5px. Text: 'We're Building the Infrastructure for Local Commerce.'",images:[]},
{id:106,reqNum:"R-PP06",page:"Partner Portal",feature:"Hero — Body copy (3 paragraphs)",priority:"Phase 3",status:"Backlog",epic:"Partner Portal — Hero Section",notes:"17px, #3d3d3d, line-height 1.75, max-width 720px, 18px gap between paragraphs.",images:[]},
{id:107,reqNum:"R-PP07",page:"Partner Portal",feature:"'What Relish Is' — Section label + headline",priority:"Phase 3",status:"Backlog",epic:"Partner Portal — What Relish Is",notes:"All-caps label: 11px 700 weight letter-spacing 0.12em #888. Heading: 'Powering Creator-Led Commerce for Retail, CPG, and Restaurants.'",images:[]},
{id:108,reqNum:"R-PP08",page:"Partner Portal",feature:"'What Relish Is' — Three bullet points",priority:"Phase 3",status:"Backlog",epic:"Partner Portal — What Relish Is",notes:"8×8px solid green (#1a7c5a) circle bullets. 16px 600 weight #1a1a2e. Items: Human Powered / Tech Enablement / Built for Scale.",images:[]},
{id:109,reqNum:"R-PP09",page:"Partner Portal",feature:"FAQ block — 'What do we do?'",priority:"Phase 3",status:"Backlog",epic:"Partner Portal — What Relish Is",notes:"Bold 17px heading #0f1f1a. Body: 15px #444 line-height 1.7.",images:[]},
{id:110,reqNum:"R-PP10",page:"Partner Portal",feature:"FAQ block — 'Why the name Relish?'",priority:"Phase 3",status:"Backlog",epic:"Partner Portal — What Relish Is",notes:"Two body paragraphs. P1: most powerful recommendations happen in moments captured by people who relish life.",images:[]},
{id:111,reqNum:"R-PP11",page:"Partner Portal",feature:"FAQ block — 'What is a Relisher?' + stat chips",priority:"Phase 3",status:"Backlog",epic:"Partner Portal — What Relish Is",notes:"Stat chips: bg #f3faf6, border #c5e8d6, 13px #1a7c5a 600 weight. Chips: 60,000+ Vetted Creators / 60+ Active Markets / US · Canada · DACH.",images:[]},
{id:112,reqNum:"R-PP12",page:"Partner Portal",feature:"Our Values — Section heading + 3-column cards",priority:"Phase 3",status:"Backlog",epic:"Partner Portal — Our Values",notes:"3-column grid (auto-fit minmax 240px), 28px gap. Cards: 🤝 KEEP IT HUMAN / 💪 BUILD THINGS THAT LAST / 👍 RELISH EVERY MOMENT.",images:[]},
{id:113,reqNum:"R-PP13",page:"Partner Portal",feature:"Useful Links — 7-card grid",priority:"Phase 3",status:"Backlog",epic:"Partner Portal — Useful Links",notes:"3-column grid, collapses to 2-col under 640px. Cards: Sales Deck / One Pager / Product Demo / Case Studies / Pitch Deck / Brand Guidelines / Positioning Strategy.",images:[]},
{id:114,reqNum:"R-PP14",page:"Partner Portal",feature:"Strategic Programs — 4-card grid",priority:"Phase 3",status:"Backlog",epic:"Partner Portal — Strategic Programs",notes:"2-col grid, collapses to 1-col under 600px. Cards: Platform Enhancements / Attribution Technology / Ecosystem Infrastructure / Creator Financial Layer.",images:[]},
{id:115,reqNum:"R-PP15",page:"Partner Portal",feature:"The Long Game — Dark card",priority:"Phase 3",status:"Backlog",epic:"Partner Portal — The Long Game",notes:"Background #0f1f1a, border-radius 16px, padding 40px. Tag pills: Global Community Infrastructure / Proprietary Data Platform / Creator Financial Layer / Discovery Super App.",images:[]},
{id:116,reqNum:"R-PP16",page:"Partner Portal",feature:"Section dividers, responsive layout & nav wiring",priority:"Phase 3",status:"Backlog",epic:"Partner Portal — Layout & Polish",notes:"1px #ebebeb dividers between sections. Links grid 3→2col at 640px. Programs grid 2→1col at 600px.",images:[]},
];
const RECORDINGS=[
{id:1,num:1,date:"Mar 28, 2026",title:"Home Page Review",tags:["Homepage","Case Studies","Creator Page"],color:"#6366f1",icon:"🏠",url:"https://app.fyxer.com/share/call-recordings/a8987ee8-be11-4191-a3d1-e9eff94db950:huWuFc993CUgD6gJau3bJMNpMD93:1774716771903?ctx=eyJ0IjoiSG9tZSBQYWdlIFJldmlldyIsImgiOiJNaWNoYWVsIERpZWdvIiwiciI6IiIsImQiOiIyMDI2LTAzLTI4VDE2OjM2OjI1LjE4MVoiLCJwIjoxLCJpIjoiTWljaGFlbCBEaWVnbyJ9&utm_source=call-recording-share"},
{id:2,num:2,date:"Mar 28, 2026",title:"Dashboard Review",tags:["Dashboard","Metrics"],color:"#10b981",icon:"📊",url:"https://app.fyxer.com/share/call-recordings/a8987ee8-be11-4191-a3d1-e9eff94db950:huWuFc993CUgD6gJau3bJMNpMD93:1774716931375?ctx=eyJ0IjoiRGFzaGJvYXJkIFJldmlldyIsImgiOiJNaWNoYWVsIERpZWdvIiwiciI6IiIsImQiOiIyMDI2LTAzLTI4VDE2OjM5OjI2LjYyMFoiLCJwIjoxLCJpIjoiTWljaGFlbCBEaWVnbyJ9&utm_source=call-recording-share"},
{id:3,num:3,date:"Mar 28, 2026",title:"Campaign & App Manager Review",tags:["Campaigns","Creators","App Manager"],color:"#f97316",icon:"🗂️",url:"https://app.fyxer.com/share/call-recordings/a8987ee8-be11-4191-a3d1-e9eff94db950:huWuFc993CUgD6gJau3bJMNpMD93:1774717041976?ctx=eyJ0IjoiQ2FtcGFpZ24gJiBBcHAgTWFuYWdlciBSZXZpZXciLCJoIjoiTWljaGFlbCBEaWVnbyIsInIiOiIiLCJkIjoiMjAyNi0wMy0yOFQxNjozODowMC43NDlaIiwicCI6MSwiaSI6Ik1pY2hhZWwgRGllZ28ifQ%3D%3D&utm_source=call-recording-share"},
];
const INIT_POSTS=[{id:1,author:"Michael",date:"Mar 28, 2026",text:"Set up this log so we can stay aligned across our workflow while you're away — recordings, requests, and updates all in one place. Ping me here as you make progress or have questions. Really excited about v3!"}];
const INIT_DREAMS=[
{id:1,author:"Michael",title:"Agentic Campaign Management",text:"Campaigns that run themselves — AI sets goals, schedules creators, monitors performance, and adjusts in real time. The brand just approves and watches results come in."},
{id:2,author:"Michael",title:"CPG Command Center",text:"A purpose-built dashboard for CPG brands expanding into retail. Track creator performance by market, monitor in-store velocity, and coordinate campaign timing with retail launch windows — all in one view."},
];
const selStyle={border:"1px solid #e5e7eb",borderRadius:7,padding:"7px 10px",fontSize:13,color:"#374151",background:"#fff",outline:"none"};
const greenBtn={background:G,color:"#fff",border:"none",borderRadius:6,padding:"7px 14px",fontWeight:700,fontSize:13,cursor:"pointer"};
const linkBtn={background:"none",border:"none",padding:0,fontSize:13,color:"#6b7280",cursor:"pointer"};
function PhaseBadge({label}){
const c=PHASE_COLORS[label]||PHASE_COLORS["Phase 3"];
return <span style={{fontSize:11,fontWeight:600,padding:"2px 8px",borderRadius:99,background:c.bg,color:c.text,border:`1px solid ${c.border}`,whiteSpace:"nowrap"}}>{label}</span>;
}
function PageTag({label}){
return <span style={{fontSize:11,fontWeight:600,padding:"2px 8px",borderRadius:99,background:"#f3e8ff",color:"#7c3aed",border:"1px solid #ddd6fe",whiteSpace:"nowrap"}}>{label}</span>;
}
function RequestCard({req,onChange,onStatus}){
const [open,setOpen]=useState(false);
const [noteDraft,setNoteDraft]=useState(req.notes||"");
const [lightbox,setLightbox]=useState(null);
const fileRef=useRef();
const saveNote=()=>onChange(req.id,"notes",noteDraft);
const handleFiles=(e)=>{
Array.from(e.target.files).forEach(f=>{
const r=new FileReader();
r.onload=ev=>onChange(req.id,"addImage",{src:ev.target.result,name:f.name});
r.readAsDataURL(f);
});
e.target.value="";
};
return (
<>
<div style={{background:"#fff",border:"1px solid #e5e7eb",borderRadius:10,overflow:"hidden",marginBottom:8}}>
<div style={{padding:"12px 16px",cursor:"pointer",display:"flex",alignItems:"flex-start",gap:12}} onClick={()=>setOpen(o=>!o)}>
<div style={{flex:1,minWidth:0}}>
<div style={{display:"flex",alignItems:"center",gap:6,flexWrap:"wrap",marginBottom:4}}>
<span style={{fontSize:11,fontWeight:700,color:"#9ca3af",fontFamily:"monospace"}}>{req.reqNum}</span>
{req.epic&&<span style={{fontSize:10,fontWeight:600,color:G,background:GL,border:"1px solid #a7f3d0",padding:"1px 7px",borderRadius:99}}>{req.epic}</span>}
<PhaseBadge label={req.priority}/>
<PageTag label={req.page}/>
{req.notes&&req.notes.trim()&&<span style={{fontSize:11,color:"#9ca3af"}}>📝</span>}
{req.images.length>0&&<span style={{fontSize:11,color:"#9ca3af"}}>🖼 {req.images.length}</span>}
</div>
<div style={{fontWeight:600,fontSize:14,color:"#111827"}}>{req.feature}</div>
</div>
<div style={{display:"flex",alignItems:"center",gap:8,flexShrink:0}}>
<select value={req.status} onChange={e=>{e.stopPropagation();onStatus(req.id,e.target.value);}} onClick={e=>e.stopPropagation()}
style={{fontSize:11,fontWeight:600,padding:"3px 6px",borderRadius:6,border:`1px solid ${STATUS_COLORS[req.status].border}`,background:STATUS_COLORS[req.status].bg,color:STATUS_COLORS[req.status].text,cursor:"pointer"}}>
{STATUSES.map(s=><option key={s}>{s}</option>)}
</select>
<span style={{fontSize:12,color:"#9ca3af",display:"inline-block",transform:open?"rotate(180deg)":"rotate(0deg)",transition:"transform 0.2s"}}>▾</span>
</div>
</div>
{open&&(
<div style={{borderTop:"1px solid #f3f4f6",padding:"14px 16px"}}>
<div style={{marginBottom:16}}>
<div style={{fontSize:11,fontWeight:700,color:"#9ca3af",textTransform:"uppercase",letterSpacing:1,marginBottom:8}}>Notes</div>
<textarea value={noteDraft} onChange={e=>setNoteDraft(e.target.value)} onBlur={saveNote}
placeholder="Add notes, context, links, or implementation details…" rows={5}
style={{width:"100%",border:"1px solid #e5e7eb",borderRadius:8,padding:"10px 12px",fontSize:13,color:"#374151",resize:"vertical",outline:"none",boxSizing:"border-box",lineHeight:1.6,fontFamily:"inherit"}}/>
{noteDraft!==req.notes&&<button onClick={saveNote} style={{...greenBtn,marginTop:6,fontSize:12,padding:"5px 12px"}}>Save</button>}
</div>
<div>
<div style={{fontSize:11,fontWeight:700,color:"#9ca3af",textTransform:"uppercase",letterSpacing:1,marginBottom:8}}>Attachments</div>
{req.images.length>0&&(
<div style={{display:"grid",gridTemplateColumns:"repeat(auto-fill, minmax(100px, 1fr))",gap:8,marginBottom:12}}>
{req.images.map((img,idx)=>(
<div key={idx} style={{position:"relative",borderRadius:8,overflow:"hidden",border:"1px solid #e5e7eb",aspectRatio:"1",cursor:"zoom-in"}} onClick={()=>setLightbox(img.src)}>
<img src={img.src} alt={img.name} style={{width:"100%",height:"100%",objectFit:"cover"}}/>
<button onClick={e=>{e.stopPropagation();onChange(req.id,"removeImage",idx);}} style={{position:"absolute",top:4,right:4,width:20,height:20,borderRadius:"50%",background:"rgba(0,0,0,0.6)",color:"#fff",border:"none",cursor:"pointer",fontSize:11,display:"flex",alignItems:"center",justifyContent:"center"}}>×</button>
</div>
))}
</div>
)}
<input ref={fileRef} type="file" accept="image/*" multiple style={{display:"none"}} onChange={handleFiles}/>
<button onClick={()=>fileRef.current.click()} style={{display:"flex",alignItems:"center",gap:6,padding:"8px 14px",border:"1px dashed #d1d5db",borderRadius:8,background:"#f9fafb",color:"#6b7280",fontSize:13,fontWeight:500,cursor:"pointer"}}>
<span style={{fontSize:16}}>+</span> Upload images
</button>
</div>
</div>
)}
</div>
{lightbox&&(
<div onClick={()=>setLightbox(null)} style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.85)",zIndex:1000,display:"flex",alignItems:"center",justifyContent:"center",padding:24,cursor:"zoom-out"}}>
<img src={lightbox} alt="" style={{maxWidth:"90vw",maxHeight:"90vh",borderRadius:10}}/>
<button onClick={()=>setLightbox(null)} style={{position:"absolute",top:20,right:24,background:"rgba(255,255,255,0.15)",border:"none",color:"#fff",borderRadius:"50%",width:36,height:36,fontSize:18,cursor:"pointer"}}>×</button>
</div>
)}
</>
);
}
function RequestsPage({requests,setRequests}){
const [filterPage,setFilterPage]=useState("All");
const [filterPhase,setFilterPhase]=useState("All");
const [filterStatus,setFilterStatus]=useState("All");
const [search,setSearch]=useState("");
const [groupBy,setGroupBy]=useState("phase");
const filtered=requests.filter(r=>{
if(filterPage!=="All"&&r.page!==filterPage)return false;
if(filterPhase!=="All"&&r.priority!==filterPhase)return false;
if(filterStatus!=="All"&&r.status!==filterStatus)return false;
if(search&&!r.feature.toLowerCase().includes(search.toLowerCase())&&!r.reqNum.toLowerCase().includes(search.toLowerCase()))return false;
return true;
});
const handleChange=(id,field,val)=>{
setRequests(rs=>rs.map(r=>{
if(r.id!==id)return r;
if(field==="notes")return{...r,notes:val};
if(field==="addImage")return{...r,images:[...r.images,val]};
if(field==="removeImage")return{...r,images:r.images.filter((_,i)=>i!==val)};
return r;
}));
};
const changeStatus=(id,s)=>setRequests(rs=>rs.map(r=>r.id!==id?r:{...r,status:s}));
let groups={};
if(groupBy==="phase"){
["Phase 1","Phase 2","Phase 3"].forEach(p=>{const items=filtered.filter(r=>r.priority===p);if(items.length)groups[p]=items;});
} else {
filtered.forEach(r=>{const k=r.epic||r.page;if(!groups[k])groups[k]=[];groups[k].push(r);});
}
const p1=requests.filter(r=>r.priority==="Phase 1").length;
const p2=requests.filter(r=>r.priority==="Phase 2").length;
const inProg=requests.filter(r=>r.status==="In Progress").length;
const done=requests.filter(r=>r.status==="Done").length;
return (
<div>
<div style={{display:"flex",gap:12,marginBottom:20,flexWrap:"wrap"}}>
{[{label:"Phase 1",val:p1,c:"#1d4ed8"},{label:"Phase 2",val:p2,c:"#7c3aed"},{label:"In Progress",val:inProg,c:"#2563eb"},{label:"Done ✅",val:done,c:G}].map(s=>(
<div key={s.label} style={{background:"#fff",border:"1px solid #e5e7eb",borderRadius:10,padding:"12px 20px",minWidth:90}}>
<div style={{fontSize:26,fontWeight:800,color:s.c}}>{s.val}</div>
<div style={{fontSize:11,color:"#9ca3af",marginTop:2}}>{s.label}</div>
</div>
))}
</div>
<div style={{display:"flex",gap:8,marginBottom:12,flexWrap:"wrap"}}>
<input value={search} onChange={e=>setSearch(e.target.value)} placeholder="Search requests…"
style={{flex:1,minWidth:160,border:"1px solid #e5e7eb",borderRadius:7,padding:"7px 12px",fontSize:13,outline:"none"}}/>
<select value={filterPage} onChange={e=>setFilterPage(e.target.value)} style={selStyle}>{PAGES.map(p=><option key={p}>{p}</option>)}</select>
<select value={filterPhase} onChange={e=>setFilterPhase(e.target.value)} style={selStyle}>{PHASES.map(p=><option key={p}>{p}</option>)}</select>
<select value={filterStatus} onChange={e=>setFilterStatus(e.target.value)} style={selStyle}><option>All</option>{STATUSES.map(s=><option key={s}>{s}</option>)}</select>
</div>
<div style={{display:"flex",alignItems:"center",justifyContent:"space-between",marginBottom:14}}>
<span style={{fontSize:12,color:"#9ca3af"}}>{filtered.length} request{filtered.length!==1?"s":""}</span>
<div style={{display:"flex",background:"#f3f4f6",borderRadius:8,padding:2,gap:2}}>
{[["phase","By Phase"],["epic","By Epic"]].map(([v,l])=>(
<button key={v} onClick={()=>setGroupBy(v)}
style={{padding:"5px 12px",borderRadius:6,border:"none",fontWeight:600,fontSize:12,cursor:"pointer",background:groupBy===v?"#111827":"transparent",color:groupBy===v?"#fff":"#6b7280"}}>{l}</button>
))}
</div>
</div>
{Object.entries(groups).map(([key,reqs])=>(
<div key={key} style={{marginBottom:28}}>
<div style={{display:"flex",alignItems:"center",gap:10,marginBottom:10}}>
{groupBy==="phase"&&<span style={{width:8,height:8,borderRadius:"50%",flexShrink:0,display:"inline-block",background:key==="Phase 1"?"#1d4ed8":key==="Phase 2"?"#7c3aed":"#16a34a"}}/>}
<span style={{fontWeight:800,fontSize:13,color:"#111827"}}>{key}</span>
<span style={{fontSize:11,color:"#9ca3af",background:"#f3f4f6",borderRadius:99,padding:"2px 8px"}}>{reqs.length}</span>
<div style={{flex:1,height:1,background:"#e5e7eb"}}/>
</div>
{reqs.map(r=><RequestCard key={r.id} req={r} onChange={handleChange} onStatus={changeStatus}/>)}
</div>
))}
</div>
);
}
function Section({title,action,children}){
return (
<div style={{marginBottom:32}}>
<div style={{display:"flex",alignItems:"center",justifyContent:"space-between",marginBottom:14}}>
<div style={{fontWeight:800,fontSize:16,color:"#111827"}}>{title}</div>
<div style={{display:"flex",gap:8,alignItems:"center"}}>{action}</div>
</div>
{children}
</div>
);
}
function HomePage({requests,posts,setPosts,dreams,setDreams,onNav}){
const [postOpen,setPostOpen]=useState(false);
const [postDraft,setPostDraft]=useState("");
const [postName,setPostName]=useState("Michael");
const [dreamForm,setDreamForm]=useState(false);
const [dTitle,setDTitle]=useState("");
const [dText,setDText]=useState("");
const [dAuthor,setDAuthor]=useState("Michael");
const p1=requests.filter(r=>r.priority==="Phase 1").length;
const p2=requests.filter(r=>r.priority==="Phase 2").length;
const p3=requests.filter(r=>r.priority==="Phase 3").length;
const inProg=requests.filter(r=>r.status==="In Progress").length;
const done=requests.filter(r=>r.status==="Done").length;
const total=requests.length;
const addPost=()=>{
if(!postDraft.trim())return;
setPosts(ps=>[...ps,{id:Date.now(),author:postName||"Anonymous",date:new Date().toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"}),text:postDraft}]);
setPostDraft("");setPostOpen(false);
};
const addDream=()=>{if(!dTitle||!dText)return;setDreams(ds=>[...ds,{id:Date.now(),author:dAuthor||"Anonymous",title:dTitle,text:dText}]);setDTitle("");setDText("");setDreamForm(false);};
return (
<div>
<div style={{marginBottom:24}}>
<h1 style={{fontSize:28,fontWeight:900,color:"#111827",margin:"0 0 8px",letterSpacing:-0.5}}>Engineering Command Center</h1>
<p style={{fontSize:14,color:"#6b7280",margin:"0 0 4px",lineHeight:1.6}}>A shared workspace to continue building the core Relish app while remote — recordings, requests, and updates in one place.</p>
<p style={{fontSize:14,color:"#6b7280",margin:0}}>Leave posts, view a full list of requested updates, and see dream functionality.</p>
</div>
<div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:10,marginBottom:10}}>
{[{label:"In Progress",val:inProg,c:"#2563eb"},{label:"Done ✅",val:done,c:G}].map(s=>(
<div key={s.label} style={{background:"#fff",border:"1px solid #e5e7eb",borderRadius:10,padding:"14px 16px",display:"flex",alignItems:"center",justifyContent:"space-between"}}>
<div>
<div style={{fontSize:28,fontWeight:800,color:s.c}}>{s.val}</div>
<div style={{fontSize:11,color:"#9ca3af",marginTop:2}}>{s.label}</div>
</div>
<button onClick={()=>onNav("requests")} style={{fontSize:12,color:"#6366f1",background:"none",border:"none",padding:0,cursor:"pointer"}}>View →</button>
</div>
))}
</div>
<div style={{background:"#fff",border:"1px solid #e5e7eb",borderRadius:10,padding:"16px 18px",marginBottom:24}}>
<div style={{display:"flex",alignItems:"center",justifyContent:"space-between",marginBottom:14}}>
<div style={{fontWeight:800,fontSize:15,color:"#111827"}}>All Requests <span style={{fontSize:13,fontWeight:500,color:"#9ca3af"}}>({total})</span></div>
<button onClick={()=>onNav("requests")} style={{fontSize:12,color:"#6366f1",background:"none",border:"none",padding:0,cursor:"pointer"}}>View all →</button>
</div>
<div style={{display:"flex",flexDirection:"column",gap:10}}>
{[
{label:"Phase 1",val:p1,c:PHASE_COLORS["Phase 1"],desc:"Homepage, Case Studies, Creator Landing, Find Campaigns, Contact / Signup, Local Campaigns, Map"},
{label:"Phase 2",val:p2,c:PHASE_COLORS["Phase 2"],desc:"Dashboard, Campaign Manager, Discover"},
{label:"Phase 3",val:p3,c:PHASE_COLORS["Phase 3"],desc:"Partner Portal — all portal requests"},
].map(ph=>(
<div key={ph.label} style={{display:"flex",alignItems:"center",gap:12}}>
<div style={{width:10,height:10,borderRadius:"50%",background:ph.c.text,flexShrink:0}}/>
<div style={{flex:1}}>
<div style={{display:"flex",alignItems:"center",gap:8,marginBottom:2}}>
<span style={{fontWeight:700,fontSize:13,color:"#111827"}}>{ph.label}</span>
<span style={{fontSize:11,fontWeight:600,padding:"1px 7px",borderRadius:99,background:ph.c.bg,color:ph.c.text,border:`1px solid ${ph.c.border}`}}>{ph.val} requests</span>
</div>
<div style={{fontSize:11,color:"#9ca3af"}}>{ph.desc}</div>
</div>
<div style={{width:80,height:5,background:"#f3f4f6",borderRadius:99,overflow:"hidden",flexShrink:0}}>
<div style={{width:`${Math.round((ph.val/total)*100)}%`,height:"100%",background:ph.c.text,borderRadius:99}}/>
</div>
<span style={{fontSize:11,color:"#9ca3af",width:32,textAlign:"right",flexShrink:0}}>{Math.round((ph.val/total)*100)}%</span>
</div>
))}
</div>
</div>
<Section title="📌 Posts" action={
<><button onClick={()=>onNav("updates")} style={linkBtn}>View all →</button>
<button style={greenBtn} onClick={()=>setPostOpen(o=>!o)}>{postOpen?"Cancel":"+ Post"}</button></>
}>
<div style={{fontSize:13,color:"#6b7280",marginBottom:12}}>Updates, questions, and progress notes.</div>
{posts.slice(0,2).map(p=>(
<div key={p.id} style={{background:"#fff",border:"1px solid #e5e7eb",borderRadius:10,padding:"14px 16px",marginBottom:10}}>
<div style={{display:"flex",gap:10,alignItems:"center",marginBottom:8}}>
<div style={{width:32,height:32,borderRadius:"50%",background:G,color:"#fff",display:"flex",alignItems:"center",justifyContent:"center",fontWeight:700,fontSize:13,flexShrink:0}}>{p.author[0]}</div>
<div><div style={{fontWeight:700,fontSize:13,color:G}}>{p.author}</div><div style={{fontSize:11,color:"#9ca3af"}}>{p.date}</div></div>
</div>
<p style={{fontSize:13,color:"#374151",margin:0,lineHeight:1.6}}>{p.text.length>160?p.text.slice(0,160)+"…":p.text}</p>
</div>
))}
{postOpen&&(
<div style={{background:"#f9fafb",border:"1px solid #e5e7eb",borderRadius:10,padding:14,marginTop:4}}>
<input value={postName} onChange={e=>setPostName(e.target.value)} placeholder="Your name"
style={{border:"1px solid #e5e7eb",borderRadius:6,padding:"5px 10px",fontSize:12,width:120,outline:"none",marginBottom:8}}/>
<textarea value={postDraft} onChange={e=>setPostDraft(e.target.value)} placeholder="Leave an update…" rows={3} autoFocus
style={{width:"100%",border:"1px solid #e5e7eb",borderRadius:6,padding:"7px 10px",fontSize:13,resize:"vertical",outline:"none",boxSizing:"border-box"}}/>
<button onClick={addPost} style={{...greenBtn,marginTop:8}}>Post</button>
</div>
)}
</Section>
<Section title="🎬 Recordings" action={null}>
<div style={{fontSize:13,color:"#6b7280",marginBottom:12}}>Watch in order for full context. Each maps to requests in the tracker.</div>
{RECORDINGS.map(r=>(
<div key={r.id} style={{display:"flex",alignItems:"center",gap:14,padding:"14px 0",borderTop:`3px solid ${r.color}`}}>
<div style={{width:44,height:44,borderRadius:10,background:r.color,display:"flex",alignItems:"center",justifyContent:"center",fontSize:18,flexShrink:0}}>{r.icon}</div>
<div style={{flex:1}}>
<div style={{fontSize:11,color:"#9ca3af",marginBottom:2}}>#{r.num} · {r.date}</div>
<div style={{fontWeight:700,fontSize:14,color:"#111827",marginBottom:4}}>{r.title}</div>
<div style={{display:"flex",gap:6,flexWrap:"wrap"}}>{r.tags.map(t=><span key={t} style={{fontSize:11,background:"#f3f4f6",color:"#6b7280",borderRadius:99,padding:"2px 8px"}}>{t}</span>)}</div>
</div>
<a href={r.url} target="_blank" rel="noopener noreferrer"
style={{background:"#6366f1",color:"#fff",border:"none",borderRadius:8,padding:"8px 16px",fontWeight:700,fontSize:13,cursor:"pointer",textDecoration:"none",whiteSpace:"nowrap"}}>
▶ Watch
</a>
</div>
))}
</Section>
<div style={{display:"flex",alignItems:"center",justifyContent:"center",gap:8,margin:"24px 0",color:"#d1d5db",fontSize:18}}>○ ○ ✦ ○ ○</div>
<Section title="💭 Dreams" action={<button style={greenBtn} onClick={()=>setDreamForm(f=>!f)}>+ Add</button>}>
<div style={{fontSize:13,color:"#6b7280",marginBottom:14}}>Big ideas and inspiration — pinned here so you know how I'm thinking!</div>
{dreams.map(d=>(
<div key={d.id} style={{display:"flex",alignItems:"flex-start",gap:12,marginBottom:14}}>
<div style={{width:32,height:32,borderRadius:"50%",border:"2px solid #e5e7eb",flexShrink:0,marginTop:2}}/>
<div style={{flex:1}}>
<div style={{fontWeight:700,fontSize:14,color:"#111827",marginBottom:4}}>{d.title}</div>
<div style={{fontSize:13,color:"#6b7280",lineHeight:1.5}}>{d.text.length>120?d.text.slice(0,120)+"…":d.text}</div>
</div>
<span style={{fontSize:16,color:"#9ca3af"}}>→</span>
</div>
))}
{dreamForm&&(
<div style={{background:"#fff",border:"1px solid #e5e7eb",borderRadius:10,padding:14,marginTop:8}}>
<input value={dAuthor} onChange={e=>setDAuthor(e.target.value)} placeholder="Your name" style={{width:"100%",border:"1px solid #e5e7eb",borderRadius:6,padding:"7px 10px",fontSize:13,marginBottom:8,boxSizing:"border-box",outline:"none"}}/>
<input value={dTitle} onChange={e=>setDTitle(e.target.value)} placeholder="Dream title" style={{width:"100%",border:"1px solid #e5e7eb",borderRadius:6,padding:"7px 10px",fontSize:13,marginBottom:8,boxSizing:"border-box",outline:"none"}}/>
<textarea value={dText} onChange={e=>setDText(e.target.value)} placeholder="Describe the dream…" rows={3} style={{width:"100%",border:"1px solid #e5e7eb",borderRadius:6,padding:"8px 10px",fontSize:13,resize:"vertical",outline:"none",boxSizing:"border-box"}}/>
<div style={{display:"flex",gap:8,marginTop:8}}>
<button onClick={addDream} style={greenBtn}>Add</button>
<button onClick={()=>setDreamForm(false)} style={{background:"#f3f4f6",color:"#374151",border:"none",borderRadius:6,padding:"8px 16px",fontWeight:600,fontSize:13,cursor:"pointer"}}>Cancel</button>
</div>
</div>
)}
</Section>
</div>
);
}
function UpdatesPage({posts,setPosts}){
const [draft,setDraft]=useState("");
const [name,setName]=useState("Michael");
const add=()=>{if(!draft.trim())return;setPosts(ps=>[...ps,{id:Date.now(),author:name||"Anonymous",date:new Date().toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"}),text:draft}]);setDraft("");};
return (
<div>
{posts.map(p=>(
<div key={p.id} style={{background:"#fff",border:"1px solid #e5e7eb",borderRadius:10,padding:"14px 16px",marginBottom:12}}>
<div style={{display:"flex",gap:10,alignItems:"center",marginBottom:8}}>
<div style={{width:34,height:34,borderRadius:"50%",background:G,color:"#fff",display:"flex",alignItems:"center",justifyContent:"center",fontWeight:700,fontSize:13,flexShrink:0}}>{p.author[0]}</div>
<div><div style={{fontWeight:700,fontSize:13,color:G}}>{p.author}</div><div style={{fontSize:11,color:"#9ca3af"}}>{p.date}</div></div>
</div>
<p style={{fontSize:14,color:"#374151",margin:0,lineHeight:1.6}}>{p.text}</p>
</div>
))}
<div style={{background:"#fff",border:"1px solid #e5e7eb",borderRadius:10,padding:16}}>
<input value={name} onChange={e=>setName(e.target.value)} placeholder="Your name" style={{border:"1px solid #e5e7eb",borderRadius:6,padding:"6px 10px",fontSize:13,width:140,outline:"none",marginBottom:8}}/>
<textarea value={draft} onChange={e=>setDraft(e.target.value)} placeholder="Write an update…" rows={3}
style={{width:"100%",border:"1px solid #e5e7eb",borderRadius:6,padding:"8px 10px",fontSize:13,resize:"vertical",outline:"none",boxSizing:"border-box"}}/>
<button onClick={add} style={{...greenBtn,marginTop:8}}>Post Update</button>
</div>
</div>
);
}
function DreamBoardPage({dreams,setDreams}){
const [form,setForm]=useState(false);
const [t,setT]=useState("");const [x,setX]=useState("");const [a,setA]=useState("Michael");
const add=()=>{if(!t||!x)return;setDreams(ds=>[...ds,{id:Date.now(),author:a||"Anonymous",title:t,text:x}]);setT("");setX("");setForm(false);};
return (
<div>
<div style={{display:"grid",gridTemplateColumns:"repeat(auto-fill,minmax(260px,1fr))",gap:14,marginBottom:16}}>
{dreams.map(d=>(
<div key={d.id} style={{background:"#0f1f1a",border:"1px solid #1e3a2a",borderRadius:12,padding:"18px 20px"}}>
<div style={{fontSize:11,fontWeight:700,color:"#4ade80",textTransform:"uppercase",letterSpacing:1,marginBottom:8}}>Dream</div>
<div style={{fontWeight:700,fontSize:16,color:"#fff",marginBottom:8}}>{d.title}</div>
<p style={{fontSize:13,color:"#9ca3af",margin:0,lineHeight:1.6}}>{d.text}</p>
<div style={{marginTop:12,fontSize:11,color:"#4b5563"}}>— {d.author}</div>
</div>
))}
</div>
{!form
?<button onClick={()=>setForm(true)} style={{background:"transparent",border:`1px dashed ${G}`,color:G,borderRadius:8,padding:"8px 16px",fontWeight:600,fontSize:13,cursor:"pointer"}}>+ Add a Dream</button>
:(
<div style={{background:"#fff",border:"1px solid #e5e7eb",borderRadius:10,padding:16}}>
<input value={a} onChange={e=>setA(e.target.value)} placeholder="Your name" style={{width:"100%",border:"1px solid #e5e7eb",borderRadius:6,padding:"7px 10px",fontSize:13,marginBottom:8,boxSizing:"border-box",outline:"none"}}/>
<input value={t} onChange={e=>setT(e.target.value)} placeholder="Dream title" style={{width:"100%",border:"1px solid #e5e7eb",borderRadius:6,padding:"7px 10px",fontSize:13,marginBottom:8,boxSizing:"border-box",outline:"none"}}/>
<textarea value={x} onChange={e=>setX(e.target.value)} placeholder="Describe the dream…" rows={3} style={{width:"100%",border:"1px solid #e5e7eb",borderRadius:6,padding:"8px 10px",fontSize:13,resize:"vertical",outline:"none",boxSizing:"border-box"}}/>
<div style={{display:"flex",gap:8,marginTop:8}}>
<button onClick={add} style={greenBtn}>Add</button>
<button onClick={()=>setForm(false)} style={{background:"#f3f4f6",color:"#374151",border:"none",borderRadius:6,padding:"8px 16px",fontWeight:600,fontSize:13,cursor:"pointer"}}>Cancel</button>
</div>
</div>
)}
</div>
);
}
const NAV_ITEMS=[{id:"home",label:"Home"},{id:"requests",label:"Requests"},{id:"updates",label:"Updates"},{id:"dreams",label:"Dream Board"}];
const PAGE_TITLES={requests:"All Requests",updates:"Updates",dreams:"Dream Board"};
function App(){
const [tab,setTab]=useState("home");
const [requests,setRequests]=useState(ALL_REQUESTS);
const [posts,setPosts]=useState(INIT_POSTS);
const [dreams,setDreams]=useState(INIT_DREAMS);
return (
<div style={{fontFamily:"-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",background:"#f8fafc",minHeight:"100vh"}}>
<div style={{background:"#fff",borderBottom:"1px solid #e5e7eb",padding:"0 24px",position:"sticky",top:0,zIndex:10}}>
<div style={{maxWidth:900,margin:"0 auto",display:"flex",alignItems:"center",justifyContent:"space-between",height:52}}>
<div style={{display:"flex",alignItems:"center",gap:10,cursor:"pointer"}} onClick={()=>setTab("home")}>
<div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:3,width:20,height:20}}>
{[0,1,2,3].map(i=><div key={i} style={{background:G,borderRadius:2}}/>)}
</div>
<span style={{fontWeight:800,fontSize:15,color:"#111827"}}>Relish</span>
<span style={{color:"#d1d5db"}}>/</span>
<span style={{fontWeight:600,fontSize:13,color:"#6b7280"}}>Engineering Log</span>
</div>
<nav style={{display:"flex",gap:2}}>
{NAV_ITEMS.map(n=>(
<button key={n.id} onClick={()=>setTab(n.id)}
style={{padding:"6px 12px",borderRadius:6,border:"none",fontWeight:600,fontSize:13,cursor:"pointer",background:tab===n.id?GL:"transparent",color:tab===n.id?G:"#6b7280"}}>
{n.label}
</button>
))}
</nav>
</div>
</div>
{tab!=="home"&&(
<div style={{background:"#fff",borderBottom:"1px solid #f3f4f6",padding:"16px 24px"}}>
<div style={{maxWidth:900,margin:"0 auto"}}>
<h2 style={{margin:0,fontSize:20,fontWeight:800,color:"#111827"}}>
{PAGE_TITLES[tab]}{tab==="requests"?` (${requests.length})`:""}</h2>
</div>
</div>
)}
<div style={{maxWidth:900,margin:"0 auto",padding:"24px 16px"}}>
{tab==="home" &&<HomePage requests={requests} posts={posts} setPosts={setPosts} dreams={dreams} setDreams={setDreams} onNav={setTab}/>}
{tab==="requests"&&<RequestsPage requests={requests} setRequests={setRequests}/>}
{tab==="updates" &&<UpdatesPage posts={posts} setPosts={setPosts}/>}
{tab==="dreams" &&<DreamBoardPage dreams={dreams} setDreams={setDreams}/>}
</div>
</div>
);
}
ReactDOM.createRoot(document.getElementById("root")).render(<App/>);
</script>
</body>
</html>