From 129012769d7129ae9d74f8d9b8c79835e6b4f4c9 Mon Sep 17 00:00:00 2001 From: TZGyn Date: Tue, 15 Aug 2023 19:04:32 +0800 Subject: [PATCH] New feature: bookmark categories --- app/dashboard/page.tsx | 24 ++--- components/newBookmarkForm.tsx | 1 + components/tabs.tsx | 34 +++++++ drizzle/0001_busy_legion.sql | 6 ++ drizzle/meta/0001_snapshot.json | 83 +++++++++++++++++ drizzle/meta/_journal.json | 7 ++ lib/schema.ts | 31 +++++-- package.json | 2 + pnpm-lock.yaml | 152 ++++++++++++++++++++++++++++++++ types/index.ts | 27 +++++- 10 files changed, 345 insertions(+), 22 deletions(-) create mode 100644 components/tabs.tsx create mode 100644 drizzle/0001_busy_legion.sql create mode 100644 drizzle/meta/0001_snapshot.json diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index f146c18..172003f 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -1,24 +1,26 @@ -import { BookmarkCard } from '@/components/bookmarkCard' -import { bookmarkSchema } from '@/types' +import { bookmarkCategoryWithBookmarksSchema } from '@/types' import { bookmark } from '@/lib/schema' import { db } from '@/lib/db' import EditBookmarkForm from '@/components/editBookmarkForm' +import BookmarkTabs from '@/components/tabs' export const dynamic = 'force-dynamic' export const fetchCache = 'force-no-store' export default async function DashboardPage() { - const data = await db.select().from(bookmark) + const data = await db.query.bookmarkCategory.findMany({ + with: { + bookmark: true, + }, + }) + + const bookmarkCategoryWithBookmarks = bookmarkCategoryWithBookmarksSchema + .array() + .parse(data) - const bookmarks = bookmarkSchema.array().parse(data) return ( -
- {bookmarks.map((data, index) => ( - - ))} +
+
) diff --git a/components/newBookmarkForm.tsx b/components/newBookmarkForm.tsx index 5f793fa..798aa80 100644 --- a/components/newBookmarkForm.tsx +++ b/components/newBookmarkForm.tsx @@ -24,6 +24,7 @@ export default function NewBookmarkForm() { link, description, url, + category_id: 2, } await fetch('/api/bookmark', { method: 'POST', diff --git a/components/tabs.tsx b/components/tabs.tsx new file mode 100644 index 0000000..e16aabb --- /dev/null +++ b/components/tabs.tsx @@ -0,0 +1,34 @@ +'use client' + +import { BookmarkCategoryWithBookmarks } from '@/types' +import { Tabs, Tab } from '@nextui-org/tabs' +import { BookmarkCard } from './bookmarkCard' + +export default function BookmarkTabs({ + data, +}: { + data: BookmarkCategoryWithBookmarks[] +}) { + return ( +
+ + {data.map((bookmarkCategory, index) => ( + +
+ {bookmarkCategory.bookmark.map((data, index) => ( + + ))} +
+
+ ))} +
+
+ ) +} diff --git a/drizzle/0001_busy_legion.sql b/drizzle/0001_busy_legion.sql new file mode 100644 index 0000000..167f9cf --- /dev/null +++ b/drizzle/0001_busy_legion.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS "bookmark_categories" ( + "id" serial PRIMARY KEY NOT NULL, + "name" text +); +--> statement-breakpoint +ALTER TABLE "bookmarks" ADD COLUMN "category_id" integer; \ No newline at end of file diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..1c29e8c --- /dev/null +++ b/drizzle/meta/0001_snapshot.json @@ -0,0 +1,83 @@ +{ + "version": "5", + "dialect": "pg", + "id": "288ceda5-82ca-464f-a1f1-ab4e1aad475d", + "prevId": "e3dded07-6461-44e7-9c2e-e9e750dfcbd7", + "tables": { + "bookmarks": { + "name": "bookmarks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "category_id": { + "name": "category_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "link": { + "name": "link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "bookmark_categories": { + "name": "bookmark_categories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index b99cb0b..eb31c3c 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1691820164272, "tag": "0000_charming_warstar", "breakpoints": true + }, + { + "idx": 1, + "version": "5", + "when": 1692095367785, + "tag": "0001_busy_legion", + "breakpoints": true } ] } \ No newline at end of file diff --git a/lib/schema.ts b/lib/schema.ts index 46fda81..8e65468 100644 --- a/lib/schema.ts +++ b/lib/schema.ts @@ -20,19 +20,36 @@ import { // export const userRelations = relations(user, ({ many }) => ({ // bookmark: many(bookmark), // })) -// + export const bookmark = pgTable('bookmarks', { id: serial('id').primaryKey(), // userId: integer('user_id').references(() => user.id), + category_id: integer('category_id'), name: text('name'), link: text('link'), description: text('description'), url: text('url'), }) -// export const bookmarkRelations = relations(bookmark, ({ one }) => ({ -// user: one(user, { -// fields: [bookmark.userId], -// references: [user.id], -// }), -// })) +export const bookmarkRelations = relations(bookmark, ({ one }) => ({ + // user: one(user, { + // fields: [bookmark.userId], + // references: [user.id], + // }), + bookmarkCategory: one(bookmarkCategory, { + fields: [bookmark.category_id], + references: [bookmarkCategory.id], + }), +})) + +export const bookmarkCategory = pgTable('bookmark_categories', { + id: serial('id').primaryKey(), + name: text('name'), +}) + +export const bookmarkCategoryRelations = relations( + bookmarkCategory, + ({ many }) => ({ + bookmark: many(bookmark), + }) +) diff --git a/package.json b/package.json index c7b7f2f..d630676 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@nextui-org/snippet": "2.0.0", "@nextui-org/switch": "2.0.0", "@nextui-org/system": "2.0.0", + "@nextui-org/tabs": "^2.0.11", "@nextui-org/theme": "2.0.0", "@react-aria/ssr": "^3.6.0", "@react-aria/visually-hidden": "^3.8.1", @@ -39,6 +40,7 @@ "intl-messageformat": "^10.1.0", "next": "13.4.9", "next-themes": "^0.2.1", + "pocketbase": "^0.16.0", "postcss": "8.4.24", "postgres": "^3.3.5", "react": "18.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8eb6697..8e320ff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,6 +50,9 @@ dependencies: '@nextui-org/system': specifier: 2.0.0 version: 2.0.0(react-dom@18.2.0)(react@18.2.0)(tailwindcss@3.3.2) + '@nextui-org/tabs': + specifier: ^2.0.11 + version: 2.0.11(framer-motion@10.12.16)(react-dom@18.2.0)(react@18.2.0)(tailwindcss@3.3.2) '@nextui-org/theme': specifier: 2.0.0 version: 2.0.0(tailwindcss@3.3.2) @@ -95,6 +98,9 @@ dependencies: next-themes: specifier: ^0.2.1 version: 0.2.1(next@13.4.9)(react-dom@18.2.0)(react@18.2.0) + pocketbase: + specifier: ^0.16.0 + version: 0.16.0 postcss: specifier: 8.4.24 version: 8.4.24 @@ -1269,6 +1275,10 @@ packages: resolution: {integrity: sha512-hYGZ6vgyF1QhatU1E3BhhOy/sOrw5D813lXD/bQ+YkSAS5eRhhWFgpka+PLiZGq8GWAyXKfsOuRucdoGSo4g1w==} dev: false + /@nextui-org/react-rsc-utils@2.0.6: + resolution: {integrity: sha512-32hglPfvA77lFO1XbDBf8a/bVthRjFTWWsYAMGWFF4GKhl/WYBzVRaFmIQjimd6FOfLFhPvolWHEukH3kTlyyQ==} + dev: false + /@nextui-org/react-utils@2.0.0(react@18.2.0): resolution: {integrity: sha512-CFAsbxv1Dw9Hp6x6wvUuhgyqP4w4cFgMva5YhLaWE/5gH9c1PpHRMtTvxouA523r1iRDRuImyNJeSinvvTZPkw==} peerDependencies: @@ -1309,6 +1319,16 @@ packages: react: 18.2.0 dev: false + /@nextui-org/react-utils@2.0.6(react@18.2.0): + resolution: {integrity: sha512-9mRDJj1hNNdKTxt9idQc/OVI+3UzvSErRCjN2hfVVi2c5JrR5b0/Zpx7PV0PHy7yfGUn1hyvwK5ZTVkNhxISKQ==} + peerDependencies: + react: '>=18' + dependencies: + '@nextui-org/react-rsc-utils': 2.0.6 + '@nextui-org/shared-utils': 2.0.2(react@18.2.0) + react: 18.2.0 + dev: false + /@nextui-org/ripple@2.0.0(framer-motion@10.12.16)(react-dom@18.2.0)(react@18.2.0)(tailwindcss@3.3.2): resolution: {integrity: sha512-9pHZwwRVkMVU8SjNBwhEpwX5Lwb2HXx53N4O0bVy63/yA6L+jt50ZBuL8k0F42Sx2ApdkIi6o8Y10yBqowthsg==} peerDependencies: @@ -1522,6 +1542,35 @@ packages: - tailwindcss dev: false + /@nextui-org/tabs@2.0.11(framer-motion@10.12.16)(react-dom@18.2.0)(react@18.2.0)(tailwindcss@3.3.2): + resolution: {integrity: sha512-7LsOAuhDF1MscYM1l0v0S1l77cS6CT99C6Mr5VDu9VbFraSiB+I2sFAwlwkTRxYvrX5JVSwywyU3zvwQcJ/e+w==} + peerDependencies: + framer-motion: '>=4.0.0' + react: '>=18' + dependencies: + '@nextui-org/aria-utils': 2.0.5(react-dom@18.2.0)(react@18.2.0)(tailwindcss@3.3.2) + '@nextui-org/framer-transitions': 2.0.5(framer-motion@10.12.16)(react-dom@18.2.0)(react@18.2.0)(tailwindcss@3.3.2) + '@nextui-org/react-utils': 2.0.6(react@18.2.0) + '@nextui-org/shared-utils': 2.0.2(react@18.2.0) + '@nextui-org/system': 2.0.5(react-dom@18.2.0)(react@18.2.0)(tailwindcss@3.3.2) + '@nextui-org/theme': 2.0.5(tailwindcss@3.3.2) + '@nextui-org/use-is-mounted': 2.0.2(react-dom@18.2.0)(react@18.2.0) + '@nextui-org/use-update-effect': 2.0.2(react@18.2.0) + '@react-aria/focus': 3.14.0(react@18.2.0) + '@react-aria/interactions': 3.17.0(react@18.2.0) + '@react-aria/tabs': 3.6.2(react@18.2.0) + '@react-aria/utils': 3.19.0(react@18.2.0) + '@react-stately/tabs': 3.5.1(react@18.2.0) + '@react-types/shared': 3.19.0(react@18.2.0) + '@react-types/tabs': 3.3.1(react@18.2.0) + framer-motion: 10.12.16(react-dom@18.2.0)(react@18.2.0) + react: 18.2.0 + scroll-into-view-if-needed: 3.0.10 + transitivePeerDependencies: + - react-dom + - tailwindcss + dev: false + /@nextui-org/theme@2.0.0(tailwindcss@3.3.2): resolution: {integrity: sha512-Pe19Ts3xg9uOv/Azv1s6aUIQT16Fhc6ox//25zFCctQv5AwpW56gmt09wcQTCUXG+0niIvh2ODFZzKlN3v6cmQ==} peerDependencies: @@ -1558,6 +1607,24 @@ packages: tailwindcss: 3.3.2 dev: false + /@nextui-org/theme@2.0.5(tailwindcss@3.3.2): + resolution: {integrity: sha512-4br6xRTYGbDCL8Vwm4CP0VqEXORROyQCmLSwNFaXr6s0vB5nX5tnJHqyDIvpq5Al7I2H8HMG2KOUHyhbaKOKFA==} + peerDependencies: + tailwindcss: '*' + dependencies: + color: 4.2.3 + color2k: 2.0.2 + deepmerge: 4.3.1 + flat: 5.0.2 + lodash.foreach: 4.5.0 + lodash.get: 4.4.2 + lodash.kebabcase: 4.1.1 + lodash.mapkeys: 4.6.0 + lodash.omit: 4.5.0 + tailwind-variants: 0.1.13(tailwindcss@3.3.2) + tailwindcss: 3.3.2 + dev: false + /@nextui-org/tooltip@2.0.0(framer-motion@10.12.16)(react-dom@18.2.0)(react@18.2.0)(tailwindcss@3.3.2): resolution: {integrity: sha512-uY+NbZaWLevKAWZSpPPG3JXApMEXemZZmQnkPmKWLHBTxO3TNIcYDIePfQiSQs4AxbWpeJaSgv0BCykatTQSwg==} peerDependencies: @@ -1696,6 +1763,16 @@ packages: react: 18.2.0 dev: false + /@nextui-org/use-is-mounted@2.0.2(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-PjwpTkl5f+bTVU9l5GzgZDHd+uOwCZ3bhuYzbbamw1J5kBWruVnKUqZihS3zrLtJxKNxk/f7RT0UWK2a4wGpDw==} + peerDependencies: + react: '>=18' + react-dom: '>=16.8.0' + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@nextui-org/use-safe-layout-effect@2.0.2(react@18.2.0): resolution: {integrity: sha512-HsFP2e+o2eSiQyAXdiicPBj6qj1naHuiNqqeTPqeJBsr0aUZI8l+7vZ5OXjLc8Qou4AOyNyJBBGFNhwsraxdpw==} peerDependencies: @@ -1712,6 +1789,14 @@ packages: react: 18.2.0 dev: false + /@nextui-org/use-update-effect@2.0.2(react@18.2.0): + resolution: {integrity: sha512-yN2LWvG2QNDz6XDjRZq6jBQ7+Jaz2eihy+Q7IR+XLXi6fsyKQuYKxphw5VANa0ZbvKVuN/n5m5WRDRmWmeeOWw==} + peerDependencies: + react: '>=18' + dependencies: + react: 18.2.0 + dev: false + /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -2049,6 +2134,24 @@ packages: react: 18.2.0 dev: false + /@react-aria/tabs@3.6.2(react@18.2.0): + resolution: {integrity: sha512-FjI0h1Z4TsLOvIODhdDrVLz0O8RAqxDi58DO88CwkdUrWwZspNEpSpHhDarzUT7MlX3X72lsAUwvQLqY1OmaBQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + dependencies: + '@react-aria/focus': 3.14.0(react@18.2.0) + '@react-aria/i18n': 3.8.1(react@18.2.0) + '@react-aria/interactions': 3.17.0(react@18.2.0) + '@react-aria/selection': 3.16.1(react@18.2.0) + '@react-aria/utils': 3.19.0(react@18.2.0) + '@react-stately/list': 3.9.1(react@18.2.0) + '@react-stately/tabs': 3.5.1(react@18.2.0) + '@react-types/shared': 3.19.0(react@18.2.0) + '@react-types/tabs': 3.3.1(react@18.2.0) + '@swc/helpers': 0.5.1 + react: 18.2.0 + dev: false + /@react-aria/textfield@3.10.0(react@18.2.0): resolution: {integrity: sha512-TYFgDTlxrljakD0TGOkoSCvot9BfVCZSrTKy3+/PICSTkPIzXThLIQmpX6yObLMXQSNW6SvBCl6CMetJMJHcbw==} peerDependencies: @@ -2208,6 +2311,19 @@ packages: react: 18.2.0 dev: false + /@react-stately/list@3.9.1(react@18.2.0): + resolution: {integrity: sha512-GiKrxGakzMTZKe3mp410l4xKiHbZplJCGrtqlxq/+YRD0uCQwWGYpRG+z9A7tTCusruRD3m91/OjWsbfbGdiEw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + dependencies: + '@react-stately/collections': 3.10.0(react@18.2.0) + '@react-stately/selection': 3.13.3(react@18.2.0) + '@react-stately/utils': 3.7.0(react@18.2.0) + '@react-types/shared': 3.19.0(react@18.2.0) + '@swc/helpers': 0.5.1 + react: 18.2.0 + dev: false + /@react-stately/menu@3.5.4(react@18.2.0): resolution: {integrity: sha512-+Q71fMDhMM1iARPFtwqpXY/8qkb0dN4PBJbcjwjGCumGs+ja2YbZxLBHCP0DYBElS9l6m3ssF47RKNMtF/Oi5w==} peerDependencies: @@ -2255,6 +2371,19 @@ packages: react: 18.2.0 dev: false + /@react-stately/tabs@3.5.1(react@18.2.0): + resolution: {integrity: sha512-p1vZOuIS98GMF9jfEHQA6Pir1wYY6j+Gni6DcluNnWj90rLEubuwARNw7uscoOaXKlK/DiZIhkLKSDsA5tbadQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + dependencies: + '@react-stately/list': 3.9.1(react@18.2.0) + '@react-stately/utils': 3.7.0(react@18.2.0) + '@react-types/shared': 3.19.0(react@18.2.0) + '@react-types/tabs': 3.3.1(react@18.2.0) + '@swc/helpers': 0.5.1 + react: 18.2.0 + dev: false + /@react-stately/toggle@3.6.0(react@18.2.0): resolution: {integrity: sha512-w+Aqh78H9MLs0FDUYTjAzYhrHQWaDJ2zWjyg2oYcSvERES0+D0obmPvtJLWsFrJ8fHJrTmxd7ezVFBY9BbPeFQ==} peerDependencies: @@ -2451,6 +2580,15 @@ packages: react: 18.2.0 dev: false + /@react-types/tabs@3.3.1(react@18.2.0): + resolution: {integrity: sha512-vPxSbLCU7RT+Rupvu/1uOAesxlR/53GD5ZbgLuQRr/oEZRbsjY8Cs3CE3LGv49VdvBWivXUvHiF5wSE7CdWs1w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + dependencies: + '@react-types/shared': 3.19.0(react@18.2.0) + react: 18.2.0 + dev: false + /@react-types/textfield@3.7.2(react@18.2.0): resolution: {integrity: sha512-TsZTf1+4Ve9QHm6mbXr26uLOA4QtZPgyjYgYclL2nHoOl67algeQIFxIVfdlNIKFFMOw5BtC6Mer0I3KUWtbOQ==} peerDependencies: @@ -2934,6 +3072,10 @@ packages: engines: {node: ^12.20.0 || >=14} dev: true + /compute-scroll-into-view@3.0.3: + resolution: {integrity: sha512-nadqwNxghAGTamwIqQSG433W6OADZx2vCo3UXHNrzTRHK/htu+7+L0zhjEoaeaQVNAi3YgqWDv8+tzf0hRfR+A==} + dev: false + /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} dev: false @@ -4730,6 +4872,10 @@ packages: engines: {node: '>= 6'} dev: false + /pocketbase@0.16.0: + resolution: {integrity: sha512-sndPu4Mbu1Y2VefvO2gIa/No5aIdP6E5br9HUGomvDYSfkmGfBxJNkivJSSKlr9KwWnaF8otf14304UUeFGOrA==} + dev: false + /postcss-import@15.1.0(postcss@8.4.24): resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} engines: {node: '>=14.0.0'} @@ -5092,6 +5238,12 @@ packages: loose-envify: 1.4.0 dev: false + /scroll-into-view-if-needed@3.0.10: + resolution: {integrity: sha512-t44QCeDKAPf1mtQH3fYpWz8IM/DyvHLjs8wUvvwMYxk5moOqCzrMSxK6HQVD0QVmVjXFavoFIPRVrMuJPKAvtg==} + dependencies: + compute-scroll-into-view: 3.0.3 + dev: false + /semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true diff --git a/types/index.ts b/types/index.ts index d2070cb..fc6ed32 100644 --- a/types/index.ts +++ b/types/index.ts @@ -6,10 +6,11 @@ export type IconSvgProps = SVGProps & { } const bookmark = { - name: z.string(), - link: z.string(), - description: z.string(), - url: z.string().url(), + name: z.string().nonempty(), + link: z.string().nonempty(), + description: z.string().nonempty(), + url: z.string().url().nonempty(), + category_id: z.number(), } export const bookmarkSchema = z.object({ @@ -22,3 +23,21 @@ export const newBookmarkSchema = z.object({ }) export type Bookmark = z.infer + +const bookmarkCategory = { + name: z.string().nonempty(), +} + +export const bookmarkCategorySchema = z.object({ + id: z.number(), +}) + +export const bookmarkCategoryWithBookmarksSchema = z.object({ + id: z.number(), + ...bookmarkCategory, + bookmark: bookmarkSchema.array(), +}) + +export type BookmarkCategoryWithBookmarks = z.infer< + typeof bookmarkCategoryWithBookmarksSchema +>