"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"; import { ParsedBookmark, parseHoarderBookmarkFile, parseNetscapeBookmarkFile, parseOmnivoreBookmarkFile, parsePocketBookmarkFile, } from "@/lib/importBookmarkParser"; import { cn } from "@/lib/utils"; import { useMutation } from "@tanstack/react-query"; import { TRPCClientError } from "@trpc/client"; import { Download, Upload } from "lucide-react"; import { useCreateBookmarkWithPostHook, useUpdateBookmark, useUpdateBookmarkTags, } from "@hoarder/shared-react/hooks/bookmarks"; import { useAddBookmarkToList, useCreateBookmarkList, } from "@hoarder/shared-react/hooks/lists"; import { BookmarkTypes } from "@hoarder/shared/types/bookmarks"; export function ExportButton() { return (

Export Links and Notes

); } export function ImportExportRow() { const router = useRouter(); const [importProgress, setImportProgress] = useState<{ done: number; total: number; } | null>(null); const { mutateAsync: createBookmark } = useCreateBookmarkWithPostHook(); const { mutateAsync: updateBookmark } = useUpdateBookmark(); const { mutateAsync: createList } = useCreateBookmarkList(); const { mutateAsync: addToList } = useAddBookmarkToList(); const { mutateAsync: updateTags } = useUpdateBookmarkTags(); const { mutateAsync: parseAndCreateBookmark } = useMutation({ mutationFn: async (toImport: { bookmark: ParsedBookmark; listId: string; }) => { const bookmark = toImport.bookmark; if (bookmark.content === undefined) { throw new Error("Content is undefined"); } const created = await createBookmark( bookmark.content.type === BookmarkTypes.LINK ? { type: BookmarkTypes.LINK, url: bookmark.content.url, } : { type: BookmarkTypes.TEXT, text: bookmark.content.text, }, ); await Promise.all([ // Update title and createdAt if they're set bookmark.title.length > 0 || bookmark.addDate ? updateBookmark({ bookmarkId: created.id, title: bookmark.title, createdAt: bookmark.addDate ? new Date(bookmark.addDate * 1000) : undefined, note: bookmark.notes, }).catch(() => { /* empty */ }) : undefined, // Add to import list addToList({ bookmarkId: created.id, listId: toImport.listId, }).catch((e) => { if ( e instanceof TRPCClientError && e.message.includes("already in the list") ) { /* empty */ } else { throw e; } }), // Update tags bookmark.tags.length > 0 ? updateTags({ bookmarkId: created.id, attach: bookmark.tags.map((t) => ({ tagName: t })), detach: [], }) : undefined, ]); return created; }, }); const { mutateAsync: runUploadBookmarkFile } = useMutation({ mutationFn: async ({ file, source, }: { file: File; source: "html" | "pocket" | "omnivore" | "hoarder"; }) => { if (source === "html") { return await parseNetscapeBookmarkFile(file); } else if (source === "pocket") { return await parsePocketBookmarkFile(file); } else if (source === "hoarder") { return await parseHoarderBookmarkFile(file); } else if (source === "omnivore") { return await parseOmnivoreBookmarkFile(file); } else { throw new Error("Unknown source"); } }, onSuccess: async (resp) => { const importList = await createList({ name: `Imported Bookmarks`, icon: "⬆️", }); setImportProgress({ done: 0, total: resp.length }); const successes = []; const failed = []; const alreadyExisted = []; // Do the imports one by one for (const parsedBookmark of resp) { try { const result = await parseAndCreateBookmark({ bookmark: parsedBookmark, listId: importList.id, }); if (result.alreadyExists) { alreadyExisted.push(parsedBookmark); } else { successes.push(parsedBookmark); } } catch (e) { failed.push(parsedBookmark); } setImportProgress((prev) => ({ done: (prev?.done ?? 0) + 1, total: resp.length, })); } if (successes.length > 0 || alreadyExisted.length > 0) { toast({ description: `Imported ${successes.length} bookmarks and skipped ${alreadyExisted.length} bookmarks that already existed`, variant: "default", }); } if (failed.length > 0) { toast({ description: `Failed to import ${failed.length} bookmarks`, variant: "destructive", }); } router.push(`/dashboard/lists/${importList.id}`); }, onError: (error) => { toast({ description: error.message, variant: "destructive", }); }, }); return (
runUploadBookmarkFile({ file, source: "html" }) } >

Import Bookmarks from HTML file

runUploadBookmarkFile({ file, source: "pocket" }) } >

Import Bookmarks from Pocket export

runUploadBookmarkFile({ file, source: "omnivore" }) } >

Import Bookmarks from Omnivore export

runUploadBookmarkFile({ file, source: "hoarder" }) } >

Import Bookmarks from Hoarder export

{importProgress && (

Processed {importProgress.done} of {importProgress.total} bookmarks

)}
); } export default function ImportExport() { return (

Import / Export Bookmarks

); }