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 --- .../[bookmarkId]/assets/[assetId]/route.ts | 4 +- .../api/v1/bookmarks/[bookmarkId]/assets/route.ts | 2 +- apps/web/app/settings/assets/page.tsx | 182 +++++++++++++++++++++ apps/web/app/settings/layout.tsx | 6 + 4 files changed, 191 insertions(+), 3 deletions(-) create mode 100644 apps/web/app/settings/assets/page.tsx (limited to 'apps/web/app') diff --git a/apps/web/app/api/v1/bookmarks/[bookmarkId]/assets/[assetId]/route.ts b/apps/web/app/api/v1/bookmarks/[bookmarkId]/assets/[assetId]/route.ts index 3fc50801..88e203de 100644 --- a/apps/web/app/api/v1/bookmarks/[bookmarkId]/assets/[assetId]/route.ts +++ b/apps/web/app/api/v1/bookmarks/[bookmarkId]/assets/[assetId]/route.ts @@ -12,7 +12,7 @@ export const PUT = ( req, bodySchema: z.object({ assetId: z.string() }), handler: async ({ api, body }) => { - await api.bookmarks.replaceAsset({ + await api.assets.replaceAsset({ bookmarkId: params.params.bookmarkId, oldAssetId: params.params.assetId, newAssetId: body!.assetId, @@ -28,7 +28,7 @@ export const DELETE = ( buildHandler({ req, handler: async ({ api }) => { - await api.bookmarks.detachAsset({ + await api.assets.detachAsset({ bookmarkId: params.params.bookmarkId, assetId: params.params.assetId, }); diff --git a/apps/web/app/api/v1/bookmarks/[bookmarkId]/assets/route.ts b/apps/web/app/api/v1/bookmarks/[bookmarkId]/assets/route.ts index e5284a39..156876b6 100644 --- a/apps/web/app/api/v1/bookmarks/[bookmarkId]/assets/route.ts +++ b/apps/web/app/api/v1/bookmarks/[bookmarkId]/assets/route.ts @@ -27,7 +27,7 @@ export const POST = ( req, bodySchema: zAssetSchema, handler: async ({ api, body }) => { - const asset = await api.bookmarks.attachAsset({ + const asset = await api.assets.attachAsset({ bookmarkId: params.params.bookmarkId, asset: body!, }); 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