From 705d539c8e9c6a86882825ee4dabeff3027ba827 Mon Sep 17 00:00:00 2001
From: Mohamed Bassem <me@mbassem.com>
Date: Sat, 30 Nov 2024 19:12:45 +0000
Subject: feature: Store crawling status code and allow users to find broken
 links. Fixes #169

---
 apps/web/app/settings/broken-links/page.tsx | 131 ++++++++++++++++++++++++++++
 1 file changed, 131 insertions(+)
 create mode 100644 apps/web/app/settings/broken-links/page.tsx

(limited to 'apps/web/app/settings/broken-links/page.tsx')

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>
+  );
+}
-- 
cgit v1.2.3-70-g09d2

