diff --git a/.env.example b/.env.example index b998483df..d61111ebf 100644 --- a/.env.example +++ b/.env.example @@ -61,9 +61,9 @@ TURBO_TELEMETRY_DISABLED=1 # Enable mock integration UNSAFE_ENABLE_MOCK_INTEGRATION=true -# Community marketplace (PocketBase) base URL used by the in-app "Install from marketplace" buttons +# Community workshop (PocketBase) base URL used by the in-app "Install from Workshop" buttons # In production this defaults to https://store.homarr.dev — override for local dev: NEXT_PUBLIC_WIDGET_STORE_URL=http://localhost:8090 -# Docs site marketplace URL used for "Open Marketplace" links -# In production this defaults to https://homarr.dev/marketplace -# NEXT_PUBLIC_MARKETPLACE_URL=http://localhost:3003/marketplace +# Docs site workshop URL used for "Open Workshop" links +# In production this defaults to https://homarr.dev/workshop +# NEXT_PUBLIC_WORKSHOP_URL=http://localhost:3003/workshop diff --git a/apps/docs/docs/management/store/index.mdx b/apps/docs/docs/management/store/index.mdx index bd8aa7eae..876736b52 100644 --- a/apps/docs/docs/management/store/index.mdx +++ b/apps/docs/docs/management/store/index.mdx @@ -1,19 +1,19 @@ --- tags: - - Store + - Workshop - Custom Widgets - Custom CSS --- -# Community Store +# Community Workshop -The [Community Store](pathname:///store) is where Homarr users share and discover +The [Community Workshop](pathname:///workshop) is where Homarr users share and discover **custom CSS themes** and **custom widgets**. You can browse submissions, upvote the ones you like, report abuse, and install entries directly into your own Homarr instance. :::info -The store is community-contributed content. Always review CSS and widget JSON before +The workshop is community-contributed content. Always review CSS and widget JSON before installing it on your dashboard. ::: @@ -37,19 +37,19 @@ Submissions are published immediately. Reported entries are reviewed and may be ## Installing into Homarr -You can install store entries without leaving Homarr: +You can install workshop entries without leaving Homarr: -- **Custom widgets** — go to **Manage → Custom Widgets**, click **Install from store**, pick an +- **Custom widgets** — go to **Manage → Custom Widgets**, click **Install from Workshop**, pick an entry, and it is imported (secrets such as API keys must be re-entered afterwards). -- **Custom CSS** — open a board's **Settings → Custom CSS**, click **Install from store**, and +- **Custom CSS** — open a board's **Settings → Custom CSS**, click **Install from Workshop**, and pick a theme to load it into the editor before saving. Alternatively, use **Copy** (or **Download** for widgets) on any submission and import it manually. -## Self-hosting the store backend +## Self-hosting the workshop backend -The store is powered by a [PocketBase](https://pocketbase.io) instance (GitHub OAuth, database, +The workshop is powered by a [PocketBase](https://pocketbase.io) instance (GitHub OAuth, database, file uploads, and an admin moderation dashboard). The deployment lives in `apps/store/` with a `docker-compose.yml`, version-controlled collection migrations, and vote-counter hooks. See the app's `README.md` for GitHub OAuth setup and the `STORE_URL` / `NEXT_PUBLIC_WIDGET_STORE_URL` diff --git a/apps/docs/docusaurus.config.ts b/apps/docs/docusaurus.config.ts index 3f8373f0d..ec5cb83bd 100644 --- a/apps/docs/docusaurus.config.ts +++ b/apps/docs/docusaurus.config.ts @@ -110,9 +110,9 @@ const config: Config = { to: "/blog", }, { - label: "Marketplace", + label: "Workshop", position: "left", - to: "/marketplace", + to: "/workshop", }, { label: "About us", @@ -304,12 +304,12 @@ const config: Config = { }, }; }, - function marketplaceDetailRoutePlugin() { + function workshopDetailRoutePlugin() { return { - name: "marketplace-detail-route", + name: "workshop-detail-route", async contentLoaded({ actions }) { actions.addRoute({ - path: "/marketplace/:id", + path: "/workshop/:id", component: "@site/src/components/store/DetailPage", exact: true, }); diff --git a/apps/docs/src/components/store/DetailPage.tsx b/apps/docs/src/components/store/DetailPage.tsx index d3afd25df..e6cb65225 100644 --- a/apps/docs/src/components/store/DetailPage.tsx +++ b/apps/docs/src/components/store/DetailPage.tsx @@ -32,7 +32,7 @@ import { Input } from "@/components/ui/input"; import { cn, errorMessage } from "@/lib/utils"; const typeLabels: Record = { css: "CSS", widget: "Widget" }; -const typeBadgeVariants: Record = { css: "secondary", widget: "default" }; +const typeDotColors: Record = { css: "bg-blue-500", widget: "bg-yellow-500" }; const contentLanguages: Record = { 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 }) => {

Submission not found

This listing may have been removed or the link is invalid.

- - Back to Marketplace + + Back to Workshop
); @@ -504,8 +504,8 @@ const MarketplaceDetail = ({ storeUrl }: { storeUrl: string }) => { return (
- - Back to Marketplace + + Back to Workshop {error && ( @@ -524,7 +524,10 @@ const MarketplaceDetail = ({ storeUrl }: { storeUrl: string }) => {

{submission.title}

- {typeLabels[submission.type]} + + + {typeLabels[submission.type]} +

{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 ( - +

}> {() => } diff --git a/apps/docs/src/components/store/StoreApp.tsx b/apps/docs/src/components/store/StoreApp.tsx index d3542547d..bde61fdc2 100644 --- a/apps/docs/src/components/store/StoreApp.tsx +++ b/apps/docs/src/components/store/StoreApp.tsx @@ -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> = { - css: IconBrandCss3, - widget: IconPuzzle, -}; -const typeBadgeVariants: Record = { css: "secondary", widget: "default" }; +const typeDotColors: Record = { css: "bg-blue-500", widget: "bg-yellow-500" }; +const typeLabels: Record = { 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 }) => {
-

Marketplace

+

Workshop

Community custom widgets and CSS themes for Homarr.

@@ -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.dot && } {opt.label} ))} @@ -290,21 +284,20 @@ const SubmissionCard = ({ if (reason && reason.trim().length >= 3) void onReport(submission.id, reason.trim()); }; - const TypeIcon = typeIcons[submission.type]; - return ( - + {screenshotUrls.length > 0 && }
- + {submission.title} - - + + + {typeLabels[submission.type]}
diff --git a/apps/docs/src/components/store/SubmitForm.tsx b/apps/docs/src/components/store/SubmitForm.tsx index 79a72eb1c..7c7c610d7 100644 --- a/apps/docs/src/components/store/SubmitForm.tsx +++ b/apps/docs/src/components/store/SubmitForm.tsx @@ -211,7 +211,7 @@ export const SubmitForm = ({ onClose, onSubmit }: Props) => { Share with the community - Submit a custom CSS theme or widget to the marketplace. + Submit a custom CSS theme or widget to the Workshop.
diff --git a/apps/docs/src/components/store/useStore.ts b/apps/docs/src/components/store/useStore.ts index 3c30302cb..7153707bf 100644 --- a/apps/docs/src/components/store/useStore.ts +++ b/apps/docs/src/components/store/useStore.ts @@ -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); } diff --git a/apps/docs/src/pages/marketplace/index.tsx b/apps/docs/src/pages/workshop/index.tsx similarity index 88% rename from apps/docs/src/pages/marketplace/index.tsx rename to apps/docs/src/pages/workshop/index.tsx index c0b7dee2f..5c9ea695a 100644 --- a/apps/docs/src/pages/marketplace/index.tsx +++ b/apps/docs/src/pages/workshop/index.tsx @@ -8,7 +8,7 @@ export default function StorePage() { const storeUrl = (siteConfig.customFields?.storeUrl as string | undefined) ?? "http://localhost:8090"; return ( - +
}> {() => { diff --git a/apps/nextjs/src/components/store-install/install-from-store-buttons.tsx b/apps/nextjs/src/components/store-install/install-from-store-buttons.tsx index ce2979be7..88c810b5a 100644 --- a/apps/nextjs/src/components/store-install/install-from-store-buttons.tsx +++ b/apps/nextjs/src/components/store-install/install-from-store-buttons.tsx @@ -33,14 +33,14 @@ export const InstallWidgetFromStoreButton = () => { const utils = clientApi.useUtils(); const importMutation = clientApi.customWidget.import.useMutation({ onSuccess: () => { - showSuccessNotification({ title: "Marketplace", message: "Custom widget installed" }); + showSuccessNotification({ title: "Workshop", message: "Custom widget installed" }); void utils.customWidget.all.invalidate(); void revalidatePathActionAsync("/manage/custom-widgets"); setPendingId(null); close(); }, onError: (err) => { - showErrorNotification({ title: "Marketplace", message: err.message || "Failed to install the custom widget" }); + showErrorNotification({ title: "Workshop", message: err.message || "Failed to install the custom widget" }); setPendingId(null); }, }); @@ -48,7 +48,7 @@ export const InstallWidgetFromStoreButton = () => { const handleSelect = (submission: { id: string; content: string }) => { const parsed = parseWidgetContent(submission.content); if (!parsed.ok) { - showErrorNotification({ title: "Marketplace", message: parsed.message }); + showErrorNotification({ title: "Workshop", message: parsed.message }); return; } setPendingId(submission.id); @@ -58,7 +58,7 @@ export const InstallWidgetFromStoreButton = () => { return ( <> } onClick={open}> - Install from marketplace + Install from Workshop - Install from marketplace + Install from Workshop - Open Marketplace + Open Workshop } @@ -81,7 +81,7 @@ export const StoreBrowserModal = ({ type, opened, onClose, onSelect, actionLabel )} - {error && Failed to load the marketplace: {errorText}} + {error && Failed to load the workshop: {errorText}} {data && data.items.length === 0 && ( @@ -174,6 +174,11 @@ const SubmissionCard = ({ + {submission.title} @@ -201,10 +206,10 @@ const SubmissionCard = ({ variant="light" size="sm" component="a" - href={`${MARKETPLACE_URL}/${submission.id}/`} + href={`${WORKSHOP_URL}/${submission.id}/`} target="_blank" rel="noopener noreferrer" - aria-label="View in marketplace" + aria-label="View in workshop" > diff --git a/package.json b/package.json index d8d789583..37bf34fd5 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "db:studio": "pnpm -F db studio", "dev": "pnpm -F nextjs dev", "dev:docs": "pnpm -F @homarr/docs dev", - "dev:marketplace": "docker compose -f ./apps/store/docker-compose.yml up", + "dev:workshop": "docker compose -f ./apps/store/docker-compose.yml up", "docker:dev": "docker compose -f ./development/development.docker-compose.yml up", "format": "turbo format --continue -- --check", "format:fix": "turbo format --continue",