/* ============================================================ Shared state + mock data layer. Everything here reads/writes localStorage so the four screens can be clicked through without a backend. Swap the functions in this file for fetch() calls once the API exists — the shape of the returned objects is meant to match what the FastAPI endpoints will eventually return. ============================================================ */ const TIERS = { high_school: { label: "High school", desc: "Popular kid in a small pond" }, college: { label: "College", desc: "Building a name on campus" }, postgrad: { label: "Post-grad", desc: "Working the world at large" }, }; const ADJ = ["quiet", "bright", "lucky", "vivid", "loose", "sharp", "plain", "sunny", "faded", "wild"]; const NOUN = ["fern", "harbor", "kestrel", "static", "current", "atlas", "orbit", "linen", "ember", "gravel"]; function generateUsername(name){ const base = (name || "user").trim().split(/\s+/)[0].toLowerCase().replace(/[^a-z0-9]/g, ""); const word = NOUN[Math.floor(Math.random() * NOUN.length)]; const num = Math.floor(Math.random() * 89) + 10; return `${base || "user"}.${word}${num}`; } function getCharacter(){ const raw = localStorage.getItem("sf_character"); return raw ? JSON.parse(raw) : null; } function saveCharacter(c){ localStorage.setItem("sf_character", JSON.stringify(c)); } function requireOnboarding(){ if (!getCharacter()){ window.location.href = "onboarding.html"; } } function initials(name){ return (name || "?").trim().split(/\s+/).map(w => w[0]).slice(0,2).join("").toUpperCase(); } function timeAgo(ts){ const diff = Math.max(0, Date.now() - ts); const m = Math.floor(diff / 60000); if (m < 1) return "now"; if (m < 60) return `${m}m`; const h = Math.floor(m / 60); if (h < 24) return `${h}h`; return `${Math.floor(h / 24)}d`; } /* ---- NPC roster (shared across explore / dms / feed reactions) ---- */ const NPCS = [ { handle: "rae.holloway41", name: "Rae Holloway", role: "Class of 2027, front row energy", tier: "high_school" }, { handle: "denny.arc09", name: "Denny Okafor", role: "Runs the campus radio slot", tier: "college" }, { handle: "marlo.fenn77", name: "Marlo Fenn", role: "Small business owner, ships fast", tier: "postgrad" }, { handle: "pia.lund22", name: "Pia Lund", role: "Yearbook committee, sees everything", tier: "high_school" }, { handle: "theo.gravel14", name: "Theo Gravel", role: "Local news, campus desk", tier: "college" }, { handle: "wren.static88", name: "Wren Ashby", role: "Talent scout, always in your mentions", tier: "postgrad" }, { handle: "cole.ember03", name: "Cole Bishara", role: "Used to post a lot. Doesn't anymore.", tier: "high_school", retired: true }, ]; const NEWS_CHANNELS = [ { handle: "the.dispatch", name: "The Dispatch", role: "Local coverage, mostly rumor" }, { handle: "campus.wire", name: "Campus Wire", role: "Who's up, who's out" }, ]; function seedPosts(tier){ const now = Date.now(); return [ { id: "p1", author: NPCS[0], body: "spent four hours on a project due tomorrow and used none of it wisely", ts: now - 1000 * 60 * 42, seen: 118, followed: 34, likes: 22, comments: [ { author: NPCS[3], body: "this is a weekly occurrence at this point" }, { author: NPCS[1], body: "same energy every single time" }, ], }, { id: "p2", author: NPCS[2], body: "small batch of the candles restocked. shipping starts friday if anyone wants in", ts: now - 1000 * 60 * 60 * 3, seen: 203, followed: 61, likes: 47, comments: [ { author: NPCS[4], body: "these sold out last time in about a day" }, ], }, { id: "p3", author: NPCS[6], body: "i remember when i thought i was the main character. couldn't make it as a social media giant. be careful what you believe online", ts: now - 1000 * 60 * 60 * 20, seen: 76, followed: 12, likes: 9, comments: [], retired: true, }, ]; } function seedDMs(){ const now = Date.now(); return [ { handle: NPCS[2].handle, unread: true, messages: [ { from: "them", body: "hey, saw your posts. if you send a few of my products around and it lands, I'll cover you for it.", ts: now - 1000 * 60 * 50 }, ], }, { handle: NPCS[0].handle, unread: false, messages: [ { from: "them", body: "you coming to the thing friday", ts: now - 1000 * 60 * 60 * 5 }, { from: "me", body: "thinking about it", ts: now - 1000 * 60 * 60 * 4 }, ], }, { handle: NPCS[5].handle, unread: true, messages: [ { from: "them", body: "your numbers this week are worth a real conversation. free later?", ts: now - 1000 * 60 * 60 * 9 }, ], }, ]; } function getPosts(){ let raw = localStorage.getItem("sf_posts"); if (!raw){ const seeded = seedPosts(); localStorage.setItem("sf_posts", JSON.stringify(seeded)); return seeded; } return JSON.parse(raw); } function savePosts(posts){ localStorage.setItem("sf_posts", JSON.stringify(posts)); } function getDMs(){ let raw = localStorage.getItem("sf_dms"); if (!raw){ const seeded = seedDMs(); localStorage.setItem("sf_dms", JSON.stringify(seeded)); return seeded; } return JSON.parse(raw); } function saveDMs(dms){ localStorage.setItem("sf_dms", JSON.stringify(dms)); } function npcByHandle(handle){ return NPCS.find(n => n.handle === handle) || NEWS_CHANNELS.find(n => n.handle === handle); } /* ---- likes ---- */ function getLikedIds(){ const raw = localStorage.getItem("sf_likes"); return raw ? JSON.parse(raw) : []; } function saveLikedIds(ids){ localStorage.setItem("sf_likes", JSON.stringify(ids)); } /* Toggles the like on a post for the current user, persists both the post's like count and the user's liked-id set, and returns the updated { liked, likes } so the caller can update the DOM in place. */ function toggleLike(postId){ const posts = getPosts(); const post = posts.find(p => p.id === postId); if (!post) return null; let liked = getLikedIds(); const isLiked = liked.includes(postId); if (isLiked){ liked = liked.filter(id => id !== postId); post.likes = Math.max(0, (post.likes || 0) - 1); } else { liked.push(postId); post.likes = (post.likes || 0) + 1; } saveLikedIds(liked); savePosts(posts); return { liked: !isLiked, likes: post.likes }; }