aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
authorMohamedBassem <me@mbassem.com>2024-02-28 20:45:28 +0000
committerMohamedBassem <me@mbassem.com>2024-02-28 20:45:28 +0000
commit3208dda3848ad739f54cebf44c423e2b68e85b2d (patch)
tree25602c451354a296e8779197fdd42acab7526502 /packages
parent7096fb3941579e5c045796361745d597e03ff7fc (diff)
downloadkarakeep-3208dda3848ad739f54cebf44c423e2b68e85b2d.tar.zst
feature: Add support for storing and previewing raw notes
Diffstat (limited to 'packages')
-rw-r--r--packages/db/drizzle/0001_dapper_trauma.sql5
-rw-r--r--packages/db/drizzle/meta/0001_snapshot.json652
-rw-r--r--packages/db/drizzle/meta/_journal.json9
-rw-r--r--packages/db/package.json5
-rw-r--r--packages/db/schema.ts13
-rw-r--r--packages/web/app/dashboard/bookmarks/components/AddBookmark.tsx (renamed from packages/web/app/dashboard/bookmarks/components/AddLink.tsx)43
-rw-r--r--packages/web/app/dashboard/bookmarks/components/BookmarkOptions.tsx68
-rw-r--r--packages/web/app/dashboard/bookmarks/components/BookmarkedTextEditor.tsx108
-rw-r--r--packages/web/app/dashboard/bookmarks/components/BookmarksGrid.tsx20
-rw-r--r--packages/web/app/dashboard/bookmarks/components/LinkCard.tsx50
-rw-r--r--packages/web/app/dashboard/bookmarks/components/TagList.tsx40
-rw-r--r--packages/web/app/dashboard/bookmarks/components/TagModal.tsx5
-rw-r--r--packages/web/app/dashboard/bookmarks/components/TextCard.tsx62
-rw-r--r--packages/web/app/dashboard/bookmarks/layout.tsx4
-rw-r--r--packages/web/components/ui/textarea.tsx24
-rw-r--r--packages/web/lib/types/api/bookmarks.ts7
-rw-r--r--packages/web/server/api/routers/bookmarks.test.ts36
-rw-r--r--packages/web/server/api/routers/bookmarks.ts89
18 files changed, 1129 insertions, 111 deletions
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/AddLink.tsx b/packages/web/app/dashboard/bookmarks/components/AddBookmark.tsx
index 242a52a5..4f0de87a 100644
--- a/packages/web/app/dashboard/bookmarks/components/AddLink.tsx
+++ b/packages/web/app/dashboard/bookmarks/components/AddBookmark.tsx
@@ -2,25 +2,40 @@
import { Form, FormControl, FormField, FormItem } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
-import { Plus } from "lucide-react";
+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";
-const formSchema = z.object({
- url: z.string().url({ message: "The link must be a valid URL" }),
-});
+function AddText() {
+ const [isEditorOpen, setEditorOpen] = useState(false);
-export default function AddLink() {
+ return (
+ <div className="flex">
+ <BookmarkedTextEditor open={isEditorOpen} setOpen={setEditorOpen} />
+ <Button className="m-auto" onClick={() => setEditorOpen(true)}>
+ <Pencil />
+ </Button>
+ </div>
+ );
+}
+
+function AddLink() {
+ const formSchema = z.object({
+ url: z.string().url({ message: "The link must be a valid URL" }),
+ });
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
});
const invalidateBookmarksCache = api.useUtils().bookmarks.invalidate;
- const bookmarkLinkMutator = api.bookmarks.bookmarkLink.useMutation({
+ const createBookmarkMutator = api.bookmarks.createBookmark.useMutation({
onSuccess: () => {
invalidateBookmarksCache();
},
@@ -41,13 +56,14 @@ export default function AddLink() {
return (
<Form {...form}>
<form
+ className="flex-grow"
onSubmit={form.handleSubmit(
(value) =>
- bookmarkLinkMutator.mutate({ url: value.url, type: "link" }),
+ createBookmarkMutator.mutate({ url: value.url, type: "link" }),
onError,
)}
>
- <div className="container flex w-full items-center space-x-2 py-4">
+ <div className="flex w-full items-center space-x-2 py-4">
<FormField
control={form.control}
name="url"
@@ -61,7 +77,7 @@ export default function AddLink() {
);
}}
/>
- <ActionButton type="submit" loading={bookmarkLinkMutator.isPending}>
+ <ActionButton type="submit" loading={createBookmarkMutator.isPending}>
<Plus />
</ActionButton>
</div>
@@ -69,3 +85,12 @@ export default function AddLink() {
</Form>
);
}
+
+export default function AddBookmark() {
+ return (
+ <div className="container flex gap-2">
+ <AddLink />
+ <AddText />
+ </div>
+ );
+}
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}
+ <BookmarkedTextEditor
+ bookmark={bookmark}
+ open={isTextEditorOpen}
+ setOpen={setTextEditorOpen}
+ />
<DropdownMenu>
<DropdownMenuTrigger asChild>
- <Button variant="ghost">
+ <Button
+ variant="ghost"
+ className="focus-visible:ring-0 focus-visible:ring-offset-0"
+ >
<MoreHorizontal />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-fit">
+ {bookmark.content.type === "text" && (
+ <DropdownMenuItem onClick={() => setTextEditorOpen(true)}>
+ <Pencil className="mr-2 size-4" />
+ <span>Edit</span>
+ </DropdownMenuItem>
+ )}
<DropdownMenuItem
onClick={() =>
updateBookmarkMutator.mutate({
@@ -101,29 +120,36 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) {
<Archive className="mr-2 size-4" />
<span>{bookmark.archived ? "Un-archive" : "Archive"}</span>
</DropdownMenuItem>
- <DropdownMenuItem
- onClick={() => {
- navigator.clipboard.writeText(bookmark.content.url);
- toast({
- description: "Link was added to your clipboard!",
- });
- }}
- >
- <Link className="mr-2 size-4" />
- <span>Copy Link</span>
- </DropdownMenuItem>
+ {bookmark.content.type === "link" && (
+ <DropdownMenuItem
+ onClick={() => {
+ navigator.clipboard.writeText(
+ (bookmark.content as ZBookmarkedLink).url,
+ );
+ toast({
+ description: "Link was added to your clipboard!",
+ });
+ }}
+ >
+ <Link className="mr-2 size-4" />
+ <span>Copy Link</span>
+ </DropdownMenuItem>
+ )}
<DropdownMenuItem onClick={() => setTagModalIsOpen(true)}>
<Tags className="mr-2 size-4" />
<span>Edit Tags</span>
</DropdownMenuItem>
- <DropdownMenuItem
- onClick={() =>
- crawlBookmarkMutator.mutate({ bookmarkId: bookmark.id })
- }
- >
- <RotateCw className="mr-2 size-4" />
- <span>Refresh</span>
- </DropdownMenuItem>
+
+ {bookmark.content.type === "link" && (
+ <DropdownMenuItem
+ onClick={() =>
+ crawlBookmarkMutator.mutate({ bookmarkId: bookmark.id })
+ }
+ >
+ <RotateCw className="mr-2 size-4" />
+ <span>Refresh</span>
+ </DropdownMenuItem>
+ )}
<DropdownMenuItem
className="text-destructive"
onClick={() =>
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 (
+ <Dialog open={open} onOpenChange={setOpen}>
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle className="pb-4">
+ {isNewBookmark ? "New Note" : "Edit Note"}
+ </DialogTitle>
+ <Textarea
+ value={noteText}
+ onChange={(e) => setNoteText(e.target.value)}
+ className="h-52 grow"
+ />
+ </DialogHeader>
+ <DialogFooter className="flex-shrink gap-1 sm:justify-end">
+ <DialogClose asChild>
+ <Button type="button" variant="secondary">
+ Close
+ </Button>
+ </DialogClose>
+ <ActionButton type="button" loading={isPending} onClick={onSave}>
+ Save
+ </ActionButton>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ );
+}
diff --git a/packages/web/app/dashboard/bookmarks/components/BookmarksGrid.tsx b/packages/web/app/dashboard/bookmarks/components/BookmarksGrid.tsx
index e07d48b6..c960c8b7 100644
--- a/packages/web/app/dashboard/bookmarks/components/BookmarksGrid.tsx
+++ b/packages/web/app/dashboard/bookmarks/components/BookmarksGrid.tsx
@@ -3,11 +3,18 @@
import LinkCard from "./LinkCard";
import { ZBookmark, ZGetBookmarksRequest } from "@/lib/types/api/bookmarks";
import { api } from "@/lib/trpc";
+import TextCard from "./TextCard";
-function renderBookmark(bookmark: ZBookmark) {
+function renderBookmark(bookmark: ZBookmark, className: string) {
switch (bookmark.content.type) {
case "link":
- return <LinkCard key={bookmark.id} bookmark={bookmark} />;
+ return (
+ <LinkCard key={bookmark.id} bookmark={bookmark} className={className} />
+ );
+ case "text":
+ return (
+ <TextCard key={bookmark.id} bookmark={bookmark} className={className} />
+ );
}
}
@@ -25,8 +32,13 @@ export default function BookmarksGrid({
return <p>No bookmarks</p>;
}
return (
- <div className="container grid grid-cols-1 gap-4 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
- {data.bookmarks.map((b) => renderBookmark(b))}
+ <div className="container grid grid-flow-row-dense grid-cols-1 gap-4 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
+ {data.bookmarks.map((b) =>
+ renderBookmark(
+ b,
+ "border-grey-100 border bg-gray-50 duration-300 ease-in hover:border-blue-300 hover:transition-all",
+ ),
+ )}
</div>
);
}
diff --git a/packages/web/app/dashboard/bookmarks/components/LinkCard.tsx b/packages/web/app/dashboard/bookmarks/components/LinkCard.tsx
index cd0f128c..73d3f300 100644
--- a/packages/web/app/dashboard/bookmarks/components/LinkCard.tsx
+++ b/packages/web/app/dashboard/bookmarks/components/LinkCard.tsx
@@ -1,6 +1,5 @@
"use client";
-import { Badge } from "@/components/ui/badge";
import {
ImageCard,
ImageCardBanner,
@@ -13,50 +12,24 @@ import { ZBookmark } from "@/lib/types/api/bookmarks";
import Link from "next/link";
import BookmarkOptions from "./BookmarkOptions";
import { api } from "@/lib/trpc";
-import { Skeleton } from "@/components/ui/skeleton";
import { Star } from "lucide-react";
+import { cn } from "@/lib/utils";
+import TagList from "./TagList";
function isStillCrawling(bookmark: ZBookmark) {
return (
+ bookmark.content.type == "link" &&
!bookmark.content.crawledAt &&
Date.now().valueOf() - bookmark.createdAt.valueOf() < 30 * 1000
);
}
-function TagList(bookmark: ZBookmark, stillCrawling: boolean) {
- if (stillCrawling) {
- return (
- <ImageCardBody className="space-y-2">
- <Skeleton className="h-4 w-full" />
- <Skeleton className="h-4 w-full" />
- <Skeleton className="h-4 w-full" />
- </ImageCardBody>
- );
- }
- return (
- <ImageCardBody className="flex h-full flex-wrap space-x-1 overflow-hidden">
- {bookmark.tags.map((t) => (
- <Link
- className="flex h-full flex-col justify-end"
- key={t.id}
- href={`/dashboard/tags/${t.name}`}
- >
- <Badge
- variant="default"
- className="text-nowrap bg-gray-300 text-gray-500 hover:text-white"
- >
- #{t.name}
- </Badge>
- </Link>
- ))}
- </ImageCardBody>
- );
-}
-
export default function LinkCard({
bookmark: initialData,
+ className,
}: {
bookmark: ZBookmark;
+ className: string;
}) {
const { data: bookmark } = api.bookmarks.getBookmark.useQuery(
{
@@ -78,6 +51,9 @@ export default function LinkCard({
},
);
const link = bookmark.content;
+ if (link.type != "link") {
+ throw new Error("Unexpected bookmark type");
+ }
const isCrawling = isStillCrawling(bookmark);
const parsedUrl = new URL(link.url);
@@ -88,11 +64,7 @@ export default function LinkCard({
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdj+P///38ACfsD/QVDRcoAAAAASUVORK5CYII=";
return (
- <ImageCard
- className={
- "border-grey-100 border bg-gray-50 duration-300 ease-in hover:border-blue-300 hover:transition-all"
- }
- >
+ <ImageCard className={cn(className, "row-span-2")}>
{bookmark.favourited && (
<Star
className="absolute m-1 size-8 rounded bg-gray-100 p-1"
@@ -111,7 +83,9 @@ export default function LinkCard({
</ImageCardTitle>
{/* There's a hack here. Every tag has the full hight of the container itself. That why, when we enable flex-wrap,
the overflowed don't show up. */}
- {TagList(bookmark, isCrawling)}
+ <ImageCardBody className="flex h-full flex-wrap space-x-1 overflow-hidden">
+ <TagList bookmark={bookmark} loading={isCrawling} />
+ </ImageCardBody>
<ImageCardFooter>
<div className="flex justify-between text-gray-500">
<div className="my-auto">
diff --git a/packages/web/app/dashboard/bookmarks/components/TagList.tsx b/packages/web/app/dashboard/bookmarks/components/TagList.tsx
new file mode 100644
index 00000000..440452a7
--- /dev/null
+++ b/packages/web/app/dashboard/bookmarks/components/TagList.tsx
@@ -0,0 +1,40 @@
+import { Badge } from "@/components/ui/badge";
+import Link from "next/link";
+import { Skeleton } from "@/components/ui/skeleton";
+import { ZBookmark } from "@/lib/types/api/bookmarks";
+
+export default function TagList({
+ bookmark,
+ loading,
+}: {
+ bookmark: ZBookmark;
+ loading?: boolean;
+}) {
+ if (loading) {
+ return (
+ <div className="space-y-2">
+ <Skeleton className="h-4 w-full" />
+ <Skeleton className="h-4 w-full" />
+ <Skeleton className="h-4 w-full" />
+ </div>
+ );
+ }
+ return (
+ <>
+ {bookmark.tags.map((t) => (
+ <Link
+ className="flex h-full flex-col justify-end"
+ key={t.id}
+ href={`/dashboard/tags/${t.name}`}
+ >
+ <Badge
+ variant="default"
+ className="text-nowrap bg-gray-300 text-gray-500 hover:text-white"
+ >
+ #{t.name}
+ </Badge>
+ </Link>
+ ))}
+ </>
+ );
+}
diff --git a/packages/web/app/dashboard/bookmarks/components/TagModal.tsx b/packages/web/app/dashboard/bookmarks/components/TagModal.tsx
index b0e391b7..3b6a0bd4 100644
--- a/packages/web/app/dashboard/bookmarks/components/TagModal.tsx
+++ b/packages/web/app/dashboard/bookmarks/components/TagModal.tsx
@@ -4,7 +4,6 @@ import {
Dialog,
DialogClose,
DialogContent,
- DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
@@ -168,10 +167,8 @@ export default function TagModal({
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Tags</DialogTitle>
- <DialogDescription>
- <TagEditor tags={tags} setTags={setTags} />
- </DialogDescription>
</DialogHeader>
+ <TagEditor tags={tags} setTags={setTags} />
<DialogFooter className="sm:justify-end">
<DialogClose asChild>
<Button type="button" variant="secondary">
diff --git a/packages/web/app/dashboard/bookmarks/components/TextCard.tsx b/packages/web/app/dashboard/bookmarks/components/TextCard.tsx
new file mode 100644
index 00000000..7ee1a90b
--- /dev/null
+++ b/packages/web/app/dashboard/bookmarks/components/TextCard.tsx
@@ -0,0 +1,62 @@
+"use client";
+
+import { ZBookmark } from "@/lib/types/api/bookmarks";
+import BookmarkOptions from "./BookmarkOptions";
+import { api } from "@/lib/trpc";
+import { Star } from "lucide-react";
+import { cn } from "@/lib/utils";
+import TagList from "./TagList";
+
+export default function TextCard({
+ bookmark: initialData,
+ className,
+}: {
+ bookmark: ZBookmark;
+ className: string;
+}) {
+ const { data: bookmark } = api.bookmarks.getBookmark.useQuery(
+ {
+ bookmarkId: initialData.id,
+ },
+ {
+ initialData,
+ },
+ );
+ const bookmarkedText = bookmark.content;
+ if (bookmarkedText.type != "text") {
+ throw new Error("Unexpected bookmark type");
+ }
+
+ const numWords = bookmarkedText.text.split(" ").length;
+
+ return (
+ <div
+ className={cn(
+ className,
+ cn(
+ "flex flex-col gap-y-1 overflow-hidden rounded-lg p-2 shadow-md",
+ numWords > 12 ? "row-span-2 h-96" : "row-span-1 h-40",
+ ),
+ )}
+ >
+ <p className="grow overflow-hidden text-ellipsis">
+ {bookmarkedText.text}
+ </p>
+ <div className="flex flex-none flex-wrap gap-1 overflow-hidden">
+ <TagList bookmark={bookmark} />
+ </div>
+ <div className="flex w-full justify-between">
+ <div>
+ {bookmark.favourited && (
+ <Star
+ className="my-1 size-8 rounded p-1"
+ color="#ebb434"
+ fill="#ebb434"
+ />
+ )}
+ </div>
+ <BookmarkOptions bookmark={bookmark} />
+ </div>
+ </div>
+ );
+}
diff --git a/packages/web/app/dashboard/bookmarks/layout.tsx b/packages/web/app/dashboard/bookmarks/layout.tsx
index 68704a2b..98aa75a4 100644
--- a/packages/web/app/dashboard/bookmarks/layout.tsx
+++ b/packages/web/app/dashboard/bookmarks/layout.tsx
@@ -1,5 +1,5 @@
import React from "react";
-import AddLink from "./components/AddLink";
+import AddBookmark from "./components/AddBookmark";
import type { Metadata } from "next";
export const metadata: Metadata = {
@@ -14,7 +14,7 @@ export default function BookmarksLayout({
return (
<div className="flex h-full flex-col">
<div>
- <AddLink />
+ <AddBookmark />
</div>
<hr />
<div className="my-4 flex-1">{children}</div>
diff --git a/packages/web/components/ui/textarea.tsx b/packages/web/components/ui/textarea.tsx
new file mode 100644
index 00000000..a0de3371
--- /dev/null
+++ b/packages/web/components/ui/textarea.tsx
@@ -0,0 +1,24 @@
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+export interface TextareaProps
+ extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
+
+const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
+ ({ className, ...props }, ref) => {
+ return (
+ <textarea
+ className={cn(
+ "border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex min-h-[80px] w-full rounded-md border px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
+ className,
+ )}
+ ref={ref}
+ {...props}
+ />
+ );
+ },
+);
+Textarea.displayName = "Textarea";
+
+export { Textarea };
diff --git a/packages/web/lib/types/api/bookmarks.ts b/packages/web/lib/types/api/bookmarks.ts
index 0970a7ed..9b0ae371 100644
--- a/packages/web/lib/types/api/bookmarks.ts
+++ b/packages/web/lib/types/api/bookmarks.ts
@@ -12,8 +12,15 @@ export const zBookmarkedLinkSchema = z.object({
});
export type ZBookmarkedLink = z.infer<typeof zBookmarkedLinkSchema>;
+export const zBookmarkedTextSchema = z.object({
+ type: z.literal("text"),
+ text: z.string().max(2000),
+});
+export type ZBookmarkedText = z.infer<typeof zBookmarkedTextSchema>;
+
export const zBookmarkContentSchema = z.discriminatedUnion("type", [
zBookmarkedLinkSchema,
+ zBookmarkedTextSchema,
]);
export type ZBookmarkContent = z.infer<typeof zBookmarkContentSchema>;
diff --git a/packages/web/server/api/routers/bookmarks.test.ts b/packages/web/server/api/routers/bookmarks.test.ts
index 16f82992..603a173e 100644
--- a/packages/web/server/api/routers/bookmarks.test.ts
+++ b/packages/web/server/api/routers/bookmarks.test.ts
@@ -1,17 +1,18 @@
import { CustomTestContext, defaultBeforeEach } from "@/lib/testUtils";
-import { expect, describe, test, beforeEach } from "vitest";
+import { expect, describe, test, beforeEach, assert } from "vitest";
beforeEach<CustomTestContext>(defaultBeforeEach);
describe("Bookmark Routes", () => {
test<CustomTestContext>("create bookmark", async ({ apiCallers }) => {
const api = apiCallers[0].bookmarks;
- const bookmark = await api.bookmarkLink({
+ const bookmark = await api.createBookmark({
url: "https://google.com",
type: "link",
});
const res = await api.getBookmark({ bookmarkId: bookmark.id });
+ assert(res.content.type == "link");
expect(res.content.url).toEqual("https://google.com");
expect(res.favourited).toEqual(false);
expect(res.archived).toEqual(false);
@@ -22,7 +23,7 @@ describe("Bookmark Routes", () => {
const api = apiCallers[0].bookmarks;
// Create the bookmark
- const bookmark = await api.bookmarkLink({
+ const bookmark = await api.createBookmark({
url: "https://google.com",
type: "link",
});
@@ -43,7 +44,7 @@ describe("Bookmark Routes", () => {
const api = apiCallers[0].bookmarks;
// Create the bookmark
- const bookmark = await api.bookmarkLink({
+ const bookmark = await api.createBookmark({
url: "https://google.com",
type: "link",
});
@@ -64,12 +65,12 @@ describe("Bookmark Routes", () => {
const emptyBookmarks = await api.getBookmarks({});
expect(emptyBookmarks.bookmarks.length).toEqual(0);
- const bookmark1 = await api.bookmarkLink({
+ const bookmark1 = await api.createBookmark({
url: "https://google.com",
type: "link",
});
- const bookmark2 = await api.bookmarkLink({
+ const bookmark2 = await api.createBookmark({
url: "https://google2.com",
type: "link",
});
@@ -113,7 +114,7 @@ describe("Bookmark Routes", () => {
test<CustomTestContext>("update tags", async ({ apiCallers }) => {
const api = apiCallers[0].bookmarks;
- let bookmark = await api.bookmarkLink({
+ let bookmark = await api.createBookmark({
url: "https://google.com",
type: "link",
});
@@ -139,12 +140,29 @@ describe("Bookmark Routes", () => {
expect(bookmark.tags.map((t) => t.name).sort()).toEqual(["tag2", "tag3"]);
});
+ test<CustomTestContext>("update bookmark text", async ({ apiCallers }) => {
+ const api = apiCallers[0].bookmarks;
+ let bookmark = await api.createBookmark({
+ text: "HELLO WORLD",
+ type: "text",
+ });
+
+ await api.updateBookmarkText({
+ bookmarkId: bookmark.id,
+ text: "WORLD HELLO",
+ });
+
+ bookmark = await api.getBookmark({ bookmarkId: bookmark.id });
+ assert(bookmark.content.type == "text");
+ expect(bookmark.content.text).toEqual("WORLD HELLO");
+ });
+
test<CustomTestContext>("privacy", async ({ apiCallers }) => {
- const user1Bookmark = await apiCallers[0].bookmarks.bookmarkLink({
+ const user1Bookmark = await apiCallers[0].bookmarks.createBookmark({
type: "link",
url: "https://google.com",
});
- const user2Bookmark = await apiCallers[1].bookmarks.bookmarkLink({
+ const user2Bookmark = await apiCallers[1].bookmarks.createBookmark({
type: "link",
url: "https://google.com",
});
diff --git a/packages/web/server/api/routers/bookmarks.ts b/packages/web/server/api/routers/bookmarks.ts
index 64755e4e..bfa0580f 100644
--- a/packages/web/server/api/routers/bookmarks.ts
+++ b/packages/web/server/api/routers/bookmarks.ts
@@ -13,6 +13,7 @@ import {
import {
bookmarkLinks,
bookmarkTags,
+ bookmarkTexts,
bookmarks,
tagsOnBookmarks,
} from "@hoarder/db/schema";
@@ -64,6 +65,7 @@ async function dummyDrizzleReturnType() {
},
},
link: true,
+ text: true,
},
});
if (!x) {
@@ -75,11 +77,13 @@ async function dummyDrizzleReturnType() {
function toZodSchema(
bookmark: Awaited<ReturnType<typeof dummyDrizzleReturnType>>,
): ZBookmark {
- const { tagsOnBookmarks, link, ...rest } = bookmark;
+ const { tagsOnBookmarks, link, text, ...rest } = bookmark;
let content: ZBookmarkContent;
if (link) {
content = { type: "link", ...link };
+ } else if (text) {
+ content = { type: "text", text: text.text || "" };
} else {
throw new Error("Unknown content type");
}
@@ -95,12 +99,10 @@ function toZodSchema(
}
export const bookmarksAppRouter = router({
- bookmarkLink: authedProcedure
+ createBookmark: authedProcedure
.input(zNewBookmarkRequestSchema)
.output(zBookmarkSchema)
.mutation(async ({ input, ctx }) => {
- const { url } = input;
-
const bookmark = await ctx.db.transaction(
async (tx): Promise<ZBookmark> => {
const bookmark = (
@@ -112,20 +114,39 @@ export const bookmarksAppRouter = router({
.returning()
)[0];
- const link = (
- await tx
- .insert(bookmarkLinks)
- .values({
- id: bookmark.id,
- url,
- })
- .returning()
- )[0];
+ let content: ZBookmarkContent;
- const content: ZBookmarkContent = {
- type: "link",
- ...link,
- };
+ switch (input.type) {
+ case "link": {
+ const link = (
+ await tx
+ .insert(bookmarkLinks)
+ .values({
+ id: bookmark.id,
+ url: input.url,
+ })
+ .returning()
+ )[0];
+ content = {
+ type: "link",
+ ...link,
+ };
+ break;
+ }
+ case "text": {
+ const text = (
+ await tx
+ .insert(bookmarkTexts)
+ .values({ id: bookmark.id, text: input.text })
+ .returning()
+ )[0];
+ content = {
+ type: "text",
+ text: text.text || "",
+ };
+ break;
+ }
+ }
return {
tags: [] as ZBookmarkTags[],
@@ -170,6 +191,30 @@ export const bookmarksAppRouter = router({
return res[0];
}),
+ updateBookmarkText: authedProcedure
+ .input(
+ z.object({
+ bookmarkId: z.string(),
+ text: z.string().max(2000),
+ }),
+ )
+ .use(ensureBookmarkOwnership)
+ .mutation(async ({ input, ctx }) => {
+ const res = await ctx.db
+ .update(bookmarkTexts)
+ .set({
+ text: input.text,
+ })
+ .where(and(eq(bookmarkTexts.id, input.bookmarkId)))
+ .returning();
+ if (res.length == 0) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Bookmark not found",
+ });
+ }
+ }),
+
deleteBookmark: authedProcedure
.input(z.object({ bookmarkId: z.string() }))
.use(ensureBookmarkOwnership)
@@ -212,6 +257,7 @@ export const bookmarksAppRouter = router({
},
},
link: true,
+ text: true,
},
});
if (!bookmark) {
@@ -246,6 +292,7 @@ export const bookmarksAppRouter = router({
},
},
link: true,
+ text: true,
},
});
@@ -271,7 +318,7 @@ export const bookmarksAppRouter = router({
await ctx.db.transaction(async (tx) => {
// Detaches
if (input.detach.length > 0) {
- await ctx.db.delete(tagsOnBookmarks).where(
+ await tx.delete(tagsOnBookmarks).where(
and(
eq(tagsOnBookmarks.bookmarkId, input.bookmarkId),
inArray(
@@ -295,7 +342,7 @@ export const bookmarksAppRouter = router({
}));
if (toBeCreatedTags.length > 0) {
- await ctx.db
+ await tx
.insert(bookmarkTags)
.values(toBeCreatedTags)
.onConflictDoNothing()
@@ -303,7 +350,7 @@ export const bookmarksAppRouter = router({
}
const allIds = (
- await ctx.db.query.bookmarkTags.findMany({
+ await tx.query.bookmarkTags.findMany({
where: and(
eq(bookmarkTags.userId, ctx.user.id),
inArray(
@@ -317,7 +364,7 @@ export const bookmarksAppRouter = router({
})
).map((t) => t.id);
- await ctx.db
+ await tx
.insert(tagsOnBookmarks)
.values(
allIds.map((i) => ({