Skip to content

feat(design): scaffold the /design inspiration gallery — schema, taxonomy, bordered shell, feed, /go redirects - #134

Open
arihantcodes wants to merge 3 commits into
mainfrom
feat/design-gallery
Open

feat(design): scaffold the /design inspiration gallery — schema, taxonomy, bordered shell, feed, /go redirects#134
arihantcodes wants to merge 3 commits into
mainfrom
feat/design-gallery

Conversation

@arihantcodes

@arihantcodes arihantcodes commented Aug 3, 2026

Copy link
Copy Markdown
Owner

First two vertical slices of Spectrum Design (/design) — a curated design-inspiration gallery modelled on recent.design's mechanics and IA, expressed in this site's own visual language. Every route renders today; the gallery is already seeded with 16 live items.

What's in this PR

Data layer (Supabase)

  • design-schema.sql (applied to Supabase already): design_items, design_media, design_outbound_links, a role column on users for future admin gating, partial indexes filtered to published rows, and RLS policies as a backstop.
  • Two deliberate departures from the original spec's Prisma sketch:
    • Taxonomy lives in code (content/design-taxonomy.ts), not the DB — items reference categories as text[] slugs (GIN-indexed). This is what lets every category page be statically known and render its SEO copy before a single row exists, and it removes four join tables.
    • CHECK constraints instead of Postgres ENUMs — editable in one statement.
  • Query layer (lib/design/queries.ts) wrapped in unstable_cache with a shared design-items tag; degrades to a "not provisioned" state instead of a 500 if the tables don't exist.

Routes

Route Notes
/design Masonry feed, ItemList JSON-LD, "Last updated Xm ago"
/design/c/[category] ×9 Real navigable routes, not query strings — each with its own H1, title, meta description, BreadcrumbList+ItemList JSON-LD, and 150–250 words of unique indexable intro copy. This is the organic-traffic engine.
/go/[code] Tracked outbound redirects (302, http(s)-only destinations, fire-and-forget click counts, noindex + nofollow on paid links)

Shell — recent.design mechanics, Spectrum's language

  • Bordered three-column layout using the site's existing container-wrapper dashed-hairline convention; rails divide from the feed with the same dashed border.
  • Left rail: section nav with item counts and active dot. Right rail: house unit (job listings land here later).
  • Sticky filter rail under the 3.5rem site header (rendered as a sibling of the title row, not inside it — position:sticky can only stick within its parent's box).
  • Sort (Recent / Staff picks / Popular) persists in a cookie, never in the URL — no duplicate-content risk from sorted views.
  • CSS multi-column masonry (no JS masonry): no measurement pass, correct from SSR. Cards reserve their box from stored media aspect ratios → ~0 CLS.
  • Video cards per spec §7: muted loop, plays at ≥50% visibility, pauses off-screen / hidden tab / prefers-reduced-motion. This is also the GIF path (GIFs transcode to mp4 at ingest).
  • AdSense is now suppressed on /design only (components/seo/adsense-script.tsx) so it can't cannibalise directly-sold sponsorship inventory; unchanged everywhere else.

Seed content (16 items, live now)

15 real product screens curated via the Mobbin MCP (web dashboards, iOS onboarding, landing heroes) with original editorial descriptions and source attribution, plus a self-rendered 6s gradient-motion mp4 exercising the video path. Media is rehosted in the public Supabase Storage bucket design-media — Mobbin's CDN URLs are signed and expire, so hotlinking would have left blank cards within days. Migration to R2 later needs no schema change (NEXT_PUBLIC_R2_PUBLIC_HOST is already wired into remotePatterns).

Verification

  • Build ✅ · 9/9 test suites ✅ · tsc --noEmit 0 errors ✅ · ESLint clean on new files ✅
  • Browser-verified against the production build: 16 cards render, video plays and pauses correctly, all five seeded category filters return the right subsets, sort reorders and persists, sticky rail pins at 56px, dark mode clean, zero console errors.
  • AdSense verified absent on /design, still loading on /docs/*.
  • Metadata-uniqueness test confirms no title/description collisions with the existing 107 public pages.

Known / deferred

  • The seeded Mobbin screens are copyrighted app screenshots — fine as seed content, but the §14 takedown flow should exist before heavy promotion.
  • /design sitemap entries, the item detail page (/design/i/[slug]), and /design/admin (ingest UI) are the next slices.
  • True ISR for the gallery is blocked by the root layout's await auth() (pre-existing, affects all 135 routes).

🤖 Generated with Claude Code


Summary by cubic

Scaffolded the /design inspiration gallery with a code-driven taxonomy, Supabase schema, bordered shell, masonry feed (with video), and tracked /go redirects. All routes render now; gallery is seeded with 16 items and AdSense is suppressed on /design.

  • New Features

    • Supabase schema: design_items, design_media, design_outbound_links, users.role; categories as text[] slugs; CHECK constraints; partial indexes; RLS.
    • Taxonomy in content/design-taxonomy.ts; static category routes with unique intro copy and JSON‑LD; /design and nine /design/c/[category] pages.
    • UI shell: bordered three‑column layout, sticky filter rail, sidebar counts; sort (Recent/Staff/Popular) via cookie.
    • Feed: CSS multi‑column masonry; video cards that play only in view and respect reduced motion; ItemList/BreadcrumbList JSON‑LD; caching via unstable_cache with the design-items tag.
    • /go/[code] tracked redirects (302, http(s)-only, fire‑and‑forget click counts; nofollow on paid).
    • AdSense kept off /design via AdSenseScript; unchanged elsewhere.
    • Image hosts added to next/image remotePatterns using NEXT_PUBLIC_R2_PUBLIC_HOST and the Supabase host.
  • Migration

    • Run design-schema.sql in Supabase to create gallery tables and policies.
    • Set NEXT_PUBLIC_R2_PUBLIC_HOST (or ensure NEXT_PUBLIC_SUPABASE_URL is set) so gallery media loads.
    • Call revalidateTag('design-items') on publish to refresh cached feeds.

Written for commit 55ade7c. Summary will update on new commits.

Review in cubic

jainarihantt and others added 3 commits August 1, 2026 22:39
…and feed

First vertical slice of Spectrum Design. Every route renders today; only the
items are missing until design-schema.sql is applied.

Schema (design-schema.sql, run manually in Supabase per repo convention):
- design_items, design_media, design_outbound_links, plus a `role` column on
  users for admin gating, and RLS policies as a backstop.
- Two deliberate departures from the original Prisma sketch. Categories and
  facets are text[] slugs against a code-side taxonomy rather than four join
  tables, so category routes are statically generatable and render their SEO
  copy before a single row exists. Enums are CHECK constraints rather than
  Postgres ENUM types, which can be edited in one statement.
- Partial indexes on (section, published_at) and (section, score) filtered to
  published rows, so the draft queue never bloats the feed indexes.

Routes:
- /design — masonry feed, ItemList JSON-LD.
- /design/c/[category] — real navigable category routes, not query strings.
  Nine statically generated pages, each with its own H1, title, meta
  description derived from its intro, BreadcrumbList + ItemList JSON-LD, and
  150-250 words of unique indexable copy. This is the organic traffic engine.

UI:
- Three-column shell under the existing site header. The global nav is kept on
  purpose: funnelling gallery traffic into the component library is the third
  business goal, so removing the links to /docs would work against it.
- CSS multi-column masonry rather than JS masonry — no measurement pass, no
  layout thrash, correct from the server on first paint.
- MediaCard reserves its box from the stored media aspect ratio, which is what
  holds CLS near zero, and pairs it with a --design-media-placeholder tint.

Also: AdSense is now suppressed on /design only (spec 12.8) so it cannot
cannibalise directly-sold sponsorship inventory; it still loads everywhere else.

Feed and section-count queries are wrapped in unstable_cache with a shared
"design-items" tag, so publishing can revalidate them and repeated renders do
not re-hit Supabase.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two gaps found while verifying the gallery against the live Supabase schema.

MediaCard already linked every card's outbound arrow at /go/<short_id>, but no
such route existed — every one of those clicks would have 404'd. /go/[code] now
resolves a code two ways: a design_outbound_links row (sponsors, tools, jobs) or
a design_items.short_id, which redirects to that item's source_url. The second
form is why feed cards do not need a pre-created link row each.

Safety: only http(s) destinations are followed, so a stored javascript: or data:
value cannot become a navigation; redirects are 302 rather than 301 because
targets change; click counting is fire-and-forget so a failed increment never
costs the user their navigation; paid placements get X-Robots-Tag noindex,
nofollow so they pass no link equity.

next/image rejects any host not in remotePatterns, so gallery media served from
Cloudflare R2 would have 400'd. The bucket host is now read from
NEXT_PUBLIC_R2_PUBLIC_HOST and filtered out when unset, rather than hardcoded.

Verified against the live schema: all three design_* tables and users.role are
present with the expected columns; a temporary three-item seed rendered through
the feed, the masonry grid and two category filters with no console errors and
no broken radii, and was then removed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eo cards, seed content

Rebuilds the /design shell to recent.design's mechanics expressed in this
site's own visual language: container-wrapper draws the dashed hairline rules
at the container edges, and the rails divide from the feed with the same
dashed border instead of whitespace. Title row carries "Last updated Xm ago";
the filter rail is sticky under the 3.5rem site header.

The filter rail is rendered as a sibling of the title row, not inside a
wrapping <header> — position:sticky can only stick within its parent's box,
so nested in a header that ends just below it, it scrolled away with the page.

Sort (Recent / Staff picks / Popular) persists in a cookie and never mints
URLs, so sorted views cannot create duplicate content; the server reads the
cookie, the client only writes it and refreshes.

MediaCard gains the video path from spec §7: muted looping inline playback,
metadata-only preload, plays at ≥50% visibility, pauses off-screen, on hidden
tabs and under prefers-reduced-motion. GIF submissions become mp4 at ingest,
so this is also the GIF path. Attribution chips are now visible at rest —
credit is part of the card, not a hover reward.

Seed content: 15 real product screens curated via the Mobbin MCP (dashboards,
iOS onboarding, landing heroes) with original editorial descriptions and
source links, plus a self-rendered 6s gradient-motion mp4 exercising the video
card. Media is rehosted in the public Supabase Storage bucket `design-media` —
Mobbin's CDN URLs are signed and expire, so hotlinking them would have left
blank cards within days. The Supabase host joins remotePatterns (derived from
NEXT_PUBLIC_SUPABASE_URL); media can move to R2 later without schema changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spectrum-ui Ready Ready Preview Aug 3, 2026 5:05pm

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

24 issues found across 21 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="components/seo/adsense-script.tsx">

<violation number="1" location="components/seo/adsense-script.tsx:18">
P3: Non-gallery paths such as `/designer` or a future `/design-system` route will also lose AdSense because this prefix check has no route-segment boundary. Match `/design` exactly or require the following slash.</violation>
</file>

<file name="components/design/section-header.tsx">

<violation number="1" location="components/design/section-header.tsx:86">
P3: Screen-reader users cannot identify the currently selected gallery filter because the active state is only visual. Expose the selected category (and `All` on the index) with `aria-current="page"`.</violation>
</file>

<file name="lib/design/format.ts">

<violation number="1" location="lib/design/format.ts:9">
P3: Future timestamps are displayed as past: any `published_at` later than the server clock becomes `1m ago`. Handling the negative elapsed case explicitly (for example as `just now`, or with a dedicated future label) would keep the gallery metadata truthful.</violation>
</file>

<file name="components/design/design-sidebar.tsx">

<violation number="1" location="components/design/design-sidebar.tsx:34">
P2: The section rail currently sends users to 404s for every section other than the feed: `DESIGN_SECTIONS` includes `/design/websites`, `/design/og-images`, `/design/app-screenshots`, `/design/app-icons`, and `/design/showcase`, but none of those routes exists yet. Either add the section pages (and their category routes) before exposing these links, or restrict the sidebar to implemented sections until those slices land.</violation>

<violation number="2" location="components/design/design-sidebar.tsx:35">
P3: Screen-reader users cannot identify the current design section: active state is only visual and the dot is `aria-hidden`. Expose `aria-current="page"` on the active Link.</violation>
</file>

<file name="app/(design)/design/c/[category]/page.tsx">

<violation number="1" location="app/(design)/design/c/[category]/page.tsx:55">
P2: Category pages render dynamically for every request, so the intended pre-rendered/300-second ISR SEO routes are not produced. The `cookies()` sort read is a Dynamic API; move sort state to a client-side view or accept an explicit dynamic-rendering strategy rather than relying on `generateStaticParams` here.</violation>

<violation number="2" location="app/(design)/design/c/[category]/page.tsx:58">
P3: Category pages misreport an unprovisioned gallery as an empty published category. The query already provides `unavailable` for this case, so carrying that flag through and using the same setup-state messaging as the feed would make the fallback accurate.</violation>
</file>

<file name="lib/design/types.ts">

<violation number="1" location="lib/design/types.ts:64">
P3: Consumers following this exported cursor contract will send the last item's UUID, which parses to `0` and repeats the first page. Describe it as an offset (or implement an ID-based cursor) so callers can paginate correctly.</violation>
</file>

<file name="lib/design/queries.ts">

<violation number="1" location="lib/design/queries.ts:130">
P2: Pagination can duplicate or skip gallery items when the feed changes between requests because the cursor is only an offset into a mutable ordering. A keyset cursor containing the ordering value and a stable ID would keep subsequent pages anchored to the same feed position.</violation>

<violation number="2" location="lib/design/queries.ts:151">
P2: Items sharing `published_at` have no stable order, so paginating a timestamp tie can duplicate or omit cards between requests. Add a unique secondary order key (for example `id`) for this and the other sortable feeds.</violation>

<violation number="3" location="lib/design/queries.ts:176">
P2: A transient feed failure is presented as “Gallery not provisioned yet,” which sends operators toward the wrong remediation and hides the distinction between an un-applied schema and an outage. The catch path could return `unavailable: false` (or classify the exception with `isNotProvisioned`) while retaining the existing logging.</violation>
</file>

<file name="app/layout.tsx">

<violation number="1" location="app/layout.tsx:160">
P2: AdSense remains loaded when users navigate to `/design` from another page in the SPA. Because this component lives in the persistent root layout and `next/script` injects the script as a side effect, returning `null` after `usePathname()` changes does not unload it; placing the script in non-design route-group layouts (or otherwise managing the script lifecycle) would preserve the suppression on client-side navigation.</violation>
</file>

<file name="design-schema.sql">

<violation number="1" location="design-schema.sql:31">
P2: A published item can enter the feed without a publication date, leaving it unsorted at the end and excluded from “Last updated.” Require a timestamp whenever status is published.</violation>

<violation number="2" location="design-schema.sql:56">
P3: `updated_at` is set only on insert and becomes stale after every item edit. Add an update trigger or omit the field until it is maintained.</violation>

<violation number="3" location="design-schema.sql:73">
P3: The schema creates duplicate indexes for three unique keys, increasing storage and write cost for no query benefit. Removing the explicit single-/same-key indexes and relying on the unique constraint indexes would keep the schema leaner.</violation>

<violation number="4" location="design-schema.sql:83">
P2: Invalid media dimensions can make a card’s computed aspect ratio invalid and prevent the feed from reserving the intended media box. The schema would protect this invariant by requiring both width and height to be positive, not merely non-null.</violation>
</file>

<file name="app/(design)/design/page.tsx">

<violation number="1" location="app/(design)/design/page.tsx:80">
P2: Published gallery cards are currently clickable links to `/design/i/[slug]`, but that detail route is not part of this slice, so every card click produces a 404. The gallery could defer card linking until the detail page exists, link to an available destination, or ship the detail route together with the feed.</violation>
</file>

<file name="app/go/[code]/route.ts">

<violation number="1" location="app/go/[code]/route.ts:46">
P2: Concurrent redirects can be undercounted because the counter is read and incremented in application code before being written. Two requests observing the same value overwrite each other with the same next value, and the same bug exists for `design_items.outbound_clicks`; an atomic database-side increment (for example, an RPC or SQL `counter = counter + 1`) would preserve every click.</violation>
</file>

<file name="app/(design)/design/layout.tsx">

<violation number="1" location="app/(design)/design/layout.tsx:51">
P2: The design route introduces a nested `<main>` landmark because the root layout already supplies the page-level `<main>`. Using a non-landmark wrapper here, such as a `div`, keeps the design column layout without exposing invalid nested main landmarks to assistive technology.</violation>
</file>

<file name="components/design/card-video.tsx">

<violation number="1" location="components/design/card-video.tsx:42">
P2: Videos keep playing whenever any part of the card intersects the viewport, rather than only while at least 50% is visible. The observer is configured with a 0.5 threshold, but `isIntersecting` remains true for ratios such as 0.1, so scrolling a card mostly off-screen does not pause it and can leave multiple off-screen videos consuming CPU and bandwidth. Using the reported `intersectionRatio` for the state check keeps the implementation aligned with the documented 50% behavior.</violation>
</file>

<file name="content/design-taxonomy.ts">

<violation number="1" location="content/design-taxonomy.ts:115">
P2: The category pages do not meet the declared 150–250-word intro requirement: the newly added section/category copy is generally only 39–67 words. That leaves the intended SEO body substantially underlength, so expanding each intro to the planned range would make the taxonomy match the gallery’s content contract.</violation>
</file>

<file name="components/design/feed-grid.tsx">

<violation number="1" location="components/design/feed-grid.tsx:22">
P2: On narrow screens this default marks the first eight cards as priority images even though the feed has only one column (or three icon columns), so several rows below the fold are eagerly prioritized. Each of those cards passes the flag to `next/image` as `priority`, creating avoidable high-priority image requests and competing with the actual LCP image. The fixed count should be reduced or made responsive so only the cards that can be above the fold are prioritized.</violation>
</file>

<file name="components/design/media-card.tsx">

<violation number="1" location="components/design/media-card.tsx:11">
P2: The gallery's image `sizes` substantially overstates the rendered card width on desktop, causing `next/image` to select unnecessarily large source variants. The design layout removes a 220px left rail and, at xl, a 256px right rail before the feed is divided into four or five columns; the icons layout divides that remaining width into eight columns, yet this constant tells the browser each image is 20–25vw wide. Making `sizes` aware of the active layout and rail widths would avoid the resulting bandwidth and image-decoding overhead.</violation>
</file>

<file name="components/design/sort-select.tsx">

<violation number="1" location="components/design/sort-select.tsx:28">
P3: Keyboard users get no visible focus indication for the sort control because the focused native select is fully transparent and its parent has no focus styling. Adding a visible `focus-within` ring or equivalent to the label would preserve the custom appearance while keeping the control navigable.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

const category = findDesignCategory("feed", slug);
if (!category) notFound();

const rawSort = (await cookies()).get("design_sort")?.value;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Category pages render dynamically for every request, so the intended pre-rendered/300-second ISR SEO routes are not produced. The cookies() sort read is a Dynamic API; move sort state to a client-side view or accept an explicit dynamic-rendering strategy rather than relying on generateStaticParams here.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/(design)/design/c/[category]/page.tsx, line 55:

<comment>Category pages render dynamically for every request, so the intended pre-rendered/300-second ISR SEO routes are not produced. The `cookies()` sort read is a Dynamic API; move sort state to a client-side view or accept an explicit dynamic-rendering strategy rather than relying on `generateStaticParams` here.</comment>

<file context>
@@ -0,0 +1,127 @@
+  const category = findDesignCategory("feed", slug);
+  if (!category) notFound();
+
+  const rawSort = (await cookies()).get("design_sort")?.value;
+  const sort: DesignSort =
+    rawSort === "popular" || rawSort === "staff" ? rawSort : "recent";
</file context>

Comment thread lib/design/queries.ts
.order("staff_pick", { ascending: false })
.order("published_at", { ascending: false, nullsFirst: false });
} else {
q = q.order("published_at", { ascending: false, nullsFirst: false });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Items sharing published_at have no stable order, so paginating a timestamp tie can duplicate or omit cards between requests. Add a unique secondary order key (for example id) for this and the other sortable feeds.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/design/queries.ts, line 151:

<comment>Items sharing `published_at` have no stable order, so paginating a timestamp tie can duplicate or omit cards between requests. Add a unique secondary order key (for example `id`) for this and the other sortable feeds.</comment>

<file context>
@@ -0,0 +1,269 @@
+        .order("staff_pick", { ascending: false })
+        .order("published_at", { ascending: false, nullsFirst: false });
+    } else {
+      q = q.order("published_at", { ascending: false, nullsFirst: false });
+    }
+
</file context>

return (
<li key={section.slug}>
<Link
href={section.path}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: The section rail currently sends users to 404s for every section other than the feed: DESIGN_SECTIONS includes /design/websites, /design/og-images, /design/app-screenshots, /design/app-icons, and /design/showcase, but none of those routes exists yet. Either add the section pages (and their category routes) before exposing these links, or restrict the sidebar to implemented sections until those slices land.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At components/design/design-sidebar.tsx, line 34:

<comment>The section rail currently sends users to 404s for every section other than the feed: `DESIGN_SECTIONS` includes `/design/websites`, `/design/og-images`, `/design/app-screenshots`, `/design/app-icons`, and `/design/showcase`, but none of those routes exists yet. Either add the section pages (and their category routes) before exposing these links, or restrict the sidebar to implemented sections until those slices land.</comment>

<file context>
@@ -0,0 +1,63 @@
+          return (
+            <li key={section.slug}>
+              <Link
+                href={section.path}
+                className={cn(
+                  "flex items-center justify-between py-1 text-[13px] transition-colors",
</file context>

Comment thread app/layout.tsx
strategy="lazyOnload"
crossOrigin="anonymous"
/>
<AdSenseScript />

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: AdSense remains loaded when users navigate to /design from another page in the SPA. Because this component lives in the persistent root layout and next/script injects the script as a side effect, returning null after usePathname() changes does not unload it; placing the script in non-design route-group layouts (or otherwise managing the script lifecycle) would preserve the suppression on client-side navigation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/layout.tsx, line 160:

<comment>AdSense remains loaded when users navigate to `/design` from another page in the SPA. Because this component lives in the persistent root layout and `next/script` injects the script as a side effect, returning `null` after `usePathname()` changes does not unload it; placing the script in non-design route-group layouts (or otherwise managing the script lifecycle) would preserve the suppression on client-side navigation.</comment>

<file context>
@@ -156,12 +157,7 @@ gtag('config', 'G-K7ZP6JB4MG');
-          strategy="lazyOnload"
-          crossOrigin="anonymous"
-        />
+        <AdSenseScript />
       </head>
       <body className="font-regular" suppressHydrationWarning>
</file context>

Comment thread design-schema.sql
status TEXT NOT NULL DEFAULT 'draft'
CHECK (status IN ('draft','published','hidden')),
staff_pick BOOLEAN NOT NULL DEFAULT FALSE,
published_at TIMESTAMPTZ,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: A published item can enter the feed without a publication date, leaving it unsorted at the end and excluded from “Last updated.” Require a timestamp whenever status is published.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At design-schema.sql, line 31:

<comment>A published item can enter the feed without a publication date, leaving it unsorted at the end and excluded from “Last updated.” Require a timestamp whenever status is published.</comment>

<file context>
@@ -0,0 +1,136 @@
+  status          TEXT NOT NULL DEFAULT 'draft'
+                    CHECK (status IN ('draft','published','hidden')),
+  staff_pick      BOOLEAN NOT NULL DEFAULT FALSE,
+  published_at    TIMESTAMPTZ,
+
+  -- Attribution. Never publish without it.
</file context>

Comment thread lib/design/types.ts
section: DesignSectionSlug;
category?: string;
sort?: DesignSort;
/** Cursor is the previous page's last item id. */

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: Consumers following this exported cursor contract will send the last item's UUID, which parses to 0 and repeats the first page. Describe it as an offset (or implement an ID-based cursor) so callers can paginate correctly.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/design/types.ts, line 64:

<comment>Consumers following this exported cursor contract will send the last item's UUID, which parses to `0` and repeats the first page. Describe it as an offset (or implement an ID-based cursor) so callers can paginate correctly.</comment>

<file context>
@@ -0,0 +1,75 @@
+  section: DesignSectionSlug;
+  category?: string;
+  sort?: DesignSort;
+  /** Cursor is the previous page's last item id. */
+  cursor?: string;
+  limit?: number;
</file context>

Comment thread design-schema.sql
score DOUBLE PRECISION NOT NULL DEFAULT 0,

created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: updated_at is set only on insert and becomes stale after every item edit. Add an update trigger or omit the field until it is maintained.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At design-schema.sql, line 56:

<comment>`updated_at` is set only on insert and becomes stale after every item edit. Add an update trigger or omit the field until it is maintained.</comment>

<file context>
@@ -0,0 +1,136 @@
+  score           DOUBLE PRECISION NOT NULL DEFAULT 0,
+
+  created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+  updated_at      TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
</file context>

const rawSort = (await cookies()).get("design_sort")?.value;
const sort: DesignSort =
rawSort === "popular" || rawSort === "staff" ? rawSort : "recent";
const { items } = await getFeed({ section: "feed", category: slug, sort });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: Category pages misreport an unprovisioned gallery as an empty published category. The query already provides unavailable for this case, so carrying that flag through and using the same setup-state messaging as the feed would make the fallback accurate.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/(design)/design/c/[category]/page.tsx, line 58:

<comment>Category pages misreport an unprovisioned gallery as an empty published category. The query already provides `unavailable` for this case, so carrying that flag through and using the same setup-state messaging as the feed would make the fallback accurate.</comment>

<file context>
@@ -0,0 +1,127 @@
+  const rawSort = (await cookies()).get("design_sort")?.value;
+  const sort: DesignSort =
+    rawSort === "popular" || rawSort === "staff" ? rawSort : "recent";
+  const { items } = await getFeed({ section: "feed", category: slug, sort });
+
+  return (
</file context>

Comment thread design-schema.sql
CREATE INDEX IF NOT EXISTS idx_design_items_categories
ON design_items USING GIN (categories);

CREATE INDEX IF NOT EXISTS idx_design_items_slug ON design_items (slug);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The schema creates duplicate indexes for three unique keys, increasing storage and write cost for no query benefit. Removing the explicit single-/same-key indexes and relying on the unique constraint indexes would keep the schema leaner.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At design-schema.sql, line 73:

<comment>The schema creates duplicate indexes for three unique keys, increasing storage and write cost for no query benefit. Removing the explicit single-/same-key indexes and relying on the unique constraint indexes would keep the schema leaner.</comment>

<file context>
@@ -0,0 +1,136 @@
+CREATE INDEX IF NOT EXISTS idx_design_items_categories
+  ON design_items USING GIN (categories);
+
+CREATE INDEX IF NOT EXISTS idx_design_items_slug ON design_items (slug);
+
+CREATE TABLE IF NOT EXISTS design_media (
</file context>

return (
<label
className={cn(
"relative flex shrink-0 cursor-pointer items-center gap-1 text-[12px] text-muted-foreground transition-opacity hover:text-foreground",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: Keyboard users get no visible focus indication for the sort control because the focused native select is fully transparent and its parent has no focus styling. Adding a visible focus-within ring or equivalent to the label would preserve the custom appearance while keeping the control navigable.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At components/design/sort-select.tsx, line 28:

<comment>Keyboard users get no visible focus indication for the sort control because the focused native select is fully transparent and its parent has no focus styling. Adding a visible `focus-within` ring or equivalent to the label would preserve the custom appearance while keeping the control navigable.</comment>

<file context>
@@ -0,0 +1,52 @@
+  return (
+    <label
+      className={cn(
+        "relative flex shrink-0 cursor-pointer items-center gap-1 text-[12px] text-muted-foreground transition-opacity hover:text-foreground",
+        pending && "opacity-50",
+      )}
</file context>

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.

2 participants