Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/web/src/components/tool-listing-detail.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const meta = {
title: "Contrast Workbench",
description: "A focused visual tool for comparing interface colors and checking readable combinations.",
category: "Utilities",
listedAt: "2026-07-15T18:00:00.000Z",
listingKind: "tool",
site: { url: "https://example.com", domain: "example.com", ogTitle: "Contrast Workbench", ogDescription: "Compare interface colors.", ogImageUrl: null, showcaseImageUrls: [], pricing: "freemium" },
evidence: [{ type: "domain-verified", status: "passed", issuer: "modulora-platform", timestamp: new Date().toISOString() }],
Expand Down
5 changes: 3 additions & 2 deletions apps/web/src/components/tool-listing-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import { ToolImageCarousel } from "@/components/tool-image-carousel";
import { ToolListingImage } from "@/components/tool-listing-image";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import type { CatalogItem } from "@/data/catalog";
import { formatListingDate, type CatalogItem } from "@/data/catalog";

export function ToolListingDetail({ item }: { item: CatalogItem }) {
const site = item.site;
if (!site) return null;
const images = site.showcaseImageUrls.length ? site.showcaseImageUrls : site.ogImageUrl ? [site.ogImageUrl] : [];
const listedDate = formatListingDate(item.listedAt);
const domainEvidence = item.evidence.find((record) => record.type === "domain-verified" && record.status === "passed");
return (
<div className="mx-auto w-full max-w-7xl px-4 py-8 sm:px-6">
Expand All @@ -22,7 +23,7 @@ export function ToolListingDetail({ item }: { item: CatalogItem }) {
<div className="mb-2 flex flex-wrap items-center gap-2"><Badge variant="outline">Tool</Badge><Badge variant="secondary">{site.pricing === "freemium" ? "Freemium" : site.pricing === "paid" ? "Paid" : "Free"}</Badge><Badge variant="secondary">{item.category}</Badge></div>
<h1 className="text-3xl font-bold tracking-tight">{item.title}</h1>
<p className="mt-2 max-w-3xl text-sm leading-relaxed text-muted-foreground">{item.description}</p>
<p className="mt-3 text-xs text-muted-foreground">Listed by <Link to="/$username" params={{ username: item.namespace }} className="underline underline-offset-2">@{item.namespace}</Link></p>
<p className="mt-3 text-xs text-muted-foreground">Listed by <Link to="/$username" params={{ username: item.namespace }} className="underline underline-offset-2">@{item.namespace}</Link>{listedDate ? <> on <time dateTime={item.listedAt}>{listedDate}</time></> : null}</p>
</div>
<Button asChild><a href={site.url} target="_blank" rel="noreferrer"><External className="size-4" /> Visit {site.domain}</a></Button>
</div>
Expand Down
14 changes: 14 additions & 0 deletions apps/web/src/data/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ export interface CatalogItem {
title: string;
description: string;
category: string;
/** First public listing decision, or submission time when not yet reviewed. */
listedAt?: string;
listingKind?: "component" | "tool";
site?: {
url: string;
Expand Down Expand Up @@ -137,6 +139,18 @@ export function isPaidCatalogItem(item: CatalogItem): boolean {
return (item.sourceModel !== "open-source" && item.sourceModel !== "external-site") || item.marketplacePrice != null || item.inPaidCollection === true;
}

export function formatListingDate(value?: string): string | null {
if (!value) return null;
const date = new Date(value);
if (Number.isNaN(date.getTime())) return null;
return new Intl.DateTimeFormat("en-US", {
month: "short",
day: "numeric",
year: "numeric",
timeZone: "UTC",
}).format(date);
}

export const catalog: CatalogItem[] = [
{
schemaVersion: "0",
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/lib/catalog-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ function toCatalogItem(
title: component.title,
description: component.description,
category: categoryLabel(component.category),
listedAt: (component.reviewedAt ?? component.submittedAt ?? component.createdAt).toISOString(),
listingKind: component.listingKind as CatalogItem["listingKind"],
site: component.listingKind === "tool" && component.siteUrl && component.siteDomain
? {
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/routes/$username.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ function Profile() {
</span>
{profile.sections.components ? (
<span className="inline-flex items-center gap-1.5">
<Blocks className="size-3.5" /> {components.length} component{components.length === 1 ? "" : "s"}
<Blocks className="size-3.5" /> {components.length} Listing{components.length === 1 ? "" : "s"}
</span>
) : null}
{profile.websiteUrl ? (
Expand Down Expand Up @@ -257,7 +257,7 @@ function Profile() {
className="flex min-h-48 flex-col items-center justify-center gap-2 rounded-xl border border-dashed border-border/70 text-center"
>
<Blocks className="size-5 text-muted-foreground" />
<p className="text-sm text-muted-foreground">No public components yet.</p>
<p className="text-sm text-muted-foreground">No public listings yet.</p>
</motion.div>
) : (
<div className="grid grid-cols-[repeat(auto-fill,minmax(min(100%,17rem),22rem))] gap-4">
Expand Down
5 changes: 3 additions & 2 deletions apps/web/src/routes/components.$namespace.$name.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import {
import { HiCheckBadge } from "react-icons/hi2";
import { reportComponent, REPORT_REASONS } from "@/lib/report";
import { fetchCatalogDetail } from "@/lib/catalog-db";
import { isPaidCatalogItem, needsInteractionHint, type CatalogItem, type EvidenceRecord } from "../data/catalog";
import { formatListingDate, isPaidCatalogItem, needsInteractionHint, type CatalogItem, type EvidenceRecord } from "../data/catalog";
import { ComponentDetailError, ComponentDetailLoading } from "@/components/component-detail-state";
import { externalDomainDisclosure } from "@/lib/external-sales";

Expand Down Expand Up @@ -100,6 +100,7 @@ function ComponentDetail() {
}

function ComponentDetailInner({ item, files, colorVisionMode, viewerPlus }: { item: NonNullable<Awaited<ReturnType<typeof fetchCatalogDetail>>>; files: HighlightedFile[]; colorVisionMode: ColorVisionMode; viewerPlus: boolean }) {
const listedDate = formatListingDate(item.listedAt);
const [workspaceTab, setWorkspaceTab] = useState("preview");
const [installTab, setInstallTab] = useState(
item.distributionChannels?.includes("shadcn")
Expand Down Expand Up @@ -184,7 +185,7 @@ function ComponentDetailInner({ item, files, colorVisionMode, viewerPlus }: { it
) : null}
<div className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
<span>
by <Link to="/$username" params={{ username: item.namespace }} className="rounded-sm text-foreground/80 underline-offset-4 hover:text-foreground hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50">{item.namespace}</Link>
Listed by <Link to="/$username" params={{ username: item.namespace }} className="rounded-sm text-foreground/80 underline-offset-4 hover:text-foreground hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50">@{item.namespace}</Link>{listedDate ? <> on <time dateTime={item.listedAt}>{listedDate}</time></> : null}
{item.memberOf?.length ? (
<>
{" "}in <Link to="/$username" params={{ username: item.namespace }} className="text-foreground/80 hover:text-foreground">{item.memberOf[0]!.title}</Link>
Expand Down
8 changes: 7 additions & 1 deletion apps/web/test/catalog.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { catalog, findItem } from "../src/data/catalog";
import { catalog, findItem, formatListingDate } from "../src/data/catalog";

describe("catalog seed", () => {
it("contains creator-authorized items", () => {
Expand Down Expand Up @@ -30,4 +30,10 @@ describe("catalog seed", () => {
expect(findItem("northstar", "calendar")?.title).toBe("Calendar");
expect(findItem("northstar", "missing")).toBeUndefined();
});

it("formats listing dates consistently", () => {
expect(formatListingDate("2026-07-15T23:30:00.000Z")).toBe("Jul 15, 2026");
expect(formatListingDate("not-a-date")).toBeNull();
expect(formatListingDate()).toBeNull();
});
});
Loading