diff options
Diffstat (limited to 'apps/web/components/dashboard/preview')
8 files changed, 589 insertions, 40 deletions
diff --git a/apps/web/components/dashboard/preview/ActionBar.tsx b/apps/web/components/dashboard/preview/ActionBar.tsx index 6e4cd5a2..9603465e 100644 --- a/apps/web/components/dashboard/preview/ActionBar.tsx +++ b/apps/web/components/dashboard/preview/ActionBar.tsx @@ -1,12 +1,12 @@ import { useState } from "react"; import { ActionButton } from "@/components/ui/action-button"; import { Button } from "@/components/ui/button"; +import { toast } from "@/components/ui/sonner"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; -import { toast } from "@/components/ui/use-toast"; import { useTranslation } from "@/lib/i18n/client"; import { Pencil, Trash2 } from "lucide-react"; diff --git a/apps/web/components/dashboard/preview/AttachmentBox.tsx b/apps/web/components/dashboard/preview/AttachmentBox.tsx index 73eea640..654f3211 100644 --- a/apps/web/components/dashboard/preview/AttachmentBox.tsx +++ b/apps/web/components/dashboard/preview/AttachmentBox.tsx @@ -8,7 +8,7 @@ import { CollapsibleTrigger, } from "@/components/ui/collapsible"; import FilePickerButton from "@/components/ui/file-picker-button"; -import { toast } from "@/components/ui/use-toast"; +import { toast } from "@/components/ui/sonner"; import { ASSET_TYPE_TO_ICON } from "@/lib/attachments"; import useUpload from "@/lib/hooks/upload-file"; import { useTranslation } from "@/lib/i18n/client"; diff --git a/apps/web/components/dashboard/preview/BookmarkPreview.tsx b/apps/web/components/dashboard/preview/BookmarkPreview.tsx index 7e6bf814..719cdff8 100644 --- a/apps/web/components/dashboard/preview/BookmarkPreview.tsx +++ b/apps/web/components/dashboard/preview/BookmarkPreview.tsx @@ -13,12 +13,13 @@ import { TooltipPortal, TooltipTrigger, } from "@/components/ui/tooltip"; +import { useSession } from "@/lib/auth/client"; import useRelativeTime from "@/lib/hooks/relative-time"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useQuery } from "@tanstack/react-query"; import { Building, CalendarDays, ExternalLink, User } from "lucide-react"; -import { useSession } from "next-auth/react"; +import { useTRPC } from "@karakeep/shared-react/trpc"; import { BookmarkTypes, ZBookmark } from "@karakeep/shared/types/bookmarks"; import { getBookmarkRefreshInterval, @@ -116,24 +117,27 @@ export default function BookmarkPreview({ bookmarkId: string; initialData?: ZBookmark; }) { + const api = useTRPC(); const { t } = useTranslation(); const [activeTab, setActiveTab] = useState<string>("content"); const { data: session } = useSession(); - const { data: bookmark } = api.bookmarks.getBookmark.useQuery( - { - bookmarkId, - }, - { - initialData, - refetchInterval: (query) => { - const data = query.state.data; - if (!data) { - return false; - } - return getBookmarkRefreshInterval(data); + const { data: bookmark } = useQuery( + api.bookmarks.getBookmark.queryOptions( + { + bookmarkId, }, - }, + { + initialData, + refetchInterval: (query) => { + const data = query.state.data; + if (!data) { + return false; + } + return getBookmarkRefreshInterval(data); + }, + }, + ), ); if (!bookmark) { diff --git a/apps/web/components/dashboard/preview/HighlightsBox.tsx b/apps/web/components/dashboard/preview/HighlightsBox.tsx index 41ab7d74..e8503fd9 100644 --- a/apps/web/components/dashboard/preview/HighlightsBox.tsx +++ b/apps/web/components/dashboard/preview/HighlightsBox.tsx @@ -5,10 +5,12 @@ import { CollapsibleTrigger, } from "@/components/ui/collapsible"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; import { Separator } from "@radix-ui/react-dropdown-menu"; +import { useQuery } from "@tanstack/react-query"; import { ChevronsDownUp } from "lucide-react"; +import { useTRPC } from "@karakeep/shared-react/trpc"; + import HighlightCard from "../highlights/HighlightCard"; export default function HighlightsBox({ @@ -18,10 +20,12 @@ export default function HighlightsBox({ bookmarkId: string; readOnly: boolean; }) { + const api = useTRPC(); const { t } = useTranslation(); - const { data: highlights, isPending: isLoading } = - api.highlights.getForBookmark.useQuery({ bookmarkId }); + const { data: highlights, isPending: isLoading } = useQuery( + api.highlights.getForBookmark.queryOptions({ bookmarkId }), + ); if (isLoading || !highlights || highlights?.highlights.length === 0) { return null; diff --git a/apps/web/components/dashboard/preview/LinkContentSection.tsx b/apps/web/components/dashboard/preview/LinkContentSection.tsx index 64b62df6..f4e344ac 100644 --- a/apps/web/components/dashboard/preview/LinkContentSection.tsx +++ b/apps/web/components/dashboard/preview/LinkContentSection.tsx @@ -16,16 +16,19 @@ import { TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; -import { useTranslation } from "@/lib/i18n/client"; +import { useSession } from "@/lib/auth/client"; +import { Trans, useTranslation } from "@/lib/i18n/client"; +import { useReaderSettings } from "@/lib/readerSettings"; import { AlertTriangle, Archive, BookOpen, Camera, ExpandIcon, + FileText, + Info, Video, } from "lucide-react"; -import { useSession } from "next-auth/react"; import { useQueryState } from "nuqs"; import { ErrorBoundary } from "react-error-boundary"; @@ -34,8 +37,10 @@ import { ZBookmark, ZBookmarkedLink, } from "@karakeep/shared/types/bookmarks"; +import { READER_FONT_FAMILIES } from "@karakeep/shared/types/readers"; import { contentRendererRegistry } from "./content-renderers"; +import ReaderSettingsPopover from "./ReaderSettingsPopover"; import ReaderView from "./ReaderView"; function CustomRendererErrorFallback({ error }: { error: Error }) { @@ -100,12 +105,23 @@ function VideoSection({ link }: { link: ZBookmarkedLink }) { ); } +function PDFSection({ link }: { link: ZBookmarkedLink }) { + return ( + <iframe + title="PDF Viewer" + src={`/api/assets/${link.pdfAssetId}`} + className="relative h-full min-w-full" + /> + ); +} + export default function LinkContentSection({ bookmark, }: { bookmark: ZBookmark; }) { const { t } = useTranslation(); + const { settings } = useReaderSettings(); const availableRenderers = contentRendererRegistry.getRenderers(bookmark); const defaultSection = availableRenderers.length > 0 ? availableRenderers[0].id : "cached"; @@ -135,6 +151,11 @@ export default function LinkContentSection({ <ScrollArea className="h-full"> <ReaderView className="prose mx-auto dark:prose-invert" + style={{ + fontFamily: READER_FONT_FAMILIES[settings.fontFamily], + fontSize: `${settings.fontSize}px`, + lineHeight: settings.lineHeight, + }} bookmarkId={bookmark.id} readOnly={!isOwner} /> @@ -144,6 +165,8 @@ export default function LinkContentSection({ content = <FullPageArchiveSection link={bookmark.content} />; } else if (section === "video") { content = <VideoSection link={bookmark.content} />; + } else if (section === "pdf") { + content = <PDFSection link={bookmark.content} />; } else { content = <ScreenshotSection link={bookmark.content} />; } @@ -188,6 +211,12 @@ export default function LinkContentSection({ {t("common.screenshot")} </div> </SelectItem> + <SelectItem value="pdf" disabled={!bookmark.content.pdfAssetId}> + <div className="flex items-center"> + <FileText className="mr-2 h-4 w-4" /> + {t("common.pdf")} + </div> + </SelectItem> <SelectItem value="archive" disabled={ @@ -213,16 +242,47 @@ export default function LinkContentSection({ </SelectContent> </Select> {section === "cached" && ( + <> + <ReaderSettingsPopover /> + <Tooltip> + <TooltipTrigger> + <Link + href={`/reader/${bookmark.id}`} + className={buttonVariants({ variant: "outline" })} + > + <ExpandIcon className="h-4 w-4" /> + </Link> + </TooltipTrigger> + <TooltipContent side="bottom">FullScreen</TooltipContent> + </Tooltip> + </> + )} + {section === "archive" && ( <Tooltip> - <TooltipTrigger> - <Link - href={`/reader/${bookmark.id}`} - className={buttonVariants({ variant: "outline" })} - > - <ExpandIcon className="h-4 w-4" /> - </Link> + <TooltipTrigger asChild> + <div className="flex h-10 items-center gap-1 rounded-md border border-blue-500/50 bg-blue-50 px-3 text-blue-700 dark:bg-blue-950 dark:text-blue-300"> + <Info className="h-4 w-4" /> + </div> </TooltipTrigger> - <TooltipContent side="bottom">FullScreen</TooltipContent> + <TooltipContent side="bottom" className="max-w-sm"> + <p className="text-sm"> + <Trans + i18nKey="preview.archive_info" + components={{ + 1: ( + <Link + prefetch={false} + href={`/api/assets/${bookmark.content.fullPageArchiveAssetId ?? bookmark.content.precrawledArchiveAssetId}`} + download + className="font-medium underline" + > + link + </Link> + ), + }} + /> + </p> + </TooltipContent> </Tooltip> )} </div> diff --git a/apps/web/components/dashboard/preview/NoteEditor.tsx b/apps/web/components/dashboard/preview/NoteEditor.tsx index 538aff2e..86807569 100644 --- a/apps/web/components/dashboard/preview/NoteEditor.tsx +++ b/apps/web/components/dashboard/preview/NoteEditor.tsx @@ -1,5 +1,5 @@ +import { toast } from "@/components/ui/sonner"; import { Textarea } from "@/components/ui/textarea"; -import { toast } from "@/components/ui/use-toast"; import { useClientConfig } from "@/lib/clientConfig"; import type { ZBookmark } from "@karakeep/shared/types/bookmarks"; diff --git a/apps/web/components/dashboard/preview/ReaderSettingsPopover.tsx b/apps/web/components/dashboard/preview/ReaderSettingsPopover.tsx new file mode 100644 index 00000000..f37b8263 --- /dev/null +++ b/apps/web/components/dashboard/preview/ReaderSettingsPopover.tsx @@ -0,0 +1,457 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Separator } from "@/components/ui/separator"; +import { Slider } from "@/components/ui/slider"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { useTranslation } from "@/lib/i18n/client"; +import { useReaderSettings } from "@/lib/readerSettings"; +import { + Globe, + Laptop, + Minus, + Plus, + RotateCcw, + Settings, + Type, + X, +} from "lucide-react"; + +import { + formatFontSize, + formatLineHeight, + READER_DEFAULTS, + READER_SETTING_CONSTRAINTS, +} from "@karakeep/shared/types/readers"; + +interface ReaderSettingsPopoverProps { + open?: boolean; + onOpenChange?: (open: boolean) => void; + variant?: "outline" | "ghost"; +} + +export default function ReaderSettingsPopover({ + open, + onOpenChange, + variant = "outline", +}: ReaderSettingsPopoverProps) { + const { t } = useTranslation(); + const { + settings, + serverSettings, + localOverrides, + sessionOverrides, + hasSessionChanges, + hasLocalOverrides, + isSaving, + updateSession, + clearSession, + saveToDevice, + clearLocalOverride, + saveToServer, + } = useReaderSettings(); + + // Helper to get the effective server value (server setting or default) + const getServerValue = <K extends keyof typeof serverSettings>(key: K) => { + return serverSettings[key] ?? READER_DEFAULTS[key]; + }; + + // Helper to check if a setting has a local override + const hasLocalOverride = (key: keyof typeof localOverrides) => { + return localOverrides[key] !== undefined; + }; + + // Build tooltip message for the settings button + const getSettingsTooltip = () => { + if (hasSessionChanges && hasLocalOverrides) { + return t("settings.info.reader_settings.tooltip_preview_and_local"); + } + if (hasSessionChanges) { + return t("settings.info.reader_settings.tooltip_preview"); + } + if (hasLocalOverrides) { + return t("settings.info.reader_settings.tooltip_local"); + } + return t("settings.info.reader_settings.tooltip_default"); + }; + + return ( + <Popover open={open} onOpenChange={onOpenChange}> + <Tooltip> + <TooltipTrigger asChild> + <PopoverTrigger asChild> + <Button variant={variant} size="icon" className="relative"> + <Settings className="h-4 w-4" /> + {(hasSessionChanges || hasLocalOverrides) && ( + <span className="absolute -right-0.5 -top-0.5 h-2 w-2 rounded-full bg-primary" /> + )} + </Button> + </PopoverTrigger> + </TooltipTrigger> + <TooltipContent side="bottom"> + <p>{getSettingsTooltip()}</p> + </TooltipContent> + </Tooltip> + <PopoverContent + side="bottom" + align="center" + collisionPadding={32} + className="flex w-80 flex-col overflow-hidden p-0" + style={{ + maxHeight: "var(--radix-popover-content-available-height)", + }} + > + <div className="min-h-0 flex-1 space-y-4 overflow-y-auto p-4"> + <div className="flex items-center justify-between pb-2"> + <div className="flex items-center gap-2"> + <Type className="h-4 w-4" /> + <h3 className="font-semibold"> + {t("settings.info.reader_settings.title")} + </h3> + </div> + {hasSessionChanges && ( + <span className="rounded-full bg-primary/10 px-2 py-0.5 text-xs text-primary"> + {t("settings.info.reader_settings.preview")} + </span> + )} + </div> + + <div className="space-y-4"> + <div className="space-y-2"> + <div className="flex items-center justify-between"> + <label className="text-sm font-medium"> + {t("settings.info.reader_settings.font_family")} + </label> + <div className="flex items-center gap-1"> + {sessionOverrides.fontFamily !== undefined && ( + <span className="text-xs text-muted-foreground"> + {t("settings.info.reader_settings.preview_inline")} + </span> + )} + {hasLocalOverride("fontFamily") && + sessionOverrides.fontFamily === undefined && ( + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="ghost" + size="icon" + className="h-5 w-5 text-muted-foreground hover:text-foreground" + onClick={() => clearLocalOverride("fontFamily")} + > + <X className="h-3 w-3" /> + </Button> + </TooltipTrigger> + <TooltipContent> + <p> + {t( + "settings.info.reader_settings.clear_override_hint", + { + value: t( + `settings.info.reader_settings.${getServerValue("fontFamily")}` as const, + ), + }, + )} + </p> + </TooltipContent> + </Tooltip> + )} + </div> + </div> + <Select + value={settings.fontFamily} + onValueChange={(value) => + updateSession({ + fontFamily: value as "serif" | "sans" | "mono", + }) + } + > + <SelectTrigger + className={ + hasLocalOverride("fontFamily") && + sessionOverrides.fontFamily === undefined + ? "border-primary/50" + : "" + } + > + <SelectValue /> + </SelectTrigger> + <SelectContent> + <SelectItem value="serif"> + {t("settings.info.reader_settings.serif")} + </SelectItem> + <SelectItem value="sans"> + {t("settings.info.reader_settings.sans")} + </SelectItem> + <SelectItem value="mono"> + {t("settings.info.reader_settings.mono")} + </SelectItem> + </SelectContent> + </Select> + </div> + + <div className="space-y-2"> + <div className="flex items-center justify-between"> + <label className="text-sm font-medium"> + {t("settings.info.reader_settings.font_size")} + </label> + <div className="flex items-center gap-1"> + <span className="text-sm text-muted-foreground"> + {formatFontSize(settings.fontSize)} + {sessionOverrides.fontSize !== undefined && ( + <span className="ml-1 text-xs"> + {t("settings.info.reader_settings.preview_inline")} + </span> + )} + </span> + {hasLocalOverride("fontSize") && + sessionOverrides.fontSize === undefined && ( + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="ghost" + size="icon" + className="h-5 w-5 text-muted-foreground hover:text-foreground" + onClick={() => clearLocalOverride("fontSize")} + > + <X className="h-3 w-3" /> + </Button> + </TooltipTrigger> + <TooltipContent> + <p> + {t( + "settings.info.reader_settings.clear_override_hint", + { + value: formatFontSize( + getServerValue("fontSize"), + ), + }, + )} + </p> + </TooltipContent> + </Tooltip> + )} + </div> + </div> + <div className="flex items-center gap-2"> + <Button + variant="outline" + size="icon" + className="h-7 w-7 bg-transparent" + onClick={() => + updateSession({ + fontSize: Math.max( + READER_SETTING_CONSTRAINTS.fontSize.min, + settings.fontSize - + READER_SETTING_CONSTRAINTS.fontSize.step, + ), + }) + } + > + <Minus className="h-3 w-3" /> + </Button> + <Slider + value={[settings.fontSize]} + onValueChange={([value]) => + updateSession({ fontSize: value }) + } + max={READER_SETTING_CONSTRAINTS.fontSize.max} + min={READER_SETTING_CONSTRAINTS.fontSize.min} + step={READER_SETTING_CONSTRAINTS.fontSize.step} + className={`flex-1 ${ + hasLocalOverride("fontSize") && + sessionOverrides.fontSize === undefined + ? "[&_[role=slider]]:border-primary/50" + : "" + }`} + /> + <Button + variant="outline" + size="icon" + className="h-7 w-7 bg-transparent" + onClick={() => + updateSession({ + fontSize: Math.min( + READER_SETTING_CONSTRAINTS.fontSize.max, + settings.fontSize + + READER_SETTING_CONSTRAINTS.fontSize.step, + ), + }) + } + > + <Plus className="h-3 w-3" /> + </Button> + </div> + </div> + + <div className="space-y-2"> + <div className="flex items-center justify-between"> + <label className="text-sm font-medium"> + {t("settings.info.reader_settings.line_height")} + </label> + <div className="flex items-center gap-1"> + <span className="text-sm text-muted-foreground"> + {formatLineHeight(settings.lineHeight)} + {sessionOverrides.lineHeight !== undefined && ( + <span className="ml-1 text-xs"> + {t("settings.info.reader_settings.preview_inline")} + </span> + )} + </span> + {hasLocalOverride("lineHeight") && + sessionOverrides.lineHeight === undefined && ( + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="ghost" + size="icon" + className="h-5 w-5 text-muted-foreground hover:text-foreground" + onClick={() => clearLocalOverride("lineHeight")} + > + <X className="h-3 w-3" /> + </Button> + </TooltipTrigger> + <TooltipContent> + <p> + {t( + "settings.info.reader_settings.clear_override_hint", + { + value: formatLineHeight( + getServerValue("lineHeight"), + ), + }, + )} + </p> + </TooltipContent> + </Tooltip> + )} + </div> + </div> + <div className="flex items-center gap-2"> + <Button + variant="outline" + size="icon" + className="h-7 w-7 bg-transparent" + onClick={() => + updateSession({ + lineHeight: Math.max( + READER_SETTING_CONSTRAINTS.lineHeight.min, + Math.round( + (settings.lineHeight - + READER_SETTING_CONSTRAINTS.lineHeight.step) * + 10, + ) / 10, + ), + }) + } + > + <Minus className="h-3 w-3" /> + </Button> + <Slider + value={[settings.lineHeight]} + onValueChange={([value]) => + updateSession({ lineHeight: value }) + } + max={READER_SETTING_CONSTRAINTS.lineHeight.max} + min={READER_SETTING_CONSTRAINTS.lineHeight.min} + step={READER_SETTING_CONSTRAINTS.lineHeight.step} + className={`flex-1 ${ + hasLocalOverride("lineHeight") && + sessionOverrides.lineHeight === undefined + ? "[&_[role=slider]]:border-primary/50" + : "" + }`} + /> + <Button + variant="outline" + size="icon" + className="h-7 w-7 bg-transparent" + onClick={() => + updateSession({ + lineHeight: Math.min( + READER_SETTING_CONSTRAINTS.lineHeight.max, + Math.round( + (settings.lineHeight + + READER_SETTING_CONSTRAINTS.lineHeight.step) * + 10, + ) / 10, + ), + }) + } + > + <Plus className="h-3 w-3" /> + </Button> + </div> + </div> + + {hasSessionChanges && ( + <> + <Separator /> + + <div className="space-y-2"> + <Button + variant="outline" + size="sm" + className="w-full" + onClick={() => clearSession()} + > + <RotateCcw className="mr-2 h-4 w-4" /> + {t("settings.info.reader_settings.reset_preview")} + </Button> + + <div className="flex gap-2"> + <Button + variant="outline" + size="sm" + className="flex-1" + disabled={isSaving} + onClick={() => saveToDevice()} + > + <Laptop className="mr-2 h-4 w-4" /> + {t("settings.info.reader_settings.save_to_device")} + </Button> + <Button + variant="default" + size="sm" + className="flex-1" + disabled={isSaving} + onClick={() => saveToServer()} + > + <Globe className="mr-2 h-4 w-4" /> + {t("settings.info.reader_settings.save_to_all_devices")} + </Button> + </div> + + <p className="text-center text-xs text-muted-foreground"> + {t("settings.info.reader_settings.save_hint")} + </p> + </div> + </> + )} + + {!hasSessionChanges && ( + <p className="text-center text-xs text-muted-foreground"> + {t("settings.info.reader_settings.adjust_hint")} + </p> + )} + </div> + </div> + </PopoverContent> + </Popover> + ); +} diff --git a/apps/web/components/dashboard/preview/ReaderView.tsx b/apps/web/components/dashboard/preview/ReaderView.tsx index f2f843ee..76070534 100644 --- a/apps/web/components/dashboard/preview/ReaderView.tsx +++ b/apps/web/components/dashboard/preview/ReaderView.tsx @@ -1,12 +1,15 @@ import { FullPageSpinner } from "@/components/ui/full-page-spinner"; -import { toast } from "@/components/ui/use-toast"; -import { api } from "@/lib/trpc"; +import { toast } from "@/components/ui/sonner"; +import { useTranslation } from "@/lib/i18n/client"; +import { useQuery } from "@tanstack/react-query"; +import { FileX } from "lucide-react"; import { useCreateHighlight, useDeleteHighlight, useUpdateHighlight, } from "@karakeep/shared-react/hooks/highlights"; +import { useTRPC } from "@karakeep/shared-react/trpc"; import { BookmarkTypes } from "@karakeep/shared/types/bookmarks"; import BookmarkHTMLHighlighter from "./BookmarkHtmlHighlighter"; @@ -22,11 +25,15 @@ export default function ReaderView({ style?: React.CSSProperties; readOnly: boolean; }) { - const { data: highlights } = api.highlights.getForBookmark.useQuery({ - bookmarkId, - }); - const { data: cachedContent, isPending: isCachedContentLoading } = - api.bookmarks.getBookmark.useQuery( + const { t } = useTranslation(); + const api = useTRPC(); + const { data: highlights } = useQuery( + api.highlights.getForBookmark.queryOptions({ + bookmarkId, + }), + ); + const { data: cachedContent, isPending: isCachedContentLoading } = useQuery( + api.bookmarks.getBookmark.queryOptions( { bookmarkId, includeContent: true, @@ -37,7 +44,8 @@ export default function ReaderView({ ? data.content.htmlContent : null, }, - ); + ), + ); const { mutate: createHighlight } = useCreateHighlight({ onSuccess: () => { @@ -86,7 +94,23 @@ export default function ReaderView({ content = <FullPageSpinner />; } else if (!cachedContent) { content = ( - <div className="text-destructive">Failed to fetch link content ...</div> + <div className="flex h-full w-full items-center justify-center p-4"> + <div className="max-w-sm space-y-4 text-center"> + <div className="flex justify-center"> + <div className="flex h-16 w-16 items-center justify-center rounded-full bg-muted"> + <FileX className="h-8 w-8 text-muted-foreground" /> + </div> + </div> + <div className="space-y-2"> + <h3 className="text-lg font-medium text-foreground"> + {t("preview.fetch_error_title")} + </h3> + <p className="text-sm leading-relaxed text-muted-foreground"> + {t("preview.fetch_error_description")} + </p> + </div> + </div> + </div> ); } else { content = ( |
