This commit is contained in:
shrt 2025-03-11 15:51:31 +01:00
parent bccd089c7f
commit 7135e34699
34 changed files with 633 additions and 130 deletions

View File

@ -21,11 +21,16 @@
},
"dependencies": {
"@auth/drizzle-adapter": "^1.7.2",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@hookform/resolvers": "^4.1.3",
"@paralleldrive/cuid2": "^2.2.2",
"@radix-ui/react-alert-dialog": "^1.1.6",
"@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-collapsible": "^1.1.3",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-popover": "^1.1.6",
@ -37,6 +42,7 @@
"@t3-oss/env-nextjs": "^0.10.1",
"@tanstack/react-query": "^5.50.0",
"@tanstack/react-table": "^8.21.2",
"@tiptap/core": "^2.11.5",
"@tiptap/extension-color": "^2.11.5",
"@tiptap/extension-image": "^2.11.5",
"@tiptap/extension-list-item": "^2.11.5",
@ -58,8 +64,10 @@
"next-auth": "5.0.0-beta.25",
"next-themes": "^0.4.4",
"postgres": "^3.4.4",
"prosemirror-state": "^1.4.3",
"react": "^18.3.1",
"react-colorful": "^5.6.1",
"react-dnd": "^16.0.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.54.2",
"react-textarea-autosize": "^8.5.7",
@ -68,6 +76,7 @@
"superjson": "^2.2.1",
"tailwind-merge": "^3.0.2",
"tailwindcss-animate": "^1.0.7",
"tiptap-extension-global-drag-handle": "^0.1.18",
"zod": "^3.24.2"
},
"devDependencies": {

202
pnpm-lock.yaml generated
View File

@ -11,6 +11,18 @@ importers:
'@auth/drizzle-adapter':
specifier: ^1.7.2
version: 1.8.0
'@dnd-kit/core':
specifier: ^6.3.1
version: 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@dnd-kit/modifiers':
specifier: ^9.0.0
version: 9.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
'@dnd-kit/sortable':
specifier: ^10.0.0
version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
'@dnd-kit/utilities':
specifier: ^3.2.2
version: 3.2.2(react@18.3.1)
'@hookform/resolvers':
specifier: ^4.1.3
version: 4.1.3(react-hook-form@7.54.2(react@18.3.1))
@ -26,6 +38,9 @@ importers:
'@radix-ui/react-checkbox':
specifier: ^1.1.4
version: 1.1.4(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-collapsible':
specifier: ^1.1.3
version: 1.1.3(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-dialog':
specifier: ^1.1.6
version: 1.1.6(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -59,6 +74,9 @@ importers:
'@tanstack/react-table':
specifier: ^8.21.2
version: 8.21.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@tiptap/core':
specifier: ^2.11.5
version: 2.11.5(@tiptap/pm@2.11.5)
'@tiptap/extension-color':
specifier: ^2.11.5
version: 2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/extension-text-style@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5)))
@ -122,12 +140,18 @@ importers:
postgres:
specifier: ^3.4.4
version: 3.4.5
prosemirror-state:
specifier: ^1.4.3
version: 1.4.3
react:
specifier: ^18.3.1
version: 18.3.1
react-colorful:
specifier: ^5.6.1
version: 5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-dnd:
specifier: ^16.0.1
version: 16.0.1(@types/node@20.17.23)(@types/react@18.3.18)(react@18.3.1)
react-dom:
specifier: ^18.3.1
version: 18.3.1(react@18.3.1)
@ -152,6 +176,9 @@ importers:
tailwindcss-animate:
specifier: ^1.0.7
version: 1.0.7(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.17.23)(typescript@5.8.2)))
tiptap-extension-global-drag-handle:
specifier: ^0.1.18
version: 0.1.18
zod:
specifier: ^3.24.2
version: 3.24.2
@ -268,6 +295,34 @@ packages:
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
'@dnd-kit/accessibility@3.1.1':
resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==}
peerDependencies:
react: '>=16.8.0'
'@dnd-kit/core@6.3.1':
resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
'@dnd-kit/modifiers@9.0.0':
resolution: {integrity: sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==}
peerDependencies:
'@dnd-kit/core': ^6.3.0
react: '>=16.8.0'
'@dnd-kit/sortable@10.0.0':
resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==}
peerDependencies:
'@dnd-kit/core': ^6.3.0
react: '>=16.8.0'
'@dnd-kit/utilities@3.2.2':
resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==}
peerDependencies:
react: '>=16.8.0'
'@drizzle-team/brocli@0.10.2':
resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==}
@ -1031,6 +1086,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-collapsible@1.1.3':
resolution: {integrity: sha512-jFSerheto1X03MUC0g6R7LedNW9EEGWdg9W1+MlpkMLwGkgkbUXLPBH/KIuWKXUoeYRVY11llqbTBDzuLg7qrw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-collection@1.1.2':
resolution: {integrity: sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==}
peerDependencies:
@ -1505,6 +1573,15 @@ packages:
'@radix-ui/rect@1.1.0':
resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==}
'@react-dnd/asap@5.0.2':
resolution: {integrity: sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==}
'@react-dnd/invariant@4.0.2':
resolution: {integrity: sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==}
'@react-dnd/shallowequal@4.0.2':
resolution: {integrity: sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==}
'@remirror/core-constants@3.0.0':
resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==}
@ -2115,6 +2192,9 @@ packages:
dlv@1.1.3:
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
dnd-core@16.0.1:
resolution: {integrity: sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==}
doctrine@2.1.0:
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
engines: {node: '>=0.10.0'}
@ -2572,6 +2652,9 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
hoist-non-react-statics@3.3.2:
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
@ -3220,6 +3303,21 @@ packages:
react: '>=16.8.0'
react-dom: '>=16.8.0'
react-dnd@16.0.1:
resolution: {integrity: sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==}
peerDependencies:
'@types/hoist-non-react-statics': '>= 3.3.1'
'@types/node': '>= 12'
'@types/react': '>= 16'
react: '>= 16.14'
peerDependenciesMeta:
'@types/hoist-non-react-statics':
optional: true
'@types/node':
optional: true
'@types/react':
optional: true
react-dom@18.3.1:
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
peerDependencies:
@ -3291,6 +3389,9 @@ packages:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
redux@4.2.1:
resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==}
reflect.getprototypeof@1.0.10:
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
engines: {node: '>= 0.4'}
@ -3542,6 +3643,9 @@ packages:
tippy.js@6.3.7:
resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==}
tiptap-extension-global-drag-handle@0.1.18:
resolution: {integrity: sha512-jwFuy1K8DP3a4bFy76Hpc63w1Sil0B7uZ3mvhQomVvUFCU787Lg2FowNhn7NFzeyok761qY2VG+PZ/FDthWUdg==}
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
@ -3782,6 +3886,38 @@ snapshots:
dependencies:
'@jridgewell/trace-mapping': 0.3.9
'@dnd-kit/accessibility@3.1.1(react@18.3.1)':
dependencies:
react: 18.3.1
tslib: 2.8.1
'@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@dnd-kit/accessibility': 3.1.1(react@18.3.1)
'@dnd-kit/utilities': 3.2.2(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
tslib: 2.8.1
'@dnd-kit/modifiers@9.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)':
dependencies:
'@dnd-kit/core': 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@dnd-kit/utilities': 3.2.2(react@18.3.1)
react: 18.3.1
tslib: 2.8.1
'@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)':
dependencies:
'@dnd-kit/core': 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@dnd-kit/utilities': 3.2.2(react@18.3.1)
react: 18.3.1
tslib: 2.8.1
'@dnd-kit/utilities@3.2.2(react@18.3.1)':
dependencies:
react: 18.3.1
tslib: 2.8.1
'@drizzle-team/brocli@0.10.2': {}
'@emnapi/runtime@1.3.1':
@ -4288,6 +4424,22 @@ snapshots:
'@types/react': 18.3.18
'@types/react-dom': 18.3.5(@types/react@18.3.18)
'@radix-ui/react-collapsible@1.1.3(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.1
'@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1)
'@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1)
'@radix-ui/react-id': 1.1.0(@types/react@18.3.18)(react@18.3.1)
'@radix-ui/react-presence': 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@18.3.1)
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.18)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.18
'@types/react-dom': 18.3.5(@types/react@18.3.18)
'@radix-ui/react-collection@1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1)
@ -4740,6 +4892,12 @@ snapshots:
'@radix-ui/rect@1.1.0': {}
'@react-dnd/asap@5.0.2': {}
'@react-dnd/invariant@4.0.2': {}
'@react-dnd/shallowequal@4.0.2': {}
'@remirror/core-constants@3.0.0': {}
'@rtsao/scc@1.1.0': {}
@ -5391,6 +5549,12 @@ snapshots:
dlv@1.1.3: {}
dnd-core@16.0.1:
dependencies:
'@react-dnd/asap': 5.0.2
'@react-dnd/invariant': 4.0.2
redux: 4.2.1
doctrine@2.1.0:
dependencies:
esutils: 2.0.3
@ -5629,8 +5793,8 @@ snapshots:
'@typescript-eslint/parser': 8.26.0(eslint@8.57.1)(typescript@5.8.2)
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.31.0)(eslint@8.57.1)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.3)(eslint@8.57.1)
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
eslint-plugin-react: 7.37.4(eslint@8.57.1)
eslint-plugin-react-hooks: 5.2.0(eslint@8.57.1)
@ -5649,7 +5813,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1):
eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0)(eslint@8.57.1):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.0
@ -5660,18 +5824,18 @@ snapshots:
stable-hash: 0.0.4
tinyglobby: 0.2.12
optionalDependencies:
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.3)(eslint@8.57.1)
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@8.57.1):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 8.26.0(eslint@8.57.1)(typescript@5.8.2)
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1)
eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.31.0)(eslint@8.57.1)
transitivePeerDependencies:
- supports-color
@ -5679,7 +5843,7 @@ snapshots:
dependencies:
eslint: 8.57.1
eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.3)(eslint@8.57.1):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.8
@ -5690,7 +5854,7 @@ snapshots:
doctrine: 2.1.0
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@8.57.1)
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@ -5998,6 +6162,10 @@ snapshots:
dependencies:
function-bind: 1.1.2
hoist-non-react-statics@3.3.2:
dependencies:
react-is: 16.13.1
ignore@5.3.2: {}
import-fresh@3.3.1:
@ -6608,6 +6776,18 @@ snapshots:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-dnd@16.0.1(@types/node@20.17.23)(@types/react@18.3.18)(react@18.3.1):
dependencies:
'@react-dnd/invariant': 4.0.2
'@react-dnd/shallowequal': 4.0.2
dnd-core: 16.0.1
fast-deep-equal: 3.1.3
hoist-non-react-statics: 3.3.2
react: 18.3.1
optionalDependencies:
'@types/node': 20.17.23
'@types/react': 18.3.18
react-dom@18.3.1(react@18.3.1):
dependencies:
loose-envify: 1.4.0
@ -6679,6 +6859,10 @@ snapshots:
dependencies:
picomatch: 2.3.1
redux@4.2.1:
dependencies:
'@babel/runtime': 7.26.9
reflect.getprototypeof@1.0.10:
dependencies:
call-bind: 1.0.8
@ -7019,6 +7203,8 @@ snapshots:
dependencies:
'@popperjs/core': 2.11.8
tiptap-extension-global-drag-handle@0.1.18: {}
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0

View File

@ -1,3 +1,4 @@
import { appRoutes } from "@/config";
import { Category } from "@/server/db/schema";
import Link from "next/link";
import React from "react";
@ -8,7 +9,7 @@ function CategoryCard({
createdAt,
}: Pick<Category, "name" | "slug" | "createdAt">) {
return (
<Link href={`/kategorie/${slug}`}>
<Link href={appRoutes.category(slug)}>
<div className="rounded-md border p-4">{name}</div>
</Link>
);

View File

@ -3,7 +3,7 @@ import { auth } from "@/server/auth";
import { api } from "@/trpc/server";
import { notFound } from "next/navigation";
import React from "react";
import Editor from "@/components/article/editor/article-form";
import ArticleEditor from "@/components/article/article-form";
async function Page({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
@ -13,7 +13,7 @@ async function Page({ params }: { params: Promise<{ slug: string }> }) {
: false;
const article = await api.article.get({ slug: slug });
if (!article || !isEditor) return notFound();
return <Editor server_article={article} />;
return <ArticleEditor server_article={article} />;
}
export default Page;

View File

@ -1,4 +1,7 @@
import RenderArticle from "@/components/article/render-article";
import BreadNavigator from "@/components/bread-navigator";
import { Button } from "@/components/ui/button";
import { appRoutes } from "@/config";
import { hasPermission, Role } from "@/lib/validation/permissions";
import { auth } from "@/server/auth";
import { api } from "@/trpc/server";
@ -16,13 +19,28 @@ async function Page({ params }: { params: Promise<{ slug: string }> }) {
? hasPermission(session.user.role, Role.EDITOR)
: false;
return (
<div>
<div className="space-y-2">
<div className="flex w-full items-center justify-between">
<h1 className="text-4xl font-bold">{article.title}</h1>
<div className="w-full">
<BreadNavigator
links={[
...(article?.category
? [
{
label: article.category.name!,
href: appRoutes.category(article.category.slug),
},
]
: []),
{ label: "Artikel", href: appRoutes.allArticles },
{ label: article.title!, href: appRoutes.article(article.slug) },
]}
/>
</div>
{isEditor && (
<div className="space-x-2">
<div className="flex w-full justify-end space-x-2">
<Button asChild variant={"outline"}>
<Link href={`/artikel/${article.slug}/edit`}>
<Link href={appRoutes.editArticle(article.slug)}>
<Edit className="size-4" />
<span>Bearbeiten</span>
</Link>
@ -33,6 +51,11 @@ async function Page({ params }: { params: Promise<{ slug: string }> }) {
</div>
)}
</div>
<h1 className="text-4xl font-bold">{article.title}</h1>
{article?.content?.length ? (
<RenderArticle content={article.content} />
) : null}
</div>
);
}

View File

@ -6,7 +6,8 @@ import { api } from "@/trpc/server";
import { Edit } from "lucide-react";
import { notFound } from "next/navigation";
import React from "react";
import ArticleCard from "../../_components/article/article-card";
import ArticleCard from "../../../../components/article/article-card";
import { appRoutes } from "@/config";
async function Page({ params }: { params: Promise<{ name: string }> }) {
const { name } = await params;
@ -22,10 +23,10 @@ async function Page({ params }: { params: Promise<{ name: string }> }) {
<div className="flex items-center justify-between">
<BreadNavigator
links={[
{ label: "Kategorie", href: "/kategorie" },
{ label: "Kategorie", href: appRoutes.allCategories },
{
label: name,
href: `/kategorie/${name}`,
href: appRoutes.category(name),
},
]}
/>

View File

@ -2,7 +2,7 @@ import { auth } from "@/server/auth";
import { api } from "@/trpc/server";
import Link from "next/link";
import GlobalStats from "./_components/global-stats";
import ArticleCard from "./_components/article/article-card";
import ArticleCard from "../../components/article/article-card";
import CategoryCard from "./_components/category/category-card";
export default async function Home() {

View File

@ -14,7 +14,7 @@ import Link from "next/link";
import SidebarLink from "@/components/layout/sidebar-link";
import { Button } from "@/components/ui/button";
import { LogOutIcon } from "lucide-react";
import { appConfig } from "@/app.config";
import { appConfig } from "@/config/app.config";
const data = {
navMain: [

View File

@ -1,3 +1,4 @@
import { appRoutes } from "@/config";
import { Article } from "@/server/db/schema";
import Link from "next/link";
import React from "react";
@ -8,7 +9,7 @@ function ArticleCard({
createdAt,
}: Pick<Article, "title" | "slug" | "createdAt">) {
return (
<Link href={`/artikel/${slug}`}>
<Link href={appRoutes.article(slug)}>
<div className="rounded-md border p-4">{title}</div>
</Link>
);

View File

@ -19,41 +19,32 @@ import {
import { articleSchema } from "@/lib/validation/zod/article";
import { cn, debounce } from "@/lib/utils";
import { updateArticle } from "@/server/actions/article";
import Editor from "./editor";
import Editor from "../text-editor";
import { Badge } from "@/components/ui/badge";
import Link from "next/link";
import CategorySelect from "@/components/category/category-select";
import {
Check,
CheckCircle,
MessageCircleWarning,
XCircle,
} from "lucide-react";
import { Switch } from "@/components/ui/switch";
import PublishArticleAlertDialog from "../publish-article-alert-dialog";
import { CheckCircle, XCircle } from "lucide-react";
import PublishArticleAlertDialog from "./publish-article-alert-dialog";
import { Label } from "@/components/ui/label";
export default ({ server_article }: { server_article: Article }) => {
const [selectedCategory, setSelectedCategory] =
React.useState<Category | null>(null);
const [loading, setLoading] = React.useState(false);
const form = useForm<z.infer<typeof articleSchema>>({
resolver: zodResolver(articleSchema),
defaultValues: {
title: server_article?.title ?? "",
slug: server_article?.slug,
published: server_article?.published ?? false,
categoryId: server_article?.categoryId ?? "",
authorId: server_article?.authorId ?? "",
content:
server_article?.content ??
`<h2>
Hey bearbeite mich!
</h2>`,
content: server_article?.content,
},
});
// 2. Define a submit handler.
async function onSubmit(values: z.infer<typeof articleSchema>) {
console.log("Content before save", values.content);
setLoading(true);
await updateArticle(values, server_article.id);
setLoading(false);
@ -100,11 +91,17 @@ export default ({ server_article }: { server_article: Article }) => {
<FormItem className="w-full">
<FormControl>
<Editor
// content={field.value}
editorProviderProps={{
editorProps: { attributes: { class: "min-h-64" } },
content: field.value,
editorProps: { attributes: { class: "min-h-64" } },
onUpdate: (value) => {
field.onChange(value.editor.getHTML());
const newContent = value.editor.getHTML();
console.log(
"Content :: form",
JSON.stringify(newContent),
);
field.onChange(newContent);
debouncedSubmit();
},
}}

View File

@ -23,24 +23,20 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { articleSchema } from "@/lib/validation/zod/article";
import { createArticleSchema } from "@/lib/validation/zod/article";
import { createArticle } from "@/server/actions/article";
const formSchema = articleSchema.pick({
title: true,
});
function CreateArticleDialog() {
const [open, setOpen] = React.useState<boolean>(false);
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
const form = useForm<z.infer<typeof createArticleSchema>>({
resolver: zodResolver(createArticleSchema),
defaultValues: {
title: "",
},
});
// 2. Define a submit handler.
async function onSubmit(values: z.infer<typeof formSchema>) {
async function onSubmit(values: z.infer<typeof createArticleSchema>) {
setOpen(false);
await createArticle(values);
}

View File

@ -0,0 +1,8 @@
import React from "react";
import Editor from "../text-editor";
function RenderArticle({ content }: { content: string }) {
return <Editor readOnly editorProviderProps={{ content: content }} />;
}
export default RenderArticle;

View File

@ -13,6 +13,8 @@ function BreadNavigator({
}: {
links: { label: string; href: string }[];
}) {
const labelClass =
"w-full max-w-52 overflow-hidden text-ellipsis whitespace-nowrap capitalize block";
return (
<Breadcrumb>
<BreadcrumbList>
@ -20,8 +22,8 @@ function BreadNavigator({
if (idx < links.length - 1)
return (
<React.Fragment key={href}>
<BreadcrumbItem>
<BreadcrumbLink className="capitalize" href={href}>
<BreadcrumbItem className="">
<BreadcrumbLink className={labelClass} href={href}>
{label}
</BreadcrumbLink>
</BreadcrumbItem>
@ -29,7 +31,7 @@ function BreadNavigator({
</React.Fragment>
);
return (
<BreadcrumbItem className="capitalize" key={href}>
<BreadcrumbItem className={labelClass} key={href}>
<BreadcrumbPage>{label}</BreadcrumbPage>
</BreadcrumbItem>
);

View File

@ -5,23 +5,31 @@ import {
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
} from "@/components/ui/sidebar";
import Link from "next/link";
import { Separator } from "../ui/separator";
import SidebarLink from "./sidebar-link";
import { Button } from "../ui/button";
import { HomeIcon } from "lucide-react";
import { Minus, Plus } from "lucide-react";
import { api } from "@/trpc/server";
import { appConfig } from "@/app.config";
import { appConfig } from "@/config/app.config";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "../ui/collapsible";
import { appRoutes } from "@/config";
import SidebarLink from "./sidebar-link";
export async function AppSidebar({
...props
}: React.ComponentProps<typeof Sidebar>) {
const articles = await api.article.getAllPreviews();
const sidebarContent = await api.app.getSidebarContent();
return (
<Sidebar {...props}>
<SidebarHeader className="flex h-14 items-center justify-center border-b">
@ -31,39 +39,39 @@ export async function AppSidebar({
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<Button asChild>
<Link href={"/"}>
<HomeIcon className="size-4" />
<span>Start</span>
</Link>
</Button>
</SidebarGroup>
{/* We create a SidebarGroup for each parent. */}
{/* {data.navMain.map((item) => (
<SidebarGroup key={item.title}>
<SidebarGroupLabel>{item.title}</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{item.items.map((item) => (
<SidebarLink key={item.title} {...item} />
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
))} */}
<SidebarGroup>
<SidebarGroupLabel>Artikel</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{articles?.map((article) => (
<SidebarLink
key={article.slug}
title={article.title!}
url={`/artikel/${article.slug}`}
/>
))}
</SidebarMenu>
</SidebarGroupContent>
<SidebarMenu>
{sidebarContent?.map((category, index) => (
<Collapsible
key={category.id}
defaultOpen={index === 1}
className="group/collapsible"
>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton>
{category.name}{" "}
<Plus className="ml-auto group-data-[state=open]/collapsible:hidden" />
<Minus className="ml-auto group-data-[state=closed]/collapsible:hidden" />
</SidebarMenuButton>
</CollapsibleTrigger>
{category?.articles?.length ? (
<CollapsibleContent>
<SidebarMenuSub>
{category.articles.map((article) => (
<SidebarMenuSubItem key={article.slug}>
<SidebarLink
title={article.title!}
url={appRoutes.article(article.slug)}
/>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
) : null}
</SidebarMenuItem>
</Collapsible>
))}
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>

View File

@ -10,8 +10,9 @@ import {
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import CreateArticleDialog from "@/app/(PAGES)/_components/article/create-article-dialog";
import CreateArticleDialog from "@/components/article/create-article-dialog";
import CreateCategoryDialog from "@/app/(PAGES)/_components/category/create-category-dialog";
import { appRoutes } from "@/config";
async function Navbar() {
const session = await auth();
@ -44,7 +45,7 @@ async function Navbar() {
)}
{isAdmin && (
<Button asChild>
<Link href={"/admin"}>Admin Dashboard</Link>
<Link href={appRoutes.admin.base}>Admin Dashboard</Link>
</Button>
)}
{session ? (

View File

@ -2,17 +2,15 @@
import React from "react";
import { usePathname } from "next/navigation";
import { SidebarMenuButton, SidebarMenuItem } from "@/components/ui/sidebar";
import { SidebarMenuSubButton } from "@/components/ui/sidebar";
import Link from "next/link";
function SidebarLink({ url, title }: { url: string; title: string }) {
const isActive = usePathname() === url;
return (
<SidebarMenuItem>
<SidebarMenuButton asChild isActive={isActive}>
<Link href={url}>{title}</Link>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuSubButton asChild isActive={isActive}>
<Link href={url}>{title}</Link>
</SidebarMenuSubButton>
);
}

View File

View File

@ -0,0 +1,92 @@
import React from "react";
import { NodeViewWrapper, NodeViewContent } from "@tiptap/react";
import { useDrag, useDrop } from "react-dnd";
const DRAG_TYPE = "TIPTAP_BLOCK";
export const DraggableBlockView = (props) => {
const { node, getPos, editor } = props;
const [showDragHandle, setShowDragHandle] = useState(false);
// Get node position and ID
const nodePos = getPos();
const nodeId = `${node.type.name}-${nodePos}`;
// Set up drag
const [{ isDragging }, drag, dragPreview] = useDrag({
type: DRAG_TYPE,
item: { id: nodeId, pos: nodePos, type: node.type.name },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
begin: () => {
// Select the block on drag start
editor.commands.setNodeSelection(nodePos);
return { id: nodeId, pos: nodePos, type: node.type.name };
},
});
// Set up drop
const [{ isOver, canDrop }, drop] = useDrop({
accept: DRAG_TYPE,
drop: (item) => {
if (item.pos !== nodePos) {
// Move the dragged node to this position
const tr = editor.state.tr;
const sourcePos = item.pos;
const targetPos = nodePos;
// Logic to move nodes in the document
// This is the complex part that requires careful handling of ProseMirror positions
const sourceNode = tr.doc.nodeAt(sourcePos);
if (sourceNode) {
tr.delete(sourcePos, sourcePos + sourceNode.nodeSize);
const newTargetPos =
targetPos > sourcePos ? targetPos - sourceNode.nodeSize : targetPos;
tr.insert(newTargetPos, sourceNode);
editor.view.dispatch(tr);
}
}
},
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}),
});
// Reference for the drag preview (the whole block)
const dragRef = useRef(null);
return (
<NodeViewWrapper
ref={drop}
data-drag-handle={nodeId}
style={{
position: "relative",
opacity: isDragging ? 0.5 : 1,
backgroundColor: isOver ? "#f0f9ff" : "transparent",
}}
onMouseEnter={() => setShowDragHandle(true)}
onMouseLeave={() => setShowDragHandle(false)}
>
{showDragHandle && (
<div
ref={drag}
className="drag-handle"
style={{
position: "absolute",
left: "-24px",
top: "8px",
cursor: "grab",
color: "#aaa",
}}
>
</div>
)}
<div ref={dragPreview}>
<NodeViewContent />
</div>
</NodeViewWrapper>
);
};

View File

@ -3,6 +3,23 @@ import ListItem from "@tiptap/extension-list-item";
import TextStyle from "@tiptap/extension-text-style";
import StarterKit from "@tiptap/starter-kit";
import Image from "@tiptap/extension-image";
import GlobalDragHandle from "tiptap-extension-global-drag-handle";
import { Extension } from "@tiptap/core";
import { ReactNodeViewRenderer } from "@tiptap/react";
import { DraggableBlockView } from "./block-drag";
const DraggableBlocks = Extension.create({
name: "draggableBlocks",
addProseMirrorPlugins() {
return [];
},
// Apply this nodeview to all block nodes
addNodeView() {
return ReactNodeViewRenderer(DraggableBlockView);
},
});
export const extensions = [
Color.configure({ types: [TextStyle.name, ListItem.name] }),
@ -11,7 +28,9 @@ export const extensions = [
StarterKit.configure({
code: false,
codeBlock: false,
horizontalRule: {},
heading: {
levels: [1, 2, 3, 4, 5, 6],
},
bulletList: {
keepMarks: true,
keepAttributes: false, // TODO : Making this as `false` becase marks are not preserved when I try to preserve attrs, awaiting a bit of help
@ -21,5 +40,16 @@ export const extensions = [
keepAttributes: false, // TODO : Making this as `false` becase marks are not preserved when I try to preserve attrs, awaiting a bit of help
},
}),
GlobalDragHandle,
Image,
DraggableBlocks.configure({
types: [
"paragraph",
"heading",
"blockquote",
"codeBlock",
"bulletList",
"orderedList",
],
}),
];

View File

@ -5,6 +5,7 @@ import React from "react";
import { EditorProvider, EditorProviderProps } from "@tiptap/react";
import { MenuBar } from "./menu-bar";
import { extensions } from "./extentions";
import { cn } from "@/lib/utils";
function Editor({
editorProviderProps,
@ -14,15 +15,22 @@ function Editor({
readOnly?: boolean;
}) {
return (
<div className="rounded-md bg-gradient-to-b from-muted to-background p-2">
<div
className={cn(
"rounded-md p-2",
!readOnly && "bg-gradient-to-b from-muted to-background",
)}
>
<EditorProvider
immediatelyRender={false}
extensions={extensions}
slotBefore={!readOnly && <MenuBar />}
{...editorProviderProps}
editable={!readOnly}
/>
{JSON.stringify(editorProviderProps.content)}
</div>
);
}
export default Editor;

View File

@ -0,0 +1,34 @@
import { NodeViewWrapper, NodeViewContent } from "@tiptap/react";
import { useDraggable } from "@dnd-kit/core";
const DraggableBlock = ({ node }: any) => {
const { attributes, listeners, setNodeRef, transform } = useDraggable({
id: node.attrs.id,
});
return (
<NodeViewWrapper
ref={setNodeRef}
style={{
transform: transform
? `translate(${transform.x}px, ${transform.y}px)`
: "none",
}}
className="relative cursor-grab border bg-white p-2 shadow-md"
>
{/* Drag Handle */}
<div
{...listeners}
{...attributes}
className="absolute left-0 top-0 cursor-grab bg-gray-300 p-1"
>
</div>
{/* Content */}
<NodeViewContent as="div" />
</NodeViewWrapper>
);
};
export default DraggableBlock;

View File

@ -112,3 +112,24 @@ ol {
.menu-bar .is-active {
@apply border-primary;
}
.drag-handle {
@apply bg-red-500;
}
.block-drag-handle {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
color: #aaa;
background: white;
border-radius: 4px;
z-index: 10;
}
.block-drag-handle:hover {
color: #333;
background: #f1f1f1;
}

View File

@ -0,0 +1,11 @@
"use client"
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
const Collapsible = CollapsiblePrimitive.Root
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
export { Collapsible, CollapsibleTrigger, CollapsibleContent }

43
src/config/app.routes.ts Normal file
View File

@ -0,0 +1,43 @@
export type RouteWithParam = (param: string) => string;
export type Route = string | RouteWithParam;
export type AppRoutes = {
// Home and admin
home: string;
admin: {
base: string;
};
// Article routes
allArticles: string;
article: RouteWithParam;
editArticle: RouteWithParam;
// Category routes
allCategories: string;
category: RouteWithParam;
editCategory: RouteWithParam;
// Auth routes
signin: string;
signout: string;
};
export const appRoutes: AppRoutes = {
home: "/",
admin: { base: "/admin" },
// article
allArticles: "/artikel",
article: (slug) => `/artikel/${slug}`,
editArticle: (slug) => `/artikel/${slug}/bearbeiten`,
// category
allCategories: "/kategorie",
category: (slug) => `/kategorie/${slug}`,
editCategory: (slug) => `/kategorie/${slug}/bearbeiten`,
// auth
signin: "/api/auth/signin",
signout: "/api/auth/signout",
};

2
src/config/index.ts Normal file
View File

@ -0,0 +1,2 @@
export { appConfig } from "./app.config";
export { appRoutes } from "./app.routes";

View File

@ -6,7 +6,13 @@ export function cn(...inputs: ClassValue[]) {
}
export function generateSlug(title: string) {
return title.toLowerCase().replace(/\s+/g, "-");
return title
.toLowerCase() // Convert to lowercase
.trim() // Remove leading/trailing whitespace
.replace(/[^\w\s-]/g, "") // Remove special characters
.replace(/\s+/g, "-") // Replace spaces with hyphens
.replace(/-+/g, "-") // Remove consecutive hyphens
.replace(/^-+|-+$/g, ""); // Remove leading/trailing hyphens
}
export function debounce<T extends (...args: any[]) => void>(
@ -20,18 +26,3 @@ export function debounce<T extends (...args: any[]) => void>(
timeoutId = setTimeout(() => func(...args), delay);
};
}
export function RGBAToHexA(rgba: string, forceRemoveAlpha = false) {
return (
"#" +
rgba
.replace(/^rgba?\(|\s+|\)$/g, "") // Get's rgba / rgb string values
.split(",") // splits them at ","
.filter((string, index) => !forceRemoveAlpha || index !== 3)
.map((string) => parseFloat(string)) // Converts them to numbers
.map((number, index) => (index === 3 ? Math.round(number * 255) : number)) // Converts alpha to 255 number
.map((number) => number.toString(16)) // Converts numbers to hex
.map((string) => (string.length === 1 ? "0" + string : string)) // Adds 0 when length of one number is 1
.join("")
); // Puts the array to togehter to a string
}

View File

@ -2,8 +2,13 @@ import { z } from "zod";
export const articleSchema = z.object({
title: z.string().min(1),
content: z.string().optional(),
slug: z.string().min(1),
content: z.any().optional(),
authorId: z.string().optional(),
categoryId: z.string().optional(),
published: z.boolean(),
});
export const createArticleSchema = articleSchema.pick({
title: true,
});

View File

@ -1,22 +1,29 @@
"use server";
import { articleSchema } from "@/lib/validation/zod/article";
import { appRoutes } from "@/config";
import {
articleSchema,
createArticleSchema,
} from "@/lib/validation/zod/article";
import { api } from "@/trpc/server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { z } from "zod";
export async function createArticle(article: z.infer<typeof articleSchema>) {
export async function createArticle(
article: z.infer<typeof createArticleSchema>,
) {
const result = await api.article.create({
article,
});
if (!result[0]?.slug?.length) return false;
return redirect(`/artikel/${result[0].slug}/edit`);
return redirect(appRoutes.editArticle(result[0].slug));
}
export async function updateArticle(
article: z.infer<typeof articleSchema>,
articleId: string,
) {
console.log("Content :: action", JSON.stringify(article.content));
const result = await api.article.update({
article,
articleId,

View File

@ -3,7 +3,7 @@ import { categoryRouter } from "@/server/api/routers/category";
import { createCallerFactory, createTRPCRouter } from "@/server/api/trpc";
import { usersRouter } from "./routers/users";
import { authorRouter } from "./routers/author";
import { appRouter as globalRouter } from "./routers/app";
/**
* This is the primary router for your server.
*
@ -14,6 +14,7 @@ export const appRouter = createTRPCRouter({
category: categoryRouter,
users: usersRouter,
author: authorRouter,
app: globalRouter,
});
// export type definition of API

View File

@ -0,0 +1,16 @@
import { createTRPCRouter, publicProcedure } from "../trpc";
export const appRouter = createTRPCRouter({
getSidebarContent: publicProcedure.query(async ({ ctx }) => {
return await ctx.db.query.categories.findMany({
with: {
articles: {
columns: {
title: true,
slug: true,
},
},
},
});
}),
});

View File

@ -6,7 +6,10 @@ import {
publicProcedure,
} from "@/server/api/trpc";
import { articles } from "@/server/db/schema";
import { articleSchema } from "@/lib/validation/zod/article";
import {
articleSchema,
createArticleSchema,
} from "@/lib/validation/zod/article";
import { count, eq } from "drizzle-orm";
import { hasPermission, Role } from "@/lib/validation/permissions";
import { generateSlug } from "@/lib/utils";
@ -18,6 +21,7 @@ export const articleRouter = createTRPCRouter({
.query(async ({ ctx, input }) => {
return await ctx.db.query.articles.findFirst({
where: eq(articles.slug, input.slug),
with: { category: true },
});
}),
@ -60,7 +64,7 @@ export const articleRouter = createTRPCRouter({
}),
// mutations
create: protectedProcedure
.input(z.object({ article: articleSchema }))
.input(z.object({ article: createArticleSchema }))
.mutation(async ({ ctx, input }) => {
const isEditor = hasPermission(ctx.session.user.role, Role.EDITOR);
if (!isEditor) {
@ -81,6 +85,11 @@ export const articleRouter = createTRPCRouter({
if (!isEditor) {
throw new Error("You are not allowed to update articles");
}
console.log(
"Content before save",
JSON.stringify(input.article.content),
);
return await ctx.db
.update(articles)
.set(input.article)

View File

@ -4,6 +4,7 @@ import {
boolean,
index,
integer,
jsonb,
pgTableCreator,
primaryKey,
text,
@ -11,6 +12,8 @@ import {
varchar,
} from "drizzle-orm/pg-core";
import { type AdapterAccount } from "next-auth/adapters";
import { type JSONContent } from "@tiptap/react";
/**
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
* database instance for multiple projects.
@ -27,9 +30,9 @@ export const articles = createTable(
.$defaultFn(() => createId())
.notNull(),
title: varchar("title", { length: 256 }),
slug: varchar("slug", { length: 256 }).unique(),
slug: varchar("slug", { length: 256 }).unique().notNull(),
authorId: varchar("author_id", { length: 255 }),
content: text("content"),
content: text("content"),
categoryId: varchar("category_id", { length: 255 }),
published: boolean("published").default(false),
createdAt: timestamp("created_at", { withTimezone: true })
@ -44,9 +47,8 @@ export const articles = createTable(
}),
);
export type Article = typeof articles.$inferSelect;
export const articleRelations = relations(articles, ({ one }) => ({
categories: one(categories, {
category: one(categories, {
fields: [articles.categoryId],
references: [categories.id],
}),
@ -60,7 +62,7 @@ export const categories = createTable(
.$defaultFn(() => createId())
.notNull(),
name: varchar("name", { length: 256 }),
slug: varchar("slug", { length: 256 }).unique(),
slug: varchar("slug", { length: 256 }).unique().notNull(),
createdAt: timestamp("created_at", { withTimezone: true })
.default(sql`CURRENT_TIMESTAMP`)