diff --git a/.cursorrules b/.cursorrules index 52ec9ea..b9a80a0 100644 --- a/.cursorrules +++ b/.cursorrules @@ -143,7 +143,12 @@

Edit service

diff --git a/web/astro.config.mjs b/web/astro.config.mjs index b33947f..9cdb5ef 100644 --- a/web/astro.config.mjs +++ b/web/astro.config.mjs @@ -42,6 +42,10 @@ export default defineConfig({ open: false, allowedHosts: [new URL(SITE_URL).hostname], }, + image: { + domains: [new URL(SITE_URL).hostname], + remotePatterns: [{ protocol: 'https' }], + }, redirects: { // #region Redirects from old website '/pending': '/?verification=verified&verification=approved&verification=community', diff --git a/web/package-lock.json b/web/package-lock.json index dcf928a..5b084e4 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -15,6 +15,7 @@ "@astrojs/sitemap": "3.4.0", "@fontsource-variable/space-grotesk": "5.2.7", "@fontsource/inter": "5.2.5", + "@fontsource/space-grotesk": "5.2.7", "@prisma/client": "6.8.2", "@tailwindcss/vite": "4.1.7", "@types/mime-types": "2.1.4", @@ -35,6 +36,7 @@ "redis": "5.0.1", "schema-dts": "1.1.5", "seedrandom": "3.0.5", + "sharp": "0.34.1", "slugify": "1.6.6", "tailwind-merge": "3.3.0", "tailwind-variants": "1.0.0", @@ -1135,6 +1137,15 @@ "url": "https://github.com/sponsors/ayuhito" } }, + "node_modules/@fontsource/space-grotesk": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/@fontsource/space-grotesk/-/space-grotesk-5.2.7.tgz", + "integrity": "sha512-erQDZxKdwm5g+47wqJRxh6FB4SJeIgjaK5Iyuht9lGlZ4Wl4Qqv+8xan6oBRfsJHUjALJVQ/fNTIdv9/kIEf+Q==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1306,9 +1317,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", - "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.1.tgz", + "integrity": "sha512-pn44xgBtgpEbZsu+lWf2KNb6OAf70X68k+yk69Ic2Xz11zHR/w24/U49XT7AeRwJ0Px+mhALhU5LPci1Aymk7A==", "cpu": [ "arm64" ], @@ -1324,13 +1335,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.0.4" + "@img/sharp-libvips-darwin-arm64": "1.1.0" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", - "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.1.tgz", + "integrity": "sha512-VfuYgG2r8BpYiOUN+BfYeFo69nP/MIwAtSJ7/Zpxc5QF3KS22z8Pvg3FkrSFJBPNQ7mmcUcYQFBmEQp7eu1F8Q==", "cpu": [ "x64" ], @@ -1346,13 +1357,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.4" + "@img/sharp-libvips-darwin-x64": "1.1.0" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", - "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.1.0.tgz", + "integrity": "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==", "cpu": [ "arm64" ], @@ -1366,9 +1377,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", - "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.1.0.tgz", + "integrity": "sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==", "cpu": [ "x64" ], @@ -1382,9 +1393,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", - "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.1.0.tgz", + "integrity": "sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==", "cpu": [ "arm" ], @@ -1398,9 +1409,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", - "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.1.0.tgz", + "integrity": "sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==", "cpu": [ "arm64" ], @@ -1413,10 +1424,26 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.1.0.tgz", + "integrity": "sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", - "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.1.0.tgz", + "integrity": "sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==", "cpu": [ "s390x" ], @@ -1430,9 +1457,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", - "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.1.0.tgz", + "integrity": "sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==", "cpu": [ "x64" ], @@ -1446,9 +1473,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", - "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.1.0.tgz", + "integrity": "sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==", "cpu": [ "arm64" ], @@ -1462,9 +1489,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", - "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.1.0.tgz", + "integrity": "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==", "cpu": [ "x64" ], @@ -1478,9 +1505,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", - "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.1.tgz", + "integrity": "sha512-anKiszvACti2sGy9CirTlNyk7BjjZPiML1jt2ZkTdcvpLU1YH6CXwRAZCA2UmRXnhiIftXQ7+Oh62Ji25W72jA==", "cpu": [ "arm" ], @@ -1496,13 +1523,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.5" + "@img/sharp-libvips-linux-arm": "1.1.0" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", - "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.1.tgz", + "integrity": "sha512-kX2c+vbvaXC6vly1RDf/IWNXxrlxLNpBVWkdpRq5Ka7OOKj6nr66etKy2IENf6FtOgklkg9ZdGpEu9kwdlcwOQ==", "cpu": [ "arm64" ], @@ -1518,13 +1545,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.4" + "@img/sharp-libvips-linux-arm64": "1.1.0" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", - "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.1.tgz", + "integrity": "sha512-7s0KX2tI9mZI2buRipKIw2X1ufdTeaRgwmRabt5bi9chYfhur+/C1OXg3TKg/eag1W+6CCWLVmSauV1owmRPxA==", "cpu": [ "s390x" ], @@ -1540,13 +1567,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.0.4" + "@img/sharp-libvips-linux-s390x": "1.1.0" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", - "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.1.tgz", + "integrity": "sha512-wExv7SH9nmoBW3Wr2gvQopX1k8q2g5V5Iag8Zk6AVENsjwd+3adjwxtp3Dcu2QhOXr8W9NusBU6XcQUohBZ5MA==", "cpu": [ "x64" ], @@ -1562,13 +1589,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.4" + "@img/sharp-libvips-linux-x64": "1.1.0" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", - "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.1.tgz", + "integrity": "sha512-DfvyxzHxw4WGdPiTF0SOHnm11Xv4aQexvqhRDAoD00MzHekAj9a/jADXeXYCDFH/DzYruwHbXU7uz+H+nWmSOQ==", "cpu": [ "arm64" ], @@ -1584,13 +1611,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + "@img/sharp-libvips-linuxmusl-arm64": "1.1.0" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", - "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.1.tgz", + "integrity": "sha512-pax/kTR407vNb9qaSIiWVnQplPcGU8LRIJpDT5o8PdAx5aAA7AS3X9PS8Isw1/WfqgQorPotjrZL3Pqh6C5EBg==", "cpu": [ "x64" ], @@ -1606,20 +1633,20 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + "@img/sharp-libvips-linuxmusl-x64": "1.1.0" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", - "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.1.tgz", + "integrity": "sha512-YDybQnYrLQfEpzGOQe7OKcyLUCML4YOXl428gOOzBgN6Gw0rv8dpsJ7PqTHxBnXnwXr8S1mYFSLSa727tpz0xg==", "cpu": [ "wasm32" ], "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.2.0" + "@emnapi/runtime": "^1.4.0" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -1629,9 +1656,9 @@ } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", - "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.1.tgz", + "integrity": "sha512-WKf/NAZITnonBf3U1LfdjoMgNO5JYRSlhovhRhMxXVdvWYveM4kM3L8m35onYIdh75cOMCo1BexgVQcCDzyoWw==", "cpu": [ "ia32" ], @@ -1648,9 +1675,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", - "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.1.tgz", + "integrity": "sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw==", "cpu": [ "x64" ], @@ -4210,12 +4237,413 @@ "schema-dts": "^1.1.0" } }, + "node_modules/astro/node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/astro/node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/astro/node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/astro/node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/astro/node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/astro/node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/astro/node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/astro/node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/astro/node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/astro/node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/astro/node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/astro/node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/astro/node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/astro/node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/astro/node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/astro/node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/astro/node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/astro/node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/astro/node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/astro/node_modules/package-manager-detector": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.1.0.tgz", "integrity": "sha512-Y8f9qUlBzW8qauJjd/eu6jlpJZsuPJm2ZAV0cDVd420o4EdpH5RPdoCv+60/TdJflGatr4sDfpAL6ArWZbM5tA==", "license": "MIT" }, + "node_modules/astro/node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, "node_modules/astrojs-compiler-sync": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/astrojs-compiler-sync/-/astrojs-compiler-sync-1.0.1.tgz", @@ -4873,7 +5301,6 @@ "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", "license": "MIT", - "optional": true, "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" @@ -4905,7 +5332,6 @@ "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", "license": "MIT", - "optional": true, "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" @@ -7918,8 +8344,7 @@ "version": "0.3.2", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/is-async-function": { "version": "2.1.1", @@ -12170,16 +12595,15 @@ "license": "ISC" }, "node_modules/sharp": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", - "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.1.tgz", + "integrity": "sha512-1j0w61+eVxu7DawFJtnfYcvSv6qPFvfTaqzTQ2BLknVhHTwGS8sc63ZBF4rzkWMBVKybo4S5OBtDdZahh2A1xg==", "hasInstallScript": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", - "semver": "^7.6.3" + "semver": "^7.7.1" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -12188,25 +12612,26 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.5", - "@img/sharp-darwin-x64": "0.33.5", - "@img/sharp-libvips-darwin-arm64": "1.0.4", - "@img/sharp-libvips-darwin-x64": "1.0.4", - "@img/sharp-libvips-linux-arm": "1.0.5", - "@img/sharp-libvips-linux-arm64": "1.0.4", - "@img/sharp-libvips-linux-s390x": "1.0.4", - "@img/sharp-libvips-linux-x64": "1.0.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", - "@img/sharp-libvips-linuxmusl-x64": "1.0.4", - "@img/sharp-linux-arm": "0.33.5", - "@img/sharp-linux-arm64": "0.33.5", - "@img/sharp-linux-s390x": "0.33.5", - "@img/sharp-linux-x64": "0.33.5", - "@img/sharp-linuxmusl-arm64": "0.33.5", - "@img/sharp-linuxmusl-x64": "0.33.5", - "@img/sharp-wasm32": "0.33.5", - "@img/sharp-win32-ia32": "0.33.5", - "@img/sharp-win32-x64": "0.33.5" + "@img/sharp-darwin-arm64": "0.34.1", + "@img/sharp-darwin-x64": "0.34.1", + "@img/sharp-libvips-darwin-arm64": "1.1.0", + "@img/sharp-libvips-darwin-x64": "1.1.0", + "@img/sharp-libvips-linux-arm": "1.1.0", + "@img/sharp-libvips-linux-arm64": "1.1.0", + "@img/sharp-libvips-linux-ppc64": "1.1.0", + "@img/sharp-libvips-linux-s390x": "1.1.0", + "@img/sharp-libvips-linux-x64": "1.1.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.1.0", + "@img/sharp-libvips-linuxmusl-x64": "1.1.0", + "@img/sharp-linux-arm": "0.34.1", + "@img/sharp-linux-arm64": "0.34.1", + "@img/sharp-linux-s390x": "0.34.1", + "@img/sharp-linux-x64": "0.34.1", + "@img/sharp-linuxmusl-arm64": "0.34.1", + "@img/sharp-linuxmusl-x64": "0.34.1", + "@img/sharp-wasm32": "0.34.1", + "@img/sharp-win32-ia32": "0.34.1", + "@img/sharp-win32-x64": "0.34.1" } }, "node_modules/shebang-command": { @@ -12374,7 +12799,6 @@ "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", "license": "MIT", - "optional": true, "dependencies": { "is-arrayish": "^0.3.1" } diff --git a/web/package.json b/web/package.json index fe20ef2..9a5600d 100644 --- a/web/package.json +++ b/web/package.json @@ -26,6 +26,7 @@ "@astrojs/node": "9.2.1", "@astrojs/sitemap": "3.4.0", "@fontsource-variable/space-grotesk": "5.2.7", + "@fontsource/space-grotesk": "5.2.7", "@fontsource/inter": "5.2.5", "@prisma/client": "6.8.2", "@tailwindcss/vite": "4.1.7", @@ -47,6 +48,7 @@ "redis": "5.0.1", "schema-dts": "1.1.5", "seedrandom": "3.0.5", + "sharp": "0.34.1", "slugify": "1.6.6", "tailwind-merge": "3.3.0", "tailwind-variants": "1.0.0", diff --git a/web/src/components/Captcha.astro b/web/src/components/Captcha.astro index a9cb8fa..4e20df5 100644 --- a/web/src/components/Captcha.astro +++ b/web/src/components/Captcha.astro @@ -1,7 +1,6 @@ --- import { Icon } from 'astro-icon/components' import { isInputError, type ActionAccept, type ActionClient } from 'astro:actions' -import { Image } from 'astro:assets' import { CAPTCHA_LENGTH, generateCaptcha } from '../lib/captcha' import { cn } from '../lib/cn' diff --git a/web/src/components/ChatMessages.astro b/web/src/components/ChatMessages.astro index 2bac799..a87d70d 100644 --- a/web/src/components/ChatMessages.astro +++ b/web/src/components/ChatMessages.astro @@ -1,9 +1,9 @@ --- -import { Picture } from 'astro:assets' - import { cn } from '../lib/cn' import { formatDateShort } from '../lib/timeAgo' +import MyPicture from './MyPicture.astro' + import type { Prisma } from '@prisma/client' import type { HTMLAttributes } from 'astro/types' @@ -73,13 +73,12 @@ const { messages, userId, class: className, ...htmlProps } = Astro.props {!isCurrentUser && !isNextFromSameUser && (

{!!message.user.picture && ( - )} {message.user.name} diff --git a/web/src/components/CommentItem.astro b/web/src/components/CommentItem.astro index cf904b8..a6c3e14 100644 --- a/web/src/components/CommentItem.astro +++ b/web/src/components/CommentItem.astro @@ -1,5 +1,4 @@ --- -import Image from 'astro/components/Image.astro' import { Icon } from 'astro-icon/components' import { Markdown } from 'astro-remote' import { Schema } from 'astro-seo-schema' @@ -19,6 +18,7 @@ import { formatDateShort } from '../lib/timeAgo' import BadgeSmall from './BadgeSmall.astro' import CommentModeration from './CommentModeration.astro' import CommentReply from './CommentReply.astro' +import MyPicture from './MyPicture.astro' import TimeFormatted from './TimeFormatted.astro' import Tooltip from './Tooltip.astro' @@ -158,11 +158,10 @@ const commentUrl = makeCommentUrl({ serviceSlug, commentId: comment.id, origin: { comment.author.picture && ( - {`Profile diff --git a/web/src/components/MyPicture.astro b/web/src/components/MyPicture.astro new file mode 100644 index 0000000..f4b28d8 --- /dev/null +++ b/web/src/components/MyPicture.astro @@ -0,0 +1,42 @@ +--- +import type { ComponentProps } from 'react' + +import { Picture } from 'astro:assets' + +import defaultServiceImage from '../assets/fallback-service-image.jpg' + +const fallbackImages = { + service: defaultServiceImage, +} as const satisfies Record + +type Props = Omit, 'src'> & { + src: ComponentProps['src'] | null | undefined + fallback?: keyof typeof fallbackImages +} + +const { + src, + formats = ['avif', 'webp'], + fallback = undefined as keyof typeof fallbackImages | undefined, + height, + width, + ...props +} = Astro.props + +const fallbackImage = fallback ? fallbackImages[fallback] : undefined +--- + +{/* eslint-disable @typescript-eslint/no-explicit-any */} +{ + !!(src ?? fallbackImage) && ( + + ) +} diff --git a/web/src/components/OgImage.tsx b/web/src/components/OgImage.tsx index 2676339..43f603d 100644 --- a/web/src/components/OgImage.tsx +++ b/web/src/components/OgImage.tsx @@ -2,16 +2,16 @@ import fs from 'node:fs' import path from 'node:path' import { ImageResponse } from '@vercel/og' +import sharp from 'sharp' import defaultOGImageBg from '../assets/ogimage-bg.png' import defaultOGImage from '../assets/ogimage.png' +import { makeOverallScoreInfo } from '../lib/overallScore' import { urlWithParams } from '../lib/urls' import type { APIContext } from 'astro' import type { Prettify } from 'ts-essentials' -export type GenericOgImageProps = Partial> - ////////////////////////////////////////////////////// // NOTE // // Use this website to create and preview templates // @@ -52,15 +52,41 @@ const defaultOptions = { ) ), }, + { + name: 'Space Grotesk', + weight: 400, + style: 'normal', + data: fs.readFileSync( + path.resolve( + process.cwd(), + 'node_modules', + '@fontsource', + 'space-grotesk', + 'files', + 'space-grotesk-latin-400-normal.woff' + ) + ), + }, + { + name: 'Space Grotesk', + weight: 700, + style: 'normal', + data: fs.readFileSync( + path.resolve( + process.cwd(), + 'node_modules', + '@fontsource', + 'space-grotesk', + 'files', + 'space-grotesk-latin-700-normal.woff' + ) + ), + }, ], } as const satisfies ConstructorParameters[1] -function absoluteUrl(url: string, context: Pick) { - return new URL(url, context.url.origin).href -} - export const ogImageTemplates = { - default: (_props: Record = {}, context: APIContext) => { + default: (_props: Record = {}, context) => { return new ImageResponse( ( { + service: async ( + { + title, + description, + categories, + score, + imageUrl, + }: { + title: string + description: string + categories: { + name: string + icon: string + }[] + score: number + imageUrl: string | null + }, + context + ) => { + const scoreInfo = makeOverallScoreInfo(score, 10) + const scoreColors = { + 'bg-score-1': '#e26136', + 'bg-score-2': '#eba370', + 'bg-score-3': '#eddb82', + 'bg-score-4': '#8de2d7', + 'bg-score-5': '#3cdd71', + } as const satisfies Record + const scoreColor = + Object.entries(scoreColors).find(([className]) => scoreInfo.classNameBg?.includes(className))?.[1] ?? + 'white' + + const PADING = 80 + return new ImageResponse( (

- {title} +
+ {!!imageUrl && ( + + )} +
+ + {title} + +
+
+ +
+
+ + {description} + +
+ {await Promise.all( + categories.map(async (category) => ( + + + {category.name} + + )) + )} +
+
+
+
+ {score} +
+
+
+ + +
), defaultOptions ) }, -} as const satisfies Record ImageResponse | null> + generic: async ( + { + title, + description, + icon, + }: { + title: string + description?: string | null + icon?: string | null + }, + context + ) => { + const PADING = 80 + + return new ImageResponse( + ( +
+ + + + +
+ + {title} + +
+ + + {description} + + + {!!icon && ( + + )} +
+ ), + defaultOptions + ) + }, +} as const satisfies Record< + string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (props: any, context: APIContext) => ImageResponse | Promise | null +> type OgImageTemplate = keyof typeof ogImageTemplates type OgImageProps = Parameters<(typeof ogImageTemplates)[T]>[0] -// eslint-disable-next-line @typescript-eslint/sort-type-constituents -export type OgImageAllTemplatesWithGenericProps = { template: OgImageTemplate } & GenericOgImageProps export type OgImageAllTemplatesWithProps = Prettify< { @@ -119,5 +386,44 @@ export function makeOgImageUrl( ) { return typeof ogImage === 'string' ? new URL(ogImage, baseUrl).href - : urlWithParams(new URL('/ogimage.png', baseUrl), ogImage ?? {}) + : urlWithParams(new URL('/ogimage.png', baseUrl), { data: JSON.stringify(ogImage ?? {}) }) +} + +// Utilities ------------------------------------------------------------ + +function absoluteUrl(url: string, context: Pick) { + return new URL(url, context.url.origin).href +} + +async function svgUrlToBase64Png(svgUrl: string, width?: number, height?: number): Promise { + // 1. Fetch the SVG file + const response = await fetch(svgUrl) + if (!response.ok) { + throw new Error(`Failed to fetch SVG: ${response.statusText}`) + } + + const svgBuffer = await response.arrayBuffer() + + // 2. Convert SVG to PNG using sharp + let image = sharp(svgBuffer).png().negate({ alpha: false }) + if (width || height) { + image = image.resize(width, height, { + fit: 'contain', + background: { r: 0, g: 0, b: 0, alpha: 0 }, + }) + } + + const pngBuffer = await image.toBuffer() + + // 3. Convert to base64 string + const base64 = pngBuffer.toString('base64') + return `data:image/png;base64,${base64}` +} + +async function iconUrl(icon: string, size = 30) { + const [, prefix, name] = /^([^:]+):(.*)$/.exec(icon) ?? [] + if (!prefix || !name) return undefined + const url = `https://api.iconify.design/${prefix}/${name}.svg` + const result = await svgUrlToBase64Png(url, size, size) + return result } diff --git a/web/src/components/ScoreSquare.astro b/web/src/components/ScoreSquare.astro index 84b0a5e..480cd95 100644 --- a/web/src/components/ScoreSquare.astro +++ b/web/src/components/ScoreSquare.astro @@ -2,6 +2,7 @@ import { Schema } from 'astro-seo-schema' import { cn } from '../lib/cn' +import { makeOverallScoreInfo } from '../lib/overallScore' import { KYCNOTME_SCHEMA_MINI } from '../lib/schema' import { transformCase } from '../lib/strings' @@ -16,33 +17,6 @@ export type Props = HTMLAttributes<'div'> & { const { score, label, total = 10, class: className, itemReviewedId, ...htmlProps } = Astro.props -export function makeOverallScoreInfo(score: number, total = 10) { - const classNamesByColor = { - red: 'bg-score-1 text-black', - orange: 'bg-score-2 text-black', - yellow: 'bg-score-3 text-black', - blue: 'bg-score-4 text-black', - green: 'bg-score-5 text-black', - } as const satisfies Record - - const formattedScore = Math.round(score).toLocaleString() - const n = score / total - - if (n > 1) return { text: '', classNameBg: classNamesByColor.green, formattedScore } - if (n >= 0.9 && n <= 1) return { text: 'Excellent', classNameBg: classNamesByColor.green, formattedScore } - if (n >= 0.8 && n < 0.9) return { text: 'Very Good', classNameBg: classNamesByColor.blue, formattedScore } - if (n >= 0.7 && n < 0.8) return { text: 'Good', classNameBg: classNamesByColor.blue, formattedScore } - if (n >= 0.6 && n < 0.7) return { text: 'Okay', classNameBg: classNamesByColor.yellow, formattedScore } - if (n >= 0.5 && n < 0.6) { - return { text: 'Acceptable', classNameBg: classNamesByColor.yellow, formattedScore } - } - if (n >= 0.4 && n < 0.5) return { text: 'Bad', classNameBg: classNamesByColor.orange, formattedScore } - if (n >= 0.3 && n < 0.4) return { text: 'Very Bad', classNameBg: classNamesByColor.orange, formattedScore } - if (n >= 0.2 && n < 0.3) return { text: 'Really Bad', classNameBg: classNamesByColor.red, formattedScore } - if (n >= 0 && n < 0.2) return { text: 'Terrible', classNameBg: classNamesByColor.red, formattedScore } - return { text: '', classNameBg: undefined, formattedScore } -} - const { text, classNameBg, formattedScore } = makeOverallScoreInfo(score, total) --- diff --git a/web/src/components/ServiceCard.astro b/web/src/components/ServiceCard.astro index d4ca06f..6aca091 100644 --- a/web/src/components/ServiceCard.astro +++ b/web/src/components/ServiceCard.astro @@ -1,14 +1,13 @@ --- import { Icon } from 'astro-icon/components' -import { Image } from 'astro:assets' -import defaultImage from '../assets/fallback-service-image.jpg' import { currencies } from '../constants/currencies' import { verificationStatusesByValue } from '../constants/verificationStatus' import { cn } from '../lib/cn' +import { makeOverallScoreInfo } from '../lib/overallScore' import { transformCase } from '../lib/strings' -import { makeOverallScoreInfo } from './ScoreSquare.astro' +import MyPicture from './MyPicture.astro' import Tooltip from './Tooltip.astro' import type { Prisma } from '@prisma/client' @@ -76,9 +75,9 @@ const overallScoreInfo = makeOverallScoreInfo(overallScore) >
- {name & author: string pubDate: string description: string + icon?: string }> const { frontmatter, schemas, ...baseLayoutProps } = Astro.props @@ -23,6 +24,8 @@ const publishDate = frontmatter.pubDate ? new Date(frontmatter.pubDate) : null const ogImageTemplateData = { template: 'generic', title: frontmatter.title, + description: frontmatter.description, + icon: frontmatter.icon, } satisfies OgImageAllTemplatesWithProps const weAreAuthor = frontmatter.author.toLowerCase().trim() === 'kycnot.me' --- diff --git a/web/src/lib/overallScore.ts b/web/src/lib/overallScore.ts new file mode 100644 index 0000000..e652ac6 --- /dev/null +++ b/web/src/lib/overallScore.ts @@ -0,0 +1,26 @@ +export function makeOverallScoreInfo(score: number, total = 10) { + const classNamesByColor = { + red: 'bg-score-1 text-black', + orange: 'bg-score-2 text-black', + yellow: 'bg-score-3 text-black', + blue: 'bg-score-4 text-black', + green: 'bg-score-5 text-black', + } as const satisfies Record + + const formattedScore = Math.round(score).toLocaleString() + const n = score / total + + if (n > 1) return { text: '', classNameBg: classNamesByColor.green, formattedScore } + if (n >= 0.9 && n <= 1) return { text: 'Excellent', classNameBg: classNamesByColor.green, formattedScore } + if (n >= 0.8 && n < 0.9) return { text: 'Very Good', classNameBg: classNamesByColor.blue, formattedScore } + if (n >= 0.7 && n < 0.8) return { text: 'Good', classNameBg: classNamesByColor.blue, formattedScore } + if (n >= 0.6 && n < 0.7) return { text: 'Okay', classNameBg: classNamesByColor.yellow, formattedScore } + if (n >= 0.5 && n < 0.6) { + return { text: 'Acceptable', classNameBg: classNamesByColor.yellow, formattedScore } + } + if (n >= 0.4 && n < 0.5) return { text: 'Bad', classNameBg: classNamesByColor.orange, formattedScore } + if (n >= 0.3 && n < 0.4) return { text: 'Very Bad', classNameBg: classNamesByColor.orange, formattedScore } + if (n >= 0.2 && n < 0.3) return { text: 'Really Bad', classNameBg: classNamesByColor.red, formattedScore } + if (n >= 0 && n < 0.2) return { text: 'Terrible', classNameBg: classNamesByColor.red, formattedScore } + return { text: '', classNameBg: undefined, formattedScore } +} diff --git a/web/src/lib/zodUtils.ts b/web/src/lib/zodUtils.ts index aeda57b..34cfa35 100644 --- a/web/src/lib/zodUtils.ts +++ b/web/src/lib/zodUtils.ts @@ -50,7 +50,6 @@ export const ACCEPTED_IMAGE_TYPES = [ 'image/svg+xml', 'image/png', 'image/jpeg', - 'image/jxl', 'image/avif', 'image/webp', ] as const satisfies string[] @@ -66,7 +65,7 @@ export const imageFileSchema = z ) .refine( (file) => !file || ACCEPTED_IMAGE_TYPES.some((type) => file.type === type), - 'Only SVG, PNG, JPG, JPEG XL, AVIF, WebP formats are supported.' + 'Only SVG, PNG, JPG, AVIF, WebP formats are supported.' ) export const imageFileSchemaRequired = imageFileSchema.refine((file) => !!file, 'Required') diff --git a/web/src/pages/about.md b/web/src/pages/about.md index 9e52372..27be689 100644 --- a/web/src/pages/about.md +++ b/web/src/pages/about.md @@ -4,6 +4,7 @@ title: About author: KYCnot.me pubDate: 2025-05-15 description: 'Learn how KYCnot.me website works and about our mission to protect privacy in cryptocurrency.' +icon: 'ri:information-line' --- ## What is this page? diff --git a/web/src/pages/account/edit.astro b/web/src/pages/account/edit.astro index 489ba41..63fdffe 100644 --- a/web/src/pages/account/edit.astro +++ b/web/src/pages/account/edit.astro @@ -24,7 +24,7 @@ const inputErrors = isInputError(result?.error) ? result.error.fields : {} { user.picture ? ( - + ) : (
@@ -460,8 +470,9 @@ if (!user) return Astro.rewrite('/404') href={`/service/${affiliation.service.slug}`} class="text-day-300 group flex min-w-32 items-center gap-2 text-sm" > - - Current service image {
- {service.imageUrl ? ( - {service.name} - ) : ( - {service.name} - )} +
{service.name}
diff --git a/web/src/pages/admin/users/[username].astro b/web/src/pages/admin/users/[username].astro index ed32221..e48eed0 100644 --- a/web/src/pages/admin/users/[username].astro +++ b/web/src/pages/admin/users/[username].astro @@ -1,7 +1,6 @@ --- import { Icon } from 'astro-icon/components' import { actions, isInputError } from 'astro:actions' -import { Image } from 'astro:assets' import BadgeSmall from '../../../components/BadgeSmall.astro' import Button from '../../../components/Button.astro' @@ -11,6 +10,7 @@ import InputSelect from '../../../components/InputSelect.astro' import InputSubmitButton from '../../../components/InputSubmitButton.astro' import InputText from '../../../components/InputText.astro' import InputTextArea from '../../../components/InputTextArea.astro' +import MyPicture from '../../../components/MyPicture.astro' import TimeFormatted from '../../../components/TimeFormatted.astro' import { getServiceUserRoleInfo, serviceUserRoles } from '../../../constants/serviceUserRoles' import BaseLayout from '../../../layouts/BaseLayout.astro' @@ -123,7 +123,7 @@ if (!user) return Astro.rewrite('/404')
{ !!user.picture && ( -
{!!note.addedByUser?.picture && ( - ) => {

Service attributes

@@ -202,12 +207,11 @@ const makeSortUrl = (slug: NonNullable<(typeof filters)['sort-by']>) => { class="flex items-center gap-2 rounded-md p-2 transition-colors hover:bg-zinc-800" > {service.imageUrl ? ( - ) : ( @@ -349,12 +353,11 @@ const makeSortUrl = (slug: NonNullable<(typeof filters)['sort-by']>) => { class="flex items-center gap-2 rounded-md p-2 transition-colors hover:bg-zinc-800" > {service.imageUrl ? ( - ) : ( diff --git a/web/src/pages/events.astro b/web/src/pages/events.astro index 04ccb56..7d08f07 100644 --- a/web/src/pages/events.astro +++ b/web/src/pages/events.astro @@ -1,11 +1,11 @@ --- import { z } from 'astro/zod' import { Icon } from 'astro-icon/components' -import { Picture } from 'astro:assets' import { orderBy } from 'lodash-es' import Button from '../components/Button.astro' import FormatTimeInterval from '../components/FormatTimeInterval.astro' +import MyPicture from '../components/MyPicture.astro' import TimeFormatted from '../components/TimeFormatted.astro' import { eventTypes, @@ -151,7 +151,12 @@ const createUrlWithoutFilter = (paramName: keyof typeof params) => { description="Discover important events, updates, and news about KYC-free services in chronological order." widthClassName="max-w-screen-lg" className={{ main: 'sm:flex sm:items-start sm:gap-6' }} - ogImage={{ template: 'generic', title: 'Events' }} + ogImage={{ + template: 'generic', + title: 'Events', + description: 'Discover important events, updates, and news about KYC-free services', + icon: 'ri:calendar-event-line', + }} htmx >

@@ -287,12 +292,11 @@ const createUrlWithoutFilter = (paramName: keyof typeof params) => { class="group flex h-8 items-center gap-2 rounded-full border border-green-500/30 bg-black/40 px-3 text-sm text-white" > {service?.imageUrl && ( - )} @@ -385,12 +389,11 @@ const createUrlWithoutFilter = (paramName: keyof typeof params) => { class="-m-1.5 flex w-fit items-center rounded-md p-1.5 leading-none transition-colors hover:bg-zinc-800" > {event.service.imageUrl && ( - )} diff --git a/web/src/pages/karma.mdx b/web/src/pages/karma.mdx index 853a1f0..602fc18 100644 --- a/web/src/pages/karma.mdx +++ b/web/src/pages/karma.mdx @@ -2,6 +2,7 @@ layout: ../layouts/MarkdownLayout.astro title: How does karma work? description: "KYCnot.me has a user karma system, here's how it works" +icon: 'ri:hearts-line' author: KYCnot.me pubDate: 2025-05-15 --- diff --git a/web/src/pages/notifications.astro b/web/src/pages/notifications.astro index 660cfcb..459bc91 100644 --- a/web/src/pages/notifications.astro +++ b/web/src/pages/notifications.astro @@ -183,7 +183,12 @@ const notifications = dbNotifications.map((notification) => ({ pageTitle="Notifications" description="View your notifications and manage your notification preferences." widthClassName="max-w-screen-lg" - ogImage={{ template: 'generic', title: 'Notifications' }} + ogImage={{ + template: 'generic', + title: 'Notifications', + description: 'View and manage your notifications', + icon: 'ri:notification-line', + }} >
diff --git a/web/src/pages/ogimage.png.ts b/web/src/pages/ogimage.png.ts index 4eeae71..9ea8e91 100644 --- a/web/src/pages/ogimage.png.ts +++ b/web/src/pages/ogimage.png.ts @@ -1,19 +1,31 @@ -import { ogImageTemplates } from '../components/OgImage' -import { urlParamsToObject } from '../lib/urls' +import { ogImageTemplates, type OgImageAllTemplatesWithProps } from '../components/OgImage' import type { APIRoute } from 'astro' +import type { Misc } from 'ts-toolbelt' -export const GET: APIRoute = (context) => { - const { template, ...props } = urlParamsToObject(context.url.searchParams) +function toJSON(data: string | null | undefined): T | undefined { + if (!data) return undefined + try { + return JSON.parse(data) as T + } catch (_error) { + return undefined + } +} - if (!template) return ogImageTemplates.default({}, context) +export const GET: APIRoute = async (context) => { + const { template, ...props } = toJSON( + context.url.searchParams.get('data') + ) ?? { template: 'default' } + + if (!template as unknown) return ogImageTemplates.default({}, context) if (!(template in ogImageTemplates)) { console.error(`Invalid template: "${template}"`) return ogImageTemplates.default({}, context) } - const response = ogImageTemplates[template as keyof typeof ogImageTemplates](props, context) + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any + const response = await ogImageTemplates[template](props as any, context) // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!response) { console.error(`Cannot generate image for template: ${template} and props: ${JSON.stringify(props)}`) diff --git a/web/src/pages/service-suggestion/[id].astro b/web/src/pages/service-suggestion/[id].astro index a18239e..1f16b86 100644 --- a/web/src/pages/service-suggestion/[id].astro +++ b/web/src/pages/service-suggestion/[id].astro @@ -86,7 +86,12 @@ const statusInfo = getServiceSuggestionStatusInfo(serviceSuggestion.status) - {suggestion.service.name} diff --git a/web/src/pages/service-suggestion/new.astro b/web/src/pages/service-suggestion/new.astro index 1878b7e..4e544e6 100644 --- a/web/src/pages/service-suggestion/new.astro +++ b/web/src/pages/service-suggestion/new.astro @@ -65,7 +65,12 @@ const [categories, attributes] = await Astro.locals.banners.tryMany([ { const itemReviewedId = new URL(`/service/${service.slug}`, Astro.url).href const ogImageTemplateData = { - template: 'generic', + template: 'service', title: service.name, + description: service.description, + categories: service.categories.map((category) => pick(category, ['name', 'icon'])), + score: service.overallScore, + imageUrl: service.imageUrl, } satisfies OgImageAllTemplatesWithProps --- @@ -477,9 +481,8 @@ const ogImageTemplateData = {
{ !!service.imageUrl && ( - { user.picture ? ( - + ) : (
@@ -555,8 +566,9 @@ const isCurrentUser = !!Astro.locals.user && user.id === Astro.locals.user.id href={`/service/${affiliation.service.slug}`} class="text-day-300 group flex min-w-32 items-center gap-2 text-sm" > -