Skip to content

Instantly share code, notes, and snippets.

@mode-mercury
Created December 10, 2025 21:54
Show Gist options
  • Select an option

  • Save mode-mercury/82015112e852b9dc5800dde81f2e9f09 to your computer and use it in GitHub Desktop.

Select an option

Save mode-mercury/82015112e852b9dc5800dde81f2e9f09 to your computer and use it in GitHub Desktop.
Untitled
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>grumpster · the dumpster for happiness</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap');
:root {
color-scheme: dark;
--bg: #020617;
--bg-card: #020617;
--bg-card-soft: #020617;
--accent: #22d3ee; /* cyan */
--accent-soft: rgba(34,211,238,0.3);
--accent-alt: #a855f7; /* purple */
--danger: #fb923c; /* orange dumpster fire */
--danger-soft: rgba(251,146,60,0.3);
--good: #4ade80;
--good-soft: rgba(74,222,128,0.25);
--border-soft: #1e293b;
--fg: #e5e7eb;
--fg-soft: #9ca3af;
--shadow-soft: 0 22px 55px rgba(0,0,0,0.9);
font-family: "Space Grotesk", system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
min-height: 100%;
color: var(--fg);
background:
radial-gradient(circle at top left, #0f172a 0, transparent 52%),
radial-gradient(circle at bottom right, #111827 0, transparent 55%),
radial-gradient(circle at top right, #1e293b 0, #020617 65%);
}
body {
padding: 0.75rem;
padding-bottom: 3.25rem; /* space for bottom nav */
display: flex;
flex-direction: column;
gap: 0.75rem;
}
header {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.title-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.6rem;
}
.logo {
display: inline-flex;
align-items: center;
gap: 0.08rem;
margin: 0;
font-size: 1.6rem;
text-transform: lowercase;
letter-spacing: 0.12em;
text-shadow:
0 0 10px rgba(148,163,184,0.6),
0 0 26px rgba(34,211,238,0.5);
}
.logo-text {
margin: 0;
}
.logo-cross {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.1em;
height: 1.1em;
margin-inline: 0.08rem;
border-radius: 999px;
border: 2px solid #ef4444;
box-shadow: 0 0 12px rgba(239,68,68,0.7);
overflow: hidden;
color: transparent; /* hide literal 'e' */
}
.logo-cross::before {
content: "e";
position: relative;
font-size: 0.7em;
color: #fecaca;
opacity: 0.9;
}
.logo-cross::after {
content: "";
position: absolute;
width: 140%;
height: 2px;
background: #ef4444;
transform: rotate(-40deg);
box-shadow: 0 0 8px rgba(239,68,68,0.9);
}
.chip {
font-size: 0.7rem;
padding: 0.25rem 0.7rem;
border-radius: 999px;
border: 1px solid rgba(148,163,184,0.6);
background: radial-gradient(circle at top, rgba(248,250,252,0.2), rgba(15,23,42,0.98));
display: inline-flex;
gap: 0.3rem;
align-items: center;
box-shadow: 0 0 20px rgba(15,23,42,0.95);
}
.chip-dot {
width: 8px;
height: 8px;
border-radius: 999px;
background: var(--danger);
box-shadow: 0 0 12px var(--danger-soft);
}
.tagline {
font-size: 0.8rem;
opacity: 0.8;
max-width: 580px;
}
.meta-row {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
justify-content: space-between;
margin-top: 0.1rem;
}
.stats {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
font-size: 0.75rem;
}
.stat-pill {
padding: 0.16rem 0.6rem;
border-radius: 999px;
border: 1px solid rgba(148,163,184,0.7);
background: radial-gradient(circle at top left, rgba(248,250,252,0.16), rgba(15,23,42,0.98));
display: inline-flex;
gap: 0.25rem;
align-items: center;
box-shadow: 0 10px 22px rgba(0,0,0,0.7);
}
.stat-label { opacity: 0.7; }
.stat-value { font-weight: 500; }
.happiness-wrap {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 0.1rem;
}
.happiness-bar {
position: relative;
width: 200px;
height: 11px;
border-radius: 999px;
background: rgba(15,23,42,0.9);
border: 1px solid rgba(148,163,184,0.8);
overflow: hidden;
box-shadow:
0 0 12px rgba(15,23,42,0.9),
0 0 20px rgba(34,211,238,0.25);
}
.happiness-fill {
position: absolute;
inset: 0;
width: 25%;
background: linear-gradient(90deg, var(--danger), var(--accent), var(--accent-alt), var(--good));
background-size: 220% 100%;
animation: shift 9s linear infinite;
transition: width 220ms ease-out;
}
@keyframes shift {
0% { background-position: 0% 0; }
100% { background-position: 200% 0; }
}
.happiness-label {
font-size: 0.68rem;
opacity: 0.75;
text-align: right;
}
/* Header dumpster-fire badge */
.dumpster-badge {
display: inline-flex;
align-items: center;
gap: 0.3rem;
font-size: 0.7rem;
padding: 0.18rem 0.55rem;
border-radius: 999px;
border: 1px solid rgba(248,113,113,0.7);
background: radial-gradient(circle at top, rgba(239,68,68,0.2), rgba(15,23,42,0.98));
box-shadow: 0 0 16px rgba(248,113,113,0.7);
}
.dumpster-icon {
width: 18px;
height: 18px;
border-radius: 4px;
border: 1px solid rgba(148,163,184,0.8);
background:
linear-gradient(to top, #020617 40%, #0f172a 100%);
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
font-size: 0.85rem;
}
.dumpster-icon::before {
content: "";
position: absolute;
inset: auto 2px 6px 2px;
height: 3px;
background: #0f172a;
border-radius: 1px;
}
.dumpster-icon span {
position: relative;
top: -3px;
}
main {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.panel {
background: radial-gradient(circle at top, rgba(248,250,252,0.08), rgba(15,23,42,0.98));
border-radius: 1rem;
border: 1px solid var(--border-soft);
box-shadow: var(--shadow-soft);
padding: 0.75rem;
display: flex;
flex-direction: column;
gap: 0.6rem;
min-height: 0;
position: relative;
overflow: hidden;
}
.panel::before {
content: "";
position: absolute;
inset: -40%;
background:
radial-gradient(circle at 0 0, rgba(34,211,238,0.16), transparent 55%),
radial-gradient(circle at 100% 0, rgba(168,85,247,0.14), transparent 55%);
mix-blend-mode: screen;
opacity: 0.4;
pointer-events: none;
}
.panel-inner {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
gap: 0.6rem;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 0.5rem;
}
.panel-title {
font-size: 0.82rem;
text-transform: uppercase;
letter-spacing: 0.16em;
opacity: 0.9;
}
.panel-sub {
font-size: 0.72rem;
opacity: 0.7;
}
.btn {
font-size: 0.68rem;
padding: 0.26rem 0.7rem;
border-radius: 999px;
border: 1px solid rgba(148,163,184,0.8);
background: linear-gradient(135deg, rgba(15,23,42,0.98), rgba(30,64,175,0.9));
color: var(--fg-soft);
display: inline-flex;
align-items: center;
gap: 0.26rem;
cursor: pointer;
transition: background 90ms ease, transform 80ms ease, box-shadow 120ms ease, border-color 120ms ease;
white-space: nowrap;
}
.btn span.icon { font-size: 0.8rem; }
.btn:hover {
background: linear-gradient(135deg, rgba(30,64,175,1), rgba(37,99,235,1));
transform: translateY(-0.5px);
box-shadow: 0 10px 22px rgba(15,23,42,0.9);
border-color: rgba(191,219,254,1);
color: #e5e7eb;
}
.btn-ghost {
background: linear-gradient(135deg, rgba(15,23,42,0.95), rgba(15,23,42,0.98));
border-style: dashed;
border-color: rgba(148,163,184,0.8);
}
.pane-section-label {
font-size: 0.68rem;
text-transform: uppercase;
letter-spacing: 0.16em;
opacity: 0.8;
}
/* VIEWS */
.panel-view { display: none; }
.panel-view.active { display: flex; }
/* RANT composer & feed */
.composer {
border-radius: 0.9rem;
padding: 0.55rem 0.6rem;
background: linear-gradient(135deg, rgba(15,23,42,0.98), rgba(30,64,175,0.98));
border: 1px solid rgba(129,140,248,0.9);
box-shadow: 0 15px 28px rgba(15,23,42,0.96);
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.composer-row {
display: flex;
gap: 0.4rem;
align-items: flex-start;
}
.composer-avatar {
width: 32px;
height: 32px;
border-radius: 999px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.1rem;
background: radial-gradient(circle at top, rgba(248,250,252,0.4), rgba(15,23,42,0.98));
border: 1px solid rgba(191,219,254,0.9);
flex-shrink: 0;
}
.composer-main {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.composer-input {
width: 100%;
resize: none;
border-radius: 0.7rem;
border: 1px solid rgba(148,163,184,0.8);
background: rgba(15,23,42,0.98);
color: var(--fg);
padding: 0.45rem 0.55rem;
font-size: 0.78rem;
min-height: 52px;
max-height: 90px;
outline: none;
box-shadow: inset 0 0 0 1px rgba(15,23,42,0.95);
}
.composer-input:focus {
border-color: var(--accent-soft);
box-shadow:
inset 0 0 0 1px rgba(15,23,42,0.98),
0 0 18px rgba(34,211,238,0.4);
}
.composer-meta {
display: flex;
justify-content: space-between;
align-items: center;
gap: 0.5rem;
font-size: 0.68rem;
opacity: 0.8;
}
.char-count {
font-variant-numeric: tabular-nums;
}
.char-count.over { color: var(--danger); }
.btn-post {
border-color: var(--accent-soft);
background: linear-gradient(135deg, rgba(56,189,248,1), rgba(251,113,133,1));
color: #020617;
font-weight: 600;
box-shadow: 0 12px 26px rgba(15,23,42,0.98);
}
.btn-post:disabled {
opacity: 0.45;
cursor: default;
box-shadow: none;
transform: none;
}
.rant-feed {
margin-top: 0.35rem;
display: flex;
flex-direction: column;
gap: 0.4rem;
max-height: 320px;
overflow-y: auto;
padding-right: 0.1rem;
scrollbar-width: thin;
scrollbar-color: rgba(148,163,184,0.7) transparent;
}
.rant-feed::-webkit-scrollbar { width: 4px; }
.rant-feed::-webkit-scrollbar-thumb {
background: rgba(148,163,184,0.8);
border-radius: 999px;
}
.rant {
border-radius: 0.85rem;
padding: 0.5rem 0.55rem 0.45rem;
background: linear-gradient(135deg, rgba(15,23,42,0.98), rgba(30,64,175,0.96));
border: 1px solid rgba(129,140,248,0.9);
display: flex;
flex-direction: column;
gap: 0.3rem;
box-shadow: 0 10px 22px rgba(15,23,42,0.98);
}
.rant-top {
display: flex;
justify-content: space-between;
gap: 0.3rem;
align-items: center;
}
.rant-author-block {
display: flex;
gap: 0.4rem;
align-items: center;
}
.rant-avatar {
width: 28px;
height: 28px;
border-radius: 999px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
background: radial-gradient(circle at top, rgba(248,250,252,0.4), rgba(15,23,42,0.98));
border: 1px solid rgba(191,219,254,0.9);
flex-shrink: 0;
}
.rant-author {
font-size: 0.76rem;
display: flex;
flex-direction: column;
gap: 0.02rem;
}
.rant-name-row {
display: flex;
gap: 0.25rem;
align-items: baseline;
}
.rant-display-name { font-weight: 500; }
.rant-handle { font-size: 0.7rem; opacity: 0.7; }
.rant-time { font-size: 0.66rem; opacity: 0.6; }
.rant-text {
font-size: 0.8rem;
line-height: 1.4;
}
.rant-image {
border-radius: 0.75rem;
overflow: hidden;
border: 1px solid rgba(148,163,184,0.8);
margin-top: 0.15rem;
max-height: 220px;
}
.rant-image img {
display: block;
width: 100%;
height: auto;
}
.rant-actions {
display: flex;
gap: 0.3rem;
align-items: center;
font-size: 0.7rem;
margin-top: 0.1rem;
}
.reaction-btn {
border-radius: 999px;
padding: 0.18rem 0.45rem;
border: 1px solid rgba(148,163,184,0.8);
background: rgba(15,23,42,0.98);
display: inline-flex;
gap: 0.18rem;
align-items: center;
cursor: pointer;
transition: background 80ms ease, border-color 80ms ease, transform 80ms ease;
}
.reaction-btn span.icon { font-size: 0.8rem; }
.reaction-btn span.count {
font-variant-numeric: tabular-nums;
opacity: 0.7;
}
.reaction-btn:hover {
transform: translateY(-0.5px);
border-color: rgba(248,250,252,0.9);
}
.reaction-btn.active.down {
border-color: rgba(148,163,184,1);
background: rgba(15,23,42,0.95);
}
.reaction-btn.active.fire {
border-color: var(--danger-soft);
background: rgba(24,20,12,0.98);
}
.reaction-btn.active.unfollow {
border-color: rgba(56,189,248,0.8);
background: rgba(8,47,73,0.98);
}
/* FRIEND LIST VIEW */
.friend-list {
margin-top: 0.1rem;
display: flex;
flex-direction: column;
gap: 0.45rem;
max-height: 360px;
overflow-y: auto;
padding-right: 0.1rem;
scrollbar-width: thin;
scrollbar-color: rgba(148,163,184,0.7) transparent;
}
.friend-list::-webkit-scrollbar { width: 4px; }
.friend-list::-webkit-scrollbar-thumb {
background: rgba(148,163,184,0.8);
border-radius: 999px;
}
.friend-card {
position: relative;
border-radius: 0.85rem;
overflow: hidden;
background: linear-gradient(135deg, rgba(15,23,42,0.98), rgba(30,64,175,0.96));
border: 1px solid rgba(148,163,184,0.8);
padding: 0.5rem 0.55rem;
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
gap: 0.45rem;
align-items: center;
box-shadow: 0 14px 28px rgba(15,23,42,0.98);
transition: transform 90ms ease-out, box-shadow 90ms ease-out, border-color 120ms ease-out;
}
.friend-card.favorite {
border-color: var(--good-soft);
box-shadow: 0 0 0 1px rgba(74,222,128,0.25), 0 16px 32px rgba(15,23,42,0.98);
}
.friend-card.dumped {
opacity: 0.45;
border-style: dashed;
border-color: rgba(251,146,60,0.7);
}
.friend-avatar {
width: 34px;
height: 34px;
border-radius: 999px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
background: radial-gradient(circle at top, rgba(248,250,252,0.4), rgba(15,23,42,0.98));
border: 1px solid rgba(191,219,254,0.9);
box-shadow: 0 0 14px rgba(15,23,42,1);
z-index: 1;
}
.friend-main {
position: relative;
z-index: 1;
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.18rem;
}
.friend-name-row {
display: flex;
justify-content: space-between;
gap: 0.45rem;
align-items: baseline;
}
.friend-name {
font-size: 0.88rem;
font-weight: 500;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.pill {
font-size: 0.63rem;
padding: 0.14rem 0.5rem;
border-radius: 999px;
border: 1px solid rgba(148,163,184,0.9);
background: rgba(15,23,42,0.98);
white-space: nowrap;
}
.friend-tag {
font-size: 0.72rem;
opacity: 0.8;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.friend-meta {
display: flex;
gap: 0.35rem;
align-items: center;
font-size: 0.68rem;
opacity: 0.78;
}
.meter-dots {
display: inline-flex;
gap: 2px;
}
.dot {
width: 6px;
height: 6px;
border-radius: 999px;
background: rgba(148,163,184,0.7);
}
.dot.hot {
background: var(--danger);
box-shadow: 0 0 10px var(--danger-soft);
}
.dot.chill {
background: var(--good);
box-shadow: 0 0 10px var(--good-soft);
}
.friend-actions {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
gap: 0.25rem;
align-items: flex-end;
}
.btn-fav {
border-color: var(--good-soft);
color: var(--good);
background: linear-gradient(135deg, rgba(5,46,22,0.98), rgba(22,163,74,0.98));
}
.btn-dump {
border-color: var(--danger-soft);
color: var(--danger);
background: linear-gradient(135deg, rgba(30,64,175,0.98), rgba(251,146,60,0.98));
}
footer {
text-align: center;
font-size: 0.66rem;
opacity: 0.6;
margin-top: 0.35rem;
}
footer span.brand {
letter-spacing: 0.18em;
text-transform: uppercase;
}
/* Bottom nav */
.bottom-nav {
position: fixed;
left: 0;
right: 0;
bottom: 0;
height: 3rem;
background: radial-gradient(circle at top, rgba(15,23,42,0.96), rgba(2,6,23,1));
border-top: 1px solid rgba(148,163,184,0.8);
display: flex;
justify-content: space-around;
align-items: center;
z-index: 20;
box-shadow: 0 -10px 28px rgba(15,23,42,1);
backdrop-filter: blur(16px);
}
.nav-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.05rem;
font-size: 0.62rem;
opacity: 0.7;
cursor: pointer;
transition: opacity 80ms ease, transform 80ms ease;
}
.nav-item-icon { font-size: 1.2rem; line-height: 1; }
.nav-item.active {
opacity: 1;
transform: translateY(-1px);
}
.nav-item.active .nav-item-icon {
text-shadow: 0 0 14px rgba(248,250,252,0.9);
}
@media (min-width: 900px) {
.bottom-nav {
max-width: 720px;
margin: 0 auto;
border-radius: 999px 999px 0 0;
}
body {
max-width: 900px;
margin: 0 auto;
}
}
</style>
</head>
<body>
<a id="top"></a>
<header>
<div class="title-row">
<h1 class="logo">
<span class="logo-text">grumpst</span>
<span class="logo-cross">e</span>
<span class="logo-text">r</span>
</h1>
<span class="chip">
<span class="chip-dot"></span>
the dumpster for happiness
</span>
<span class="dumpster-badge">
<span class="dumpster-icon"><span>🔥</span></span>
live dumpster fire simulator
</span>
</div>
<div class="tagline">
You start friends with everyone. Your job is ruthless curation: keep your favorites, dump the rest, and post tiny rants instead of pretending it’s fine.
</div>
<div class="meta-row">
<div class="stats">
<div class="stat-pill">
<span class="stat-label">Friends:</span>
<span class="stat-value" id="statTotal">0</span>
</div>
<div class="stat-pill">
<span class="stat-label">Favorites:</span>
<span class="stat-value" id="statFavs">0</span>
</div>
<div class="stat-pill">
<span class="stat-label">Dumped:</span>
<span class="stat-value" id="statDumped">0</span>
</div>
<div class="stat-pill">
<span class="stat-label">Rants:</span>
<span class="stat-value" id="statRants">0</span>
</div>
</div>
<div class="happiness-wrap">
<div class="happiness-bar">
<div class="happiness-fill" id="happinessFill"></div>
</div>
<div class="happiness-label" id="happinessLabel">
Happiness: 0% curated · 0 rants released
</div>
</div>
</div>
</header>
<main>
<!-- FEED VIEW (default) -->
<section class="panel panel-view active" id="feedView">
<div class="panel-inner">
<div class="panel-header">
<div>
<div class="panel-title">micro-rants feed</div>
<div class="panel-sub">
Character cap: <strong>144</strong>. No likes. Only 👎, 🔥, and 🚫.
</div>
</div>
</div>
<div class="composer">
<div class="composer-row">
<div class="composer-avatar">😶‍🌫️</div>
<div class="composer-main">
<textarea
id="rantInput"
class="composer-input"
maxlength="144"
placeholder="Type a tiny update about your social chaos… (max 144 chars)"
></textarea>
<div class="composer-meta">
<span class="char-count" id="charCount">0 / 144</span>
<div style="display:flex;gap:0.3rem;align-items:center;">
<span style="font-size:0.65rem;opacity:0.7;">Mode: public venting</span>
<button class="btn btn-post" id="postBtn" disabled>
<span class="icon">🗑</span> post to dumpster
</button>
</div>
</div>
</div>
</div>
</div>
<div class="pane-section-label">latest rants</div>
<div class="rant-feed" id="rantFeed">
<!-- rants injected -->
</div>
</div>
</section>
<!-- FRIENDS VIEW -->
<section class="panel panel-view" id="friendsView">
<div class="panel-inner">
<div class="panel-header">
<div>
<div class="panel-title">everyone you know (for now)</div>
<div class="panel-sub">
Starts as “friends with everyone”. Ends as “people you’d actually text back”.
</div>
</div>
<button class="btn btn-ghost" id="resetBtn">
<span class="icon">⟲</span> reset experiment
</button>
</div>
<div class="friend-list" id="friendList">
<!-- friend cards injected -->
</div>
</div>
</section>
</main>
<footer id="footer">
<span class="brand">grumpster · beta</span> · no dopamine hits, just boundary hygiene
</footer>
<!-- Bottom nav -->
<nav class="bottom-nav">
<div class="nav-item active" data-view="feedView">
<div class="nav-item-icon">🔥</div>
<div>Feed</div>
</div>
<div class="nav-item" data-view="friendsView">
<div class="nav-item-icon">👥</div>
<div>Friends</div>
</div>
<div class="nav-item" data-anchor="top">
<div class="nav-item-icon">🏠</div>
<div>Top</div>
</div>
<div class="nav-item" data-anchor="footer">
<div class="nav-item-icon">ℹ️</div>
<div>About</div>
</div>
</nav>
<script>
// ------------------ FRIEND MODEL --------------------
const seedFriends = [
{ name: "Alex from high school", tag: "Sends reels at 3:17am", avatar: "📹" },
{ name: "Jamie (coworker)", tag: "Calendar pings your soul", avatar: "📅" },
{ name: "Rae the oversharer", tag: "Voice notes, 7 min each", avatar: "🎙" },
{ name: "Theo from the skatepark", tag: "Actually pretty great", avatar: "🛹" },
{ name: "Jess (family thread)", tag: "Replies with only GIFs", avatar: "📱" },
{ name: "Casey the hustle guru", tag: "DMs ‘you up? let’s build’", avatar: "💼" },
{ name: "Old bandmate", tag: "Still tagging you in throwbacks", avatar: "🎸" },
{ name: "Neighbor with leaf blower", tag: "Mows at weird spiritual hours", avatar: "🍂" },
{ name: "That one barista", tag: "Knows your order, your trauma", avatar: "☕️" },
{ name: "Online game squad", tag: "Disappear for months, then 6hr raid", avatar: "🎮" },
{ name: "Mystery number", tag: "You’re too scared to ask who this is", avatar: "❓" },
{ name: "You from three years ago", tag: "Still haunting the group chat", avatar: "👻" }
];
let friends = [];
const friendListEl = document.getElementById("friendList");
const statTotalEl = document.getElementById("statTotal");
const statFavsEl = document.getElementById("statFavs");
const statDumpedEl = document.getElementById("statDumped");
const statRantsEl = document.getElementById("statRants");
const happinessFillEl = document.getElementById("happinessFill");
const happinessLabelEl = document.getElementById("happinessLabel");
const resetBtn = document.getElementById("resetBtn");
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function initFriends() {
friends = seedFriends.map((f, index) => ({
id: index + 1,
name: f.name,
tag: f.tag,
avatar: f.avatar,
heat: randomInt(1, 5),
favorite: false,
dumped: false
}));
}
function renderFriends() {
friendListEl.innerHTML = "";
friends.forEach(friend => {
const card = document.createElement("div");
card.className = "friend-card";
if (friend.favorite) card.classList.add("favorite");
if (friend.dumped) card.classList.add("dumped");
const avatar = document.createElement("div");
avatar.className = "friend-avatar";
avatar.textContent = friend.avatar || "🙂";
const main = document.createElement("div");
main.className = "friend-main";
const nameRow = document.createElement("div");
nameRow.className = "friend-name-row";
const nameEl = document.createElement("div");
nameEl.className = "friend-name";
nameEl.textContent = friend.name;
const pill = document.createElement("div");
pill.className = "pill";
pill.textContent = friend.favorite
? "Protected"
: friend.dumped
? "Dumped"
: "Undecided";
nameRow.appendChild(nameEl);
nameRow.appendChild(pill);
const tagEl = document.createElement("div");
tagEl.className = "friend-tag";
tagEl.textContent = friend.tag;
const meta = document.createElement("div");
meta.className = "friend-meta";
const meter = document.createElement("div");
meter.className = "meter-dots";
for (let i = 0; i < 5; i++) {
const dot = document.createElement("div");
dot.className = "dot";
if (i < friend.heat) {
dot.classList.add(friend.heat >= 4 ? "hot" : "chill");
}
meter.appendChild(dot);
}
const heatLabel = document.createElement("span");
heatLabel.textContent =
friend.heat >= 4 ? "High chaos" :
friend.heat >= 2 ? "Mild static" :
"Low noise";
meta.appendChild(meter);
meta.appendChild(heatLabel);
main.appendChild(nameRow);
main.appendChild(tagEl);
main.appendChild(meta);
const actions = document.createElement("div");
actions.className = "friend-actions";
const favBtn = document.createElement("button");
favBtn.className = "btn btn-fav";
favBtn.innerHTML = '<span class="icon">★</span> keep';
favBtn.disabled = friend.dumped;
favBtn.addEventListener("click", () => toggleFavorite(friend.id));
const dumpBtn = document.createElement("button");
dumpBtn.className = "btn btn-dump";
dumpBtn.innerHTML = '<span class="icon">🗑</span> dump';
dumpBtn.disabled = friend.favorite || friend.dumped;
dumpBtn.addEventListener("click", () => dumpFriend(friend.id));
actions.appendChild(favBtn);
actions.appendChild(dumpBtn);
card.appendChild(avatar);
card.appendChild(main);
card.appendChild(actions);
friendListEl.appendChild(card);
});
}
function toggleFavorite(id) {
const f = friends.find(fr => fr.id === id);
if (!f || f.dumped) return;
f.favorite = !f.favorite;
renderAll();
}
function dumpFriend(id) {
const f = friends.find(fr => fr.id === id);
if (!f || f.dumped) return;
f.dumped = true;
f.favorite = false;
renderAll();
}
function renderStats() {
const total = friends.length;
const favs = friends.filter(f => f.favorite).length;
const dumped = friends.filter(f => f.dumped).length;
const rantCount = rants.length;
statTotalEl.textContent = total;
statFavsEl.textContent = favs;
statDumpedEl.textContent = dumped;
statRantsEl.textContent = rantCount;
const curatedRatio = total === 0 ? 0 : (favs / total) * 100;
const barWidth = Math.min(100, Math.max(6, curatedRatio));
happinessFillEl.style.width = barWidth + "%";
happinessLabelEl.textContent =
`Happiness: ${Math.round(curatedRatio)}% curated · ${rantCount} rant${rantCount === 1 ? "" : "s"} released`;
}
function resetAll() {
initFriends();
initDummyRants();
renderAll();
}
function renderAll() {
renderFriends();
renderRants();
renderStats();
}
// ------------------ RANT MODEL --------------------
const rantInput = document.getElementById("rantInput");
const charCountEl = document.getElementById("charCount");
const postBtn = document.getElementById("postBtn");
const rantFeed = document.getElementById("rantFeed");
const RANT_LIMIT = 144;
let rants = [];
let rantIdCounter = 1;
function initDummyRants() {
const now = Date.now();
rants = [
{
id: 1,
text: "Muting 27 group chats today. Emotional bandwidth is not a shared resource.",
time: new Date(now - 5 * 60 * 1000),
authorName: "You",
handle: "@you",
avatar: "😶‍🌫️",
imageUrl: null,
reactions: { down: 1, fire: 6, unfollow: 0 },
userReact: { down: false, fire: false, unfollow: false }
},
{
id: 2,
text: "Being everyone’s therapist is wild when nobody booked an appointment.",
time: new Date(now - 25 * 60 * 1000),
authorName: "Rae",
handle: "@voiceNote3000",
avatar: "🎙",
imageUrl: "https://picsum.photos/seed/grump1/600/360",
reactions: { down: 0, fire: 9, unfollow: 2 },
userReact: { down: false, fire: false, unfollow: false }
},
{
id: 3,
text: "Unfriending people in my head like: no beef, just quieter air.",
time: new Date(now - 60 * 60 * 1000),
authorName: "Theo",
handle: "@kickpush",
avatar: "🛹",
imageUrl: "https://picsum.photos/seed/grump2/600/340",
reactions: { down: 2, fire: 7, unfollow: 1 },
userReact: { down: false, fire: false, unfollow: false }
},
{
id: 4,
text: "Social media needs a “don’t show me their face for 30 days” button.",
time: new Date(now - 3 * 60 * 60 * 1000),
authorName: "Jess",
handle: "@replyGifOnly",
avatar: "📱",
imageUrl: null,
reactions: { down: 0, fire: 4, unfollow: 3 },
userReact: { down: false, fire: false, unfollow: false }
}
];
rantIdCounter = rants.length + 1;
}
function updateCharCount() {
const text = rantInput.value || "";
const len = text.length;
charCountEl.textContent = `${len} / ${RANT_LIMIT}`;
if (len > RANT_LIMIT) {
charCountEl.classList.add("over");
} else {
charCountEl.classList.remove("over");
}
postBtn.disabled = len === 0 || len > RANT_LIMIT;
}
function postRant() {
const text = (rantInput.value || "").trim();
if (!text || text.length > RANT_LIMIT) return;
const rant = {
id: rantIdCounter++,
text,
time: new Date(),
authorName: "You",
handle: "@you",
avatar: "😶‍🌫️",
imageUrl: null,
reactions: { down: 0, fire: 0, unfollow: 0 },
userReact: { down: false, fire: false, unfollow: false }
};
rants.unshift(rant);
rantInput.value = "";
updateCharCount();
renderAll();
}
function renderRants() {
rantFeed.innerHTML = "";
if (rants.length === 0) {
const empty = document.createElement("div");
empty.className = "rant-text";
empty.style.opacity = "0.7";
empty.textContent = "No rants yet. The silence is suspiciously healthy.";
rantFeed.appendChild(empty);
return;
}
rants.forEach(r => {
const rantEl = document.createElement("div");
rantEl.className = "rant";
const top = document.createElement("div");
top.className = "rant-top";
const authorBlock = document.createElement("div");
authorBlock.className = "rant-author-block";
const avatar = document.createElement("div");
avatar.className = "rant-avatar";
avatar.textContent = r.avatar || "🙂";
const author = document.createElement("div");
author.className = "rant-author";
const nameRow = document.createElement("div");
nameRow.className = "rant-name-row";
const displayName = document.createElement("span");
displayName.className = "rant-display-name";
displayName.textContent = r.authorName || "Unknown";
const handle = document.createElement("span");
handle.className = "rant-handle";
handle.textContent = r.handle || "@anon";
nameRow.appendChild(displayName);
nameRow.appendChild(handle);
const time = document.createElement("span");
time.className = "rant-time";
time.textContent = r.time.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"});
author.appendChild(nameRow);
author.appendChild(time);
authorBlock.appendChild(avatar);
authorBlock.appendChild(author);
top.appendChild(authorBlock);
const text = document.createElement("div");
text.className = "rant-text";
text.textContent = r.text;
rantEl.appendChild(top);
rantEl.appendChild(text);
if (r.imageUrl) {
const imgWrap = document.createElement("div");
imgWrap.className = "rant-image";
const img = document.createElement("img");
img.src = r.imageUrl;
img.alt = "attached image";
imgWrap.appendChild(img);
rantEl.appendChild(imgWrap);
}
const actions = document.createElement("div");
actions.className = "rant-actions";
const downBtn = makeReactionButton("👎", "down", r);
const fireBtn = makeReactionButton("🔥", "fire", r);
const unfollowBtn = makeReactionButton("🚫", "unfollow", r);
actions.appendChild(downBtn);
actions.appendChild(fireBtn);
actions.appendChild(unfollowBtn);
rantEl.appendChild(actions);
rantFeed.appendChild(rantEl);
});
}
function makeReactionButton(emoji, key, rant) {
const btn = document.createElement("button");
btn.className = "reaction-btn";
btn.classList.add(key === "down" ? "down" : key === "fire" ? "fire" : "unfollow");
if (rant.userReact[key]) {
btn.classList.add("active");
}
const iconSpan = document.createElement("span");
iconSpan.className = "icon";
iconSpan.textContent = emoji;
const countSpan = document.createElement("span");
countSpan.className = "count";
countSpan.textContent = rant.reactions[key];
btn.appendChild(iconSpan);
btn.appendChild(countSpan);
btn.addEventListener("click", () => {
toggleReaction(rant.id, key);
});
return btn;
}
function toggleReaction(id, key) {
const r = rants.find(rr => rr.id === id);
if (!r) return;
const currentlyOn = r.userReact[key];
if (currentlyOn) {
r.userReact[key] = false;
r.reactions[key] = Math.max(0, r.reactions[key] - 1);
} else {
r.userReact[key] = true;
r.reactions[key] += 1;
}
renderRants();
renderStats();
}
// ------------------ BOTTOM NAV --------------------
const navItems = document.querySelectorAll(".bottom-nav .nav-item");
const views = document.querySelectorAll(".panel-view");
navItems.forEach(item => {
item.addEventListener("click", () => {
navItems.forEach(i => i.classList.remove("active"));
item.classList.add("active");
const viewId = item.dataset.view;
const anchorId = item.dataset.anchor;
if (viewId) {
views.forEach(v => v.classList.remove("active"));
const targetView = document.getElementById(viewId);
if (targetView) {
targetView.classList.add("active");
targetView.scrollIntoView({ behavior: "smooth", block: "start" });
}
} else if (anchorId) {
const el = document.getElementById(anchorId);
if (el) {
el.scrollIntoView({ behavior: "smooth", block: "start" });
}
}
});
});
// ------------------ INIT & EVENTS --------------------
rantInput.addEventListener("input", updateCharCount);
postBtn.addEventListener("click", postRant);
resetBtn.addEventListener("click", resetAll);
initFriends();
initDummyRants();
renderAll();
updateCharCount();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment