fixed build performance issues; optimized queries

This commit is contained in:
mr-shortman 2025-03-15 20:31:38 +01:00
parent cff9dad54c
commit 324451e082
27 changed files with 289 additions and 208 deletions

View File

@ -6,6 +6,9 @@ import "./src/env.js";
/** @type {import("next").NextConfig} */ /** @type {import("next").NextConfig} */
const config = { const config = {
eslint: {
ignoreDuringBuilds: true,
},
experimental: { experimental: {
serverActions: { serverActions: {
bodySizeLimit: "2mb", bodySizeLimit: "2mb",

View File

@ -68,6 +68,7 @@
"tailwind-merge": "^3.0.2", "tailwind-merge": "^3.0.2",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"use-debounce": "^10.0.4", "use-debounce": "^10.0.4",
"winston": "^3.17.0",
"zod": "^3.24.2" "zod": "^3.24.2"
}, },
"devDependencies": { "devDependencies": {

179
pnpm-lock.yaml generated
View File

@ -152,6 +152,9 @@ importers:
use-debounce: use-debounce:
specifier: ^10.0.4 specifier: ^10.0.4
version: 10.0.4(react@18.3.1) version: 10.0.4(react@18.3.1)
winston:
specifier: ^3.17.0
version: 3.17.0
zod: zod:
specifier: ^3.24.2 specifier: ^3.24.2
version: 3.24.2 version: 3.24.2
@ -255,10 +258,17 @@ packages:
'@cfcs/core@0.0.6': '@cfcs/core@0.0.6':
resolution: {integrity: sha512-FxfJMwoLB8MEMConeXUCqtMGqxdtePQxRBOiGip9ULcYYam3WfCgoY6xdnMaSkYvRvmosp5iuG+TiPofm65+Pw==} resolution: {integrity: sha512-FxfJMwoLB8MEMConeXUCqtMGqxdtePQxRBOiGip9ULcYYam3WfCgoY6xdnMaSkYvRvmosp5iuG+TiPofm65+Pw==}
'@colors/colors@1.6.0':
resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==}
engines: {node: '>=0.1.90'}
'@cspotcode/source-map-support@0.8.1': '@cspotcode/source-map-support@0.8.1':
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'} engines: {node: '>=12'}
'@dabh/diagnostics@2.0.3':
resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==}
'@daybrush/utils@1.13.0': '@daybrush/utils@1.13.0':
resolution: {integrity: sha512-ALK12C6SQNNHw1enXK+UO8bdyQ+jaWNQ1Af7Z3FNxeAwjYhQT7do+TRE4RASAJ3ObaS2+TJ7TXR3oz2Gzbw0PQ==} resolution: {integrity: sha512-ALK12C6SQNNHw1enXK+UO8bdyQ+jaWNQ1Af7Z3FNxeAwjYhQT7do+TRE4RASAJ3ObaS2+TJ7TXR3oz2Gzbw0PQ==}
@ -1930,6 +1940,9 @@ packages:
'@types/react@18.3.18': '@types/react@18.3.18':
resolution: {integrity: sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==} resolution: {integrity: sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==}
'@types/triple-beam@1.3.5':
resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==}
'@types/unist@2.0.11': '@types/unist@2.0.11':
resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
@ -2085,6 +2098,9 @@ packages:
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
async@3.2.6:
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
available-typed-arrays@1.0.7: available-typed-arrays@1.0.7:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -2202,20 +2218,32 @@ packages:
react: ^18 || ^19 || ^19.0.0-rc react: ^18 || ^19 || ^19.0.0-rc
react-dom: ^18 || ^19 || ^19.0.0-rc react-dom: ^18 || ^19 || ^19.0.0-rc
color-convert@1.9.3:
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
color-convert@2.0.1: color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'} engines: {node: '>=7.0.0'}
color-name@1.1.3:
resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
color-name@1.1.4: color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
color-string@1.9.1: color-string@1.9.1:
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
color@3.2.1:
resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==}
color@4.2.3: color@4.2.3:
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
engines: {node: '>=12.5.0'} engines: {node: '>=12.5.0'}
colorspace@1.1.4:
resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==}
comma-separated-tokens@2.0.3: comma-separated-tokens@2.0.3:
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
@ -2470,6 +2498,9 @@ packages:
emoji-regex@9.2.2: emoji-regex@9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
enabled@2.0.0:
resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==}
encoding-sniffer@0.2.0: encoding-sniffer@0.2.0:
resolution: {integrity: sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==} resolution: {integrity: sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==}
@ -2688,6 +2719,9 @@ packages:
picomatch: picomatch:
optional: true optional: true
fecha@4.2.3:
resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==}
file-entry-cache@6.0.1: file-entry-cache@6.0.1:
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
engines: {node: ^10.12.0 || >=12.0.0} engines: {node: ^10.12.0 || >=12.0.0}
@ -2707,6 +2741,9 @@ packages:
flatted@3.3.3: flatted@3.3.3:
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
fn.name@1.1.0:
resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==}
for-each@0.3.5: for-each@0.3.5:
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -2976,6 +3013,10 @@ packages:
resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
is-stream@2.0.1:
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
engines: {node: '>=8'}
is-string@1.1.1: is-string@1.1.1:
resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -3076,6 +3117,9 @@ packages:
keyv@4.5.4: keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
kuler@2.0.0:
resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==}
language-subtag-registry@0.3.23: language-subtag-registry@0.3.23:
resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==}
@ -3107,6 +3151,10 @@ packages:
lodash.merge@4.6.2: lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
logform@2.7.0:
resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==}
engines: {node: '>= 12.0.0'}
longest-streak@3.1.0: longest-streak@3.1.0:
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
@ -3359,6 +3407,9 @@ packages:
once@1.4.0: once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
one-time@1.0.0:
resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==}
optionator@0.9.4: optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@ -3737,6 +3788,10 @@ packages:
read-cache@1.0.0: read-cache@1.0.0:
resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
readdirp@3.6.0: readdirp@3.6.0:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'} engines: {node: '>=8.10.0'}
@ -3793,6 +3848,9 @@ packages:
resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==}
engines: {node: '>=0.4'} engines: {node: '>=0.4'}
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
safe-push-apply@1.0.0: safe-push-apply@1.0.0:
resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -3801,6 +3859,10 @@ packages:
resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
safe-stable-stringify@2.5.0:
resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
engines: {node: '>=10'}
safer-buffer@2.1.2: safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
@ -3892,6 +3954,9 @@ packages:
stable-hash@0.0.4: stable-hash@0.0.4:
resolution: {integrity: sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==} resolution: {integrity: sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==}
stack-trace@0.0.10:
resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==}
streamsearch@1.1.0: streamsearch@1.1.0:
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
engines: {node: '>=10.0.0'} engines: {node: '>=10.0.0'}
@ -3927,6 +3992,9 @@ packages:
resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
stringify-entities@4.0.4: stringify-entities@4.0.4:
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
@ -4004,6 +4072,9 @@ packages:
resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
text-hex@1.0.0:
resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==}
text-table@0.2.0: text-table@0.2.0:
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
@ -4031,6 +4102,10 @@ packages:
trim-lines@3.0.1: trim-lines@3.0.1:
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
triple-beam@1.4.1:
resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==}
engines: {node: '>= 14.0.0'}
trough@2.2.0: trough@2.2.0:
resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==}
@ -4240,6 +4315,14 @@ packages:
engines: {node: '>= 8'} engines: {node: '>= 8'}
hasBin: true hasBin: true
winston-transport@4.9.0:
resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==}
engines: {node: '>= 12.0.0'}
winston@3.17.0:
resolution: {integrity: sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==}
engines: {node: '>= 12.0.0'}
word-wrap@1.2.5: word-wrap@1.2.5:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -4327,10 +4410,18 @@ snapshots:
dependencies: dependencies:
'@egjs/component': 3.0.5 '@egjs/component': 3.0.5
'@colors/colors@1.6.0': {}
'@cspotcode/source-map-support@0.8.1': '@cspotcode/source-map-support@0.8.1':
dependencies: dependencies:
'@jridgewell/trace-mapping': 0.3.9 '@jridgewell/trace-mapping': 0.3.9
'@dabh/diagnostics@2.0.3':
dependencies:
colorspace: 1.1.4
enabled: 2.0.0
kuler: 2.0.0
'@daybrush/utils@1.13.0': {} '@daybrush/utils@1.13.0': {}
'@drizzle-team/brocli@0.10.2': {} '@drizzle-team/brocli@0.10.2': {}
@ -5737,6 +5828,8 @@ snapshots:
'@types/prop-types': 15.7.14 '@types/prop-types': 15.7.14
csstype: 3.1.3 csstype: 3.1.3
'@types/triple-beam@1.3.5': {}
'@types/unist@2.0.11': {} '@types/unist@2.0.11': {}
'@types/unist@3.0.3': {} '@types/unist@3.0.3': {}
@ -5936,6 +6029,8 @@ snapshots:
async-function@1.0.0: {} async-function@1.0.0: {}
async@3.2.6: {}
available-typed-arrays@1.0.7: available-typed-arrays@1.0.7:
dependencies: dependencies:
possible-typed-array-names: 1.1.0 possible-typed-array-names: 1.1.0
@ -6074,17 +6169,27 @@ snapshots:
- '@types/react' - '@types/react'
- '@types/react-dom' - '@types/react-dom'
color-convert@1.9.3:
dependencies:
color-name: 1.1.3
color-convert@2.0.1: color-convert@2.0.1:
dependencies: dependencies:
color-name: 1.1.4 color-name: 1.1.4
color-name@1.1.3: {}
color-name@1.1.4: {} color-name@1.1.4: {}
color-string@1.9.1: color-string@1.9.1:
dependencies: dependencies:
color-name: 1.1.4 color-name: 1.1.4
simple-swizzle: 0.2.2 simple-swizzle: 0.2.2
optional: true
color@3.2.1:
dependencies:
color-convert: 1.9.3
color-string: 1.9.1
color@4.2.3: color@4.2.3:
dependencies: dependencies:
@ -6092,6 +6197,11 @@ snapshots:
color-string: 1.9.1 color-string: 1.9.1
optional: true optional: true
colorspace@1.1.4:
dependencies:
color: 3.2.1
text-hex: 1.0.0
comma-separated-tokens@2.0.3: {} comma-separated-tokens@2.0.3: {}
commander@4.1.1: {} commander@4.1.1: {}
@ -6257,6 +6367,8 @@ snapshots:
emoji-regex@9.2.2: {} emoji-regex@9.2.2: {}
enabled@2.0.0: {}
encoding-sniffer@0.2.0: encoding-sniffer@0.2.0:
dependencies: dependencies:
iconv-lite: 0.6.3 iconv-lite: 0.6.3
@ -6691,6 +6803,8 @@ snapshots:
optionalDependencies: optionalDependencies:
picomatch: 4.0.2 picomatch: 4.0.2
fecha@4.2.3: {}
file-entry-cache@6.0.1: file-entry-cache@6.0.1:
dependencies: dependencies:
flat-cache: 3.2.0 flat-cache: 3.2.0
@ -6712,6 +6826,8 @@ snapshots:
flatted@3.3.3: {} flatted@3.3.3: {}
fn.name@1.1.0: {}
for-each@0.3.5: for-each@0.3.5:
dependencies: dependencies:
is-callable: 1.2.7 is-callable: 1.2.7
@ -6919,8 +7035,7 @@ snapshots:
call-bound: 1.0.4 call-bound: 1.0.4
get-intrinsic: 1.3.0 get-intrinsic: 1.3.0
is-arrayish@0.3.2: is-arrayish@0.3.2: {}
optional: true
is-async-function@2.1.1: is-async-function@2.1.1:
dependencies: dependencies:
@ -7013,6 +7128,8 @@ snapshots:
dependencies: dependencies:
call-bound: 1.0.4 call-bound: 1.0.4
is-stream@2.0.1: {}
is-string@1.1.1: is-string@1.1.1:
dependencies: dependencies:
call-bound: 1.0.4 call-bound: 1.0.4
@ -7111,6 +7228,8 @@ snapshots:
dependencies: dependencies:
json-buffer: 3.0.1 json-buffer: 3.0.1
kuler@2.0.0: {}
language-subtag-registry@0.3.23: {} language-subtag-registry@0.3.23: {}
language-tags@1.0.9: language-tags@1.0.9:
@ -7138,6 +7257,15 @@ snapshots:
lodash.merge@4.6.2: {} lodash.merge@4.6.2: {}
logform@2.7.0:
dependencies:
'@colors/colors': 1.6.0
'@types/triple-beam': 1.3.5
fecha: 4.2.3
ms: 2.1.3
safe-stable-stringify: 2.5.0
triple-beam: 1.4.1
longest-streak@3.1.0: {} longest-streak@3.1.0: {}
loose-envify@1.4.0: loose-envify@1.4.0:
@ -7557,6 +7685,10 @@ snapshots:
dependencies: dependencies:
wrappy: 1.0.2 wrappy: 1.0.2
one-time@1.0.0:
dependencies:
fn.name: 1.1.0
optionator@0.9.4: optionator@0.9.4:
dependencies: dependencies:
deep-is: 0.1.4 deep-is: 0.1.4
@ -7944,6 +8076,12 @@ snapshots:
dependencies: dependencies:
pify: 2.3.0 pify: 2.3.0
readable-stream@3.6.2:
dependencies:
inherits: 2.0.4
string_decoder: 1.3.0
util-deprecate: 1.0.2
readdirp@3.6.0: readdirp@3.6.0:
dependencies: dependencies:
picomatch: 2.3.1 picomatch: 2.3.1
@ -8023,6 +8161,8 @@ snapshots:
has-symbols: 1.1.0 has-symbols: 1.1.0
isarray: 2.0.5 isarray: 2.0.5
safe-buffer@5.2.1: {}
safe-push-apply@1.0.0: safe-push-apply@1.0.0:
dependencies: dependencies:
es-errors: 1.3.0 es-errors: 1.3.0
@ -8034,6 +8174,8 @@ snapshots:
es-errors: 1.3.0 es-errors: 1.3.0
is-regex: 1.2.1 is-regex: 1.2.1
safe-stable-stringify@2.5.0: {}
safer-buffer@2.1.2: {} safer-buffer@2.1.2: {}
scheduler@0.23.2: scheduler@0.23.2:
@ -8147,7 +8289,6 @@ snapshots:
simple-swizzle@0.2.2: simple-swizzle@0.2.2:
dependencies: dependencies:
is-arrayish: 0.3.2 is-arrayish: 0.3.2
optional: true
sonner@2.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): sonner@2.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies: dependencies:
@ -8167,6 +8308,8 @@ snapshots:
stable-hash@0.0.4: {} stable-hash@0.0.4: {}
stack-trace@0.0.10: {}
streamsearch@1.1.0: {} streamsearch@1.1.0: {}
string-width@4.2.3: string-width@4.2.3:
@ -8231,6 +8374,10 @@ snapshots:
define-properties: 1.2.1 define-properties: 1.2.1
es-object-atoms: 1.1.1 es-object-atoms: 1.1.1
string_decoder@1.3.0:
dependencies:
safe-buffer: 5.2.1
stringify-entities@4.0.4: stringify-entities@4.0.4:
dependencies: dependencies:
character-entities-html4: 2.1.0 character-entities-html4: 2.1.0
@ -8322,6 +8469,8 @@ snapshots:
tapable@2.2.1: {} tapable@2.2.1: {}
text-hex@1.0.0: {}
text-table@0.2.0: {} text-table@0.2.0: {}
thenify-all@1.6.0: thenify-all@1.6.0:
@ -8349,6 +8498,8 @@ snapshots:
trim-lines@3.0.1: {} trim-lines@3.0.1: {}
triple-beam@1.4.1: {}
trough@2.2.0: {} trough@2.2.0: {}
ts-api-utils@2.0.1(typescript@5.8.2): ts-api-utils@2.0.1(typescript@5.8.2):
@ -8600,6 +8751,26 @@ snapshots:
dependencies: dependencies:
isexe: 2.0.0 isexe: 2.0.0
winston-transport@4.9.0:
dependencies:
logform: 2.7.0
readable-stream: 3.6.2
triple-beam: 1.4.1
winston@3.17.0:
dependencies:
'@colors/colors': 1.6.0
'@dabh/diagnostics': 2.0.3
async: 3.2.6
is-stream: 2.0.1
logform: 2.7.0
one-time: 1.0.0
readable-stream: 3.6.2
safe-stable-stringify: 2.5.0
stack-trace: 0.0.10
triple-beam: 1.4.1
winston-transport: 4.9.0
word-wrap@1.2.5: {} word-wrap@1.2.5: {}
wrap-ansi@7.0.0: wrap-ansi@7.0.0:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

View File

@ -1,8 +1,9 @@
import "dotenv/config"; import "dotenv/config";
import { env } from "@/env";
import { db } from "../src/server/db"; import { db } from "../src/server/db";
import { articles, categories, users } from "../src/server/db/schema"; import { articles, categories, users } from "../src/server/db/schema";
async function seed() { async function developmentSeed() {
const usersData = Array.from({ length: 100 }).map((_, i) => ({ const usersData = Array.from({ length: 100 }).map((_, i) => ({
name: `User ${i + 1}`, name: `User ${i + 1}`,
email: `user${i + 1}@example.com`, email: `user${i + 1}@example.com`,
@ -37,9 +38,29 @@ async function seed() {
console.log("Seeded " + a.length + " articles"); console.log("Seeded " + a.length + " articles");
} }
async function productionSeed() {
const user = await db.query.users.findFirst();
if (user) {
console.log("Skipped seeding, user already exists");
return;
}
const initialUser = {
name: "Admin",
email: "payblot@gmail.com",
role: 7,
};
await db.insert(users).values(initialUser);
console.log("Seeded user");
}
async function init() { async function init() {
try { try {
await seed(); if (env.NODE_ENV === "development") await developmentSeed();
else if (env.NODE_ENV === "production") await productionSeed();
else
console.log(
"Skipped seeding, NODE_ENV is not set to development or production",
);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} finally { } finally {

View File

@ -10,8 +10,8 @@ import { Button } from "@/components/ui/button";
import Link from "next/link"; import Link from "next/link";
export default async function Home() { export default async function Home() {
const categories = await api.category.getAll({ limit: 6 }); const categories = await api.category.getMany({ limit: 6 });
const articles = await api.article.getAll({ limit: 6 }); const articles = await api.article.getMany({ limit: 6 });
return ( return (
<> <>
<Alert> <Alert>

View File

@ -1,8 +1,7 @@
import * as cheerio from "cheerio"; import * as cheerio from "cheerio";
import { NextApiRequest } from "next";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
export async function GET(req: NextApiRequest) { export async function GET(req: Request) {
if (req.method !== "GET") { if (req.method !== "GET") {
return NextResponse.json({ error: "Method not allowed" }); return NextResponse.json({ error: "Method not allowed" });
} }

View File

@ -25,7 +25,8 @@ import CategorySelect from "@/components/category/category-select";
import { CheckCircle, XCircle } from "lucide-react"; import { CheckCircle, XCircle } from "lucide-react";
import PublishArticleAlertDialog from "./publish-article-alert-dialog"; import PublishArticleAlertDialog from "./publish-article-alert-dialog";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import Editor from "../editor"; import dynamic from "next/dynamic";
const Editor = dynamic(() => import("../editor"), { ssr: false });
export default ({ server_article }: { server_article: Article }) => { export default ({ server_article }: { server_article: Article }) => {
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = React.useState(false);

View File

@ -19,6 +19,9 @@ function InfiniteArticlesGrid() {
}, },
{ {
getNextPageParam: (lastPage) => lastPage.nextCursor, getNextPageParam: (lastPage) => lastPage.nextCursor,
staleTime: 60 * 4 * 1000, // 4 minutes stale time
refetchOnMount: false, // Prevents unnecessary refetching
refetchOnWindowFocus: false, // Avoids refetch when switching tabs
}, },
); );
// Calculate all visible items across all loaded pages // Calculate all visible items across all loaded pages

View File

@ -1,6 +1,6 @@
import React from "react"; import { JSONContent } from "novel";
function RenderArticle({ content }: { content: string }) { function RenderArticle({ content }: { content: JSONContent }) {
return "render article: in work"; //<Editor readOnly editorProviderProps={{ content: content }} />; return "render article: in work"; //<Editor readOnly editorProviderProps={{ content: content }} />;
} }

View File

@ -5,7 +5,7 @@ import { Combobox, ComboboxProps } from "../combobox";
import { Icons } from "../icons"; import { Icons } from "../icons";
function CategorySelect(props: Partial<ComboboxProps>) { function CategorySelect(props: Partial<ComboboxProps>) {
const { data: categories } = api.category.getAll.useQuery(); const { data: categories } = api.category.getMany.useQuery();
return ( return (
<Combobox <Combobox
{...(props as ComboboxProps)} {...(props as ComboboxProps)}

View File

@ -9,8 +9,8 @@ export const CATEGORY_GRID_CLASS =
function CategoryGrid({ categories }: { categories: Category[] }) { function CategoryGrid({ categories }: { categories: Category[] }) {
return ( return (
<menu className={CATEGORY_GRID_CLASS}> <menu className={CATEGORY_GRID_CLASS}>
{categories.map((category) => ( {categories.map((category, idx) => (
<li key={category.id}> <li key={idx}>
<CategoryCard {...category} /> <CategoryCard {...category} />
</li> </li>
))} ))}

View File

@ -18,6 +18,9 @@ export default function InfiniteCategoryGrid() {
}, },
{ {
getNextPageParam: (lastPage) => lastPage.nextCursor, getNextPageParam: (lastPage) => lastPage.nextCursor,
staleTime: 60 * 4 * 1000, // 4 minutes stale time
refetchOnMount: false, // Prevents unnecessary refetching
refetchOnWindowFocus: false, // Avoids refetch when switching tabs
}, },
); );
// Calculate all visible items across all loaded pages // Calculate all visible items across all loaded pages

View File

@ -1,20 +0,0 @@
"use client";
import ColorPicker, { type ColorPickerProps } from ".";
import { Button } from "../ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
export default function ColorPickerPopover(props: ColorPickerProps) {
return (
<Popover>
<PopoverTrigger asChild>
<Button style={{ backgroundColor: props.initialColor }} className="">
color
</Button>
</PopoverTrigger>
<PopoverContent>
<ColorPicker {...props} />
</PopoverContent>
</Popover>
);
}

View File

@ -1,16 +0,0 @@
.color-picker .react-colorful {
width: 100%;
height: 240px;
}
.color-picker .react-colorful__saturation {
border-radius: 4px 4px 0 0;
}
.color-picker .react-colorful__hue {
height: 40px;
border-radius: 0 0 4px 4px;
}
.color-picker .react-colorful__hue-pointer {
width: 12px;
height: inherit;
border-radius: 0;
}

View File

@ -1,103 +0,0 @@
"use client";
import "./color-picker.css";
import React from "react";
import { useState, useEffect } from "react";
import { HexColorPicker } from "react-colorful";
import { Input } from "../ui/input";
import { debounce } from "@/lib/utils";
const STORAGE_KEY = "savedColors";
const MAX_COLORS = 12;
const SELECT_DEBOUNCE = 500;
export type ColorPickerProps = {
onInput?: (color: string) => void;
initialColor?: string;
};
function ColorPicker({ onInput, initialColor }: ColorPickerProps) {
const [mounted, setMounted] = useState(false);
const [customColors, setCustomColors] = useState<string[]>([]);
const [color, setColor] = useState(initialColor ?? "#ff0000");
// Load colors from localStorage on mount
useEffect(() => {
if (initialColor?.length) setColor(initialColor);
const storageColors = localStorage.getItem(STORAGE_KEY);
const storedColors = storageColors ? JSON.parse(storageColors) : [];
if (storedColors.length) {
setCustomColors(storedColors);
if (!initialColor?.length) setColor(storedColors[0]);
setMounted(true);
}
}, []);
const persistColor = (newColor: string) => {
if (newColor.length < 2) return;
if (customColors[0] === newColor) return; // Prevent duplicate consecutive colors
const updatedColors = [
newColor,
...customColors.filter((c) => c !== newColor),
].slice(0, MAX_COLORS);
console.log(updatedColors);
setCustomColors(updatedColors);
localStorage.setItem(STORAGE_KEY, JSON.stringify(updatedColors));
};
const selectColor = (newColor: string) => {
persistColor(newColor);
onInput?.(newColor);
};
const selectColorDebounced = debounce((newColor: string) => {
selectColor(newColor);
}, SELECT_DEBOUNCE);
const handleColorChange = (newColor: string, skipDebounce = false) => {
setColor(newColor);
if (skipDebounce)
selectColor(newColor); // Delayed save
else selectColorDebounced(newColor);
};
return (
<div className="color-picker flex flex-col items-center gap-4">
{/* React Colorful Picker */}
<HexColorPicker
color={color}
onChange={handleColorChange}
className="w-full"
/>
{/* Default Input (Native Color Picker) */}
<div className="flex w-full gap-2">
<div
className="size-8 rounded-md border"
style={{ backgroundColor: color }}
/>
<Input
className="h-8"
value={color}
onInput={(e) => handleColorChange(e.currentTarget.value)}
/>
</div>
{/* Display Recent Colors */}
<div className="flex flex-wrap gap-2">
{customColors.map((col) => (
<button
key={col}
className="size-8 rounded border"
style={{ backgroundColor: col }}
onClick={() => handleColorChange(col, true)}
/>
))}
</div>
</div>
);
}
export default ColorPicker;

View File

@ -36,7 +36,6 @@ export function DataTable<TData, TValue>({
columns, columns,
data, data,
}: DataTableProps<TData, TValue>) { }: DataTableProps<TData, TValue>) {
"use no memo";
const [pagination, setPagination] = React.useState<PaginationState>({ const [pagination, setPagination] = React.useState<PaginationState>({
pageSize: 25, pageSize: 25,
pageIndex: 0, pageIndex: 0,

27
src/lib/logger.ts Normal file
View File

@ -0,0 +1,27 @@
import winston from "winston";
const customLevels = {
levels: {
error: 0,
warn: 1,
info: 2,
sql: 3,
debug: 4,
},
colors: {
error: "red",
warn: "yellow",
info: "green",
sql: "magenta",
debug: "blue",
},
};
winston.addColors(customLevels.colors);
export const logger = winston.createLogger({
levels: customLevels.levels,
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: "db_queries.log", level: "sql" }),
new winston.transports.File({ filename: "errors.log", level: "error" }),
],
});

View File

@ -1,8 +1,12 @@
"use server"; "use server";
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { auth } from "../auth";
import { hasPermission, Role } from "@/lib/validation/permissions";
export async function uploadFile(formData: FormData) { export async function uploadFile(formData: FormData) {
const session = await auth();
if (!session || hasPermission(session.user.role, Role.EDITOR)) return false;
const file = formData.get("file") as File; const file = formData.get("file") as File;
const arrayBuffer = await file.arrayBuffer(); const arrayBuffer = await file.arrayBuffer();
const buffer = new Uint8Array(arrayBuffer); const buffer = new Uint8Array(arrayBuffer);

View File

@ -1,29 +1,6 @@
import { z } from "zod";
import { createTRPCRouter, publicProcedure } from "../trpc"; import { createTRPCRouter, publicProcedure } from "../trpc";
import { desc, ilike, like } from "drizzle-orm";
import {
articles as articlesTable,
categories as categoriesTable,
lower,
} from "@/server/db/schema";
export const appRouter = createTRPCRouter({ export const appRouter = createTRPCRouter({
searchContent: publicProcedure
.input(
z.object({
query: z.string().optional(),
}),
)
.query(async ({ ctx, input }) => {
const articles = await ctx.db.query.articles.findMany({
where: ilike(articlesTable.title, "%" + input.query + "%"),
});
const categories = await ctx.db.query.categories.findMany({
where: like(categoriesTable.name, "%" + input.query + "%"),
});
return { articles, categories };
}),
getSidebarMain: publicProcedure.query(async ({ ctx }) => { getSidebarMain: publicProcedure.query(async ({ ctx }) => {
const categories = await ctx.db.query.categories.findMany({ const categories = await ctx.db.query.categories.findMany({
limit: 3, limit: 3,

View File

@ -71,10 +71,10 @@ export const articleRouter = createTRPCRouter({
get: publicProcedure get: publicProcedure
.input(z.object({ slug: z.string() })) .input(z.object({ slug: z.string() }))
.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 }, with: { category: true },
}); })) as Article;
}), }),
getByCursor: publicProcedure getByCursor: publicProcedure
.input( .input(
@ -131,7 +131,6 @@ export const articleRouter = createTRPCRouter({
let nextCursor: string | undefined = undefined; let nextCursor: string | undefined = undefined;
if (items.length > limit) { if (items.length > limit) {
console.log("Configure next cursor");
const cursorItem = items.pop(); const cursorItem = items.pop();
// Create a cursor object with the relevant fields for sorting // Create a cursor object with the relevant fields for sorting
const cursorData: ArticleCursor = { const cursorData: ArticleCursor = {
@ -150,7 +149,7 @@ export const articleRouter = createTRPCRouter({
nextCursor, nextCursor,
}; };
}), }),
getAll: publicProcedure getMany: publicProcedure
.input( .input(
z z
.object({ .object({
@ -160,11 +159,12 @@ export const articleRouter = createTRPCRouter({
.optional(), .optional(),
) )
.query(async ({ ctx, input }) => { .query(async ({ ctx, input }) => {
const limit = input?.limit ?? 50;
return (await ctx.db.query.articles.findMany({ return (await ctx.db.query.articles.findMany({
where: input?.categoryId where: input?.categoryId
? eq(articles.categoryId, input.categoryId) ? eq(articles.categoryId, input.categoryId)
: undefined, : undefined,
limit: input?.limit, limit: limit,
columns: { columns: {
title: true, title: true,
slug: true, slug: true,

View File

@ -1,4 +1,4 @@
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc"; import { createTRPCRouter, publicProcedure } from "../trpc";
export const authorRouter = createTRPCRouter({ export const authorRouter = createTRPCRouter({
getCount: publicProcedure.query(async ({ ctx }) => { getCount: publicProcedure.query(async ({ ctx }) => {

View File

@ -12,11 +12,11 @@ import {
count, count,
desc, desc,
eq, eq,
gt,
gte, gte,
ilike, ilike,
like, like,
lte, lte,
sql,
} from "drizzle-orm"; } from "drizzle-orm";
import { hasPermission, Role } from "@/lib/validation/permissions"; import { hasPermission, Role } from "@/lib/validation/permissions";
import { import {
@ -60,13 +60,6 @@ const getCategorySorting = (sort: string, cursor?: CategoryCursor) => {
}; };
export const categoryRouter = createTRPCRouter({ export const categoryRouter = createTRPCRouter({
search: publicProcedure
.input(z.object({ query: z.string() }))
.query(async ({ ctx, input }) => {
return await ctx.db.query.categories.findMany({
where: like(categories.name, "%" + input.query + "%"),
});
}),
get: publicProcedure get: publicProcedure
.input(z.object({ slug: z.string(), with: z.any() })) .input(z.object({ slug: z.string(), with: z.any() }))
.query(async ({ ctx, input }) => { .query(async ({ ctx, input }) => {
@ -76,7 +69,7 @@ export const categoryRouter = createTRPCRouter({
})) as Category; })) as Category;
}), }),
getAll: publicProcedure getMany: publicProcedure
.input( .input(
z z
.object({ .object({
@ -85,9 +78,15 @@ export const categoryRouter = createTRPCRouter({
.optional(), .optional(),
) )
.query(async ({ ctx, input }) => { .query(async ({ ctx, input }) => {
return await ctx.db.query.categories.findMany({ const limit = input?.limit ?? 50;
limit: input?.limit, return (await ctx.db.query.categories.findMany({
}); limit: limit,
columns: {
name: true,
slug: true,
createdAt: true,
},
})) as Category[];
}), }),
getByCursor: publicProcedure getByCursor: publicProcedure
@ -135,7 +134,6 @@ export const categoryRouter = createTRPCRouter({
let nextCursor: string | undefined = undefined; let nextCursor: string | undefined = undefined;
if (items.length > limit) { if (items.length > limit) {
console.log("Configure next cursor");
const cursorItem = items.pop(); const cursorItem = items.pop();
// Create a cursor object with the relevant fields for sorting // Create a cursor object with the relevant fields for sorting
const cursorData: CategoryCursor = { const cursorData: CategoryCursor = {
@ -172,6 +170,12 @@ export const categoryRouter = createTRPCRouter({
.values({ ...input.category, slug }) .values({ ...input.category, slug })
.returning({ .returning({
slug: categories.slug, slug: categories.slug,
})
.onConflictDoUpdate({
target: categories.slug,
set: {
slug: sql`${slug} || '-' || (SELECT COUNT(*) FROM ${categories} WHERE slug LIKE ${slug + "-%"})`,
},
}); });
}), }),
update: protectedProcedure update: protectedProcedure

View File

@ -13,7 +13,7 @@ export const usersRouter = createTRPCRouter({
.update(users) .update(users)
.set(input.profile) .set(input.profile)
.where(eq(users.id, ctx.session.user.id)) .where(eq(users.id, ctx.session.user.id))
.returning(); .returning({ id: users.id });
}), }),
getAll: protectedProcedure.query(async ({ ctx }) => { getAll: protectedProcedure.query(async ({ ctx }) => {
@ -21,6 +21,12 @@ export const usersRouter = createTRPCRouter({
if (!isAdmin) throw new Error("You are not allowed to get all users"); if (!isAdmin) throw new Error("You are not allowed to get all users");
return await ctx.db.query.users.findMany({ return await ctx.db.query.users.findMany({
orderBy: desc(users.role), orderBy: desc(users.role),
columns: {
name: true,
email: true,
role: true,
id: true,
},
}); });
}), }),

View File

@ -15,6 +15,8 @@ const globalForDb = globalThis as unknown as {
const conn = globalForDb.conn ?? postgres(env.DATABASE_URL); const conn = globalForDb.conn ?? postgres(env.DATABASE_URL);
if (env.NODE_ENV !== "production") globalForDb.conn = conn; if (env.NODE_ENV !== "production") globalForDb.conn = conn;
export const db = drizzle(conn, { schema }); export const db = drizzle(conn, {
schema,
});
export type DBType = typeof db; export type DBType = typeof db;

View File

@ -1,7 +1,6 @@
import { createId } from "@paralleldrive/cuid2"; import { createId } from "@paralleldrive/cuid2";
import { relations, SQL, sql } from "drizzle-orm"; import { relations, sql } from "drizzle-orm";
import { import {
AnyPgColumn,
boolean, boolean,
index, index,
integer, integer,
@ -16,10 +15,6 @@ import { User } from "next-auth";
import { type AdapterAccount } from "next-auth/adapters"; import { type AdapterAccount } from "next-auth/adapters";
import { JSONContent } from "novel"; import { JSONContent } from "novel";
export function lower(value: AnyPgColumn): SQL {
return sql`lower(${value})`;
}
export const createTable = pgTableCreator((name) => `wiki-antifa_${name}`); export const createTable = pgTableCreator((name) => `wiki-antifa_${name}`);
export const articles = createTable( export const articles = createTable(
@ -43,7 +38,9 @@ export const articles = createTable(
), ),
}, },
(example) => ({ (example) => ({
articleTitleIndex: index("article_title_idx").on(example.title), titleIndex: index("article_title_idx").on(example.title),
slugIndex: index("article_slug_idx").on(example.slug),
createdAtIndex: index("article_created_at_idx").on(example.createdAt),
}), }),
); );
export type Article = typeof articles.$inferSelect & { export type Article = typeof articles.$inferSelect & {
@ -81,7 +78,9 @@ export const categories = createTable(
), ),
}, },
(example) => ({ (example) => ({
categoryNameIndex: index("category_name_idx").on(example.name), nameIndex: index("category_name_idx").on(example.name),
slugameIndex: index("category_slug_idx").on(example.slug),
createdAtIndex: index("category_created_at_idx").on(example.createdAt),
}), }),
); );
export type Category = typeof categories.$inferSelect & { export type Category = typeof categories.$inferSelect & {