diff options
Diffstat (limited to 'apps/web')
| -rw-r--r-- | apps/web/components/dashboard/tags/AllTagsView.tsx | 2 | ||||
| -rw-r--r-- | apps/web/components/dashboard/tags/CreateTagModal.tsx | 145 | ||||
| -rw-r--r-- | apps/web/lib/i18n/locales/en/translation.json | 10 |
3 files changed, 156 insertions, 1 deletions
diff --git a/apps/web/components/dashboard/tags/AllTagsView.tsx b/apps/web/components/dashboard/tags/AllTagsView.tsx index 965afa73..73d9a595 100644 --- a/apps/web/components/dashboard/tags/AllTagsView.tsx +++ b/apps/web/components/dashboard/tags/AllTagsView.tsx @@ -29,6 +29,7 @@ import type { ZGetTagResponse, ZTagBasic } from "@karakeep/shared/types/tags"; import { useDeleteUnusedTags } from "@karakeep/shared-react/hooks/tags"; import BulkTagAction from "./BulkTagAction"; +import { CreateTagModal } from "./CreateTagModal"; import DeleteTagConfirmationDialog from "./DeleteTagConfirmationDialog"; import { MultiTagSelector } from "./MultiTagSelector"; import { TagPill } from "./TagPill"; @@ -184,6 +185,7 @@ export default function AllTagsView({ <div className="flex justify-between gap-x-2"> <span className="text-2xl">{t("tags.all_tags")}</span> <div className="flex gap-x-2"> + <CreateTagModal /> <BulkTagAction /> <Toggle variant="outline" diff --git a/apps/web/components/dashboard/tags/CreateTagModal.tsx b/apps/web/components/dashboard/tags/CreateTagModal.tsx new file mode 100644 index 00000000..3a4c4995 --- /dev/null +++ b/apps/web/components/dashboard/tags/CreateTagModal.tsx @@ -0,0 +1,145 @@ +"use client"; + +import { useState } from "react"; +import { ActionButton } from "@/components/ui/action-button"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { toast } from "@/components/ui/use-toast"; +import { useTranslation } from "@/lib/i18n/client"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Plus } from "lucide-react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +import { useCreateTag } from "@karakeep/shared-react/hooks/tags"; + +const formSchema = z.object({ + name: z.string().trim().min(1, "Tag name is required"), +}); + +export function CreateTagModal() { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + + const form = useForm<z.infer<typeof formSchema>>({ + resolver: zodResolver(formSchema), + defaultValues: { + name: "", + }, + }); + + const { mutate: createTag, isPending } = useCreateTag({ + onSuccess: () => { + toast({ + description: t("toasts.tags.created"), + }); + setOpen(false); + form.reset(); + }, + onError: (e) => { + if (e.data?.code === "BAD_REQUEST") { + if (e.data.zodError) { + toast({ + variant: "destructive", + description: Object.values(e.data.zodError.fieldErrors) + .flat() + .join("\n"), + }); + } else { + toast({ + variant: "destructive", + description: e.message, + }); + } + } else { + toast({ + variant: "destructive", + title: t("common.something_went_wrong"), + description: t("toasts.tags.failed_to_create"), + }); + } + }, + }); + + return ( + <Dialog + open={open} + onOpenChange={(isOpen) => { + setOpen(isOpen); + if (!isOpen) { + form.reset(); + } + }} + > + <DialogTrigger asChild> + <Button variant="outline" className="bg-background"> + <Plus className="mr-2 size-4" /> + {t("tags.create_tag")} + </Button> + </DialogTrigger> + <DialogContent> + <Form {...form}> + <form + onSubmit={form.handleSubmit((values) => { + createTag(values); + })} + > + <DialogHeader> + <DialogTitle>{t("tags.create_tag")}</DialogTitle> + <DialogDescription> + {t("tags.create_tag_description")} + </DialogDescription> + </DialogHeader> + <div className="py-4"> + <FormField + control={form.control} + name="name" + render={({ field }) => ( + <FormItem> + <FormLabel>{t("tags.tag_name")}</FormLabel> + <FormControl> + <Input + placeholder={t("tags.enter_tag_name")} + autoFocus + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </div> + <DialogFooter> + <DialogClose asChild> + <Button type="button" variant="outline"> + {t("actions.cancel")} + </Button> + </DialogClose> + <ActionButton type="submit" loading={isPending}> + {t("actions.create")} + </ActionButton> + </DialogFooter> + </form> + </Form> + </DialogContent> + </Dialog> + ); +} diff --git a/apps/web/lib/i18n/locales/en/translation.json b/apps/web/lib/i18n/locales/en/translation.json index a5dddd56..065b5ed6 100644 --- a/apps/web/lib/i18n/locales/en/translation.json +++ b/apps/web/lib/i18n/locales/en/translation.json @@ -443,7 +443,11 @@ "delete_all_unused_tags": "Delete All Unused Tags", "drag_and_drop_merging": "Drag & Drop Merging", "drag_and_drop_merging_info": "Drag and drop tags on each other to merge them", - "sort_by_name": "Sort by Name" + "sort_by_name": "Sort by Name", + "create_tag": "Create Tag", + "create_tag_description": "Create a new tag without attaching it to any bookmark", + "tag_name": "Tag Name", + "enter_tag_name": "Enter tag name" }, "search": { "is_favorited": "Is Favorited", @@ -573,6 +577,10 @@ "updated": "List has been updated!", "merged": "List has been merged!", "deleted": "List has been deleted!" + }, + "tags": { + "created": "Tag has been created!", + "failed_to_create": "Failed to create tag" } }, "banners": { |
