From 26723c0fd8e50619ac20099a0fdb58421b7a06ac Mon Sep 17 00:00:00 2001 From: Justin Levine <20596508+jal-co@users.noreply.github.com> Date: Wed, 15 Jul 2026 21:41:22 -0400 Subject: [PATCH] feat(catalog): allow editing tool listings Signed-off-by: Justin Levine <20596508+jal-co@users.noreply.github.com> --- apps/web/content/docs/publishing.mdx | 7 + .../tool-listing-editor.stories.tsx | 18 + .../src/components/tool-listing-editor.tsx | 32 +- apps/web/src/data/catalog.ts | 2 +- apps/web/src/lib/catalog-db.ts | 17 +- apps/web/src/lib/review.ts | 34 +- apps/web/src/lib/tool-listings.ts | 185 +- apps/web/src/lib/tool-review.ts | 122 + apps/web/src/routeTree.gen.ts | 21 + apps/web/src/routes/dashboard.components.tsx | 5 +- .../src/routes/dashboard.review-tool.$id.tsx | 6 +- .../src/routes/dashboard.tools.edit.$name.tsx | 37 + .../test/tool-edit-review.integration.test.ts | 110 + .../db/migrations/0035_gifted_lifeguard.sql | 13 + .../db/migrations/meta/0035_snapshot.json | 4425 +++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/src/schema.ts | 37 + 17 files changed, 5009 insertions(+), 69 deletions(-) create mode 100644 apps/web/src/lib/tool-review.ts create mode 100644 apps/web/src/routes/dashboard.tools.edit.$name.tsx create mode 100644 apps/web/test/tool-edit-review.integration.test.ts create mode 100644 packages/db/migrations/0035_gifted_lifeguard.sql create mode 100644 packages/db/migrations/meta/0035_snapshot.json diff --git a/apps/web/content/docs/publishing.mdx b/apps/web/content/docs/publishing.mdx index 6908e43..6e2a1c6 100644 --- a/apps/web/content/docs/publishing.mdx +++ b/apps/web/content/docs/publishing.mdx @@ -82,6 +82,13 @@ the external site; it does not enable Modulora checkout or buyer entitlements. Tool listings also appear under the top-level **Tools** catalog category while retaining their more specific subcategory. +To edit a tool, open **Dashboard → Listings** and choose **Edit listing**. The +slug remains fixed, while the verified URL/domain, title, description, +subcategory, external pricing label, and site-thumbnail carousel can change. +Every saved edit returns to usefulness review. The last approved version stays +public until a curator approves the complete edit; requested changes and +rejections remain visible to the creator for revision and resubmission. + ## Sell externally (optional) Direct checkout through Modulora isn't live during alpha. You can link a diff --git a/apps/web/src/components/tool-listing-editor.stories.tsx b/apps/web/src/components/tool-listing-editor.stories.tsx index 4e06fa8..1b3ecec 100644 --- a/apps/web/src/components/tool-listing-editor.stories.tsx +++ b/apps/web/src/components/tool-listing-editor.stories.tsx @@ -32,3 +32,21 @@ export default meta; type Story = StoryObj; export const Default: Story = {}; + +export const Editing: Story = { + args: { + mode: "edit", + initial: { + name: "shieldcn-beautiful-readme-badges", + title: "Shieldcn — Beautiful README Badges", + description: "Beautiful GitHub README badges and charts styled as shadcn/ui, plus a visual README builder.", + category: "utilities", + siteUrl: "https://shieldcn.dev/", + showcaseImageUrls: [previewImage], + pricing: "free", + preview: { canonicalUrl: "https://shieldcn.dev/", title: "Shieldcn", description: "Beautiful README badges.", imageUrl: previewImage }, + editStatus: "changes-requested", + reviewReason: "Update the first screenshot so it matches the current landing page.", + }, + }, +}; diff --git a/apps/web/src/components/tool-listing-editor.tsx b/apps/web/src/components/tool-listing-editor.tsx index 078a068..10e0ea9 100644 --- a/apps/web/src/components/tool-listing-editor.tsx +++ b/apps/web/src/components/tool-listing-editor.tsx @@ -11,7 +11,7 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { CATEGORIES } from "@/lib/taxonomy"; -import type { ToolListingInput } from "@/lib/tool-listings"; +import type { EditableToolListing, ToolListingInput } from "@/lib/tool-listings"; export interface ToolListingPreview { canonicalUrl: string; @@ -31,6 +31,8 @@ function normalizeHttpsUrl(value: string): string | null { } export interface ToolListingEditorProps { + mode?: "create" | "edit"; + initial?: EditableToolListing; onInspect: (siteUrl: string) => Promise<{ ok: boolean; error?: string; metadata?: ToolListingPreview; verificationDomain?: string }>; onSubmit: (input: ToolListingInput) => Promise<{ ok: boolean; error?: string }>; onSubmitted: () => Promise | void; @@ -39,15 +41,16 @@ export interface ToolListingEditorProps { onDiscoverDomainConnect: (domain: string) => Promise<{ supported: boolean; provider?: string; applyUrl?: string }>; } -export function ToolListingEditor({ onInspect, onSubmit, onSubmitted, onCreateDomain, onVerifyDomain, onDiscoverDomainConnect }: ToolListingEditorProps) { - const [siteUrl, setSiteUrl] = useState(""); - const [name, setName] = useState(""); - const [title, setTitle] = useState(""); - const [description, setDescription] = useState(""); - const [category, setCategory] = useState(CATEGORIES[0].id); - const [pricing, setPricing] = useState("free"); - const [showcaseImageUrls, setShowcaseImageUrls] = useState([]); - const [preview, setPreview] = useState(null); +export function ToolListingEditor({ mode = "create", initial, onInspect, onSubmit, onSubmitted, onCreateDomain, onVerifyDomain, onDiscoverDomainConnect }: ToolListingEditorProps) { + const editing = mode === "edit"; + const [siteUrl, setSiteUrl] = useState(initial?.siteUrl ?? ""); + const [name, setName] = useState(initial?.name ?? ""); + const [title, setTitle] = useState(initial?.title ?? ""); + const [description, setDescription] = useState(initial?.description ?? ""); + const [category, setCategory] = useState(initial?.category ?? CATEGORIES[0].id); + const [pricing, setPricing] = useState(initial?.pricing ?? "free"); + const [showcaseImageUrls, setShowcaseImageUrls] = useState(initial?.showcaseImageUrls ?? []); + const [preview, setPreview] = useState(initial?.preview ?? null); const [inspecting, setInspecting] = useState(false); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(""); @@ -158,9 +161,10 @@ export function ToolListingEditor({ onInspect, onSubmit, onSubmitted, onCreateDo
-

Tool or site details

-

Only owner-authorized sites on a domain verified in Settings may be submitted.

+

{editing ? "Edit tool or site" : "Tool or site details"}

+

Only owner-authorized sites on a domain verified in Settings may be submitted. {editing ? "Every saved edit returns to curator review while the approved version stays public." : null}

+ {initial?.editStatus ?
Edit {initial.editStatus === "pending" ? "in review" : initial.editStatus.replace("-", " ")}.{initial.reviewReason ? ` ${initial.reviewReason}` : " You can revise and resubmit this draft."}
: null}
@@ -169,7 +173,7 @@ export function ToolListingEditor({ onInspect, onSubmit, onSubmitted, onCreateDo
setTitle(event.target.value)} maxLength={120} required />
-
setName(event.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ""))} maxLength={64} required />

Used in the Modulora detail URL.

+
setName(event.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ""))} maxLength={64} required />

{editing ? "The public listing URL stays fixed." : "Used in the Modulora detail URL."}