diff options
| -rw-r--r-- | apps/web/app/api/bookmarks/export/route.tsx | 66 | ||||
| -rw-r--r-- | apps/web/components/dashboard/settings/ImportExport.tsx | 29 |
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> ); |
