← Home
01Portfolio rebuild · 2026

Projects,
Reborn

One rigid grid became four filterable views. Free text became real dates. Drafts became invisible at the database rather than in React. A rebuild of /projects on sankettambare.in, and the three bugs it uncovered on the way.

React 18 · Vite · Supabase · Tailwind 3 migrations · 13 new columns · 4 views sankettambare.in/projects
02The honest before

A page that listed things, and nothing more

The old /projects rendered its rows verbatim through a fixed six-variant layout keyed off index % 6, in whatever order a hand-maintained integer said.

What was missing

  • No search, no filter, no sort — of any kind
  • Tags existed in the database and were never rendered
  • No way to tell a live product from a 2020 college project
  • No record of what anything was built with

What was broken

  • sort_order was 0 on nine of thirteen rows
  • Dates mixed 2020-10-20 with August 2026 — unsortable
  • A cover image was mandatory, so every project needed one
  • Card shape came from list position, so filtering would reshuffle it

The page was not slow or ugly. It simply had no data worth querying, so nothing could be asked of it.

03Migration 0005

Thirteen columns, one dropped

The redesign was blocked on the schema, so the schema went first.

categorystatusrole orgfeaturedvisible tech_stackhighlightslinks slide_imagesproblemsolution outcome

Dropped

sort_order — hand-maintained, mostly zero, and replaced by ordering on a real date. Nothing reintroduces it.

Relaxed

image became nullable. A project now falls back to its first screenshot, then to a placeholder.

Tech stack is a plain text[], not tags. The central tag RPC replaces an entity’s entire tag set, so it cannot back two independent tag fields on one row. Tags stay the topical axis; stack is the build axis.

04Drafts

Hidden means hidden, not filtered

A project can be toggled to draft in the admin. Filtering that out in React would be theatre — the row would still sit in the JSON payload, readable in devtools.

create policy "public read" on public.projects
  for select using (visible or public.is_owner());

Owner

13 rows · sees drafts

Other signed-in user

12 rows · no drafts

Anonymous

12 rows · no drafts

Verified three ways, including through the cross-entity tag RPC — a hidden project also drops out of its /tags pages, its share card, and the public project counts on the homepage. It is one policy, so it cannot be forgotten at one call site.

05The conversion

From free text to a real date

Thirteen rows held two formats. Sorting them meant normalising every value, then changing the column’s type underneath.

Before · text not null

  • 2020-10-20
  • August 2026
  • July 2026

Sorted alphabetically. “August” before “July”.

After · date, nullable

  • 2020-10-20
  • 2026-08-01
  • 2026-07-01

Sorted chronologically. Undated rows sink to the bottom, in both directions.

Nullable on purpose: “no date” is missing information, not an extreme value, so it never sorts as the oldest or the newest thing you own.

06The page

Four views over one list

Tabs carried in the URL, the way the Treks and Sports pages already work.

01
Showcase

Featured spotlight over a masonry grid. Cards size to their content instead of to their position.

02
Timeline

A chronological spine grouped by year — only possible once the dates were real.

03
Statistics

Per-year, per-stack, per-category and per-status breakdowns of whatever is filtered.

04
Table

Dense and sortable. The fastest way to take in everything without scrolling a gallery.

All four read from one filter hook, so a filter set in Showcase survives a switch to Table — and both ?view= and every filter live in the query string, which makes any filtered view a shareable link.

07Filtering

OR inside an axis, AND across them

Picking React and Supabase widens the stack axis. Adding a category on top of that still narrows. It is the behaviour people expect and rarely get.

Axes

  • Live text search — title, subtitle, description, organisation, role
  • Category, status and year — single-select
  • Tech stack and tags — multi-select chips

Vocabulary the data produced

Astro4
React4
Supabase4
Tailwind CSS4
Cloudflare3
Vite3

15 chips across 12 public projects.

Central tags finally appear here too. Projects had carried them for a full version already — the old page simply never rendered them.

08Interaction

Browse without losing your place

Quick-look modal

  • Screenshots, stack, highlights, tags and links
  • Traps focus, closes on Escape
  • Returns focus to the card that opened it
  • A “full case study” link through to the detail page

Cards

  • Hover cycles a project’s screenshots
  • Held back on touch devices and under prefers-reduced-motion
  • No cover image? The first screenshot stands in
  • Neither? A placeholder, never an empty tile

That last one was a real bug in waiting: a falsy src does not reliably fire onError, so the old fallback would have rendered an empty box the moment cover images became optional.

09Behind the form

Three additions to the editor

The admin is schema-driven, so new fields are usually declaration only. Three things genuinely did not exist yet.

linkList widget

Repeatable {label, url} rows with reorder and delete — repo, demo, write-up. Modelled on the existing image-slides widget.

Pooled autocomplete

A field can suggest from values already used on its own resource, so the stack field proposes stacks you have used without touching the tag tables.

Field defaults

A field can declare a default. Without it, visible would start false and every new project would be born hidden.

That third one is the kind of bug that ships quietly and is blamed on the database three weeks later.

10Migration 0006

Filled in by visiting every one

New columns are worthless empty. Every project’s live site and repository was opened and read, and the stacks were taken from evidence rather than memory.

Evidence, not recall

  • <meta name="generator"> → Astro v7.2.8
  • /_next/static/chunks/app/ → Next.js App Router
  • _astro/ and data-astro in the shell
  • supabase.co image URLs → Supabase storage

What the reading turned up

  • One project’s link had been dead for months — 404
  • Six projects shared one cover: another organisation’s logo
  • One subtitle was simply empty

Four lines carry a REVIEW marker in the SQL, where the source was a README rather than a stated fact. Inference is allowed; passing it off as evidence is not.

11What the work surfaced

Three bugs, none of them the feature

A leading space

One row is stored as " E20 ka Chakravyuha". Every where title = … in the date backfill missed it, so the row fell through to the non-ISO safety net and had its date cleared. Titles are trimmed first now, and matched on btrim.

A type that only held by luck

The cross-entity tag function declares date text and passed the column through raw. That type-checked only while the column was text — changing it would have broken every tag page. Cast in the same migration.

Both were caught by rehearsal, not by reading. Every migration was applied to a scratch PostgreSQL instance loaded with the real thirteen rows before it touched anything live — which also proved the whole chain applies clean from empty, and is idempotent.

12Standing

Where it stands

4
Views
13
Columns added
15
Stack filters
272
Tests passing

The catalogue now

Live10
Archived2

Website 4 · Web App 4 · Tool 2 · Design 1 · Data Story 1. Spanning 2020 to 2026.

What changed underneath

  • Three migrations, each rehearsed before it ran
  • Drafts enforced in Postgres, not in the client
  • Every filtered view is a shareable URL
  • Keyboard-navigable throughout, focus returned on close
See it live → sankettambare.in/projects