feat(dashboard): BRICKS analytics dashboard redesign#1
Conversation
Initial 8-section dashboard implementation with mock data. This serves as a working baseline and visual reference; a redesign is specified in docs/superpowers/specs/2026-04-16-bricks-analytics-dashboard-design.md and will replace much of this — notably cutting Audience Profile (demographics aren't scrapable), adding navigation + timeframe filters, renaming Campaign Value to Est. Brand Value, and introducing cross-posting matrix + content type performance sections. Also guards instrumentation.ts when DATABASE_URL is unset so the dev server boots without Neon configured. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Investor-pitch framing, scraper-only data constraint, 9 sections (Audience Profile cut; Cross-posting Matrix and Content Type Performance added). Rationale captured for every decision so future revisits can tell what's load-bearing vs what's a preference. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the scaffold mockup with a production-ready dashboard: - App sidebar with collapsible ToC and persisted state - Command palette (⌘K) for nav, timeframe, and member jumps - 9 sections reordered for narrative flow: Overview → Views → Platforms → Formats → Top Content → Members → Collabs → Cross-post → Trajectory - Timeframe dock (7D/30D/90D/YTD/All) and compact/comfortable density toggle, both persisted to localStorage via lazy initializers - New sections: content-type performance, cross-posting matrix, collaboration hub with solo-vs-collab lift - Removed Est. Brand Value KPI and per-platform Est. Value (not meaningfully sourceable from public signals) - Removed member-row hover preview (duplicated row data) Adds docs/data-requirements.md mapping each UI surface to the upstream signals Matt's collector will need (public-page only, no creator access). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
@codex review |
|
To use Codex here, create a Codex account and connect to github. |
There was a problem hiding this comment.
Pull request overview
This PR replaces the existing BRICKS analytics dashboard scaffold with a multi-section, production-style dashboard experience (sidebar + ToC, command palette, timeframe dock, density toggle), and adds supporting product/spec documentation describing the required upstream scraped data signals.
Changes:
- Introduces a new 9-section dashboard page (
/dashboard) with multiple new visualization components (views-over-time, platform cards, leaderboard, collaboration graph, cross-post matrix, trajectory, etc.). - Adds global UI controls and shell features: sidebar + collapsible ToC, ⌘K command palette, timeframe selector dock, and density toggle (localStorage persisted).
- Adds documentation: a design spec and a section-by-section data requirements mapping for the future scraper/collector pipeline.
Reviewed changes
Copilot reviewed 28 out of 30 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| package-lock.json | Lockfile updates from dependency install (adds peer flags to multiple entries). |
| lib/dashboard/data.ts | Adds a comprehensive mock data model backing all dashboard sections. |
| instrumentation.ts | Skips Drizzle migrations when DATABASE_URL is missing in Node runtime. |
| docs/superpowers/specs/2026-04-16-bricks-analytics-dashboard-design.md | New design spec describing goals, constraints, and section behavior. |
| docs/data-requirements.md | New data contract mapping each UI section to scraper/DB signals and cadence. |
| components/dashboard/views-over-time.tsx | New stacked-area views chart with hover/tooltip, prior overlay, and target line. |
| components/dashboard/top-content.tsx | New “Top Content” hero card grid with velocity and outbound links. |
| components/dashboard/top-bar.tsx | New sticky top bar showing active section and density toggle. |
| components/dashboard/timeframe-dock.tsx | New fixed-position timeframe selector (tablist UI). |
| components/dashboard/timeframe-context.tsx | New React context for global timeframe state. |
| components/dashboard/section.tsx | New reusable section wrapper with copy-link/download/expand actions. |
| components/dashboard/scoreboard.tsx | New scoreboard grid driven by timeframe-based mock scorecards. |
| components/dashboard/platform-icon.tsx | New inline SVG icons per platform (YT/TT/IG/FB). |
| components/dashboard/platform-cards.tsx | New 2×2 platform breakdown cards with sparkline visualization. |
| components/dashboard/member-leaderboard.tsx | New member leaderboard table with views/followers toggle and density support. |
| components/dashboard/growth-trajectory.tsx | New trajectory chart with baseline/actual/projected series and baseline table. |
| components/dashboard/density-context.tsx | New React context for comfortable/compact density (localStorage persisted). |
| components/dashboard/cross-posting-matrix.tsx | New cross-posting matrix table with incoming/outgoing summaries. |
| components/dashboard/content-type-performance.tsx | New content-type mix bars with avg views and engagement per format. |
| components/dashboard/command-palette.tsx | New ⌘K command palette to jump to sections/members/timeframes. |
| components/dashboard/coming-soon.tsx | New shared “Coming Soon” shell for stub pages (sidebar + providers). |
| components/dashboard/collaboration.tsx | New collaboration hero stats + network graph with hover card. |
| components/dashboard/app-sidebar.tsx | New sidebar with workspace nav, collapsible ToC, and persisted collapse state. |
| components/dashboard/active-section-context.tsx | New scroll-based active-section tracking for sidebar/top bar highlighting. |
| app/reports/page.tsx | Adds /reports coming-soon stub page. |
| app/members/page.tsx | Adds /members coming-soon stub page. |
| app/dashboard/page.tsx | Adds the new main dashboard page wiring all new sections/components together. |
| app/content/page.tsx | Adds /content coming-soon stub page. |
| .gitignore | Fixes tmp/ ignore entry and adds .playwright-mcp/ artifacts. |
| .claude/launch.json | Adds a Claude launch config for running npm run dev on port 3000. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <p className="font-mono text-[10px] tracking-[0.22em] text-white/45 uppercase"> | ||
| Cross-post Matrix · {tfPhrase} | ||
| </p> | ||
| <p className="mt-1 text-xs text-white/50"> | ||
| Green cell = member in that row cross-posted to the column | ||
| member's account at least once this week. | ||
| </p> |
There was a problem hiding this comment.
This section header/tooltip copy says cells indicate cross-posting "this week", but the UI also displays the selected timeframe via timeframePhrase(timeframe) (which could be 30D/90D/YTD/All-time). Either tie the matrix data + copy to the active timeframe, or make the section explicitly fixed to a weekly window and remove the timeframe-dependent label to avoid misleading users.
| const { timeframe } = useTimeframe(); | ||
| const viewsLabel = timeframeViewsLabel(timeframe); | ||
| return ( |
There was a problem hiding this comment.
viewsLabel is derived from the selected timeframe (timeframeViewsLabel(timeframe)), but the displayed p.views values come from the static platformCards mock data. This makes the UI misleading when switching timeframes (label changes, numbers don’t). Either make platformCards timeframe-aware or keep the label fixed to the window the mock values represent.
| <svg viewBox={`0 0 ${w} ${h}`} className="h-9 w-full"> | ||
| <defs> | ||
| <linearGradient id={`spark-${color}`} x1="0" x2="0" y1="0" y2="1"> | ||
| <stop offset="0%" stopColor={color} stopOpacity="0.35" /> | ||
| <stop offset="100%" stopColor={color} stopOpacity="0" /> | ||
| </linearGradient> | ||
| </defs> | ||
| <path d={area} fill={`url(#spark-${color})`} /> | ||
| <polyline |
There was a problem hiding this comment.
Sparkline uses the raw hex color (e.g. #FF0000) in the SVG linearGradient id (spark-${color}), which makes the id contain # and produces an invalid fragment reference in url(#spark-...). This can break the gradient fill (and potentially collide across cards). Use a sanitized id (e.g. based on platform key or a hash) rather than the color string.
| function onKeyDown(e: React.KeyboardEvent) { | ||
| if (e.key === "Escape") { |
There was a problem hiding this comment.
onKeyDown is typed as React.KeyboardEvent, but this file doesn't import the React namespace (and with the modern JSX runtime it won't be in scope automatically). This will cause a TS error (Cannot find namespace 'React'). Import the appropriate type from react (e.g. type KeyboardEvent as ReactKeyboardEvent) or change the annotation to use the imported types.
| const maxTotal = Math.max( | ||
| ...s.map((x) => x.total), | ||
| ...priorTotals, | ||
| ); | ||
| const rounded = Math.ceil(maxTotal / 250_000) * 250_000; | ||
| return { | ||
| stacks: s, | ||
| prior: priorTotals, | ||
| yMax: rounded, | ||
| days: current, | ||
| targetDaily: 3_333_333, // 100M/mo pace | ||
| }; |
There was a problem hiding this comment.
yMax is derived only from the current/prior data max, but the chart always draws a targetDaily line. With the provided mock data, targetDaily (3.33M/day) exceeds yMax, so the target line/text will render outside the plot area (negative y) and may not be visible. Include targetDaily when computing maxTotal (or clamp it) so the target reference line is always in-range.
| return ( | ||
| <svg viewBox="0 0 24 24" className={className}> | ||
| <defs> | ||
| <linearGradient id="ig-grad" x1="0%" y1="0%" x2="100%" y2="100%"> | ||
| <stop offset="0%" stopColor="#F58529" /> | ||
| <stop offset="50%" stopColor="#DD2A7B" /> | ||
| <stop offset="100%" stopColor="#8134AF" /> | ||
| </linearGradient> | ||
| </defs> | ||
| <path | ||
| fill="url(#ig-grad)" | ||
| d="M12 2.2c3.2 0 3.6 0 4.8.1 1.2.1 1.8.3 2.3.4.6.2 1 .5 1.5 1 .5.5.8.9 1 1.5.1.5.3 1.1.4 2.3.1 1.2.1 1.6.1 4.8s0 3.6-.1 4.8c-.1 1.2-.3 1.8-.4 2.3-.2.6-.5 1-1 1.5-.5.5-.9.8-1.5 1-.5.1-1.1.3-2.3.4-1.2.1-1.6.1-4.8.1s-3.6 0-4.8-.1c-1.2-.1-1.8-.3-2.3-.4a4 4 0 0 1-1.5-1 4 4 0 0 1-1-1.5c-.1-.5-.3-1.1-.4-2.3C2.2 15.6 2.2 15.2 2.2 12s0-3.6.1-4.8c.1-1.2.3-1.8.4-2.3.2-.6.5-1 1-1.5.5-.5.9-.8 1.5-1 .5-.1 1.1-.3 2.3-.4C8.4 2.2 8.8 2.2 12 2.2zm0 5.3a4.5 4.5 0 1 0 0 9 4.5 4.5 0 0 0 0-9zm5.8-.3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM12 9.3a2.7 2.7 0 1 1 0 5.4 2.7 2.7 0 0 1 0-5.4z" |
There was a problem hiding this comment.
The Instagram SVG uses a hard-coded gradient id (id="ig-grad"). When PlatformIcon is rendered multiple times on the same page, duplicate ids can cause the gradient reference (url(#ig-grad)) to resolve unpredictably. Use a per-instance unique id (e.g. via useId() or an idSuffix prop) and reference that in the fill URL.
| export function MemberLeaderboard() { | ||
| const [mode, setMode] = useState<Mode>("views"); | ||
| const { timeframe } = useTimeframe(); | ||
| const { density } = useDensity(); | ||
| const viewsLabel = timeframeViewsLabel(timeframe); | ||
| const compact = density === "compact"; | ||
| const cellPad = compact ? "px-3 py-1.5" : "px-4 py-3"; | ||
| const avatarSize = compact ? 24 : 32; | ||
|
|
||
| const ranked = useMemo(() => { | ||
| const sorted = [...members].sort((a, b) => | ||
| mode === "views" | ||
| ? b.views30d - a.views30d | ||
| : b.followers - a.followers, | ||
| ); | ||
| return sorted.map((m, i) => ({ ...m, rank: i + 1 })); | ||
| }, [mode]); |
There was a problem hiding this comment.
The leaderboard label adapts to the selected timeframe (timeframeViewsLabel(timeframe)), but the ranking/values are always based on views30d (and growth30d). This means switching to 7D/YTD/ALL will display a mismatched label while still showing 30D numbers. Either (1) drive the metric selection from timeframe (e.g. use views7d for 7D) or (2) keep the label fixed to 30D until other timeframe rollups exist.
Summary
docs/data-requirements.mdmapping each UI section to the upstream public-page signals Matt's collector will needWhat changed
/content,/members,/reportscoming-soon stubs so sidebar links resolvedocs/data-requirements.mdbreaks down every chart into the SQL/collector shape neededTest plan
npm run dev— all 9 sections render and scroll-link from sidebar ToCnpx tsc --noEmit— zero errorsnpx eslint .— zero errors🤖 Generated with Claude Code