aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web
diff options
context:
space:
mode:
authorMohamedBassem <me@mbassem.com>2024-10-05 14:42:22 +0000
committerMohamedBassem <me@mbassem.com>2024-10-05 14:42:22 +0000
commitc8a3c1ee02e917b2e553d403b7bf94cbc736f51d (patch)
treeef1135578055968e761fd14a7dabb505544e95f1 /apps/web
parent463d041f0b321348593087047a14a0d9a433d60f (diff)
downloadkarakeep-c8a3c1ee02e917b2e553d403b7bf94cbc736f51d.tar.zst
feature(web): Allow users to export their links and notes
Diffstat (limited to 'apps/web')
-rw-r--r--apps/web/app/api/bookmarks/export/route.tsx66
-rw-r--r--apps/web/components/dashboard/settings/ImportExport.tsx29
2 files changed, 91 insertions, 4 deletions
diff --git a/apps/web/app/api/bookmarks/export/route.tsx b/apps/web/app/api/bookmarks/export/route.tsx
new file mode 100644
index 00000000..3b12b878
--- /dev/null
+++ b/apps/web/app/api/bookmarks/export/route.tsx
@@ -0,0 +1,66 @@
+import { api, createContextFromRequest } from "@/server/api/client";
+
+import type { ZBookmark } from "@hoarder/shared/types/bookmarks";
+import {
+ BookmarkTypes,
+ MAX_NUM_BOOKMARKS_PER_PAGE,
+} from "@hoarder/shared/types/bookmarks";
+
+function toExportFormat(bookmark: ZBookmark) {
+ return {
+ createdAt: bookmark.createdAt.toISOString(),
+ title:
+ bookmark.title ??
+ (bookmark.content.type === BookmarkTypes.LINK
+ ? bookmark.content.title
+ : null),
+ tags: bookmark.tags.map((t) => t.name),
+ type: bookmark.content.type,
+ url:
+ bookmark.content.type === BookmarkTypes.LINK
+ ? bookmark.content.url
+ : undefined,
+ text:
+ bookmark.content.type === BookmarkTypes.TEXT
+ ? bookmark.content.text
+ : undefined,
+ note: bookmark.note,
+ };
+}
+
+export const dynamic = "force-dynamic";
+export async function GET(request: Request) {
+ const ctx = await createContextFromRequest(request);
+ if (!ctx.user) {
+ return Response.json({ error: "Unauthorized" }, { status: 401 });
+ }
+ const req = {
+ limit: MAX_NUM_BOOKMARKS_PER_PAGE,
+ useCursorV2: true,
+ };
+
+ let resp = await api.bookmarks.getBookmarks(req);
+ let results = resp.bookmarks.map(toExportFormat);
+
+ while (resp.nextCursor) {
+ resp = await api.bookmarks.getBookmarks({
+ ...request,
+ cursor: resp.nextCursor,
+ });
+ results = [...results, ...resp.bookmarks.map(toExportFormat)];
+ }
+
+ return new Response(
+ JSON.stringify({
+ // Exclude asset types for now
+ bookmarks: results.filter((b) => b.type !== BookmarkTypes.ASSET),
+ }),
+ {
+ status: 200,
+ headers: {
+ "Content-type": "application/json",
+ "Content-disposition": `attachment; filename="hoarder-export-${new Date().toISOString()}.json"`,
+ },
+ },
+ );
+}
diff --git a/apps/web/components/dashboard/settings/ImportExport.tsx b/apps/web/components/dashboard/settings/ImportExport.tsx
index a19db7fd..25b2073c 100644
--- a/apps/web/components/dashboard/settings/ImportExport.tsx
+++ b/apps/web/components/dashboard/settings/ImportExport.tsx
@@ -1,7 +1,9 @@
"use client";
import { useState } from "react";
+import Link from "next/link";
import { useRouter } from "next/navigation";
+import { buttonVariants } from "@/components/ui/button";
import FilePickerButton from "@/components/ui/file-picker-button";
import { Progress } from "@/components/ui/progress";
import { toast } from "@/components/ui/use-toast";
@@ -10,9 +12,10 @@ import {
parseNetscapeBookmarkFile,
parsePocketBookmarkFile,
} from "@/lib/importBookmarkParser";
+import { cn } from "@/lib/utils";
import { useMutation } from "@tanstack/react-query";
import { TRPCClientError } from "@trpc/client";
-import { Upload } from "lucide-react";
+import { Download, Upload } from "lucide-react";
import {
useCreateBookmarkWithPostHook,
@@ -25,7 +28,22 @@ import {
} from "@hoarder/shared-react/hooks/lists";
import { BookmarkTypes } from "@hoarder/shared/types/bookmarks";
-export function Import() {
+export function ExportButton() {
+ return (
+ <Link
+ href="/api/bookmarks/export"
+ className={cn(
+ buttonVariants({ variant: "default" }),
+ "flex items-center gap-2",
+ )}
+ >
+ <Download />
+ <p>Export Links and Notes</p>
+ </Link>
+ );
+}
+
+export function ImportExportRow() {
const router = useRouter();
const [importProgress, setImportProgress] = useState<{
@@ -195,6 +213,7 @@ export function Import() {
<Upload />
<p>Import Bookmarks from Pocket export</p>
</FilePickerButton>
+ <ExportButton />
</div>
{importProgress && (
<div className="flex flex-col gap-2">
@@ -216,10 +235,12 @@ export default function ImportExport() {
return (
<div>
<div className="flex items-center justify-between">
- <div className="mb-4 text-lg font-medium">Import Bookmarks</div>
+ <div className="mb-4 text-lg font-medium">
+ Import / Export Bookmarks
+ </div>
</div>
<div className="mt-2">
- <Import />
+ <ImportExportRow />
</div>
</div>
);