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:
11 files changed
+65
-64
No files matched your search
@@ -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} />}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user