aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web/app/settings/broken-links
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2024-11-30 19:12:45 +0000
committerMohamed Bassem <me@mbassem.com>2024-12-08 20:59:42 +0000
commit705d539c8e9c6a86882825ee4dabeff3027ba827 (patch)
tree9ac5d1c048393213d1302d005630a64a4789178c /apps/web/app/settings/broken-links
parenta7b13869b149edbea9bdb220614c69c9a05d79b5 (diff)
downloadkarakeep-705d539c8e9c6a86882825ee4dabeff3027ba827.tar.zst
feature: Store crawling status code and allow users to find broken links. Fixes #169
Diffstat (limited to 'apps/web/app/settings/broken-links')
-rw-r--r--apps/web/app/settings/broken-links/page.tsx131
1 files changed, 131 insertions, 0 deletions
diff --git a/apps/web/app/settings/broken-links/page.tsx b/apps/web/app/settings/broken-links/page.tsx
new file mode 100644
index 00000000..0b83dfa9
--- /dev/null
+++ b/apps/web/app/settings/broken-links/page.tsx
@@ -0,0 +1,131 @@
+"use client";
+
+import { ActionButton } from "@/components/ui/action-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 { RefreshCw, Trash2 } from "lucide-react";
+import { useTranslation } from "react-i18next";
+
+import {
+ useDeleteBookmark,
+ useRecrawlBookmark,
+} from "@hoarder/shared-react/hooks/bookmarks";
+import { api } from "@hoarder/shared-react/trpc";
+
+export default function BrokenLinksPage() {
+ const { t } = useTranslation();
+
+ const apiUtils = api.useUtils();
+ const { data, isPending } = api.bookmarks.getBrokenLinks.useQuery();
+
+ const { mutate: deleteBookmark, isPending: isDeleting } = useDeleteBookmark({
+ onSuccess: () => {
+ toast({
+ description: t("toasts.bookmarks.deleted"),
+ });
+ apiUtils.bookmarks.getBrokenLinks.invalidate();
+ },
+ onError: () => {
+ toast({
+ description: t("common.something_went_wrong"),
+ variant: "destructive",
+ });
+ },
+ });
+
+ const { mutate: recrawlBookmark, isPending: isRecrawling } =
+ useRecrawlBookmark({
+ onSuccess: () => {
+ toast({
+ description: t("toasts.bookmarks.refetch"),
+ });
+ apiUtils.bookmarks.getBrokenLinks.invalidate();
+ },
+ onError: () => {
+ toast({
+ description: t("common.something_went_wrong"),
+ variant: "destructive",
+ });
+ },
+ });
+
+ return (
+ <div className="rounded-md border bg-background p-4">
+ <div className="flex items-center justify-between">
+ <div className="mb-2 text-lg font-medium">
+ {t("settings.broken_links.broken_links")}
+ </div>
+ </div>
+ <div className="mt-2">
+ {isPending && <FullPageSpinner />}
+ {!isPending && data && data.bookmarks.length == 0 && (
+ <p className="rounded-md bg-muted p-2 text-sm text-muted-foreground">
+ No broken links found
+ </p>
+ )}
+ {!isPending && data && data.bookmarks.length > 0 && (
+ <Table>
+ <TableHeader>
+ <TableRow>
+ <TableHead>{t("common.url")}</TableHead>
+ <TableHead>{t("common.created_at")}</TableHead>
+ <TableHead>
+ {t("settings.broken_links.last_crawled_at")}
+ </TableHead>
+ <TableHead>
+ {t("settings.broken_links.crawling_status")}
+ </TableHead>
+ <TableHead>{t("common.action")}</TableHead>
+ </TableRow>
+ </TableHeader>
+ <TableBody>
+ {data.bookmarks.map((b) => (
+ <TableRow key={b.id}>
+ <TableCell>{b.url}</TableCell>
+ <TableCell>{b.createdAt?.toLocaleString()}</TableCell>
+ <TableCell>{b.crawledAt?.toLocaleString()}</TableCell>
+ <TableCell>
+ {b.isCrawlingFailure ? (
+ <span className="text-red-500">Failed</span>
+ ) : (
+ b.statusCode
+ )}
+ </TableCell>
+ <TableCell className="flex gap-2">
+ <ActionButton
+ variant="secondary"
+ loading={isRecrawling}
+ onClick={() => recrawlBookmark({ bookmarkId: b.id })}
+ className="flex items-center gap-2"
+ >
+ <RefreshCw className="size-4" />
+ {t("actions.recrawl")}
+ </ActionButton>
+ <ActionButton
+ variant="destructive"
+ onClick={() => deleteBookmark({ bookmarkId: b.id })}
+ loading={isDeleting}
+ className="flex items-center gap-2"
+ >
+ <Trash2 className="size-4" />
+ {t("actions.delete")}
+ </ActionButton>
+ </TableCell>
+ </TableRow>
+ ))}
+ <TableRow></TableRow>
+ </TableBody>
+ </Table>
+ )}
+ </div>
+ </div>
+ );
+}