From c8a3c1ee02e917b2e553d403b7bf94cbc736f51d Mon Sep 17 00:00:00 2001 From: MohamedBassem Date: Sat, 5 Oct 2024 14:42:22 +0000 Subject: feature(web): Allow users to export their links and notes --- apps/web/app/api/bookmarks/export/route.tsx | 66 ++++++++++++++++++++++ .../components/dashboard/settings/ImportExport.tsx | 29 ++++++++-- 2 files changed, 91 insertions(+), 4 deletions(-) create mode 100644 apps/web/app/api/bookmarks/export/route.tsx (limited to 'apps/web') 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 ( + + +

Export Links and Notes

+ + ); +} + +export function ImportExportRow() { const router = useRouter(); const [importProgress, setImportProgress] = useState<{ @@ -195,6 +213,7 @@ export function Import() {

Import Bookmarks from Pocket export

+ {importProgress && (
@@ -216,10 +235,12 @@ export default function ImportExport() { return (
-
Import Bookmarks
+
+ Import / Export Bookmarks +
- +
); -- cgit v1.2.3-70-g09d2