From 3208dda3848ad739f54cebf44c423e2b68e85b2d Mon Sep 17 00:00:00 2001 From: MohamedBassem Date: Wed, 28 Feb 2024 20:45:28 +0000 Subject: feature: Add support for storing and previewing raw notes --- README.md | 4 +- packages/db/drizzle/0001_dapper_trauma.sql | 5 + packages/db/drizzle/meta/0001_snapshot.json | 652 +++++++++++++++++++++ packages/db/drizzle/meta/_journal.json | 9 +- packages/db/package.json | 5 +- packages/db/schema.ts | 13 + .../dashboard/bookmarks/components/AddBookmark.tsx | 96 +++ .../app/dashboard/bookmarks/components/AddLink.tsx | 71 --- .../bookmarks/components/BookmarkOptions.tsx | 68 ++- .../bookmarks/components/BookmarkedTextEditor.tsx | 108 ++++ .../bookmarks/components/BookmarksGrid.tsx | 20 +- .../dashboard/bookmarks/components/LinkCard.tsx | 50 +- .../app/dashboard/bookmarks/components/TagList.tsx | 40 ++ .../dashboard/bookmarks/components/TagModal.tsx | 5 +- .../dashboard/bookmarks/components/TextCard.tsx | 62 ++ packages/web/app/dashboard/bookmarks/layout.tsx | 4 +- packages/web/components/ui/textarea.tsx | 24 + packages/web/lib/types/api/bookmarks.ts | 7 + packages/web/server/api/routers/bookmarks.test.ts | 36 +- packages/web/server/api/routers/bookmarks.ts | 89 ++- pnpm-lock.yaml | 3 + 21 files changed, 1196 insertions(+), 175 deletions(-) create mode 100644 packages/db/drizzle/0001_dapper_trauma.sql create mode 100644 packages/db/drizzle/meta/0001_snapshot.json create mode 100644 packages/web/app/dashboard/bookmarks/components/AddBookmark.tsx delete mode 100644 packages/web/app/dashboard/bookmarks/components/AddLink.tsx create mode 100644 packages/web/app/dashboard/bookmarks/components/BookmarkedTextEditor.tsx create mode 100644 packages/web/app/dashboard/bookmarks/components/TagList.tsx create mode 100644 packages/web/app/dashboard/bookmarks/components/TextCard.tsx create mode 100644 packages/web/components/ui/textarea.tsx diff --git a/README.md b/README.md index 90c814ff..b62e8c7d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A self-hostable bookmark-everything app with a touch of AI for the data hoarders ## Features -- 🔗 Bookmark links. +- 🔗 Bookmark links and take simple notes. - ⬇️ Automatic fetching for link titles, descriptions and images. - ✨ AI-based (aka chatgpt) automatic tagging. - 🔖 Chrome plugin for quick bookmarking. @@ -14,7 +14,7 @@ A self-hostable bookmark-everything app with a touch of AI for the data hoarders - 💾 Self-hostable first. - [Planned] Archiving the content for offline reading. - [Planned] Full text search of all the content stored. -- [Planned] Store raw notes and images. +- [Planned] Store raw images. **⚠️ This app is under heavy development and it's far from stable.** diff --git a/packages/db/drizzle/0001_dapper_trauma.sql b/packages/db/drizzle/0001_dapper_trauma.sql new file mode 100644 index 00000000..6ba5ce5c --- /dev/null +++ b/packages/db/drizzle/0001_dapper_trauma.sql @@ -0,0 +1,5 @@ +CREATE TABLE `bookmarkTexts` ( + `id` text PRIMARY KEY NOT NULL, + `text` text, + FOREIGN KEY (`id`) REFERENCES `bookmarks`(`id`) ON UPDATE no action ON DELETE cascade +); diff --git a/packages/db/drizzle/meta/0001_snapshot.json b/packages/db/drizzle/meta/0001_snapshot.json new file mode 100644 index 00000000..16915666 --- /dev/null +++ b/packages/db/drizzle/meta/0001_snapshot.json @@ -0,0 +1,652 @@ +{ + "version": "5", + "dialect": "sqlite", + "id": "30b63cde-0b6e-42fd-8755-edb258b04dd0", + "prevId": "926e135f-db63-4273-b193-4eb2b5c1784d", + "tables": { + "account": { + "name": "account", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_providerAccountId_pk": { + "columns": [ + "provider", + "providerAccountId" + ], + "name": "account_provider_providerAccountId_pk" + } + }, + "uniqueConstraints": {} + }, + "apiKey": { + "name": "apiKey", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyId": { + "name": "keyId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyHash": { + "name": "keyHash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "apiKey_name_unique": { + "name": "apiKey_name_unique", + "columns": [ + "name" + ], + "isUnique": true + }, + "apiKey_keyId_unique": { + "name": "apiKey_keyId_unique", + "columns": [ + "keyId" + ], + "isUnique": true + }, + "apiKey_name_userId_unique": { + "name": "apiKey_name_userId_unique", + "columns": [ + "name", + "userId" + ], + "isUnique": true + } + }, + "foreignKeys": { + "apiKey_userId_user_id_fk": { + "name": "apiKey_userId_user_id_fk", + "tableFrom": "apiKey", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "bookmarkLinks": { + "name": "bookmarkLinks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "imageUrl": { + "name": "imageUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "favicon": { + "name": "favicon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "crawledAt": { + "name": "crawledAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "bookmarkLinks_id_bookmarks_id_fk": { + "name": "bookmarkLinks_id_bookmarks_id_fk", + "tableFrom": "bookmarkLinks", + "tableTo": "bookmarks", + "columnsFrom": [ + "id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "bookmarkTags": { + "name": "bookmarkTags", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "bookmarkTags_userId_name_unique": { + "name": "bookmarkTags_userId_name_unique", + "columns": [ + "userId", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "bookmarkTags_userId_user_id_fk": { + "name": "bookmarkTags_userId_user_id_fk", + "tableFrom": "bookmarkTags", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "bookmarkTexts": { + "name": "bookmarkTexts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "bookmarkTexts_id_bookmarks_id_fk": { + "name": "bookmarkTexts_id_bookmarks_id_fk", + "tableFrom": "bookmarkTexts", + "tableTo": "bookmarks", + "columnsFrom": [ + "id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "bookmarks": { + "name": "bookmarks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "archived": { + "name": "archived", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "favourited": { + "name": "favourited", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "bookmarks_userId_user_id_fk": { + "name": "bookmarks_userId_user_id_fk", + "tableFrom": "bookmarks", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "tagsOnBookmarks": { + "name": "tagsOnBookmarks", + "columns": { + "bookmarkId": { + "name": "bookmarkId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tagId": { + "name": "tagId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "attachedAt": { + "name": "attachedAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attachedBy": { + "name": "attachedBy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "tagsOnBookmarks_bookmarkId_bookmarks_id_fk": { + "name": "tagsOnBookmarks_bookmarkId_bookmarks_id_fk", + "tableFrom": "tagsOnBookmarks", + "tableTo": "bookmarks", + "columnsFrom": [ + "bookmarkId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tagsOnBookmarks_tagId_bookmarkTags_id_fk": { + "name": "tagsOnBookmarks_tagId_bookmarkTags_id_fk", + "tableFrom": "tagsOnBookmarks", + "tableTo": "bookmarkTags", + "columnsFrom": [ + "tagId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "tagsOnBookmarks_bookmarkId_tagId_pk": { + "columns": [ + "bookmarkId", + "tagId" + ], + "name": "tagsOnBookmarks_bookmarkId_tagId_pk" + } + }, + "uniqueConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "verificationToken": { + "name": "verificationToken", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "columns": [ + "identifier", + "token" + ], + "name": "verificationToken_identifier_token_pk" + } + }, + "uniqueConstraints": {} + } + }, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} \ No newline at end of file diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index 2dbd6ca1..d70c6c1e 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1708710681721, "tag": "0000_luxuriant_johnny_blaze", "breakpoints": true + }, + { + "idx": 1, + "version": "5", + "when": 1709144284383, + "tag": "0001_dapper_trauma", + "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/packages/db/package.json b/packages/db/package.json index 27631095..20dee1e9 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -6,7 +6,7 @@ "main": "index.ts", "scripts": { "typecheck": "tsc --noEmit", - "migrate": "ts-node migrate.ts", + "migrate": "tsx migrate.ts", "studio": "drizzle-kit studio" }, "dependencies": { @@ -14,7 +14,8 @@ "@paralleldrive/cuid2": "^2.2.2", "better-sqlite3": "^9.4.3", "dotenv": "^16.4.1", - "drizzle-orm": "^0.29.4" + "drizzle-orm": "^0.29.4", + "tsx": "^4.7.1" }, "devDependencies": { "@tsconfig/node21": "^21.0.1", diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 94467c56..4acd86c4 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -125,6 +125,15 @@ export const bookmarkLinks = sqliteTable("bookmarkLinks", { crawledAt: integer("crawledAt", { mode: "timestamp" }), }); +export const bookmarkTexts = sqliteTable("bookmarkTexts", { + id: text("id") + .notNull() + .primaryKey() + .$defaultFn(() => createId()) + .references(() => bookmarks.id, { onDelete: "cascade" }), + text: text("text"), +}); + export const bookmarkTags = sqliteTable( "bookmarkTags", { @@ -179,6 +188,10 @@ export const bookmarkRelations = relations(bookmarks, ({ many, one }) => ({ fields: [bookmarks.id], references: [bookmarkLinks.id], }), + text: one(bookmarkTexts, { + fields: [bookmarks.id], + references: [bookmarkTexts.id], + }), tagsOnBookmarks: many(tagsOnBookmarks), })); diff --git a/packages/web/app/dashboard/bookmarks/components/AddBookmark.tsx b/packages/web/app/dashboard/bookmarks/components/AddBookmark.tsx new file mode 100644 index 00000000..4f0de87a --- /dev/null +++ b/packages/web/app/dashboard/bookmarks/components/AddBookmark.tsx @@ -0,0 +1,96 @@ +"use client"; + +import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Pencil, Plus } from "lucide-react"; +import { useForm, SubmitErrorHandler } from "react-hook-form"; +import { z } from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { toast } from "@/components/ui/use-toast"; +import { api } from "@/lib/trpc"; +import { ActionButton } from "@/components/ui/action-button"; +import { Button } from "@/components/ui/button"; +import { BookmarkedTextEditor } from "./BookmarkedTextEditor"; +import { useState } from "react"; + +function AddText() { + const [isEditorOpen, setEditorOpen] = useState(false); + + return ( +
+ + +
+ ); +} + +function AddLink() { + const formSchema = z.object({ + url: z.string().url({ message: "The link must be a valid URL" }), + }); + const form = useForm>({ + resolver: zodResolver(formSchema), + }); + + const invalidateBookmarksCache = api.useUtils().bookmarks.invalidate; + const createBookmarkMutator = api.bookmarks.createBookmark.useMutation({ + onSuccess: () => { + invalidateBookmarksCache(); + }, + onError: () => { + toast({ description: "Something went wrong", variant: "destructive" }); + }, + }); + + const onError: SubmitErrorHandler> = (errors) => { + toast({ + description: Object.values(errors) + .map((v) => v.message) + .join("\n"), + variant: "destructive", + }); + }; + + return ( +
+ + createBookmarkMutator.mutate({ url: value.url, type: "link" }), + onError, + )} + > +
+ { + return ( + + + + + + ); + }} + /> + + + +
+
+ + ); +} + +export default function AddBookmark() { + return ( +
+ + +
+ ); +} diff --git a/packages/web/app/dashboard/bookmarks/components/AddLink.tsx b/packages/web/app/dashboard/bookmarks/components/AddLink.tsx deleted file mode 100644 index 242a52a5..00000000 --- a/packages/web/app/dashboard/bookmarks/components/AddLink.tsx +++ /dev/null @@ -1,71 +0,0 @@ -"use client"; - -import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { Plus } from "lucide-react"; -import { useForm, SubmitErrorHandler } from "react-hook-form"; -import { z } from "zod"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { toast } from "@/components/ui/use-toast"; -import { api } from "@/lib/trpc"; -import { ActionButton } from "@/components/ui/action-button"; - -const formSchema = z.object({ - url: z.string().url({ message: "The link must be a valid URL" }), -}); - -export default function AddLink() { - const form = useForm>({ - resolver: zodResolver(formSchema), - }); - - const invalidateBookmarksCache = api.useUtils().bookmarks.invalidate; - const bookmarkLinkMutator = api.bookmarks.bookmarkLink.useMutation({ - onSuccess: () => { - invalidateBookmarksCache(); - }, - onError: () => { - toast({ description: "Something went wrong", variant: "destructive" }); - }, - }); - - const onError: SubmitErrorHandler> = (errors) => { - toast({ - description: Object.values(errors) - .map((v) => v.message) - .join("\n"), - variant: "destructive", - }); - }; - - return ( -
- - bookmarkLinkMutator.mutate({ url: value.url, type: "link" }), - onError, - )} - > -
- { - return ( - - - - - - ); - }} - /> - - - -
-
- - ); -} diff --git a/packages/web/app/dashboard/bookmarks/components/BookmarkOptions.tsx b/packages/web/app/dashboard/bookmarks/components/BookmarkOptions.tsx index b8f6d8f2..866a4cbd 100644 --- a/packages/web/app/dashboard/bookmarks/components/BookmarkOptions.tsx +++ b/packages/web/app/dashboard/bookmarks/components/BookmarkOptions.tsx @@ -2,7 +2,7 @@ import { useToast } from "@/components/ui/use-toast"; import { api } from "@/lib/trpc"; -import { ZBookmark } from "@/lib/types/api/bookmarks"; +import { ZBookmark, ZBookmarkedLink } from "@/lib/types/api/bookmarks"; import { Button } from "@/components/ui/button"; import { DropdownMenu, @@ -14,12 +14,15 @@ import { Archive, Link, MoreHorizontal, + Pencil, RotateCw, Star, Tags, Trash2, } from "lucide-react"; import { useTagModel } from "./TagModal"; +import { useState } from "react"; +import { BookmarkedTextEditor } from "./BookmarkedTextEditor"; export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) { const { toast } = useToast(); @@ -27,6 +30,8 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) { const [_, setTagModalIsOpen, tagModal] = useTagModel(bookmark); + const [isTextEditorOpen, setTextEditorOpen] = useState(false); + const invalidateBookmarksCache = api.useUtils().bookmarks.invalidate; const onError = () => { @@ -72,13 +77,27 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) { return ( <> {tagModal} + - + {bookmark.content.type === "text" && ( + setTextEditorOpen(true)}> + + Edit + + )} updateBookmarkMutator.mutate({ @@ -101,29 +120,36 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) { {bookmark.archived ? "Un-archive" : "Archive"} - { - navigator.clipboard.writeText(bookmark.content.url); - toast({ - description: "Link was added to your clipboard!", - }); - }} - > - - Copy Link - + {bookmark.content.type === "link" && ( + { + navigator.clipboard.writeText( + (bookmark.content as ZBookmarkedLink).url, + ); + toast({ + description: "Link was added to your clipboard!", + }); + }} + > + + Copy Link + + )} setTagModalIsOpen(true)}> Edit Tags - - crawlBookmarkMutator.mutate({ bookmarkId: bookmark.id }) - } - > - - Refresh - + + {bookmark.content.type === "link" && ( + + crawlBookmarkMutator.mutate({ bookmarkId: bookmark.id }) + } + > + + Refresh + + )} diff --git a/packages/web/app/dashboard/bookmarks/components/BookmarkedTextEditor.tsx b/packages/web/app/dashboard/bookmarks/components/BookmarkedTextEditor.tsx new file mode 100644 index 00000000..e9138e03 --- /dev/null +++ b/packages/web/app/dashboard/bookmarks/components/BookmarkedTextEditor.tsx @@ -0,0 +1,108 @@ +import { ZBookmark } from "@/lib/types/api/bookmarks"; +import { + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { ActionButton } from "@/components/ui/action-button"; +import { Button } from "@/components/ui/button"; +import { Textarea } from "@/components/ui/textarea"; +import { api } from "@/lib/trpc"; +import { useState } from "react"; +import { toast } from "@/components/ui/use-toast"; + +export function BookmarkedTextEditor({ + bookmark, + open, + setOpen, +}: { + bookmark?: ZBookmark; + open: boolean; + setOpen: (open: boolean) => void; +}) { + const isNewBookmark = bookmark === undefined; + const [noteText, setNoteText] = useState( + bookmark && bookmark.content.type == "text" ? bookmark.content.text : "", + ); + + const invalidateAllBookmarksCache = + api.useUtils().bookmarks.getBookmarks.invalidate; + const invalidateOneBookmarksCache = + api.useUtils().bookmarks.getBookmark.invalidate; + + const { mutate: createBookmarkMutator, isPending: isCreationPending } = + api.bookmarks.createBookmark.useMutation({ + onSuccess: () => { + invalidateAllBookmarksCache(); + toast({ + description: "Note created!", + }); + setOpen(false); + setNoteText(""); + }, + onError: () => { + toast({ description: "Something went wrong", variant: "destructive" }); + }, + }); + const { mutate: updateBookmarkMutator, isPending: isUpdatePending } = + api.bookmarks.updateBookmarkText.useMutation({ + onSuccess: () => { + invalidateOneBookmarksCache({ + bookmarkId: bookmark!.id, + }); + toast({ + description: "Note updated!", + }); + setOpen(false); + setNoteText(""); + }, + onError: () => { + toast({ description: "Something went wrong", variant: "destructive" }); + }, + }); + const isPending = isCreationPending || isUpdatePending; + + const onSave = () => { + if (isNewBookmark) { + createBookmarkMutator({ + type: "text", + text: noteText, + }); + } else { + updateBookmarkMutator({ + bookmarkId: bookmark.id, + text: noteText, + }); + } + }; + + return ( + + + + + {isNewBookmark ? "New Note" : "Edit Note"} + +