A professionally designed kanban dashboard that tracks recent articles from a curated list of 11 education/tech journalists. One column per journalist, one card per article. Cards are color-coded by freshness (vibrant under 24h, muted under 7d, monochrome beyond that), link out to the source, and support four column sort modes plus a "past week" text-list toggle.
┌─ Header ──────────────────────────────────────────────────────────────────┐
│ Journalist Desk · Power10 [Legend] [Kanban | This week · list] [Sort]│
├─ Board ───────────────────────────────────────────────────────────────────┤
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ … │
│ │ Megan M. │ │ Micah W. │ │ Alyson K. │ │ Alex S. │ │
│ │ Axios │ │ DA · UB │ │ Ed Week │ │ Substack │ │
│ │ │ │ │ │ │ │ │ │
│ │ ▌ fresh │ │ ▌ recent │ │ ░ stale │ │ ▌ fresh │ │
│ │ ▌ recent │ │ ░ stale │ │ ░ stale │ │ ▌ recent │ │
│ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │
└───────────────────────────────────────────────────────────────────────────┘
- Frontend: Next.js 14 (App Router, React Server Components) · TypeScript · Tailwind CSS
- Database: Supabase (Postgres + RLS)
- Ingestion: Supabase Edge Function (Deno) parsing RSS where available, scraping HTML where not
- Schedule:
pg_cron+pg_nethourly POST into the Edge Function - Hosting: Vercel (frontend) · Supabase (database, function, scheduler)
| # | Journalist | Outlet(s) | Source kind |
|---|---|---|---|
| 1 | Megan Morrone | Axios | scrape |
| 2 | Micah Ward | District Administration · University Business | RSS × 2 |
| 3 | Alyson Klein | Education Week | scrape |
| 4 | Alex Sarlin | Edtech Insiders (Substack) | RSS |
| 5 | Laura Ascione | eSchool News · eSchool Media | RSS × 2 |
| 6 | Kavitha Cardoza | The Hechinger Report | RSS |
| 7 | Sabrina Ortiz | The Deep View | scrape |
| 8 | Ray Ravaglia | Forbes | RSS |
| 9 | Lauren Coffey | EdSurge | scrape |
| 10 | Anna Merod | K-12 Dive | scrape |
| 11 | Daniel Mollenkamp | EdSurge | scrape |
Scraper selectors live in sources.selector and can be edited in the DB
without redeploying — useful when an outlet changes its markup.
# 1. install
npm install
# 2. run unit tests (freshness + sort logic)
npm test
# 3. start the dev server against sample data
npm run dev
# → http://localhost:3000 ("Sample data" badge appears until you wire Supabase)The app ships with a mock dataset in lib/mock.ts that matches the live
schema, so the UI renders immediately without Supabase credentials.
The app uses the official @supabase/ssr pattern (Next.js App Router) with
Supabase's new publishable key naming:
NEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY(starts withsb_publishable_…)SUPABASE_SERVICE_ROLE_KEY— server-only, used by the ingest function
Helpers live in:
utils/supabase/server.ts—createClient(cookieStore)for RSC / route handlersutils/supabase/client.ts—createClient()for browser componentsutils/supabase/middleware.ts— session refresh, wired frommiddleware.ts
- Create a Supabase project (already done —
vpmvqzlcinfiwrfomthq). - Copy
.env.example→.env.localand fill in your URL + publishable key. (A.env.localis already provided with the project's values.) - Run the migrations:
This creates
npx supabase link --project-ref vpmvqzlcinfiwrfomthq npx supabase db push
journalists,sources,articles, enables RLS, and seeds all 11 journalists with their source URLs. - Deploy the ingest function:
npx supabase functions deploy ingest-articles
- Schedule the hourly cron via the Supabase Dashboard (not SQL —
managed Postgres blocks
alter database … set app.settings.*):- Dashboard → Database → Cron Jobs → "Create a new cron job"
- Name:
power10-ingest-hourly - Schedule:
5 * * * * - Type: Supabase Edge Function
- Function: ingest-articles
- Method:
POST, Body:{} - Save.
- Kick off the first ingestion run manually:
Then in SQL:
npx supabase functions invoke ingest-articles --no-verify-jwt
select count(*) from articles;— you should see rows.
From this point the Dashboard cron job POSTs the function every hour
at :05.
vercel link
vercel env add NEXT_PUBLIC_SUPABASE_URL # paste URL
vercel env add NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY # paste sb_publishable_...
vercel --prodThe page is ISR-cached for 10 minutes (revalidate = 600) so the board stays
snappy even under load; cron-fresh articles appear within one revalidation
window.
- Color bands are driven entirely by a
data-freshnessattribute + Tailwind CSS inapp/globals.css. No runtime color math.< 24h→ indigo→fuchsia rail, white card, vibrant meta (the "fresh" band)< 7d→ single-tone indigo rail, off-white card, muted meta≥ 7d→ slate rail, slate-50 card, desaturated text
- Typography: Inter for UI, Fraunces serif for the page title and column headers — gives an editorial feel rather than a generic SaaS look.
- Sort modes (pure functions in
lib/sort.ts, unit-tested):- Most recent article (default — left = newest article anywhere)
- Most articles this week (left = most prolific in last 7d)
- Alphabetical · last name
- Alphabetical · outlet
- Weekly list view reuses the same dataset, filtered to the past 7 days and sorted reverse-chronologically. The sort dropdown is disabled in this view because it doesn't apply.
app/
layout.tsx root layout + font loading
page.tsx RSC: loads data from Supabase (or mock)
globals.css Tailwind + freshness color bands
_components/
DashboardShell.tsx client: view + sort state
KanbanBoard.tsx horizontal scroll of columns
JournalistColumn.tsx column header + cards
ArticleCard.tsx compact card w/ freshness styling
WeeklyList.tsx text table, past 7 days
SortDropdown.tsx 4-mode sort picker
ViewToggle.tsx Kanban ↔ Weekly
FreshnessLegend.tsx top-bar legend swatches
lib/
types.ts Journalist, Article, SortMode, ViewMode, Freshness
freshness.ts <24h / <7d / >=7d band logic
sort.ts pure sort functions + grouping
mock.ts sample dataset (fallback when no Supabase)
__tests__/ vitest
utils/supabase/
server.ts createClient(cookieStore) for RSC
client.ts createClient() for browser components
middleware.ts session refresh helper
middleware.ts Next.js middleware entrypoint
supabase/
migrations/
0001_init.sql tables + RLS
0002_seed_journalists.sql 11 journalists + their sources
0003_schedule_ingest.sql pg_cron hourly POST to the function
functions/ingest-articles/
index.ts Deno entrypoint
rss.ts zero-dep RSS/Atom parser
scrape.ts deno-dom CSS-selector scraper
npm test # freshness + sort unit tests (13 tests)
npm run build # typecheck + production build
npm run dev # renders sample data immediatelyOnce Supabase is wired:
npx supabase functions invoke ingest-articles --no-verify-jwt
# then in SQL editor:
select outlet, count(*), max(published_at)
from articles group by 1 order by 2 desc;