"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 { useTranslation } from "@/lib/i18n/client";
import {
ParsedBookmark,
parseHoarderBookmarkFile,
parseLinkwardenBookmarkFile,
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,
useUpdateBookmarkTags,
} from "@hoarder/shared-react/hooks/bookmarks";
import {
useAddBookmarkToList,
useCreateBookmarkList,
} from "@hoarder/shared-react/hooks/lists";
import { BookmarkTypes } from "@hoarder/shared/types/bookmarks";
import { Card, CardContent } from "../ui/card";
function ImportCard({
text,
description,
children,
}: {
text: string;
description: string;
children: React.ReactNode;
}) {
return (
{children}
);
}
function ExportButton() {
const { t } = useTranslation();
return (
Export File
{t("settings.import.export_links_and_notes")}
Export
);
}
export function ImportExportRow() {
const { t } = useTranslation();
const router = useRouter();
const [importProgress, setImportProgress] = useState<{
done: number;
total: number;
} | null>(null);
const { mutateAsync: createBookmark } = useCreateBookmarkWithPostHook();
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({
title: bookmark.title,
createdAt: bookmark.addDate
? new Date(bookmark.addDate * 1000)
: undefined,
note: bookmark.notes,
...(bookmark.content.type === BookmarkTypes.LINK
? {
type: BookmarkTypes.LINK,
url: bookmark.content.url,
}
: {
type: BookmarkTypes.TEXT,
text: bookmark.content.text,
}),
});
await Promise.all([
// 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" | "linkwarden";
}) => {
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 if (source === "linkwarden") {
return await parseLinkwardenBookmarkFile(file);
} else {
throw new Error("Unknown source");
}
},
onSuccess: async (resp) => {
const importList = await createList({
name: t("settings.import.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
runUploadBookmarkFile({ file, source: "pocket" })
}
>
Import
runUploadBookmarkFile({ file, source: "omnivore" })
}
>
Import
runUploadBookmarkFile({ file, source: "linkwarden" })
}
>
Import
runUploadBookmarkFile({ file, source: "hoarder" })
}
>
Import
{importProgress && (
Processed {importProgress.done} of {importProgress.total} bookmarks
)}
);
}
export default function ImportExport() {
const { t } = useTranslation();
return (
{t("settings.import.import_export_bookmarks")}
);
}