From 14e4fed321634dc014ad2f15cafef3ed0123855e Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Sun, 23 Feb 2025 22:50:12 +0000 Subject: feat: Add a setting page to manage assets. Fixes #730 --- apps/web/app/settings/assets/page.tsx | 182 ++++++++++++++++++++++++++++++++++ apps/web/app/settings/layout.tsx | 6 ++ 2 files changed, 188 insertions(+) create mode 100644 apps/web/app/settings/assets/page.tsx (limited to 'apps/web/app/settings') diff --git a/apps/web/app/settings/assets/page.tsx b/apps/web/app/settings/assets/page.tsx new file mode 100644 index 00000000..a6c13525 --- /dev/null +++ b/apps/web/app/settings/assets/page.tsx @@ -0,0 +1,182 @@ +"use client"; + +import Link from "next/link"; +import { ActionButton } from "@/components/ui/action-button"; +import ActionConfirmingDialog from "@/components/ui/action-confirming-dialog"; +import { Button } from "@/components/ui/button"; +import { FullPageSpinner } from "@/components/ui/full-page-spinner"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { toast } from "@/components/ui/use-toast"; +import { ASSET_TYPE_TO_ICON } from "@/lib/attachments"; +import { useTranslation } from "@/lib/i18n/client"; +import { api } from "@/lib/trpc"; +import { formatBytes } from "@/lib/utils"; +import { ExternalLink, Trash2 } from "lucide-react"; + +import { useDetachBookmarkAsset } from "@hoarder/shared-react/hooks/assets"; +import { getAssetUrl } from "@hoarder/shared-react/utils/assetUtils"; +import { + humanFriendlyNameForAssertType, + isAllowedToDetachAsset, +} from "@hoarder/trpc/lib/attachments"; + +export default function AssetsSettingsPage() { + const { t } = useTranslation(); + const { mutate: detachAsset, isPending: isDetaching } = + useDetachBookmarkAsset({ + onSuccess: () => { + toast({ + description: "Asset has been deleted!", + }); + }, + onError: (e) => { + toast({ + description: e.message, + variant: "destructive", + }); + }, + }); + const { + data, + isLoading: isAssetsLoading, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + } = api.assets.list.useInfiniteQuery( + { + limit: 20, + }, + { + getNextPageParam: (lastPage) => lastPage.nextCursor, + }, + ); + + const assets = data?.pages.flatMap((page) => page.assets) ?? []; + + if (isAssetsLoading) { + return ; + } + + return ( +
+
+
+ {t("settings.manage_assets.manage_assets")} +
+ {assets.length === 0 && ( +

+ {t("settings.manage_assets.no_assets")} +

+ )} + {assets.length > 0 && ( + + + + {t("settings.manage_assets.asset_type")} + {t("common.size")} + {t("settings.manage_assets.asset_link")} + + {t("settings.manage_assets.bookmark_link")} + + {t("common.actions")} + + + + {assets.map((asset) => ( + + + {ASSET_TYPE_TO_ICON[asset.assetType]} + + {humanFriendlyNameForAssertType(asset.assetType)} + + + {formatBytes(asset.size)} + + + + View Asset + + + + {asset.bookmarkId ? ( + + + View Bookmark + + ) : ( + No bookmark + )} + + + {isAllowedToDetachAsset(asset.assetType) && + asset.bookmarkId && ( + ( + + detachAsset( + { + bookmarkId: asset.bookmarkId!, + assetId: asset.id, + }, + { onSettled: () => setDialogOpen(false) }, + ) + } + > + + {t("actions.delete")} + + )} + > + + + )} + + + ))} + +
+ )} + {hasNextPage && ( +
+ fetchNextPage()} + loading={isFetchingNextPage} + ignoreDemoMode={true} + > + Load More + +
+ )} +
+
+ ); +} diff --git a/apps/web/app/settings/layout.tsx b/apps/web/app/settings/layout.tsx index 909dfd9c..62ac041c 100644 --- a/apps/web/app/settings/layout.tsx +++ b/apps/web/app/settings/layout.tsx @@ -5,6 +5,7 @@ import { TFunction } from "i18next"; import { ArrowLeft, Download, + Image, KeyRound, Link, Rss, @@ -60,6 +61,11 @@ const settingsSidebarItems = ( icon: , path: "/settings/webhooks", }, + { + name: t("settings.manage_assets.manage_assets"), + icon: , + path: "/settings/assets", + }, ]; export default async function SettingsLayout({ -- cgit v1.2.3-70-g09d2