diff --git a/apps/nextjs/src/components/workshop/workshop-browser.tsx b/apps/nextjs/src/components/workshop/workshop-browser.tsx index 89d069b30..e364f3cf8 100644 --- a/apps/nextjs/src/components/workshop/workshop-browser.tsx +++ b/apps/nextjs/src/components/workshop/workshop-browser.tsx @@ -2,6 +2,7 @@ import { useEffect, useId, useMemo, useState } from "react"; import { + Accordion, Alert, Badge, Box, @@ -21,16 +22,21 @@ import { Textarea, TextInput, Title, + VisuallyHidden, } from "@mantine/core"; import type { ModalProps } from "@mantine/core"; import { useDebouncedValue, useDisclosure } from "@mantine/hooks"; import { IconAlertTriangle, + IconArrowBigDown, + IconArrowBigUp, IconArrowRight, IconBuildingStore, IconDownload, + IconEye, IconExternalLink, IconFlag, + IconMessageCircle, IconRefresh, IconSearch, IconThumbDown, @@ -62,7 +68,7 @@ import { workshopExportFilename, } from "@homarr/workshop/schema"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; -import { useScopedI18n } from "@homarr/translation/client"; +import { useCurrentLocale, useScopedI18n } from "@homarr/translation/client"; import { createWorkshopClient, getWorkshopWebUrl } from "./workshop-client"; @@ -95,9 +101,27 @@ interface WorkshopModalStep { close(): void; } +const detailsModalStyles = { + content: { + display: "flex", + flexDirection: "column" as const, + height: "min(85dvh, 800px)", + }, + body: { + display: "flex", + flex: 1, + flexDirection: "column" as const, + minHeight: 0, + overflow: "hidden", + padding: 0, + }, +}; + export function WorkshopBrowser({ type = "customWidget", onInstall, onUseCss, modalStack }: WorkshopBrowserProps) { const t = useScopedI18n("workshop"); + const currentLocale = useCurrentLocale(); const client = useMemo(createWorkshopClient, []); + const dateFormatter = useMemo(() => new Intl.DateTimeFormat(currentLocale, { dateStyle: "medium" }), [currentLocale]); const [user, setUser] = useState(null); const [page, setPage] = useState(1); const [sort, setSort] = useState<"top" | "newest">("top"); @@ -276,45 +300,86 @@ export function WorkshopBrowser({ type = "customWidget", onInstall, onUseCss, mo ) : list.data?.items.length ? ( {list.data.items.map((item) => ( - + {item.screenshots[0] ? ( - + {t("screenshotAlt", ) : ( - {item.type === "customCss" ? "CSS" : "JSX"} + {item.type === "customCss" ? "CSS" : "JSX"} )} - - - - {t("author", { name: item.authorName || t("communityMember") })} - - - {item.upvotes} · {item.downvotes} + + + + {item.title} + + + {t("voteSummary", { upvotes: item.upvotes, downvotes: item.downvotes })} + + + + {item.score} + + + + + {t("author", { name: item.authorName || t("communityMember") })} · v{item.revision} ·{" "} + {dateFormatter.format(new Date(item.updated))} + {item.outdated && {t("outdated")}} {item.reportCount > 0 && {t("reportCount", { count: item.reportCount })}} - - {item.title} - - + {item.description || t("noDescription")} - + + + + + {t("comments", { count: item.commentCount })} + + + + ))} @@ -334,161 +399,198 @@ export function WorkshopBrowser({ type = "customWidget", onInstall, onUseCss, mo stackId={modalStack?.details.stackId} title={type === "customCss" ? t("inspectCss") : t("install")} size={modalStack?.modalProps.size ?? "xl"} + styles={detailsModalStyles} > {detail.isPending ? ( - + + + ) : detail.isError || !detail.data ? ( - {detail.error instanceof Error ? detail.error.message : t("loadError")} + + {detail.error instanceof Error ? detail.error.message : t("loadError")} + ) : ( - - - - {detail.data.title} - - {t("author", { name: detail.data.authorName || t("communityMember") })} - - - - - {detail.data.description && {detail.data.description}} - {detail.data.outdated && ( - }> - {t("outdatedWarning")} - - )} - {detail.data.reportCount > 0 && ( - - }> - {t("reportWarning", { count: detail.data.reportCount })} - - {reportSummaries.data?.map((summary) => ( - - - {t(`reportCategory.${summary.category}`)} - - - {summary.explanation} + <> + + + + + {detail.data.title} + + {t("author", { name: detail.data.authorName || t("communityMember") })} - - ))} - {reportSummaries.data?.length === 0 && ( + + + + {detail.data.description && {detail.data.description}} + {detail.data.outdated && ( + }> + {t("outdatedWarning")} + + )} + {detail.data.reportCount > 0 && ( + + }> + {t("reportWarning", { count: detail.data.reportCount })} + + {reportSummaries.data?.map((summary) => ( + + + {t(`reportCategory.${summary.category}`)} + + + {summary.explanation} + + + ))} + {reportSummaries.data?.length === 0 && ( + + {t("reportVisibility")} + + )} + + )} + {detail.data.screenshots.length > 0 && ( + + {detail.data.screenshots.map((file, index) => ( + {t("screenshotAlt", + ))} + + )} + {detailValidation && !detailValidation.success && ( + }> + {t("installErrorDescription")} {detailValidation.error} + + )} + {detailValidation?.success && !detailCompatible && ( + }> + {t("installErrorDescription")} + + )} + + + {t("technicalDetails")} + + + {type === "customWidget" && + detailValidation?.success && + typeof detailValidation.data !== "string" && ( + + )} + + + + + + {!user && ( - {t("reportVisibility")} + {t("signInHint")} )} + {vote.error && {vote.error.message || t("voteError")}} + {install.error && {install.error.message}} + {useCss.error && {useCss.error.message}} - )} - {detail.data.screenshots.length > 0 && ( - - {detail.data.screenshots.map((file, index) => ( - {t("screenshotAlt", - ))} - - )} - {type === "customWidget" && detailValidation?.success && typeof detailValidation.data !== "string" && ( - - )} - {detailValidation && !detailValidation.success && ( - }> - {t("installErrorDescription")} {detailValidation.error} - - )} - {detailValidation?.success && !detailCompatible && ( - }> - {t("installErrorDescription")} - - )} - - - - - - - + + + + + + + + + + {onInstall && ( + + )} + {onUseCss && ( + + )} - {onInstall && ( - - )} - {onUseCss && ( - - )} - - {!user && ( - - {t("signInHint")} - - )} - {vote.error && {vote.error.message || t("voteError")}} - {install.error && {install.error.message}} - {useCss.error && {useCss.error.message}} - + + )} diff --git a/apps/nextjs/src/components/workshop/workshop-install-button.tsx b/apps/nextjs/src/components/workshop/workshop-install-button.tsx index 2f00b14f8..b6ac5136a 100644 --- a/apps/nextjs/src/components/workshop/workshop-install-button.tsx +++ b/apps/nextjs/src/components/workshop/workshop-install-button.tsx @@ -25,6 +25,21 @@ const installFlowModalProps = { }, } satisfies Pick; +const installReviewModalProps = { + size: "90%", + styles: { + content: { display: "flex", flexDirection: "column", height: "min(85dvh, 900px)" }, + body: { + display: "flex", + flex: 1, + flexDirection: "column", + minHeight: 0, + overflow: "hidden", + padding: 0, + }, + }, +} satisfies Pick; + export function WorkshopInstallButton({ children, fullWidth }: WorkshopInstallButtonProps = {}) { const t = useScopedI18n("workshop"); const stack = useModalsStack(["workshop", "details", "report", "review"]); @@ -74,7 +89,7 @@ export function WorkshopInstallButton({ children, fullWidth }: WorkshopInstallBu @@ -70,65 +98,78 @@ export function ImportReviewDialog({ stackId={stackId} zIndex={zIndex} size={size ?? "lg"} - styles={styles} + styles={styles ?? importReviewModalStyles} > {review && ( - - - {messages.description} - - - - - - - -
- - {messages.methods} - - - {review.methods.map((method) => ( - - {method} - - ))} - -
-
- - {messages.permissions} - - - {review.permissions.map((permission) => ( - - {messages.permission(permission)} - - ))} - -
- {review.hasActions && ( - }> - - {messages.actionWarningTitle} + <> + + + + {messages.description} - {messages.actionWarningDescription} - - )} - {children} - - - - -
+ + + + + + + +
+ + {messages.methods} + + + {review.methods.map((method) => ( + + {method} + + ))} + +
+
+ + {messages.permissions} + + + {review.permissions.map((permission) => ( + + {messages.permission(permission)} + + ))} + +
+
+ {review.hasActions && ( + }> + + {messages.actionWarningTitle} + + {messages.actionWarningDescription} + + )} + {children} + + + + + + + + + )} ); diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index 6943988f1..8a57998e9 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -1407,6 +1407,9 @@ "noDescription": "No description", "author": "by {name}", "communityMember": "Community member", + "comments": "{count, plural, one {# comment} other {# comments}}", + "viewDetails": "View details", + "voteSummary": "{upvotes, plural, one {# upvote} other {# upvotes}}, {downvotes, plural, one {# downvote} other {# downvotes}}", "outdated": "Outdated", "outdatedWarning": "The author marked this submission as outdated. Review it carefully before importing.", "reportCount": "{count, plural, one {# report} other {# reports}}", @@ -1416,6 +1419,7 @@ "upvote": "Upvote, {count, plural, one {# vote} other {# votes}}", "downvote": "Downvote, {count, plural, one {# vote} other {# votes}}", "inspectCss": "Inspect CSS", + "technicalDetails": "Technical details", "sourceCode": "Workshop entry", "loadError": "This submission could not be loaded.", "sources": "Declared API sources",