refactor: rename marketplace to workshop, add colored type dots

Rename all user-facing "Marketplace" references to "Workshop" including
the route (/workshop), navbar label, page titles, button labels, env vars,
docs, and error messages. The PocketBase view collection stays `marketplace`
as an internal API detail.

Replace icon-based type badges with colored dots (blue = CSS, yellow = widget)
on both the filter bar and submission cards for clearer visual distinction.
This commit is contained in:
Thomas Camlong committed 2026-06-24 15:24:48 +02:00
1 parent 1fb7e16d42
commit de8d20a030
11 files changed
+65 -64

No files matched your search

+12 -9
View File
@@ -32,7 +32,7 @@ import { Input } from "@/components/ui/input";
import { cn, errorMessage } from "@/lib/utils";
const typeLabels: Record<SubmissionType, string> = { css: "CSS", widget: "Widget" };
const typeBadgeVariants: Record<SubmissionType, "secondary" | "default"> = { css: "secondary", widget: "default" };
const typeDotColors: Record<SubmissionType, string> = { css: "bg-blue-500", widget: "bg-yellow-500" };
const contentLanguages: Record<SubmissionType, string> = { css: "css", widget: "json" };
const prismThemes = { light: themes.github, dark: themes.dracula } as const;
const copyState = [
@@ -62,7 +62,7 @@ const scoreSign = (score: number): keyof typeof scoreClassBySign => {
const parseSubmissionId = (pathname: string) => {
const segments = pathname.split("/").filter(Boolean);
const last = segments[segments.length - 1];
if (!last || last === "marketplace") return null;
if (!last || last === "workshop") return null;
return last;
};
@@ -465,7 +465,7 @@ const MarketplaceDetail = ({ storeUrl }: { storeUrl: string }) => {
if (!submission) return;
try {
await pb.collection("submissions").delete(submission.id);
window.location.href = "/marketplace/";
window.location.href = "/workshop/";
} catch (caught) {
setError(errorMessage(caught, "Failed to delete submission"));
}
@@ -492,8 +492,8 @@ const MarketplaceDetail = ({ storeUrl }: { storeUrl: string }) => {
<div className="mx-auto max-w-4xl px-4 py-16 text-center">
<p className="text-lg font-medium">Submission not found</p>
<p className="mt-1 text-sm text-muted-foreground">This listing may have been removed or the link is invalid.</p>
<Link to="/marketplace" className="mt-6 inline-flex items-center gap-1.5 text-sm text-primary hover:underline">
<IconArrowLeft size={14} /> Back to Marketplace
<Link to="/workshop" className="mt-6 inline-flex items-center gap-1.5 text-sm text-primary hover:underline">
<IconArrowLeft size={14} /> Back to Workshop
</Link>
</div>
);
@@ -504,8 +504,8 @@ const MarketplaceDetail = ({ storeUrl }: { storeUrl: string }) => {
return (
<div className="mx-auto max-w-4xl px-4 pb-16 pt-8">
<Link to="/marketplace" className="mb-6 inline-flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground">
<IconArrowLeft size={14} /> Back to Marketplace
<Link to="/workshop" className="mb-6 inline-flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground">
<IconArrowLeft size={14} /> Back to Workshop
</Link>
{error && (
@@ -524,7 +524,10 @@ const MarketplaceDetail = ({ storeUrl }: { storeUrl: string }) => {
<div>
<div className="flex items-center gap-2">
<h1 className="text-2xl font-semibold tracking-tight">{submission.title}</h1>
<Badge variant={typeBadgeVariants[submission.type]}>{typeLabels[submission.type]}</Badge>
<Badge variant="secondary" className="gap-1.5">
<span className={cn("size-2 rounded-full", typeDotColors[submission.type])} />
{typeLabels[submission.type]}
</Badge>
</div>
<p className="mt-1 text-sm text-muted-foreground">
{submission.authorName} · v{submission.version} · {relativeTime(submission.created)}
@@ -615,7 +618,7 @@ export default function MarketplaceDetailPage() {
const storeUrl = (siteConfig.customFields?.storeUrl as string | undefined) ?? "http://localhost:8090";
return (
<Layout title="Marketplace" description="Community custom CSS and custom widgets for Homarr">
<Layout title="Workshop" description="Community custom CSS and custom widgets for Homarr">
<main className="marketplace bg-background text-foreground min-h-[80vh]">
<BrowserOnly fallback={<DetailSkeleton />}>
{() => <MarketplaceDetail storeUrl={storeUrl} />}
+14 -21
View File
@@ -2,7 +2,6 @@ import React, { useEffect, useMemo, useRef, useState } from "react";
import {
IconArrowBigDown,
IconArrowBigUp,
IconBrandCss3,
IconBrandGithub,
IconCheck,
IconChevronLeft,
@@ -13,10 +12,8 @@ import {
IconLogout,
IconPackage,
IconPlus,
IconPuzzle,
IconSearch,
IconTrash,
IconUser,
IconX,
} from "@tabler/icons-react";
@@ -40,11 +37,8 @@ import { SubmitForm } from "./SubmitForm";
import type { SortKey, TypeFilter } from "./useStore";
import { useStore } from "./useStore";
const typeIcons: Record<SubmissionType, React.ComponentType<{ size: number }>> = {
css: IconBrandCss3,
widget: IconPuzzle,
};
const typeBadgeVariants: Record<SubmissionType, "secondary" | "default"> = { css: "secondary", widget: "default" };
const typeDotColors: Record<SubmissionType, string> = { css: "bg-blue-500", widget: "bg-yellow-500" };
const typeLabels: Record<SubmissionType, string> = { css: "CSS", widget: "Widget" };
const filterActiveClass = "bg-background text-foreground shadow-sm";
const filterInactiveClass = "text-muted-foreground hover:text-foreground";
const copyState = [
@@ -56,11 +50,11 @@ const emptyState = {
filtered: { title: "No matching results", hint: "Try adjusting your filters or search." },
} as const;
const typeFilters: { value: TypeFilter; label: string; Icon?: React.ComponentType<{ size: number }> }[] = [
const typeFilters: { value: TypeFilter; label: string; dot?: string }[] = [
{ value: "all", label: "All" },
{ value: "widget", label: "Widgets", Icon: IconPuzzle },
{ value: "css", label: "CSS", Icon: IconBrandCss3 },
{ value: "yours", label: "Yours", Icon: IconUser },
{ value: "widget", label: "Widgets", dot: typeDotColors.widget },
{ value: "css", label: "CSS", dot: typeDotColors.css },
{ value: "yours", label: "Yours" },
];
const sortOptions: { value: SortKey; label: string }[] = [
{ value: "top", label: "Top rated" },
@@ -119,7 +113,7 @@ export const StoreApp = ({ storeUrl }: { storeUrl: string }) => {
<div className="mx-auto max-w-7xl px-4 pb-16">
<div className="flex flex-col gap-6 py-8 sm:flex-row sm:items-end sm:justify-between">
<div>
<h1 className="text-2xl font-semibold tracking-tight">Marketplace</h1>
<h1 className="text-2xl font-semibold tracking-tight">Workshop</h1>
<p className="mt-1 text-sm text-muted-foreground">Community custom widgets and CSS themes for Homarr.</p>
</div>
<div className="flex items-center gap-2">
@@ -148,11 +142,11 @@ export const StoreApp = ({ storeUrl }: { storeUrl: string }) => {
key={opt.value}
onClick={() => setTypeFilter(opt.value)}
className={cn(
"inline-flex items-center gap-1 rounded px-2.5 py-1 text-xs font-medium transition-colors",
"inline-flex items-center gap-1.5 rounded px-2.5 py-1 text-xs font-medium transition-colors",
typeFilter === opt.value ? filterActiveClass : filterInactiveClass,
)}
>
{opt.Icon && <opt.Icon size={12} />}
{opt.dot && <span className={cn("size-2 rounded-full", opt.dot)} />}
{opt.label}
</button>
))}
@@ -290,21 +284,20 @@ const SubmissionCard = ({
if (reason && reason.trim().length >= 3) void onReport(submission.id, reason.trim());
};
const TypeIcon = typeIcons[submission.type];
return (
<Card className="transition-transform duration-150 hover:scale-[1.02]">
<a href={`/marketplace/${submission.id}/`} className="block">
<a href={`/workshop/${submission.id}/`} className="block">
{screenshotUrls.length > 0 && <ScreenshotGallery urls={screenshotUrls} title={submission.title} />}
</a>
<CardHeader>
<div className="flex items-center gap-2">
<a href={`/marketplace/${submission.id}/`} className="min-w-0 truncate hover:underline">
<a href={`/workshop/${submission.id}/`} className="min-w-0 truncate hover:underline">
<CardTitle className="truncate">{submission.title}</CardTitle>
</a>
<Badge variant={typeBadgeVariants[submission.type]} className="shrink-0 px-1.5">
<TypeIcon size={12} />
<Badge variant="secondary" className="shrink-0 gap-1.5 px-2">
<span className={cn("size-2 rounded-full", typeDotColors[submission.type])} />
{typeLabels[submission.type]}
</Badge>
</div>
<CardDescription className="text-xs">
@@ -211,7 +211,7 @@ export const SubmitForm = ({ onClose, onSubmit }: Props) => {
<DialogHeader>
<DialogTitle>Share with the community</DialogTitle>
<DialogDescription>Submit a custom CSS theme or widget to the marketplace.</DialogDescription>
<DialogDescription>Submit a custom CSS theme or widget to the Workshop.</DialogDescription>
</DialogHeader>
<div className="flex items-center gap-2">
+1 -1
View File
@@ -58,7 +58,7 @@ export const useStore = (storeUrl: string) => {
);
await refreshVotes();
} catch (caught) {
setError(errorMessage(caught, "Failed to load the marketplace"));
setError(errorMessage(caught, "Failed to load the workshop"));
} finally {
setLoading(false);
}