Skip to content

feat: Phase 0–2 milestone — viewing experience and account features - #5

Merged
scr0ols merged 25 commits into
mainfrom
dev
Jul 20, 2026
Merged

feat: Phase 0–2 milestone — viewing experience and account features#5
scr0ols merged 25 commits into
mainfrom
dev

Conversation

@scr0ols

@scr0ols scr0ols commented Jul 20, 2026

Copy link
Copy Markdown
Owner

What changed

Merge from dev into main. Brings the app from an empty scaffold to an end-to-end working product against real Twitch
channels.

Phase 0 — scaffold:

  • React (Vite) frontend, Node.js/Express backend, Postgres (Neon) schema
  • Twitch OAuth round trip (app token + user token, server-side refresh)

Phase 1 — viewing experience:

  • 2–5 channel video grid with add/remove
  • Selection and Both/All audio modes (focus-follows-audio layout, per-panel volume)
  • Online/offline detection via backend Helix poll
  • Chat bar with one always-mounted tab per channel, follows active audio channel until manually overridden
  • Per-panel actions: follow, donate, channel link

Phase 2 — account features:

  • Login-gated named grid templates: save/load/delete, with channel validation on load (renamed/banned channels render a "channel not found" panel instead of failing)
  • "Online now" menu: followed channels currently live, plus top live channels on Twitch by viewer count, one-click add-to-grid
  • Account menu redesign, top bar overhaul (reorder, collapse toggle), settings modal

Type of change

  • feat — new feature

Checklist

  • Tested locally (OAuth round trip, template CRUD, live grid usage)
  • Build passes

scr0ols and others added 25 commits July 17, 2026 19:26
Vite + React frontend, Express backend, and an end-to-end Twitch OAuth
Authorization Code flow (login, callback, session cookie, /auth/me,
logout) verified against a throwaway page. Sessions are in-memory for
now, replaced by Postgres in the next phase.
Merge dev branch with docs
…irect URI

The fallback default was still 3001 (the rejected first attempt from Day 2,
see c-docs/NOTES.md), so a missing .env would silently reintroduce the
redirect_mismatch error against Twitch's registered callback URL.
Users and templates tables provisioned on Neon per the PLAN.md data
model. OAuth callback now upserts a real users row instead of holding
profile data only in memory, and /api/templates gains full owner-scoped
CRUD behind the existing session cookie. Verified end to end through a
real authenticated browser session.
The day-by-day narrative belongs in c-docs/NOTES.md, which already
has the full history; the README should only tell users what works.
The embed's ONLINE/OFFLINE events are unreliable (a long-standing Twitch
platform issue), so the frontend polls GET /api/stream-status?logins=...
instead. The endpoint batches up to 100 logins into one Helix /streams
call using a cached client-credentials app access token. No auth
required, matching anonymous read-only viewing.
- Grid reducer implements the PLAN.md GridState shape; panels stay flat
  siblings so switching audio focus never remounts a Twitch iframe.
- VideoPanel mounts one Twitch Embed per channel and controls mute and
  volume via the player API; the offline overlay reads the Helix-polled
  channel.online flag.
- Selection and Both/All audio modes, with the removal-fallback edge
  case promoting another channel.
- ChatBar keeps every chat iframe mounted for the whole session and
  switches tabs via CSS visibility only; the chat tab follows the active
  audio channel until manually overridden.
- Native per-panel actions: follow (login-gated), donate link, channel
  link, remove.
- Twitch login wired into the app shell, with a login prompt modal for
  gated actions.
- Visual design pass in hand-written CSS: color and typography tokens,
  light and dark themes, hover and focus states, transitions, inline SVG
  icon set.
visibility is overridable by descendants: .chat-panel.visible set
visibility: visible and so re-revealed itself inside the hidden
.chat-bar-body, leaving the active chat's right edge showing through
the collapsed 48px rail. Hide panels explicitly while collapsed.
…resh

- OAuth now requests the user:read:follows scope (was empty); existing
  users re-login once to grant it.
- twitchUserToken.js implements refresh-on-expiry for the stored per-user
  token pair (rotated refresh tokens persisted); a failed refresh raises
  ReloginRequiredError, which routes map to a cleared session and 401.
- GET /api/followed-streams (auth) wraps Helix /streams/followed with the
  user's own token; 401/403 from Twitch (pre-scope token or revoked
  access) forces the same clean re-login.
- GET /api/resolve-channels (public, app token) reports which logins
  exist, for template-load validation.
- Helix 429s now pass through as 429 instead of 502.

Nothing from Twitch is persisted; responses are passed straight through.
- TemplateManager (toolbar dropdown): save the current grid as a named
  template, list/load/delete, explicit saving/saved/error state, save
  blocked below 2 channels, everything login-gated with an inline login
  prompt when logged out.
- FollowedLive (header dropdown, logged-in only): live channels the user
  follows via /api/followed-streams, with one-click add-to-grid, disabled
  for channels already in the grid or at the 5-channel cap.
- Template load validates logins via /api/resolve-channels first;
  unresolvable channels render a dashed "channel not found" panel with no
  embed and only a remove action, and are skipped when picking the active
  audio channel (SET_STATE reducer action).
- Session-expiry (401) on any authed call resets the client to logged-out.
- Dev-only window.__streamhive dispatch hook for browser automation.
New order: brand, Templates, add-channel form, Online now, account,
Audio, log out/in. Templates now opens near the left edge of the bar,
so its dropdown panel anchors left instead of the default right:0 (which
would have pushed it off-screen); Online now keeps the default anchor
since it still sits close to the right edge.
New right-side order: Online now, Audio, Account. The account block
(avatar+name+logout) becomes a dropdown trigger, matching the existing
Templates/Online-now pattern, so logout no longer sits as a separate
top-level button.

Also adds a collapse toggle for the whole top bar, mirroring the chat
bar's open/close affordance to reclaim vertical space for the grid. No
iframes live in the top bar, so collapsing just unmounts the controls
instead of needing the chat bar's visibility-only hide.
Drop the redundant "AUDIO" eyebrow label above the Selection/Both-All
segmented control — the two button labels already say what they do.

Swap the Online now trigger's heart icon for the existing red live-dot
indicator (already used elsewhere for live status), since a heart reads
as "favorites" rather than "currently live".
auto-fit with minmax(320px, 1fr) packed as many columns as would fit,
so 4 channels ended up as one wide row and 5 as an unbalanced 4+1 —
"division only horizontal" for the counts where it matters most.
Explicit per-count templates fix this: 2 and 3 stay a single row,
4 becomes 2x2, 5 becomes 3-over-2. Verified live for all four counts
via getBoundingClientRect (2 rows confirmed for 4 and 5). Narrow
screens (<=900px) fold 3 and 5 down to 2 columns.
Account menu becomes a proper vertical list: Twitch account (external
link to the user's profile), Settings, a divider, then Log out — instead
of a single standalone logout button.

Settings holds two decided scopes: default audio mode / chat-bar-open
state for new sessions (both were hardcoded in the reducer's initial
state until now), and a manual light/dark theme override alongside the
existing System (prefers-color-scheme) default. All three are stored in
localStorage only, no backend involved.

Theme override works via a data-theme attribute on <html>: index.css
gets :root[data-theme="light"/"dark"] blocks after the prefers-color-
scheme query, so they win once set; removing the attribute (System) hands
control back to the OS setting. main.jsx applies any stored override
before the first paint to avoid a flash of the wrong theme.

Verified live: setTheme('dark'/'light'/'system') each resolved --bg to
the correct value immediately, and 'system' correctly cleared the
attribute and fell back to the media query. Not verified: the actual
Settings modal UI and Twitch-account link, since both only render inside
AccountMenu, which needs a real login.
- Drop the accent-colored outline ring on the header avatar; it made a
  24px Twitch profile picture read as a boxed icon rather than a photo.
- Collapsed top bar now hides the "StreamHive" text too, keeping only
  the logo mark and the toggle, with tighter padding — maximizes grid
  space instead of just hiding the controls.
- Empty-state headline replaced: "Watch up to 5 streams at once" undersold
  the app; "One screen. Every stream that matters." aims higher without
  claiming anything unbuilt.
Swap the solid-fill btn-primary for a new btn-quiet variant: same
transparent/bordered shape as the other top-bar buttons, accent color
on the icon/text only. Still reads as the form's action without
outweighing everything around it. Scoped to this one button — Save in
TemplateManager keeps btn-primary, wasn't part of the complaint.
@scr0ols
scr0ols requested a review from Copilot July 20, 2026 01:39
@scr0ols scr0ols self-assigned this Jul 20, 2026
@scr0ols
scr0ols merged commit 3f59f13 into main Jul 20, 2026
1 check passed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Merges dev into main, taking the project from scaffold to a working end-to-end Twitch multi-stream viewer with OAuth, templates, and “Online now” discovery.

Changes:

  • Added a React/Vite frontend with multi-channel grid playback, chat bar, templates, and settings (theme/audio/chat defaults).
  • Added an Express/Postgres backend supporting Twitch OAuth, token refresh, Helix polling endpoints, and template CRUD.
  • Updated repository docs and ignore files for local dev setup and secrets hygiene.

Reviewed changes

Copilot reviewed 29 out of 37 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
frontend/vite.config.js Adds Vite config enabling React plugin.
frontend/src/twitchEmbed.js Adds Twitch embed script loader with singleton promise.
frontend/src/settings.js Adds localStorage-backed user settings + theme application.
frontend/src/main.jsx Boots app and applies stored theme before first paint.
frontend/src/index.css Adds global theme tokens, base styles, and shared button variants.
frontend/src/gridReducer.js Introduces grid reducer/state for channels, audio, volumes, chat behavior.
frontend/src/components/icons.jsx Adds app icon set + logo mark components.
frontend/src/components/VideoPanel.jsx Implements Twitch embed panel with actions, overlays, and audio control.
frontend/src/components/VideoGrid.jsx Implements symmetric and focus-based grid layout logic.
frontend/src/components/TemplateManager.jsx Adds login-gated template save/load/delete UI.
frontend/src/components/SettingsModal.jsx Adds settings modal for defaults + theme override.
frontend/src/components/FollowedLive.jsx Adds “Online now” dropdown with followed + trending live streams.
frontend/src/components/ChatBar.jsx Adds always-mounted multi-tab chat sidebar.
frontend/src/components/AudioModeControl.jsx Adds segmented control for audio mode selection.
frontend/src/components/AddChannelForm.jsx Adds channel add UI w/ max channel cap behavior.
frontend/src/components/AccountMenu.jsx Adds account dropdown (Twitch link, settings, logout).
frontend/src/assets/vite.svg Adds Vite asset.
frontend/src/assets/react.svg Adds React asset.
frontend/src/App.jsx Adds main app shell wiring reducer, polling, auth, templates, UI composition.
frontend/src/App.css Adds full app styles for topbar, grid, chat, dropdowns, modals.
frontend/public/icons.svg Adds SVG symbol sprite sheet used by UI.
frontend/public/favicon.svg Adds favicon asset.
frontend/package.json Defines frontend scripts and dependencies.
frontend/index.html Adds Vite HTML entrypoint.
frontend/README.md Adds frontend scaffold README.
frontend/.oxlintrc.json Adds Oxlint configuration.
frontend/.gitignore Adds frontend ignores (dist, node_modules, editor files).
backend/src/twitchUserToken.js Adds user-token refresh + persistence logic.
backend/src/twitchAppToken.js Adds cached app-token fetcher for Helix calls.
backend/src/index.js Adds Express server with OAuth flow, Helix endpoints, and template CRUD.
backend/src/db.js Adds pg Pool initialization.
backend/package.json Defines backend scripts and dependencies.
README.md Updates root README with current status and setup instructions.
.gitignore Adds root ignores for secrets, deps, build outputs, editor files.
Files not reviewed (2)
  • backend/package-lock.json: Generated file
  • frontend/package-lock.json: Generated file
Comments suppressed due to low confidence (1)

frontend/src/twitchEmbed.js:1

  • scriptPromise is never reset on load failure, so a single transient network error will “poison” future calls (they’ll keep returning the same rejected promise and never retry). Reset scriptPromise to null in the onerror handler and reject with a real Error (the current rejection value is an event, which is hard to diagnose upstream).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


// Match the Twitch chat iframe theme to the app theme once at load; the
// param only takes effect at iframe creation anyway.
const dark = window.matchMedia('(prefers-color-scheme: dark)').matches
Comment on lines +39 to +48
<div className="chat-tabs" role="tablist" aria-label="Channel chats">
{channels.map((channel) => (
<button
key={channel.id}
type="button"
role="tab"
aria-selected={channel.id === activeChatChannelId}
className={`chat-tab${channel.id === activeChatChannelId ? ' active' : ''}`}
onClick={() => onSelectChat(channel.id)}
>
Comment on lines +36 to +38
<div className="modal-backdrop" onClick={onClose}>
<div className="modal settings-modal" role="dialog" aria-modal="true" onClick={(e) => e.stopPropagation()}>
<h2>Settings</h2>
Comment thread frontend/src/App.jsx
Comment on lines +213 to +214
<div className="modal" role="dialog" aria-modal="true" onClick={(e) => e.stopPropagation()}>
<p>{loginPrompt}</p>
Comment thread frontend/src/App.jsx
Comment on lines +54 to +57
async function pollStreamStatus() {
const res = await fetch(`${BACKEND_URL}/api/stream-status?logins=${logins}`)
if (!res.ok) return
const { online } = await res.json()
import { MAX_CHANNELS } from '../gridReducer'
import { IconChevronDown, IconPlus } from './icons'

const BACKEND_URL = 'http://localhost:3000'
Comment on lines +63 to +64
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [channel.id, channel.loginName])
Comment thread backend/src/index.js
Comment on lines +145 to +156
app.get('/auth/me', requireAuth, async (req, res) => {
const { rows } = await pool.query(
'SELECT login, display_name, avatar_url FROM users WHERE id = $1',
[req.userId],
);
const user = rows[0];
res.json({
login: user.login,
displayName: user.display_name,
avatarUrl: user.avatar_url,
});
});
Comment thread backend/src/index.js
Comment on lines +26 to +27
const sessions = new Map();
const pendingStates = new Set();
Comment thread backend/src/index.js
Comment on lines +137 to +141
res.cookie('session_id', sessionId, {
httpOnly: true,
sameSite: 'lax',
maxAge: 1000 * 60 * 60 * 24 * 7,
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants