From 52024ab52724b45c08f437f9f10805adefe2bf0e Mon Sep 17 00:00:00 2001 From: MohamedBassem Date: Sat, 21 Sep 2024 15:23:23 +0000 Subject: [PATCH] feature(web): Move bookmark imports into settings --- apps/web/app/dashboard/settings/page.tsx | 4 + .../components/dashboard/UploadDropzone.tsx | 29 +---- .../dashboard/settings/ImportExport.tsx | 108 ++++++++++++++++++ apps/web/components/ui/file-picker-button.tsx | 51 +++++++++ 4 files changed, 165 insertions(+), 27 deletions(-) create mode 100644 apps/web/components/dashboard/settings/ImportExport.tsx create mode 100644 apps/web/components/ui/file-picker-button.tsx diff --git a/apps/web/app/dashboard/settings/page.tsx b/apps/web/app/dashboard/settings/page.tsx index bab76794..e33a57ab 100644 --- a/apps/web/app/dashboard/settings/page.tsx +++ b/apps/web/app/dashboard/settings/page.tsx @@ -1,5 +1,6 @@ import ApiKeySettings from "@/components/dashboard/settings/ApiKeySettings"; import { ChangePassword } from "@/components/dashboard/settings/ChangePassword"; +import ImportExport from "@/components/dashboard/settings/ImportExport"; import UserDetails from "@/components/dashboard/settings/UserDetails"; export default async function Settings() { @@ -9,6 +10,9 @@ export default async function Settings() { +
+ +
diff --git a/apps/web/components/dashboard/UploadDropzone.tsx b/apps/web/components/dashboard/UploadDropzone.tsx index e57f9294..335ac72a 100644 --- a/apps/web/components/dashboard/UploadDropzone.tsx +++ b/apps/web/components/dashboard/UploadDropzone.tsx @@ -2,9 +2,7 @@ import React, { useCallback, useState } from "react"; import useUpload from "@/lib/hooks/upload-file"; -import { parseNetscapeBookmarkFile } from "@/lib/netscapeBookmarkParser"; import { cn } from "@/lib/utils"; -import { useMutation } from "@tanstack/react-query"; import { TRPCClientError } from "@trpc/client"; import DropZone from "react-dropzone"; @@ -46,34 +44,11 @@ export function useUploadAsset() { }, }); - const { mutateAsync: runUploadBookmarkFile } = useMutation({ - mutationFn: async (file: File) => { - return await parseNetscapeBookmarkFile(file); - }, - onSuccess: async (resp) => { - return Promise.all( - resp.map((url) => - createBookmark({ type: BookmarkTypes.LINK, url: url.toString() }), - ), - ); - }, - onError: (error) => { - toast({ - description: error.message, - variant: "destructive", - }); - }, - }); - return useCallback( (file: File) => { - if (file.type === "text/html") { - return runUploadBookmarkFile(file); - } else { - return runUploadAsset(file); - } + return runUploadAsset(file); }, - [runUploadAsset, runUploadBookmarkFile], + [runUploadAsset], ); } diff --git a/apps/web/components/dashboard/settings/ImportExport.tsx b/apps/web/components/dashboard/settings/ImportExport.tsx new file mode 100644 index 00000000..75de14ac --- /dev/null +++ b/apps/web/components/dashboard/settings/ImportExport.tsx @@ -0,0 +1,108 @@ +"use client"; + +import assert from "assert"; +import { useRouter } from "next/navigation"; +import FilePickerButton from "@/components/ui/file-picker-button"; +import { toast } from "@/components/ui/use-toast"; +import { parseNetscapeBookmarkFile } from "@/lib/netscapeBookmarkParser"; +import { useMutation } from "@tanstack/react-query"; +import { Upload } from "lucide-react"; + +import { useCreateBookmarkWithPostHook } from "@hoarder/shared-react/hooks/bookmarks"; +import { + useAddBookmarkToList, + useCreateBookmarkList, +} from "@hoarder/shared-react/hooks/lists"; +import { BookmarkTypes } from "@hoarder/shared/types/bookmarks"; + +export function Import() { + const router = useRouter(); + const { mutateAsync: createBookmark } = useCreateBookmarkWithPostHook(); + + const { mutateAsync: createList } = useCreateBookmarkList(); + const { mutateAsync: addToList } = useAddBookmarkToList(); + + const { mutateAsync: runUploadBookmarkFile } = useMutation({ + mutationFn: async (file: File) => { + return await parseNetscapeBookmarkFile(file); + }, + onSuccess: async (resp) => { + const results = await Promise.allSettled( + resp.map((url) => + createBookmark({ type: BookmarkTypes.LINK, url: url.toString() }), + ), + ); + + const failed = results.filter((r) => r.status == "rejected"); + const successes = results.filter( + (r) => r.status == "fulfilled" && !r.value.alreadyExists, + ); + const alreadyExisted = results.filter( + (r) => r.status == "fulfilled" && r.value.alreadyExists, + ); + + 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", + }); + } + + const importList = await createList({ + name: `Imported Bookmarks`, + icon: "⬆️", + }); + + if (successes.length > 0) { + await Promise.allSettled( + successes.map((r) => { + assert(r.status == "fulfilled"); + addToList({ bookmarkId: r.value.id, listId: importList.id }); + }), + ); + } + + router.push(`/dashboard/lists/${importList.id}`); + }, + onError: (error) => { + toast({ + description: error.message, + variant: "destructive", + }); + }, + }); + + return ( +
+ + +

Import Bookmarks from HTML file

+
+
+ ); +} + +export default function ImportExport() { + return ( +
+
+
Import Bookmarks
+
+
+ +
+
+ ); +} diff --git a/apps/web/components/ui/file-picker-button.tsx b/apps/web/components/ui/file-picker-button.tsx new file mode 100644 index 00000000..ccac1643 --- /dev/null +++ b/apps/web/components/ui/file-picker-button.tsx @@ -0,0 +1,51 @@ +import React, { ChangeEvent, useRef } from "react"; + +import { Button, ButtonProps } from "./button"; + +interface FilePickerButtonProps extends Omit { + onFileSelect?: (file: File) => void; + accept?: string; + multiple?: boolean; +} + +const FilePickerButton: React.FC = ({ + onFileSelect, + accept, + multiple = false, + ...buttonProps +}) => { + const fileInputRef = useRef(null); + + const handleButtonClick = () => { + fileInputRef.current?.click(); + }; + + const handleFileChange = (event: ChangeEvent) => { + const files = event.target.files; + if (files && files.length > 0) { + if (onFileSelect) { + if (multiple) { + Array.from(files).forEach(onFileSelect); + } else { + onFileSelect(files[0]); + } + } + } + }; + + return ( +
+
+ ); +}; + +export default FilePickerButton;