diff --git a/next.config.js b/next.config.js index 69fb737..5f48ad3 100644 --- a/next.config.js +++ b/next.config.js @@ -6,6 +6,9 @@ import "./src/env.js"; /** @type {import("next").NextConfig} */ const config = { + eslint: { + ignoreDuringBuilds: true, + }, experimental: { serverActions: { bodySizeLimit: "2mb", diff --git a/package.json b/package.json index 7e16507..0951d98 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "tailwind-merge": "^3.0.2", "tailwindcss-animate": "^1.0.7", "use-debounce": "^10.0.4", + "winston": "^3.17.0", "zod": "^3.24.2" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e667b4f..09bad78 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -152,6 +152,9 @@ importers: use-debounce: specifier: ^10.0.4 version: 10.0.4(react@18.3.1) + winston: + specifier: ^3.17.0 + version: 3.17.0 zod: specifier: ^3.24.2 version: 3.24.2 @@ -255,10 +258,17 @@ packages: '@cfcs/core@0.0.6': 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': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} + '@dabh/diagnostics@2.0.3': + resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} + '@daybrush/utils@1.13.0': resolution: {integrity: sha512-ALK12C6SQNNHw1enXK+UO8bdyQ+jaWNQ1Af7Z3FNxeAwjYhQT7do+TRE4RASAJ3ObaS2+TJ7TXR3oz2Gzbw0PQ==} @@ -1930,6 +1940,9 @@ packages: '@types/react@18.3.18': resolution: {integrity: sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==} + '@types/triple-beam@1.3.5': + resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -2085,6 +2098,9 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -2202,20 +2218,32 @@ packages: react: ^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: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} color-string@1.9.1: resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + color@3.2.1: + resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} + color@4.2.3: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} + colorspace@1.1.4: + resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==} + comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} @@ -2470,6 +2498,9 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + enabled@2.0.0: + resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + encoding-sniffer@0.2.0: resolution: {integrity: sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==} @@ -2688,6 +2719,9 @@ packages: picomatch: optional: true + fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -2707,6 +2741,9 @@ packages: flatted@3.3.3: 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: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} @@ -2976,6 +3013,10 @@ packages: resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} engines: {node: '>= 0.4'} + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + is-string@1.1.1: resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} engines: {node: '>= 0.4'} @@ -3076,6 +3117,9 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + kuler@2.0.0: + resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + language-subtag-registry@0.3.23: resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} @@ -3107,6 +3151,10 @@ packages: lodash.merge@4.6.2: 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: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -3359,6 +3407,9 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + one-time@1.0.0: + resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -3737,6 +3788,10 @@ packages: read-cache@1.0.0: 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: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -3793,6 +3848,9 @@ packages: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-push-apply@1.0.0: resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} engines: {node: '>= 0.4'} @@ -3801,6 +3859,10 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -3892,6 +3954,9 @@ packages: stable-hash@0.0.4: resolution: {integrity: sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==} + stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} @@ -3927,6 +3992,9 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} @@ -4004,6 +4072,9 @@ packages: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} + text-hex@1.0.0: + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -4031,6 +4102,10 @@ packages: trim-lines@3.0.1: 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: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} @@ -4240,6 +4315,14 @@ packages: engines: {node: '>= 8'} 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: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -4327,10 +4410,18 @@ snapshots: dependencies: '@egjs/component': 3.0.5 + '@colors/colors@1.6.0': {} + '@cspotcode/source-map-support@0.8.1': dependencies: '@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': {} '@drizzle-team/brocli@0.10.2': {} @@ -5737,6 +5828,8 @@ snapshots: '@types/prop-types': 15.7.14 csstype: 3.1.3 + '@types/triple-beam@1.3.5': {} + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -5936,6 +6029,8 @@ snapshots: async-function@1.0.0: {} + async@3.2.6: {} + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 @@ -6074,17 +6169,27 @@ snapshots: - '@types/react' - '@types/react-dom' + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + color-convert@2.0.1: dependencies: color-name: 1.1.4 + color-name@1.1.3: {} + color-name@1.1.4: {} color-string@1.9.1: dependencies: color-name: 1.1.4 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: dependencies: @@ -6092,6 +6197,11 @@ snapshots: color-string: 1.9.1 optional: true + colorspace@1.1.4: + dependencies: + color: 3.2.1 + text-hex: 1.0.0 + comma-separated-tokens@2.0.3: {} commander@4.1.1: {} @@ -6257,6 +6367,8 @@ snapshots: emoji-regex@9.2.2: {} + enabled@2.0.0: {} + encoding-sniffer@0.2.0: dependencies: iconv-lite: 0.6.3 @@ -6691,6 +6803,8 @@ snapshots: optionalDependencies: picomatch: 4.0.2 + fecha@4.2.3: {} + file-entry-cache@6.0.1: dependencies: flat-cache: 3.2.0 @@ -6712,6 +6826,8 @@ snapshots: flatted@3.3.3: {} + fn.name@1.1.0: {} + for-each@0.3.5: dependencies: is-callable: 1.2.7 @@ -6919,8 +7035,7 @@ snapshots: call-bound: 1.0.4 get-intrinsic: 1.3.0 - is-arrayish@0.3.2: - optional: true + is-arrayish@0.3.2: {} is-async-function@2.1.1: dependencies: @@ -7013,6 +7128,8 @@ snapshots: dependencies: call-bound: 1.0.4 + is-stream@2.0.1: {} + is-string@1.1.1: dependencies: call-bound: 1.0.4 @@ -7111,6 +7228,8 @@ snapshots: dependencies: json-buffer: 3.0.1 + kuler@2.0.0: {} + language-subtag-registry@0.3.23: {} language-tags@1.0.9: @@ -7138,6 +7257,15 @@ snapshots: 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: {} loose-envify@1.4.0: @@ -7557,6 +7685,10 @@ snapshots: dependencies: wrappy: 1.0.2 + one-time@1.0.0: + dependencies: + fn.name: 1.1.0 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -7944,6 +8076,12 @@ snapshots: dependencies: 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: dependencies: picomatch: 2.3.1 @@ -8023,6 +8161,8 @@ snapshots: has-symbols: 1.1.0 isarray: 2.0.5 + safe-buffer@5.2.1: {} + safe-push-apply@1.0.0: dependencies: es-errors: 1.3.0 @@ -8034,6 +8174,8 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 + safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} scheduler@0.23.2: @@ -8147,7 +8289,6 @@ snapshots: simple-swizzle@0.2.2: dependencies: is-arrayish: 0.3.2 - optional: true sonner@2.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: @@ -8167,6 +8308,8 @@ snapshots: stable-hash@0.0.4: {} + stack-trace@0.0.10: {} + streamsearch@1.1.0: {} string-width@4.2.3: @@ -8231,6 +8374,10 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + stringify-entities@4.0.4: dependencies: character-entities-html4: 2.1.0 @@ -8322,6 +8469,8 @@ snapshots: tapable@2.2.1: {} + text-hex@1.0.0: {} + text-table@0.2.0: {} thenify-all@1.6.0: @@ -8349,6 +8498,8 @@ snapshots: trim-lines@3.0.1: {} + triple-beam@1.4.1: {} + trough@2.2.0: {} ts-api-utils@2.0.1(typescript@5.8.2): @@ -8600,6 +8751,26 @@ snapshots: dependencies: 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: {} wrap-ansi@7.0.0: diff --git a/public/uploads/upload-1741879341639-pexels-rdne-8052216.jpg.png b/public/uploads/upload-1741879341639-pexels-rdne-8052216.jpg.png deleted file mode 100644 index f7aaa9e..0000000 Binary files a/public/uploads/upload-1741879341639-pexels-rdne-8052216.jpg.png and /dev/null differ diff --git a/public/uploads/upload-1741879517853-pexels-italo-melo-881954-2379005.jpg.png b/public/uploads/upload-1741879517853-pexels-italo-melo-881954-2379005.jpg.png deleted file mode 100644 index 029a58d..0000000 Binary files a/public/uploads/upload-1741879517853-pexels-italo-melo-881954-2379005.jpg.png and /dev/null differ diff --git a/seed-data/index.ts b/seed-data/index.ts index e5ece4c..3c1d578 100644 --- a/seed-data/index.ts +++ b/seed-data/index.ts @@ -1,8 +1,9 @@ import "dotenv/config"; +import { env } from "@/env"; import { db } from "../src/server/db"; import { articles, categories, users } from "../src/server/db/schema"; -async function seed() { +async function developmentSeed() { const usersData = Array.from({ length: 100 }).map((_, i) => ({ name: `User ${i + 1}`, email: `user${i + 1}@example.com`, @@ -37,9 +38,29 @@ async function seed() { 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() { 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) { console.error(error); } finally { diff --git a/src/app/(PAGES)/page.tsx b/src/app/(PAGES)/page.tsx index a1c4476..2ccccb0 100644 --- a/src/app/(PAGES)/page.tsx +++ b/src/app/(PAGES)/page.tsx @@ -10,8 +10,8 @@ import { Button } from "@/components/ui/button"; import Link from "next/link"; export default async function Home() { - const categories = await api.category.getAll({ limit: 6 }); - const articles = await api.article.getAll({ limit: 6 }); + const categories = await api.category.getMany({ limit: 6 }); + const articles = await api.article.getMany({ limit: 6 }); return ( <> diff --git a/src/app/api/url-preview/route.ts b/src/app/api/url-preview/route.ts index bf070c3..203fc5b 100644 --- a/src/app/api/url-preview/route.ts +++ b/src/app/api/url-preview/route.ts @@ -1,8 +1,7 @@ import * as cheerio from "cheerio"; -import { NextApiRequest } from "next"; import { NextResponse } from "next/server"; -export async function GET(req: NextApiRequest) { +export async function GET(req: Request) { if (req.method !== "GET") { return NextResponse.json({ error: "Method not allowed" }); } diff --git a/src/components/article/article-form.tsx b/src/components/article/article-form.tsx index 9628b9c..79c4bd7 100644 --- a/src/components/article/article-form.tsx +++ b/src/components/article/article-form.tsx @@ -25,7 +25,8 @@ import CategorySelect from "@/components/category/category-select"; import { CheckCircle, XCircle } from "lucide-react"; import PublishArticleAlertDialog from "./publish-article-alert-dialog"; 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 }) => { const [loading, setLoading] = React.useState(false); diff --git a/src/components/article/grid/infinite-article-grid.tsx b/src/components/article/grid/infinite-article-grid.tsx index 30a9ad2..7487142 100644 --- a/src/components/article/grid/infinite-article-grid.tsx +++ b/src/components/article/grid/infinite-article-grid.tsx @@ -19,6 +19,9 @@ function InfiniteArticlesGrid() { }, { 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 diff --git a/src/components/article/render-article.tsx b/src/components/article/render-article.tsx index ddd76c3..df57631 100644 --- a/src/components/article/render-article.tsx +++ b/src/components/article/render-article.tsx @@ -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"; //; } diff --git a/src/components/category/category-select.tsx b/src/components/category/category-select.tsx index 826b700..d8e138b 100644 --- a/src/components/category/category-select.tsx +++ b/src/components/category/category-select.tsx @@ -5,7 +5,7 @@ import { Combobox, ComboboxProps } from "../combobox"; import { Icons } from "../icons"; function CategorySelect(props: Partial) { - const { data: categories } = api.category.getAll.useQuery(); + const { data: categories } = api.category.getMany.useQuery(); return ( - {categories.map((category) => ( -
  • + {categories.map((category, idx) => ( +
  • ))} diff --git a/src/components/category/grid/infinite-category-grid.tsx b/src/components/category/grid/infinite-category-grid.tsx index ca76780..2e91e27 100644 --- a/src/components/category/grid/infinite-category-grid.tsx +++ b/src/components/category/grid/infinite-category-grid.tsx @@ -18,6 +18,9 @@ export default function InfiniteCategoryGrid() { }, { 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 diff --git a/src/components/color-picker/color-picker-popover.tsx b/src/components/color-picker/color-picker-popover.tsx deleted file mode 100644 index 6ff6e22..0000000 --- a/src/components/color-picker/color-picker-popover.tsx +++ /dev/null @@ -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 ( - - - - - - - - - ); -} diff --git a/src/components/color-picker/color-picker.css b/src/components/color-picker/color-picker.css deleted file mode 100644 index e68e5e3..0000000 --- a/src/components/color-picker/color-picker.css +++ /dev/null @@ -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; -} diff --git a/src/components/color-picker/index.tsx b/src/components/color-picker/index.tsx deleted file mode 100644 index 339430a..0000000 --- a/src/components/color-picker/index.tsx +++ /dev/null @@ -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([]); - 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 ( -
    - {/* React Colorful Picker */} - - - {/* Default Input (Native Color Picker) */} -
    -
    - handleColorChange(e.currentTarget.value)} - /> -
    - - {/* Display Recent Colors */} -
    - {customColors.map((col) => ( -
    -
    - ); -} - -export default ColorPicker; diff --git a/src/components/data-table/index.tsx b/src/components/data-table/index.tsx index 92760f7..3dde896 100644 --- a/src/components/data-table/index.tsx +++ b/src/components/data-table/index.tsx @@ -36,7 +36,6 @@ export function DataTable({ columns, data, }: DataTableProps) { - "use no memo"; const [pagination, setPagination] = React.useState({ pageSize: 25, pageIndex: 0, diff --git a/src/lib/logger.ts b/src/lib/logger.ts new file mode 100644 index 0000000..de0a9d1 --- /dev/null +++ b/src/lib/logger.ts @@ -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" }), + ], +}); diff --git a/src/server/actions/image.ts b/src/server/actions/image.ts index f981f83..d03c824 100644 --- a/src/server/actions/image.ts +++ b/src/server/actions/image.ts @@ -1,8 +1,12 @@ "use server"; import fs from "node:fs/promises"; import { revalidatePath } from "next/cache"; +import { auth } from "../auth"; +import { hasPermission, Role } from "@/lib/validation/permissions"; 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 arrayBuffer = await file.arrayBuffer(); const buffer = new Uint8Array(arrayBuffer); diff --git a/src/server/api/routers/app.ts b/src/server/api/routers/app.ts index 6e5fb1d..026e95b 100644 --- a/src/server/api/routers/app.ts +++ b/src/server/api/routers/app.ts @@ -1,29 +1,6 @@ -import { z } from "zod"; 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({ - 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 }) => { const categories = await ctx.db.query.categories.findMany({ limit: 3, diff --git a/src/server/api/routers/article.ts b/src/server/api/routers/article.ts index 03ac1d6..bcdf5a8 100644 --- a/src/server/api/routers/article.ts +++ b/src/server/api/routers/article.ts @@ -71,10 +71,10 @@ export const articleRouter = createTRPCRouter({ get: publicProcedure .input(z.object({ slug: z.string() })) .query(async ({ ctx, input }) => { - return await ctx.db.query.articles.findFirst({ + return (await ctx.db.query.articles.findFirst({ where: eq(articles.slug, input.slug), with: { category: true }, - }); + })) as Article; }), getByCursor: publicProcedure .input( @@ -131,7 +131,6 @@ export const articleRouter = createTRPCRouter({ let nextCursor: string | undefined = undefined; if (items.length > limit) { - console.log("Configure next cursor"); const cursorItem = items.pop(); // Create a cursor object with the relevant fields for sorting const cursorData: ArticleCursor = { @@ -150,7 +149,7 @@ export const articleRouter = createTRPCRouter({ nextCursor, }; }), - getAll: publicProcedure + getMany: publicProcedure .input( z .object({ @@ -160,11 +159,12 @@ export const articleRouter = createTRPCRouter({ .optional(), ) .query(async ({ ctx, input }) => { + const limit = input?.limit ?? 50; return (await ctx.db.query.articles.findMany({ where: input?.categoryId ? eq(articles.categoryId, input.categoryId) : undefined, - limit: input?.limit, + limit: limit, columns: { title: true, slug: true, diff --git a/src/server/api/routers/author.ts b/src/server/api/routers/author.ts index dd5b9d6..55e753c 100644 --- a/src/server/api/routers/author.ts +++ b/src/server/api/routers/author.ts @@ -1,4 +1,4 @@ -import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc"; +import { createTRPCRouter, publicProcedure } from "../trpc"; export const authorRouter = createTRPCRouter({ getCount: publicProcedure.query(async ({ ctx }) => { diff --git a/src/server/api/routers/category.ts b/src/server/api/routers/category.ts index ce5f003..96843c1 100644 --- a/src/server/api/routers/category.ts +++ b/src/server/api/routers/category.ts @@ -12,11 +12,11 @@ import { count, desc, eq, - gt, gte, ilike, like, lte, + sql, } from "drizzle-orm"; import { hasPermission, Role } from "@/lib/validation/permissions"; import { @@ -60,13 +60,6 @@ const getCategorySorting = (sort: string, cursor?: CategoryCursor) => { }; 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 .input(z.object({ slug: z.string(), with: z.any() })) .query(async ({ ctx, input }) => { @@ -76,7 +69,7 @@ export const categoryRouter = createTRPCRouter({ })) as Category; }), - getAll: publicProcedure + getMany: publicProcedure .input( z .object({ @@ -85,9 +78,15 @@ export const categoryRouter = createTRPCRouter({ .optional(), ) .query(async ({ ctx, input }) => { - return await ctx.db.query.categories.findMany({ - limit: input?.limit, - }); + const limit = input?.limit ?? 50; + return (await ctx.db.query.categories.findMany({ + limit: limit, + columns: { + name: true, + slug: true, + createdAt: true, + }, + })) as Category[]; }), getByCursor: publicProcedure @@ -135,7 +134,6 @@ export const categoryRouter = createTRPCRouter({ let nextCursor: string | undefined = undefined; if (items.length > limit) { - console.log("Configure next cursor"); const cursorItem = items.pop(); // Create a cursor object with the relevant fields for sorting const cursorData: CategoryCursor = { @@ -172,6 +170,12 @@ export const categoryRouter = createTRPCRouter({ .values({ ...input.category, slug }) .returning({ slug: categories.slug, + }) + .onConflictDoUpdate({ + target: categories.slug, + set: { + slug: sql`${slug} || '-' || (SELECT COUNT(*) FROM ${categories} WHERE slug LIKE ${slug + "-%"})`, + }, }); }), update: protectedProcedure diff --git a/src/server/api/routers/users.ts b/src/server/api/routers/users.ts index fd643fd..cc7e6ce 100644 --- a/src/server/api/routers/users.ts +++ b/src/server/api/routers/users.ts @@ -13,7 +13,7 @@ export const usersRouter = createTRPCRouter({ .update(users) .set(input.profile) .where(eq(users.id, ctx.session.user.id)) - .returning(); + .returning({ id: users.id }); }), 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"); return await ctx.db.query.users.findMany({ orderBy: desc(users.role), + columns: { + name: true, + email: true, + role: true, + id: true, + }, }); }), diff --git a/src/server/db/index.ts b/src/server/db/index.ts index df399a4..c4c4460 100644 --- a/src/server/db/index.ts +++ b/src/server/db/index.ts @@ -15,6 +15,8 @@ const globalForDb = globalThis as unknown as { const conn = globalForDb.conn ?? postgres(env.DATABASE_URL); 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; diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts index 1a369ab..f6910a4 100644 --- a/src/server/db/schema.ts +++ b/src/server/db/schema.ts @@ -1,7 +1,6 @@ import { createId } from "@paralleldrive/cuid2"; -import { relations, SQL, sql } from "drizzle-orm"; +import { relations, sql } from "drizzle-orm"; import { - AnyPgColumn, boolean, index, integer, @@ -16,10 +15,6 @@ import { User } from "next-auth"; import { type AdapterAccount } from "next-auth/adapters"; import { JSONContent } from "novel"; -export function lower(value: AnyPgColumn): SQL { - return sql`lower(${value})`; -} - export const createTable = pgTableCreator((name) => `wiki-antifa_${name}`); export const articles = createTable( @@ -43,7 +38,9 @@ export const articles = createTable( ), }, (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 & { @@ -81,7 +78,9 @@ export const categories = createTable( ), }, (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 & {