Skip to content

Commit

Permalink
feature(web): Move bookmark imports into settings
Browse files Browse the repository at this point in the history
  • Loading branch information
MohamedBassem committed Sep 21, 2024
1 parent 6b5c597 commit 52024ab
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 27 deletions.
4 changes: 4 additions & 0 deletions apps/web/app/dashboard/settings/page.tsx
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -9,6 +10,9 @@ export default async function Settings() {
<UserDetails />
<ChangePassword />
</div>
<div className="mt-4 rounded-md border bg-background p-4">
<ImportExport />
</div>
<div className="mt-4 rounded-md border bg-background p-4">
<ApiKeySettings />
</div>
Expand Down
29 changes: 2 additions & 27 deletions apps/web/components/dashboard/UploadDropzone.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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],
);
}

Expand Down
108 changes: 108 additions & 0 deletions apps/web/components/dashboard/settings/ImportExport.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div>
<FilePickerButton
accept=".html"
multiple={false}
className="flex items-center gap-2"
onFileSelect={runUploadBookmarkFile}
>
<Upload />
<p>Import Bookmarks from HTML file</p>
</FilePickerButton>
</div>
);
}

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>
<div className="mt-2">
<Import />
</div>
</div>
);
}
51 changes: 51 additions & 0 deletions apps/web/components/ui/file-picker-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React, { ChangeEvent, useRef } from "react";

import { Button, ButtonProps } from "./button";

interface FilePickerButtonProps extends Omit<ButtonProps, "onClick"> {
onFileSelect?: (file: File) => void;
accept?: string;
multiple?: boolean;
}

const FilePickerButton: React.FC<FilePickerButtonProps> = ({
onFileSelect,
accept,
multiple = false,
...buttonProps
}) => {
const fileInputRef = useRef<HTMLInputElement>(null);

const handleButtonClick = () => {
fileInputRef.current?.click();
};

const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
const files = event.target.files;
if (files && files.length > 0) {
if (onFileSelect) {
if (multiple) {
Array.from(files).forEach(onFileSelect);
} else {
onFileSelect(files[0]);
}
}
}
};

return (
<div>
<Button onClick={handleButtonClick} {...buttonProps} />
<input
type="file"
ref={fileInputRef}
onChange={handleFileChange}
className="hidden"
accept={accept}
multiple={multiple}
/>
</div>
);
};

export default FilePickerButton;

0 comments on commit 52024ab

Please sign in to comment.