Skip to content

Commit

Permalink
feature(web): Allow adding multiple URLs at once #158 (#167)
Browse files Browse the repository at this point in the history
Added a reusable dialog
opening a dialog that allows you to decide if you want to import multiple URLs at once if you provide only that

Co-authored-by: kamtschatka <simon.schatka@gmx.at>
  • Loading branch information
kamtschatka and kamtschatka committed May 19, 2024
1 parent cb62db7 commit d1ad84b
Show file tree
Hide file tree
Showing 2 changed files with 129 additions and 5 deletions.
79 changes: 74 additions & 5 deletions apps/web/components/dashboard/bookmarks/EditorCard.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { SubmitErrorHandler, SubmitHandler } from "react-hook-form";
import { useEffect, useImperativeHandle, useRef } from "react";
import React, { useEffect, useImperativeHandle, useRef } from "react";
import Link from "next/link";
import { ActionButton } from "@/components/ui/action-button";
import { Form, FormControl, FormItem } from "@/components/ui/form";
import InfoTooltip from "@/components/ui/info-tooltip";
import MultipleChoiceDialog from "@/components/ui/multiple-choice-dialog";
import { Separator } from "@/components/ui/separator";
import { Textarea } from "@/components/ui/textarea";
import { toast } from "@/components/ui/use-toast";
Expand Down Expand Up @@ -34,9 +35,17 @@ function useFocusOnKeyPress(inputRef: React.RefObject<HTMLTextAreaElement>) {
}, [inputRef]);
}

interface MultiUrlImportState {
urls: URL[];
text: string;
}

export default function EditorCard({ className }: { className?: string }) {
const inputRef = useRef<HTMLTextAreaElement>(null);

const [multiUrlImportState, setMultiUrlImportState] =
React.useState<MultiUrlImportState | null>(null);

const demoMode = !!useClientConfig().demoMode;
const formSchema = z.object({
text: z.string(),
Expand Down Expand Up @@ -76,14 +85,31 @@ export default function EditorCard({ className }: { className?: string }) {
},
});

const onSubmit: SubmitHandler<z.infer<typeof formSchema>> = (data) => {
const text = data.text.trim();
try {
const url = new URL(text);
function tryToImportUrls(text: string): void {
const lines = text.split("\n");
const urls: URL[] = [];
for (const line of lines) {
// parsing can also throw an exception, but will be caught outside
const url = new URL(line);
if (url.protocol != "http:" && url.protocol != "https:") {
throw new Error("Invalid URL");
}
urls.push(url);
}

if (urls.length === 1) {
// Only 1 url in the textfield --> simply import it
mutate({ type: "link", url: text });
return;
}
// multiple urls found --> ask the user if it should be imported as multiple URLs or as a text bookmark
setMultiUrlImportState({ urls, text });
}

const onSubmit: SubmitHandler<z.infer<typeof formSchema>> = (data) => {
const text = data.text.trim();
try {
tryToImportUrls(text);
} catch (e) {
// Not a URL
mutate({ type: "text", text });
Expand Down Expand Up @@ -150,6 +176,49 @@ export default function EditorCard({ className }: { className?: string }) {
: "Press ⌘ + Enter to Save"
: "Save"}
</ActionButton>

{multiUrlImportState && (
<MultipleChoiceDialog
open={true}
title={`Import URLs as separate Bookmarks?`}
description={`The input contains multiple URLs on separate lines. Do you want to import them as separate bookmarks?`}
onOpenChange={(open) => {
if (!open) {
setMultiUrlImportState(null);
}
}}
actionButtons={[
() => (
<ActionButton
type="button"
variant="secondary"
loading={isPending}
onClick={() => {
mutate({ type: "text", text: multiUrlImportState.text });
setMultiUrlImportState(null);
}}
>
Import as Text Bookmark
</ActionButton>
),
() => (
<ActionButton
type="button"
variant="destructive"
loading={isPending}
onClick={() => {
multiUrlImportState.urls.forEach((url) =>
mutate({ type: "link", url: url.toString() }),
);
setMultiUrlImportState(null);
}}
>
Import as separate Bookmarks
</ActionButton>
),
]}
></MultipleChoiceDialog>
)}
</form>
</Form>
);
Expand Down
55 changes: 55 additions & 0 deletions apps/web/components/ui/multiple-choice-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { useState } from "react";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";

export default function MultipleChoiceDialog({
open: userIsOpen,
setOpen: userSetOpen,
onOpenChange,
title,
description,
actionButtons,
children,
}: {
open?: boolean;
setOpen?: (v: boolean) => void;
onOpenChange?: (open: boolean) => void;
title: React.ReactNode;
description: React.ReactNode;
actionButtons: ((
setDialogOpen: (open: boolean) => void,
) => React.ReactNode)[];
children?: React.ReactNode;
}) {
const [customIsOpen, setCustomIsOpen] = useState(false);
const [isDialogOpen, setDialogOpen] = [
userIsOpen ?? customIsOpen,
userSetOpen ?? setCustomIsOpen,
];
return (
<Dialog
open={isDialogOpen}
onOpenChange={(isOpen) => {
onOpenChange?.(isOpen);
setDialogOpen(isOpen);
}}
>
{children && <DialogTrigger asChild>{children}</DialogTrigger>}
<DialogContent>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
</DialogHeader>
{description}
<DialogFooter className="sm:justify-end">
{actionButtons.map((actionButton) => actionButton(setDialogOpen))}
</DialogFooter>
</DialogContent>
</Dialog>
);
}

0 comments on commit d1ad84b

Please sign in to comment.