aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web/app/settings
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2025-02-23 22:50:12 +0000
committerMohamed Bassem <me@mbassem.com>2025-02-23 23:16:52 +0000
commit14e4fed321634dc014ad2f15cafef3ed0123855e (patch)
tree0b7b1f1157bacea13b93161c07ab48561544fd28 /apps/web/app/settings
parente5cb9aa848009ea22c1385e4d33b7edf372979fb (diff)
downloadkarakeep-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.tsx182
-rw-r--r--apps/web/app/settings/layout.tsx6
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({