New feature: bookmark categories

master
TZGyn 2 years ago
parent 588147a32c
commit 129012769d
Signed by: TZGyn
GPG Key ID: 122EAF77AE81FD4A

@ -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 (
<div className='mt-4 flex w-full max-w-4xl flex-row flex-wrap justify-center gap-6'>
{bookmarks.map((data, index) => (
<BookmarkCard
key={index}
data={data}
/>
))}
<div className='flex w-full max-w-4xl flex-row flex-wrap justify-start gap-6'>
<BookmarkTabs data={bookmarkCategoryWithBookmarks} />
<EditBookmarkForm />
</div>
)

@ -24,6 +24,7 @@ export default function NewBookmarkForm() {
link,
description,
url,
category_id: 2,
}
await fetch('/api/bookmark', {
method: 'POST',

@ -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 (
<div className='flex w-full max-w-4xl flex-col'>
<Tabs
fullWidth
color='primary'>
{data.map((bookmarkCategory, index) => (
<Tab
key={index}
title={bookmarkCategory.name}>
<div className='mt-4 flex flex-wrap justify-center gap-6'>
{bookmarkCategory.bookmark.map((data, index) => (
<BookmarkCard
key={index}
data={data}
/>
))}
</div>
</Tab>
))}
</Tabs>
</div>
)
}

@ -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;

@ -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": {}
}
}

@ -8,6 +8,13 @@
"when": 1691820164272,
"tag": "0000_charming_warstar",
"breakpoints": true
},
{
"idx": 1,
"version": "5",
"when": 1692095367785,
"tag": "0001_busy_legion",
"breakpoints": true
}
]
}

@ -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),
})
)

@ -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",

@ -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

@ -6,10 +6,11 @@ export type IconSvgProps = SVGProps<SVGSVGElement> & {
}
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<typeof bookmarkSchema>
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
>

Loading…
Cancel
Save