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

202
pnpm-lock.yaml generated
View File

@ -11,6 +11,18 @@ importers:
'@auth/drizzle-adapter': '@auth/drizzle-adapter':
specifier: ^1.7.2 specifier: ^1.7.2
version: 1.8.0 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': '@hookform/resolvers':
specifier: ^4.1.3 specifier: ^4.1.3
version: 4.1.3(react-hook-form@7.54.2(react@18.3.1)) version: 4.1.3(react-hook-form@7.54.2(react@18.3.1))
@ -26,6 +38,9 @@ importers:
'@radix-ui/react-checkbox': '@radix-ui/react-checkbox':
specifier: ^1.1.4 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) 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': '@radix-ui/react-dialog':
specifier: ^1.1.6 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) 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': '@tanstack/react-table':
specifier: ^8.21.2 specifier: ^8.21.2
version: 8.21.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) 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': '@tiptap/extension-color':
specifier: ^2.11.5 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))) 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: postgres:
specifier: ^3.4.4 specifier: ^3.4.4
version: 3.4.5 version: 3.4.5
prosemirror-state:
specifier: ^1.4.3
version: 1.4.3
react: react:
specifier: ^18.3.1 specifier: ^18.3.1
version: 18.3.1 version: 18.3.1
react-colorful: react-colorful:
specifier: ^5.6.1 specifier: ^5.6.1
version: 5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.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: react-dom:
specifier: ^18.3.1 specifier: ^18.3.1
version: 18.3.1(react@18.3.1) version: 18.3.1(react@18.3.1)
@ -152,6 +176,9 @@ importers:
tailwindcss-animate: tailwindcss-animate:
specifier: ^1.0.7 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))) 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: zod:
specifier: ^3.24.2 specifier: ^3.24.2
version: 3.24.2 version: 3.24.2
@ -268,6 +295,34 @@ packages:
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'} 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': '@drizzle-team/brocli@0.10.2':
resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==}
@ -1031,6 +1086,19 @@ packages:
'@types/react-dom': '@types/react-dom':
optional: true 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': '@radix-ui/react-collection@1.1.2':
resolution: {integrity: sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==} resolution: {integrity: sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==}
peerDependencies: peerDependencies:
@ -1505,6 +1573,15 @@ packages:
'@radix-ui/rect@1.1.0': '@radix-ui/rect@1.1.0':
resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} 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': '@remirror/core-constants@3.0.0':
resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==} resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==}
@ -2115,6 +2192,9 @@ packages:
dlv@1.1.3: dlv@1.1.3:
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
dnd-core@16.0.1:
resolution: {integrity: sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==}
doctrine@2.1.0: doctrine@2.1.0:
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -2572,6 +2652,9 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
hoist-non-react-statics@3.3.2:
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
ignore@5.3.2: ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'} engines: {node: '>= 4'}
@ -3220,6 +3303,21 @@ packages:
react: '>=16.8.0' react: '>=16.8.0'
react-dom: '>=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: react-dom@18.3.1:
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
peerDependencies: peerDependencies:
@ -3291,6 +3389,9 @@ packages:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'} 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: reflect.getprototypeof@1.0.10:
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -3542,6 +3643,9 @@ packages:
tippy.js@6.3.7: tippy.js@6.3.7:
resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==} 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: to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'} engines: {node: '>=8.0'}
@ -3782,6 +3886,38 @@ snapshots:
dependencies: dependencies:
'@jridgewell/trace-mapping': 0.3.9 '@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': {} '@drizzle-team/brocli@0.10.2': {}
'@emnapi/runtime@1.3.1': '@emnapi/runtime@1.3.1':
@ -4288,6 +4424,22 @@ snapshots:
'@types/react': 18.3.18 '@types/react': 18.3.18
'@types/react-dom': 18.3.5(@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)': '@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: dependencies:
'@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1) '@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': {} '@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': {} '@remirror/core-constants@3.0.0': {}
'@rtsao/scc@1.1.0': {} '@rtsao/scc@1.1.0': {}
@ -5391,6 +5549,12 @@ snapshots:
dlv@1.1.3: {} 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: doctrine@2.1.0:
dependencies: dependencies:
esutils: 2.0.3 esutils: 2.0.3
@ -5629,8 +5793,8 @@ snapshots:
'@typescript-eslint/parser': 8.26.0(eslint@8.57.1)(typescript@5.8.2) '@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-node: 0.3.9 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)
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)
eslint-plugin-jsx-a11y: 6.10.2(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: 7.37.4(eslint@8.57.1)
eslint-plugin-react-hooks: 5.2.0(eslint@8.57.1) eslint-plugin-react-hooks: 5.2.0(eslint@8.57.1)
@ -5649,7 +5813,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - 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: dependencies:
'@nolyfill/is-core-module': 1.0.39 '@nolyfill/is-core-module': 1.0.39
debug: 4.4.0 debug: 4.4.0
@ -5660,18 +5824,18 @@ snapshots:
stable-hash: 0.0.4 stable-hash: 0.0.4
tinyglobby: 0.2.12 tinyglobby: 0.2.12
optionalDependencies: 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: transitivePeerDependencies:
- supports-color - 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: dependencies:
debug: 3.2.7 debug: 3.2.7
optionalDependencies: optionalDependencies:
'@typescript-eslint/parser': 8.26.0(eslint@8.57.1)(typescript@5.8.2) '@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-node: 0.3.9 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: transitivePeerDependencies:
- supports-color - supports-color
@ -5679,7 +5843,7 @@ snapshots:
dependencies: dependencies:
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-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: dependencies:
'@rtsao/scc': 1.1.0 '@rtsao/scc': 1.1.0
array-includes: 3.1.8 array-includes: 3.1.8
@ -5690,7 +5854,7 @@ snapshots:
doctrine: 2.1.0 doctrine: 2.1.0
eslint: 8.57.1 eslint: 8.57.1
eslint-import-resolver-node: 0.3.9 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 hasown: 2.0.2
is-core-module: 2.16.1 is-core-module: 2.16.1
is-glob: 4.0.3 is-glob: 4.0.3
@ -5998,6 +6162,10 @@ snapshots:
dependencies: dependencies:
function-bind: 1.1.2 function-bind: 1.1.2
hoist-non-react-statics@3.3.2:
dependencies:
react-is: 16.13.1
ignore@5.3.2: {} ignore@5.3.2: {}
import-fresh@3.3.1: import-fresh@3.3.1:
@ -6608,6 +6776,18 @@ snapshots:
react: 18.3.1 react: 18.3.1
react-dom: 18.3.1(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): react-dom@18.3.1(react@18.3.1):
dependencies: dependencies:
loose-envify: 1.4.0 loose-envify: 1.4.0
@ -6679,6 +6859,10 @@ snapshots:
dependencies: dependencies:
picomatch: 2.3.1 picomatch: 2.3.1
redux@4.2.1:
dependencies:
'@babel/runtime': 7.26.9
reflect.getprototypeof@1.0.10: reflect.getprototypeof@1.0.10:
dependencies: dependencies:
call-bind: 1.0.8 call-bind: 1.0.8
@ -7019,6 +7203,8 @@ snapshots:
dependencies: dependencies:
'@popperjs/core': 2.11.8 '@popperjs/core': 2.11.8
tiptap-extension-global-drag-handle@0.1.18: {}
to-regex-range@5.0.1: to-regex-range@5.0.1:
dependencies: dependencies:
is-number: 7.0.0 is-number: 7.0.0

View File

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

View File

@ -3,7 +3,7 @@ import { auth } from "@/server/auth";
import { api } from "@/trpc/server"; import { api } from "@/trpc/server";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import React from "react"; 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 }> }) { async function Page({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params; const { slug } = await params;
@ -13,7 +13,7 @@ async function Page({ params }: { params: Promise<{ slug: string }> }) {
: false; : false;
const article = await api.article.get({ slug: slug }); const article = await api.article.get({ slug: slug });
if (!article || !isEditor) return notFound(); if (!article || !isEditor) return notFound();
return <Editor server_article={article} />; return <ArticleEditor server_article={article} />;
} }
export default Page; 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 { Button } from "@/components/ui/button";
import { appRoutes } from "@/config";
import { hasPermission, Role } from "@/lib/validation/permissions"; import { hasPermission, Role } from "@/lib/validation/permissions";
import { auth } from "@/server/auth"; import { auth } from "@/server/auth";
import { api } from "@/trpc/server"; import { api } from "@/trpc/server";
@ -16,13 +19,28 @@ async function Page({ params }: { params: Promise<{ slug: string }> }) {
? hasPermission(session.user.role, Role.EDITOR) ? hasPermission(session.user.role, Role.EDITOR)
: false; : false;
return ( return (
<div> <div className="space-y-2">
<div className="flex w-full items-center justify-between"> <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 && ( {isEditor && (
<div className="space-x-2"> <div className="flex w-full justify-end space-x-2">
<Button asChild variant={"outline"}> <Button asChild variant={"outline"}>
<Link href={`/artikel/${article.slug}/edit`}> <Link href={appRoutes.editArticle(article.slug)}>
<Edit className="size-4" /> <Edit className="size-4" />
<span>Bearbeiten</span> <span>Bearbeiten</span>
</Link> </Link>
@ -33,6 +51,11 @@ async function Page({ params }: { params: Promise<{ slug: string }> }) {
</div> </div>
)} )}
</div> </div>
<h1 className="text-4xl font-bold">{article.title}</h1>
{article?.content?.length ? (
<RenderArticle content={article.content} />
) : null}
</div> </div>
); );
} }

View File

@ -6,7 +6,8 @@ import { api } from "@/trpc/server";
import { Edit } from "lucide-react"; import { Edit } from "lucide-react";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import React from "react"; 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 }> }) { async function Page({ params }: { params: Promise<{ name: string }> }) {
const { name } = await params; const { name } = await params;
@ -22,10 +23,10 @@ async function Page({ params }: { params: Promise<{ name: string }> }) {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<BreadNavigator <BreadNavigator
links={[ links={[
{ label: "Kategorie", href: "/kategorie" }, { label: "Kategorie", href: appRoutes.allCategories },
{ {
label: name, 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 { api } from "@/trpc/server";
import Link from "next/link"; import Link from "next/link";
import GlobalStats from "./_components/global-stats"; 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"; import CategoryCard from "./_components/category/category-card";
export default async function Home() { export default async function Home() {

View File

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

View File

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

View File

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

View File

@ -23,24 +23,20 @@ import {
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; 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"; import { createArticle } from "@/server/actions/article";
const formSchema = articleSchema.pick({
title: true,
});
function CreateArticleDialog() { function CreateArticleDialog() {
const [open, setOpen] = React.useState<boolean>(false); const [open, setOpen] = React.useState<boolean>(false);
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof createArticleSchema>>({
resolver: zodResolver(formSchema), resolver: zodResolver(createArticleSchema),
defaultValues: { defaultValues: {
title: "", title: "",
}, },
}); });
// 2. Define a submit handler. // 2. Define a submit handler.
async function onSubmit(values: z.infer<typeof formSchema>) { async function onSubmit(values: z.infer<typeof createArticleSchema>) {
setOpen(false); setOpen(false);
await createArticle(values); 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 }[]; links: { label: string; href: string }[];
}) { }) {
const labelClass =
"w-full max-w-52 overflow-hidden text-ellipsis whitespace-nowrap capitalize block";
return ( return (
<Breadcrumb> <Breadcrumb>
<BreadcrumbList> <BreadcrumbList>
@ -20,8 +22,8 @@ function BreadNavigator({
if (idx < links.length - 1) if (idx < links.length - 1)
return ( return (
<React.Fragment key={href}> <React.Fragment key={href}>
<BreadcrumbItem> <BreadcrumbItem className="">
<BreadcrumbLink className="capitalize" href={href}> <BreadcrumbLink className={labelClass} href={href}>
{label} {label}
</BreadcrumbLink> </BreadcrumbLink>
</BreadcrumbItem> </BreadcrumbItem>
@ -29,7 +31,7 @@ function BreadNavigator({
</React.Fragment> </React.Fragment>
); );
return ( return (
<BreadcrumbItem className="capitalize" key={href}> <BreadcrumbItem className={labelClass} key={href}>
<BreadcrumbPage>{label}</BreadcrumbPage> <BreadcrumbPage>{label}</BreadcrumbPage>
</BreadcrumbItem> </BreadcrumbItem>
); );

View File

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

View File

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

View File

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

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 TextStyle from "@tiptap/extension-text-style";
import StarterKit from "@tiptap/starter-kit"; import StarterKit from "@tiptap/starter-kit";
import Image from "@tiptap/extension-image"; 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 = [ export const extensions = [
Color.configure({ types: [TextStyle.name, ListItem.name] }), Color.configure({ types: [TextStyle.name, ListItem.name] }),
@ -11,7 +28,9 @@ export const extensions = [
StarterKit.configure({ StarterKit.configure({
code: false, code: false,
codeBlock: false, codeBlock: false,
horizontalRule: {}, heading: {
levels: [1, 2, 3, 4, 5, 6],
},
bulletList: { bulletList: {
keepMarks: true, 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 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 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, 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 { EditorProvider, EditorProviderProps } from "@tiptap/react";
import { MenuBar } from "./menu-bar"; import { MenuBar } from "./menu-bar";
import { extensions } from "./extentions"; import { extensions } from "./extentions";
import { cn } from "@/lib/utils";
function Editor({ function Editor({
editorProviderProps, editorProviderProps,
@ -14,15 +15,22 @@ function Editor({
readOnly?: boolean; readOnly?: boolean;
}) { }) {
return ( 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 <EditorProvider
immediatelyRender={false} immediatelyRender={false}
extensions={extensions} extensions={extensions}
slotBefore={!readOnly && <MenuBar />} slotBefore={!readOnly && <MenuBar />}
{...editorProviderProps} {...editorProviderProps}
editable={!readOnly}
/> />
{JSON.stringify(editorProviderProps.content)}
</div> </div>
); );
} }
export default Editor; 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 { .menu-bar .is-active {
@apply border-primary; @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) { 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>( 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); 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({ export const articleSchema = z.object({
title: z.string().min(1), title: z.string().min(1),
content: z.string().optional(), slug: z.string().min(1),
content: z.any().optional(),
authorId: z.string().optional(), authorId: z.string().optional(),
categoryId: z.string().optional(), categoryId: z.string().optional(),
published: z.boolean(), published: z.boolean(),
}); });
export const createArticleSchema = articleSchema.pick({
title: true,
});

View File

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

View File

@ -3,7 +3,7 @@ import { categoryRouter } from "@/server/api/routers/category";
import { createCallerFactory, createTRPCRouter } from "@/server/api/trpc"; import { createCallerFactory, createTRPCRouter } from "@/server/api/trpc";
import { usersRouter } from "./routers/users"; import { usersRouter } from "./routers/users";
import { authorRouter } from "./routers/author"; import { authorRouter } from "./routers/author";
import { appRouter as globalRouter } from "./routers/app";
/** /**
* This is the primary router for your server. * This is the primary router for your server.
* *
@ -14,6 +14,7 @@ export const appRouter = createTRPCRouter({
category: categoryRouter, category: categoryRouter,
users: usersRouter, users: usersRouter,
author: authorRouter, author: authorRouter,
app: globalRouter,
}); });
// export type definition of API // 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, publicProcedure,
} from "@/server/api/trpc"; } from "@/server/api/trpc";
import { articles } from "@/server/db/schema"; 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 { count, eq } from "drizzle-orm";
import { hasPermission, Role } from "@/lib/validation/permissions"; import { hasPermission, Role } from "@/lib/validation/permissions";
import { generateSlug } from "@/lib/utils"; import { generateSlug } from "@/lib/utils";
@ -18,6 +21,7 @@ export const articleRouter = createTRPCRouter({
.query(async ({ ctx, input }) => { .query(async ({ ctx, input }) => {
return await ctx.db.query.articles.findFirst({ return await ctx.db.query.articles.findFirst({
where: eq(articles.slug, input.slug), where: eq(articles.slug, input.slug),
with: { category: true },
}); });
}), }),
@ -60,7 +64,7 @@ export const articleRouter = createTRPCRouter({
}), }),
// mutations // mutations
create: protectedProcedure create: protectedProcedure
.input(z.object({ article: articleSchema })) .input(z.object({ article: createArticleSchema }))
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const isEditor = hasPermission(ctx.session.user.role, Role.EDITOR); const isEditor = hasPermission(ctx.session.user.role, Role.EDITOR);
if (!isEditor) { if (!isEditor) {
@ -81,6 +85,11 @@ export const articleRouter = createTRPCRouter({
if (!isEditor) { if (!isEditor) {
throw new Error("You are not allowed to update articles"); throw new Error("You are not allowed to update articles");
} }
console.log(
"Content before save",
JSON.stringify(input.article.content),
);
return await ctx.db return await ctx.db
.update(articles) .update(articles)
.set(input.article) .set(input.article)

View File

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