aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web
diff options
context:
space:
mode:
authorMohamedBassem <me@mbassem.com>2024-03-17 08:43:10 +0000
committerMohamedBassem <me@mbassem.com>2024-03-17 08:43:10 +0000
commit0b99fe783aaebc5baca40f9d1b837278811cd228 (patch)
treec83fa0d3507d1a0e362d3444cf7eb6b4205f9760 /apps/web
parent566486fbd98f81d81c266cda4c9750afa207dc7e (diff)
downloadkarakeep-0b99fe783aaebc5baca40f9d1b837278811cd228.tar.zst
ui(web): Change TagsEditor to auto save on edit and extract ActionBar to its own component
Diffstat (limited to 'apps/web')
-rw-r--r--apps/web/components/dashboard/bookmarks/BookmarkActionBar.tsx38
-rw-r--r--apps/web/components/dashboard/bookmarks/LinkCard.tsx20
-rw-r--r--apps/web/components/dashboard/bookmarks/TagModal.tsx160
-rw-r--r--apps/web/components/dashboard/bookmarks/TagsEditor.tsx133
-rw-r--r--apps/web/components/dashboard/bookmarks/TextCard.tsx23
-rw-r--r--apps/web/next.config.mjs2
6 files changed, 179 insertions, 197 deletions
diff --git a/apps/web/components/dashboard/bookmarks/BookmarkActionBar.tsx b/apps/web/components/dashboard/bookmarks/BookmarkActionBar.tsx
new file mode 100644
index 00000000..0d98cc1f
--- /dev/null
+++ b/apps/web/components/dashboard/bookmarks/BookmarkActionBar.tsx
@@ -0,0 +1,38 @@
+import { Button } from "@/components/ui/button";
+import { DialogContent } from "@/components/ui/dialog";
+import { Dialog, DialogTrigger } from "@radix-ui/react-dialog";
+import { Maximize2, Star } from "lucide-react";
+
+import type { ZBookmark } from "@hoarder/trpc/types/bookmarks";
+
+import BookmarkOptions from "./BookmarkOptions";
+import BookmarkPreview from "./BookmarkPreview";
+
+export default function BookmarkActionBar({
+ bookmark,
+}: {
+ bookmark: ZBookmark;
+}) {
+ return (
+ <div className="flex text-gray-500">
+ {bookmark.favourited && (
+ <Star
+ className="m-1 size-8 rounded p-1"
+ color="#ebb434"
+ fill="#ebb434"
+ />
+ )}
+ <Dialog>
+ <DialogTrigger asChild>
+ <Button variant="ghost" className="my-auto block px-2">
+ <Maximize2 size="20" />
+ </Button>
+ </DialogTrigger>
+ <DialogContent className="h-[90%] max-w-[90%] overflow-hidden">
+ <BookmarkPreview initialData={bookmark} />
+ </DialogContent>
+ </Dialog>
+ <BookmarkOptions bookmark={bookmark} />
+ </div>
+ );
+}
diff --git a/apps/web/components/dashboard/bookmarks/LinkCard.tsx b/apps/web/components/dashboard/bookmarks/LinkCard.tsx
index 808e6d91..20f4dd79 100644
--- a/apps/web/components/dashboard/bookmarks/LinkCard.tsx
+++ b/apps/web/components/dashboard/bookmarks/LinkCard.tsx
@@ -15,11 +15,10 @@ import {
isBookmarkStillTagging,
} from "@/lib/bookmarkUtils";
import { api } from "@/lib/trpc";
-import { Maximize2, Star } from "lucide-react";
import type { ZBookmark } from "@hoarder/trpc/types/bookmarks";
-import BookmarkOptions from "./BookmarkOptions";
+import BookmarkActionBar from "./BookmarkActionBar";
import TagList from "./TagList";
export default function LinkCard({
@@ -92,22 +91,7 @@ export default function LinkCard({
{parsedUrl.host}
</Link>
</div>
- <div className="flex">
- {bookmark.favourited && (
- <Star
- className="m-1 size-8 rounded p-1"
- color="#ebb434"
- fill="#ebb434"
- />
- )}
- <Link
- className="my-auto block px-2"
- href={`/dashboard/preview/${bookmark.id}`}
- >
- <Maximize2 size="20" />
- </Link>
- <BookmarkOptions bookmark={bookmark} />
- </div>
+ <BookmarkActionBar bookmark={bookmark} />
</div>
</ImageCardFooter>
</ImageCardContent>
diff --git a/apps/web/components/dashboard/bookmarks/TagModal.tsx b/apps/web/components/dashboard/bookmarks/TagModal.tsx
index 367e6e7d..6bc16a89 100644
--- a/apps/web/components/dashboard/bookmarks/TagModal.tsx
+++ b/apps/web/components/dashboard/bookmarks/TagModal.tsx
@@ -1,6 +1,4 @@
-import type { KeyboardEvent } from "react";
-import { useEffect, useState } from "react";
-import { ActionButton } from "@/components/ui/action-button";
+import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
@@ -10,105 +8,10 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
-import { Input } from "@/components/ui/input";
-import { toast } from "@/components/ui/use-toast";
-import { api } from "@/lib/trpc";
-import { cn } from "@/lib/utils";
-import { Sparkles, X } from "lucide-react";
import type { ZBookmark } from "@hoarder/trpc/types/bookmarks";
-import type { ZAttachedByEnum } from "@hoarder/trpc/types/tags";
-interface EditableTag {
- attachedBy: ZAttachedByEnum;
- id?: string;
- name: string;
-}
-
-function TagAddInput({ addTag }: { addTag: (tag: string) => void }) {
- const onKeyUp = (e: KeyboardEvent<HTMLInputElement>) => {
- if (e.key === "Enter") {
- addTag(e.currentTarget.value);
- e.currentTarget.value = "";
- }
- };
- return (
- <Input
- onKeyUp={onKeyUp}
- className="h-8 w-full border-none focus-visible:ring-0 focus-visible:ring-offset-0"
- />
- );
-}
-
-function TagPill({
- tag,
- deleteCB,
-}: {
- tag: { attachedBy: ZAttachedByEnum; id?: string; name: string };
- deleteCB: () => void;
-}) {
- const isAttachedByAI = tag.attachedBy == "ai";
- return (
- <div
- className={cn(
- "flex min-h-8 space-x-1 rounded px-2",
- isAttachedByAI
- ? "bg-gradient-to-tr from-purple-500 to-purple-400 text-white"
- : "bg-gray-200",
- )}
- >
- {isAttachedByAI && <Sparkles className="m-auto size-4" />}
- <p className="m-auto">{tag.name}</p>
- <button className="m-auto size-4" onClick={deleteCB}>
- <X className="size-4" />
- </button>
- </div>
- );
-}
-
-function TagEditor({
- tags,
- setTags,
-}: {
- tags: Map<string, EditableTag>;
- setTags: (
- cb: (m: Map<string, EditableTag>) => Map<string, EditableTag>,
- ) => void;
-}) {
- return (
- <div className="mt-4 flex flex-wrap gap-2 rounded border p-2">
- {[...tags.values()].map((t) => (
- <TagPill
- key={t.name}
- tag={t}
- deleteCB={() =>
- setTags((m) => {
- const newMap = new Map(m);
- newMap.delete(t.name);
- return newMap;
- })
- }
- />
- ))}
- <div className="flex-1">
- <TagAddInput
- addTag={(val) => {
- setTags((m) => {
- if (m.has(val)) {
- // Tag already exists
- // Do nothing
- return m;
- }
- const newMap = new Map(m);
- newMap.set(val, { attachedBy: "human", name: val });
- return newMap;
- });
- }}
- />
- </div>
- </div>
- );
-}
+import { TagsEditor } from "./TagsEditor";
export default function TagModal({
bookmark,
@@ -119,76 +22,19 @@ export default function TagModal({
open: boolean;
setOpen: (open: boolean) => void;
}) {
- const [tags, setTags] = useState<Map<string, EditableTag>>(new Map());
- useEffect(() => {
- const m = new Map<string, EditableTag>();
- for (const t of bookmark.tags) {
- m.set(t.name, { attachedBy: t.attachedBy, id: t.id, name: t.name });
- }
- setTags(m);
- }, [bookmark.tags]);
-
- const bookmarkInvalidationFunction =
- api.useUtils().bookmarks.getBookmark.invalidate;
-
- const { mutate, isPending } = api.bookmarks.updateTags.useMutation({
- onSuccess: () => {
- toast({
- description: "Tags has been updated!",
- });
- bookmarkInvalidationFunction({ bookmarkId: bookmark.id });
- },
- onError: () => {
- toast({
- variant: "destructive",
- title: "Something went wrong",
- description: "There was a problem with your request.",
- });
- },
- });
-
- const onSaveButton = () => {
- const exitingTags = new Set(bookmark.tags.map((t) => t.name));
-
- const attach = [];
- const detach = [];
- for (const t of tags.values()) {
- if (!exitingTags.has(t.name)) {
- attach.push({ tag: t.name });
- }
- }
- for (const t of bookmark.tags) {
- if (!tags.has(t.name)) {
- detach.push({ tagId: t.id });
- }
- }
- mutate({
- bookmarkId: bookmark.id,
- attach,
- detach,
- });
- };
-
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Tags</DialogTitle>
</DialogHeader>
- <TagEditor tags={tags} setTags={setTags} />
+ <TagsEditor bookmark={bookmark} />
<DialogFooter className="sm:justify-end">
<DialogClose asChild>
<Button type="button" variant="secondary">
Close
</Button>
</DialogClose>
- <ActionButton
- type="button"
- loading={isPending}
- onClick={onSaveButton}
- >
- Save
- </ActionButton>
</DialogFooter>
</DialogContent>
</Dialog>
diff --git a/apps/web/components/dashboard/bookmarks/TagsEditor.tsx b/apps/web/components/dashboard/bookmarks/TagsEditor.tsx
new file mode 100644
index 00000000..8bfbce19
--- /dev/null
+++ b/apps/web/components/dashboard/bookmarks/TagsEditor.tsx
@@ -0,0 +1,133 @@
+import type { KeyboardEvent } from "react";
+import { useEffect, useState } from "react";
+import { Input } from "@/components/ui/input";
+import { toast } from "@/components/ui/use-toast";
+import { api } from "@/lib/trpc";
+import { cn } from "@/lib/utils";
+import { Sparkles, X } from "lucide-react";
+
+import type { ZBookmark } from "@hoarder/trpc/types/bookmarks";
+import type { ZAttachedByEnum } from "@hoarder/trpc/types/tags";
+
+interface EditableTag {
+ attachedBy: ZAttachedByEnum;
+ id?: string;
+ name: string;
+}
+
+function TagAddInput({ addTag }: { addTag: (tag: string) => void }) {
+ const onKeyUp = (e: KeyboardEvent<HTMLInputElement>) => {
+ if (e.key === "Enter") {
+ addTag(e.currentTarget.value);
+ e.currentTarget.value = "";
+ }
+ };
+ return (
+ <Input
+ onKeyUp={onKeyUp}
+ className="h-8 w-full border-none bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0"
+ />
+ );
+}
+
+function TagPill({
+ tag,
+ deleteCB,
+}: {
+ tag: { attachedBy: ZAttachedByEnum; id?: string; name: string };
+ deleteCB: () => void;
+}) {
+ const isAttachedByAI = tag.attachedBy == "ai";
+ return (
+ <div
+ className={cn(
+ "flex min-h-8 space-x-1 rounded px-2",
+ isAttachedByAI
+ ? "bg-gradient-to-tr from-purple-500 to-purple-400 text-white"
+ : "bg-gray-200",
+ )}
+ >
+ {isAttachedByAI && <Sparkles className="m-auto size-4" />}
+ <p className="m-auto">{tag.name}</p>
+ <button className="m-auto size-4" onClick={deleteCB}>
+ <X className="size-4" />
+ </button>
+ </div>
+ );
+}
+
+export function TagsEditor({ bookmark }: { bookmark: ZBookmark }) {
+ const [tags, setTags] = useState<Map<string, EditableTag>>(new Map());
+ useEffect(() => {
+ const m = new Map<string, EditableTag>();
+ for (const t of bookmark.tags) {
+ m.set(t.name, { attachedBy: t.attachedBy, id: t.id, name: t.name });
+ }
+ setTags(m);
+ }, [bookmark.tags]);
+
+ const bookmarkInvalidationFunction =
+ api.useUtils().bookmarks.getBookmark.invalidate;
+
+ const { mutate } = api.bookmarks.updateTags.useMutation({
+ onSuccess: () => {
+ toast({
+ description: "Tags has been updated!",
+ });
+ bookmarkInvalidationFunction({ bookmarkId: bookmark.id });
+ },
+ onError: () => {
+ toast({
+ variant: "destructive",
+ title: "Something went wrong",
+ description: "There was a problem with your request.",
+ });
+ },
+ });
+
+ return (
+ <div className="flex flex-wrap gap-2 rounded border p-2">
+ {[...tags.values()].map((t) => (
+ <TagPill
+ key={t.name}
+ tag={t}
+ deleteCB={() => {
+ setTags((m) => {
+ const newMap = new Map(m);
+ newMap.delete(t.name);
+ if (t.id) {
+ mutate({
+ bookmarkId: bookmark.id,
+ attach: [],
+ detach: [{ tagId: t.id }],
+ });
+ }
+ return newMap;
+ });
+ }}
+ />
+ ))}
+ <div className="flex-1">
+ <TagAddInput
+ addTag={(val) => {
+ setTags((m) => {
+ if (m.has(val)) {
+ // Tag already exists
+ // Do nothing
+ return m;
+ }
+ const newMap = new Map(m);
+ newMap.set(val, { attachedBy: "human", name: val });
+ mutate({
+ bookmarkId: bookmark.id,
+ attach: [{ tag: val }],
+ detach: [],
+ });
+ return newMap;
+ });
+ }}
+ />
+ </div>
+ </div>
+ );
+}
diff --git a/apps/web/components/dashboard/bookmarks/TextCard.tsx b/apps/web/components/dashboard/bookmarks/TextCard.tsx
index 5028c1bb..75733063 100644
--- a/apps/web/components/dashboard/bookmarks/TextCard.tsx
+++ b/apps/web/components/dashboard/bookmarks/TextCard.tsx
@@ -1,17 +1,15 @@
"use client";
import { useState } from "react";
-import Link from "next/link";
import { isBookmarkStillTagging } from "@/lib/bookmarkUtils";
import { api } from "@/lib/trpc";
import { cn } from "@/lib/utils";
-import { Maximize2, Star } from "lucide-react";
import Markdown from "react-markdown";
import type { ZBookmark } from "@hoarder/trpc/types/bookmarks";
+import BookmarkActionBar from "./BookmarkActionBar";
import { BookmarkedTextViewer } from "./BookmarkedTextViewer";
-import BookmarkOptions from "./BookmarkOptions";
import TagList from "./TagList";
export default function TextCard({
@@ -71,24 +69,7 @@ export default function TextCard({
</div>
<div className="flex w-full justify-between">
<div />
- <div className="flex gap-0 text-gray-500">
- <div>
- {bookmark.favourited && (
- <Star
- className="my-1 size-8 rounded p-1"
- color="#ebb434"
- fill="#ebb434"
- />
- )}
- </div>
- <Link
- className="my-auto block px-2"
- href={`/dashboard/preview/${bookmark.id}`}
- >
- <Maximize2 size="20" />
- </Link>
- <BookmarkOptions bookmark={bookmark} />
- </div>
+ <BookmarkActionBar bookmark={bookmark} />
</div>
</div>
</>
diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs
index b661bd5c..a8d60f07 100644
--- a/apps/web/next.config.mjs
+++ b/apps/web/next.config.mjs
@@ -39,7 +39,7 @@ const nextConfig = withPWA({
];
},
- transpilePackages: ["@hoarder/shared", "@hoarder/db", "@hoarder/trpc"],
+ // transpilePackages: ["@hoarder/shared", "@hoarder/db", "@hoarder/trpc"],
/** We already do linting and typechecking as separate tasks in CI */
eslint: { ignoreDuringBuilds: true },