diff options
| author | Mohamed Bassem <me@mbassem.com> | 2025-02-23 22:50:12 +0000 |
|---|---|---|
| committer | Mohamed Bassem <me@mbassem.com> | 2025-02-23 23:16:52 +0000 |
| commit | 14e4fed321634dc014ad2f15cafef3ed0123855e (patch) | |
| tree | 0b7b1f1157bacea13b93161c07ab48561544fd28 /apps/web/app/settings | |
| parent | e5cb9aa848009ea22c1385e4d33b7edf372979fb (diff) | |
| download | karakeep-14e4fed321634dc014ad2f15cafef3ed0123855e.tar.zst | |
feat: Add a setting page to manage assets. Fixes #730
Diffstat (limited to 'apps/web/app/settings')
| -rw-r--r-- | apps/web/app/settings/assets/page.tsx | 182 | ||||
| -rw-r--r-- | apps/web/app/settings/layout.tsx | 6 |
2 files changed, 188 insertions, 0 deletions
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 <FullPageSpinner />; + } + + return ( + <div className="rounded-md border bg-background p-4"> + <div className="flex flex-col gap-2"> + <div className="mb-2 text-lg font-medium"> + {t("settings.manage_assets.manage_assets")} + </div> + {assets.length === 0 && ( + <p className="rounded-md bg-muted p-2 text-sm text-muted-foreground"> + {t("settings.manage_assets.no_assets")} + </p> + )} + {assets.length > 0 && ( + <Table> + <TableHeader> + <TableRow> + <TableHead>{t("settings.manage_assets.asset_type")}</TableHead> + <TableHead>{t("common.size")}</TableHead> + <TableHead>{t("settings.manage_assets.asset_link")}</TableHead> + <TableHead> + {t("settings.manage_assets.bookmark_link")} + </TableHead> + <TableHead>{t("common.actions")}</TableHead> + </TableRow> + </TableHeader> + <TableBody> + {assets.map((asset) => ( + <TableRow key={asset.id}> + <TableCell className="flex items-center gap-2"> + {ASSET_TYPE_TO_ICON[asset.assetType]} + <span> + {humanFriendlyNameForAssertType(asset.assetType)} + </span> + </TableCell> + <TableCell>{formatBytes(asset.size)}</TableCell> + <TableCell> + <Link + href={getAssetUrl(asset.id)} + target="_blank" + rel="noopener noreferrer" + className="flex items-center gap-1" + > + <ExternalLink className="size-4" /> + <span>View Asset</span> + </Link> + </TableCell> + <TableCell> + {asset.bookmarkId ? ( + <Link + href={`/dashboard/preview/${asset.bookmarkId}`} + className="flex items-center gap-1" + > + <ExternalLink className="size-4" /> + <span>View Bookmark</span> + </Link> + ) : ( + <span className="text-muted-foreground">No bookmark</span> + )} + </TableCell> + <TableCell> + {isAllowedToDetachAsset(asset.assetType) && + asset.bookmarkId && ( + <ActionConfirmingDialog + title={t("settings.manage_assets.delete_asset")} + description={t( + "settings.manage_assets.delete_asset_confirmation", + )} + actionButton={(setDialogOpen) => ( + <ActionButton + loading={isDetaching} + variant="destructive" + onClick={() => + detachAsset( + { + bookmarkId: asset.bookmarkId!, + assetId: asset.id, + }, + { onSettled: () => setDialogOpen(false) }, + ) + } + > + <Trash2 className="mr-2 size-4" /> + {t("actions.delete")} + </ActionButton> + )} + > + <Button + variant="destructive" + size="sm" + title="Delete" + > + <Trash2 className="size-4" /> + </Button> + </ActionConfirmingDialog> + )} + </TableCell> + </TableRow> + ))} + </TableBody> + </Table> + )} + {hasNextPage && ( + <div className="flex justify-center"> + <ActionButton + variant="secondary" + onClick={() => fetchNextPage()} + loading={isFetchingNextPage} + ignoreDemoMode={true} + > + Load More + </ActionButton> + </div> + )} + </div> + </div> + ); +} 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: <Webhook size={18} />, path: "/settings/webhooks", }, + { + name: t("settings.manage_assets.manage_assets"), + icon: <Image size={18} />, + path: "/settings/assets", + }, ]; export default async function SettingsLayout({ |
