diff --git a/.gitignore b/.gitignore index e867675..1811076 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,5 @@ yarn-error.log* next-env.d.ts # seo/public -**/sitemap*.xml \ No newline at end of file +**/sitemap*.xml +/.idea diff --git a/package.json b/package.json index 9d7ba43..8daf4f3 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "html-to-image": "^1.11.11", "input-otp": "^1.4.2", "ioredis": "^5.10.1", + "isomorphic-dompurify": "^3.11.0", "jotai": "^2.9.3", "jotai-family": "^1.0.1", "lucide-react": "^0.426.0", @@ -85,6 +86,7 @@ "devDependencies": { "@react-email/ui": "6.0.8", "@solvro/config": "^2.3.0", + "@tailwindcss/typography": "^0.5.19", "@trivago/prettier-plugin-sort-imports": "^6.0.2", "@types/crypto-js": "^4.2.2", "@types/node": "^20.17.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b326f76..0632eaa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -113,6 +113,9 @@ importers: ioredis: specifier: ^5.10.1 version: 5.10.1 + isomorphic-dompurify: + specifier: ^3.11.0 + version: 3.12.0(@noble/hashes@2.0.1) jotai: specifier: ^2.9.3 version: 2.18.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.4) @@ -186,6 +189,9 @@ importers: "@solvro/config": specifier: ^2.3.0 version: 2.3.0(@next/eslint-plugin-next@16.2.1)(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(class-validator@0.15.1)(eslint@9.39.4(jiti@1.21.7))(prettier@3.8.1)(typescript@5.9.3) + "@tailwindcss/typography": + specifier: ^0.5.19 + version: 0.5.19(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2)) "@trivago/prettier-plugin-sort-imports": specifier: ^6.0.2 version: 6.0.2(prettier@3.8.1) @@ -270,6 +276,33 @@ packages: } engines: { node: ">=10" } + "@asamuzakjp/css-color@5.1.11": + resolution: + { + integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==, + } + engines: { node: ^20.19.0 || ^22.12.0 || >=24.0.0 } + + "@asamuzakjp/dom-selector@7.1.1": + resolution: + { + integrity: sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==, + } + engines: { node: ^20.19.0 || ^22.12.0 || >=24.0.0 } + + "@asamuzakjp/generational-cache@1.0.1": + resolution: + { + integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==, + } + engines: { node: ^20.19.0 || ^22.12.0 || >=24.0.0 } + + "@asamuzakjp/nwsapi@2.3.9": + resolution: + { + integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==, + } + "@babel/code-frame@7.29.0": resolution: { @@ -494,6 +527,13 @@ packages: integrity: sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==, } + "@bramus/specificity@2.4.2": + resolution: + { + integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==, + } + hasBin: true + "@chevrotain/cst-dts-gen@10.5.0": resolution: { @@ -544,6 +584,60 @@ packages: } engines: { node: ">=v18" } + "@csstools/color-helpers@6.0.2": + resolution: + { + integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==, + } + engines: { node: ">=20.19.0" } + + "@csstools/css-calc@3.2.0": + resolution: + { + integrity: sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==, + } + engines: { node: ">=20.19.0" } + peerDependencies: + "@csstools/css-parser-algorithms": ^4.0.0 + "@csstools/css-tokenizer": ^4.0.0 + + "@csstools/css-color-parser@4.1.0": + resolution: + { + integrity: sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==, + } + engines: { node: ">=20.19.0" } + peerDependencies: + "@csstools/css-parser-algorithms": ^4.0.0 + "@csstools/css-tokenizer": ^4.0.0 + + "@csstools/css-parser-algorithms@4.0.0": + resolution: + { + integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==, + } + engines: { node: ">=20.19.0" } + peerDependencies: + "@csstools/css-tokenizer": ^4.0.0 + + "@csstools/css-syntax-patches-for-csstree@1.1.3": + resolution: + { + integrity: sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==, + } + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + "@csstools/css-tokenizer@4.0.0": + resolution: + { + integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==, + } + engines: { node: ">=20.19.0" } + "@darraghor/eslint-plugin-nestjs-typed@7.1.27": resolution: { @@ -1623,6 +1717,18 @@ packages: } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + "@exodus/bytes@1.15.0": + resolution: + { + integrity: sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==, + } + engines: { node: ^20.19.0 || ^22.12.0 || >=24.0.0 } + peerDependencies: + "@noble/hashes": ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + "@noble/hashes": + optional: true + "@floating-ui/core@1.7.5": resolution: { @@ -1777,6 +1883,7 @@ packages: } cpu: [arm64] os: [linux] + libc: [glibc] "@img/sharp-libvips-linux-arm64@1.2.4": resolution: @@ -1785,6 +1892,7 @@ packages: } cpu: [arm64] os: [linux] + libc: [glibc] "@img/sharp-libvips-linux-arm@1.0.5": resolution: @@ -1793,6 +1901,7 @@ packages: } cpu: [arm] os: [linux] + libc: [glibc] "@img/sharp-libvips-linux-arm@1.2.4": resolution: @@ -1801,6 +1910,7 @@ packages: } cpu: [arm] os: [linux] + libc: [glibc] "@img/sharp-libvips-linux-ppc64@1.2.4": resolution: @@ -1809,6 +1919,7 @@ packages: } cpu: [ppc64] os: [linux] + libc: [glibc] "@img/sharp-libvips-linux-riscv64@1.2.4": resolution: @@ -1817,6 +1928,7 @@ packages: } cpu: [riscv64] os: [linux] + libc: [glibc] "@img/sharp-libvips-linux-s390x@1.0.4": resolution: @@ -1825,6 +1937,7 @@ packages: } cpu: [s390x] os: [linux] + libc: [glibc] "@img/sharp-libvips-linux-s390x@1.2.4": resolution: @@ -1833,6 +1946,7 @@ packages: } cpu: [s390x] os: [linux] + libc: [glibc] "@img/sharp-libvips-linux-x64@1.0.4": resolution: @@ -1841,6 +1955,7 @@ packages: } cpu: [x64] os: [linux] + libc: [glibc] "@img/sharp-libvips-linux-x64@1.2.4": resolution: @@ -1849,6 +1964,7 @@ packages: } cpu: [x64] os: [linux] + libc: [glibc] "@img/sharp-libvips-linuxmusl-arm64@1.0.4": resolution: @@ -1857,6 +1973,7 @@ packages: } cpu: [arm64] os: [linux] + libc: [musl] "@img/sharp-libvips-linuxmusl-arm64@1.2.4": resolution: @@ -1865,6 +1982,7 @@ packages: } cpu: [arm64] os: [linux] + libc: [musl] "@img/sharp-libvips-linuxmusl-x64@1.0.4": resolution: @@ -1873,6 +1991,7 @@ packages: } cpu: [x64] os: [linux] + libc: [musl] "@img/sharp-libvips-linuxmusl-x64@1.2.4": resolution: @@ -1881,6 +2000,7 @@ packages: } cpu: [x64] os: [linux] + libc: [musl] "@img/sharp-linux-arm64@0.33.5": resolution: @@ -1890,6 +2010,7 @@ packages: engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [arm64] os: [linux] + libc: [glibc] "@img/sharp-linux-arm64@0.34.5": resolution: @@ -1899,6 +2020,7 @@ packages: engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [arm64] os: [linux] + libc: [glibc] "@img/sharp-linux-arm@0.33.5": resolution: @@ -1908,6 +2030,7 @@ packages: engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [arm] os: [linux] + libc: [glibc] "@img/sharp-linux-arm@0.34.5": resolution: @@ -1917,6 +2040,7 @@ packages: engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [arm] os: [linux] + libc: [glibc] "@img/sharp-linux-ppc64@0.34.5": resolution: @@ -1926,6 +2050,7 @@ packages: engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [ppc64] os: [linux] + libc: [glibc] "@img/sharp-linux-riscv64@0.34.5": resolution: @@ -1935,6 +2060,7 @@ packages: engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [riscv64] os: [linux] + libc: [glibc] "@img/sharp-linux-s390x@0.33.5": resolution: @@ -1944,6 +2070,7 @@ packages: engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [s390x] os: [linux] + libc: [glibc] "@img/sharp-linux-s390x@0.34.5": resolution: @@ -1953,6 +2080,7 @@ packages: engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [s390x] os: [linux] + libc: [glibc] "@img/sharp-linux-x64@0.33.5": resolution: @@ -1962,6 +2090,7 @@ packages: engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [x64] os: [linux] + libc: [glibc] "@img/sharp-linux-x64@0.34.5": resolution: @@ -1971,6 +2100,7 @@ packages: engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [x64] os: [linux] + libc: [glibc] "@img/sharp-linuxmusl-arm64@0.33.5": resolution: @@ -1980,6 +2110,7 @@ packages: engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [arm64] os: [linux] + libc: [musl] "@img/sharp-linuxmusl-arm64@0.34.5": resolution: @@ -1989,6 +2120,7 @@ packages: engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [arm64] os: [linux] + libc: [musl] "@img/sharp-linuxmusl-x64@0.33.5": resolution: @@ -1998,6 +2130,7 @@ packages: engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [x64] os: [linux] + libc: [musl] "@img/sharp-linuxmusl-x64@0.34.5": resolution: @@ -2007,6 +2140,7 @@ packages: engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [x64] os: [linux] + libc: [musl] "@img/sharp-wasm32@0.33.5": resolution: @@ -2187,6 +2321,7 @@ packages: engines: { node: ">= 10" } cpu: [arm64] os: [linux] + libc: [glibc] "@next/swc-linux-arm64-gnu@16.2.3": resolution: @@ -2196,6 +2331,7 @@ packages: engines: { node: ">= 10" } cpu: [arm64] os: [linux] + libc: [glibc] "@next/swc-linux-arm64-musl@16.1.6": resolution: @@ -2205,6 +2341,7 @@ packages: engines: { node: ">= 10" } cpu: [arm64] os: [linux] + libc: [musl] "@next/swc-linux-arm64-musl@16.2.3": resolution: @@ -2214,6 +2351,7 @@ packages: engines: { node: ">= 10" } cpu: [arm64] os: [linux] + libc: [musl] "@next/swc-linux-x64-gnu@16.1.6": resolution: @@ -2223,6 +2361,7 @@ packages: engines: { node: ">= 10" } cpu: [x64] os: [linux] + libc: [glibc] "@next/swc-linux-x64-gnu@16.2.3": resolution: @@ -2232,6 +2371,7 @@ packages: engines: { node: ">= 10" } cpu: [x64] os: [linux] + libc: [glibc] "@next/swc-linux-x64-musl@16.1.6": resolution: @@ -2241,6 +2381,7 @@ packages: engines: { node: ">= 10" } cpu: [x64] os: [linux] + libc: [musl] "@next/swc-linux-x64-musl@16.2.3": resolution: @@ -2250,6 +2391,7 @@ packages: engines: { node: ">= 10" } cpu: [x64] os: [linux] + libc: [musl] "@next/swc-win32-arm64-msvc@16.1.6": resolution: @@ -2385,6 +2527,7 @@ packages: } cpu: [arm64] os: [linux] + libc: [glibc] "@oxc-resolver/binding-linux-arm64-musl@11.19.1": resolution: @@ -2393,6 +2536,7 @@ packages: } cpu: [arm64] os: [linux] + libc: [musl] "@oxc-resolver/binding-linux-ppc64-gnu@11.19.1": resolution: @@ -2401,6 +2545,7 @@ packages: } cpu: [ppc64] os: [linux] + libc: [glibc] "@oxc-resolver/binding-linux-riscv64-gnu@11.19.1": resolution: @@ -2409,6 +2554,7 @@ packages: } cpu: [riscv64] os: [linux] + libc: [glibc] "@oxc-resolver/binding-linux-riscv64-musl@11.19.1": resolution: @@ -2417,6 +2563,7 @@ packages: } cpu: [riscv64] os: [linux] + libc: [musl] "@oxc-resolver/binding-linux-s390x-gnu@11.19.1": resolution: @@ -2425,6 +2572,7 @@ packages: } cpu: [s390x] os: [linux] + libc: [glibc] "@oxc-resolver/binding-linux-x64-gnu@11.19.1": resolution: @@ -2433,6 +2581,7 @@ packages: } cpu: [x64] os: [linux] + libc: [glibc] "@oxc-resolver/binding-linux-x64-musl@11.19.1": resolution: @@ -2441,6 +2590,7 @@ packages: } cpu: [x64] os: [linux] + libc: [musl] "@oxc-resolver/binding-openharmony-arm64@11.19.1": resolution: @@ -3565,6 +3715,14 @@ packages: typescript: optional: true + "@tailwindcss/typography@0.5.19": + resolution: + { + integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==, + } + peerDependencies: + tailwindcss: ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + "@tanstack/eslint-plugin-query@5.91.4": resolution: { @@ -3699,6 +3857,12 @@ packages: integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==, } + "@types/trusted-types@2.0.7": + resolution: + { + integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==, + } + "@types/uuid@10.0.0": resolution: { @@ -4153,6 +4317,12 @@ packages: zod: optional: true + bidi-js@1.0.3: + resolution: + { + integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==, + } + binary-extensions@2.3.0: resolution: { @@ -4564,6 +4734,13 @@ packages: integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==, } + data-urls@7.0.0: + resolution: + { + integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==, + } + engines: { node: ^20.19.0 || ^22.12.0 || >=24.0.0 } + data-view-buffer@1.0.2: resolution: { @@ -4628,6 +4805,12 @@ packages: supports-color: optional: true + decimal.js@10.6.0: + resolution: + { + integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==, + } + deep-is@0.1.4: resolution: { @@ -4746,6 +4929,12 @@ packages: } engines: { node: ">= 4" } + dompurify@3.4.2: + resolution: + { + integrity: sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA==, + } + domutils@3.2.2: resolution: { @@ -4942,6 +5131,13 @@ packages: } engines: { node: ">=0.12" } + entities@8.0.0: + resolution: + { + integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==, + } + engines: { node: ">=20.19.0" } + env-paths@3.0.0: resolution: { @@ -5810,6 +6006,13 @@ packages: } engines: { node: ^20.17.0 || >=22.9.0 } + html-encoding-sniffer@6.0.0: + resolution: + { + integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==, + } + engines: { node: ^20.19.0 || ^22.12.0 || >=24.0.0 } + html-entities@2.6.0: resolution: { @@ -6085,6 +6288,12 @@ packages: } engines: { node: ">=12" } + is-potential-custom-element-name@1.0.1: + resolution: + { + integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==, + } + is-property@1.0.2: resolution: { @@ -6180,6 +6389,13 @@ packages: integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==, } + isomorphic-dompurify@3.12.0: + resolution: + { + integrity: sha512-8n+j+6ypTHvriJwFOQ2qusQ6bzGjZVcR3jbe1pBpLcGI1dn4WIl0ctLBngqE5QttquQBAlKXwJeTMw+X7x7qKw==, + } + engines: { node: ^20.19.0 || ^22.13.0 || >=24.0.0 } + iterator.prototype@1.1.5: resolution: { @@ -6276,6 +6492,18 @@ packages: } engines: { node: ">=20.0.0" } + jsdom@29.1.1: + resolution: + { + integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==, + } + engines: { node: ^20.19.0 || ^22.13.0 || >=24.0.0 } + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + jsesc@3.1.0: resolution: { @@ -6490,6 +6718,13 @@ packages: } engines: { node: 20 || >=22 } + lru-cache@11.3.6: + resolution: + { + integrity: sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==, + } + engines: { node: 20 || >=22 } + lru-cache@5.1.1: resolution: { @@ -7034,6 +7269,12 @@ packages: integrity: sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==, } + parse5@8.0.1: + resolution: + { + integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==, + } + parseley@0.12.1: resolution: { @@ -7265,6 +7506,13 @@ packages: peerDependencies: postcss: ^8.2.14 + postcss-selector-parser@6.0.10: + resolution: + { + integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==, + } + engines: { node: ">=4" } + postcss-selector-parser@6.1.2: resolution: { @@ -7806,6 +8054,13 @@ packages: integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==, } + saxes@6.0.0: + resolution: + { + integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==, + } + engines: { node: ">=v12.22.7" } + scheduler@0.27.0: resolution: { @@ -8263,6 +8518,12 @@ packages: } engines: { node: ">= 0.4" } + symbol-tree@3.2.4: + resolution: + { + integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==, + } + synckit@0.11.12: resolution: { @@ -8355,6 +8616,19 @@ packages: } engines: { node: ">=12.0.0" } + tldts-core@7.0.30: + resolution: + { + integrity: sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==, + } + + tldts@7.0.30: + resolution: + { + integrity: sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==, + } + hasBin: true + to-regex-range@5.0.1: resolution: { @@ -8375,6 +8649,13 @@ packages: integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==, } + tough-cookie@6.0.1: + resolution: + { + integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==, + } + engines: { node: ">=16" } + tr46@5.1.1: resolution: { @@ -8382,6 +8663,13 @@ packages: } engines: { node: ">=18" } + tr46@6.0.0: + resolution: + { + integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==, + } + engines: { node: ">=20" } + ts-api-utils@2.4.0: resolution: { @@ -8544,6 +8832,13 @@ packages: integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==, } + undici@7.25.0: + resolution: + { + integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==, + } + engines: { node: ">=20.18.1" } + unicorn-magic@0.3.0: resolution: { @@ -8660,6 +8955,13 @@ packages: react: ^16.8 || ^17.0 || ^18.0 react-dom: ^16.8 || ^17.0 || ^18.0 + w3c-xmlserializer@5.0.0: + resolution: + { + integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==, + } + engines: { node: ">=18" } + walk-up-path@4.0.0: resolution: { @@ -8674,6 +8976,20 @@ packages: } engines: { node: ">=12" } + webidl-conversions@8.0.1: + resolution: + { + integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==, + } + engines: { node: ">=20" } + + whatwg-mimetype@5.0.0: + resolution: + { + integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==, + } + engines: { node: ">=20" } + whatwg-url@14.2.0: resolution: { @@ -8681,6 +8997,13 @@ packages: } engines: { node: ">=18" } + whatwg-url@16.0.1: + resolution: + { + integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==, + } + engines: { node: ^20.19.0 || ^22.12.0 || >=24.0.0 } + when-exit@2.1.5: resolution: { @@ -8745,6 +9068,19 @@ packages: utf-8-validate: optional: true + xml-name-validator@5.0.0: + resolution: + { + integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==, + } + engines: { node: ">=18" } + + xmlchars@2.2.0: + resolution: + { + integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==, + } + xtend@4.0.2: resolution: { @@ -8829,6 +9165,26 @@ snapshots: "@alloc/quick-lru@5.2.0": {} + "@asamuzakjp/css-color@5.1.11": + dependencies: + "@asamuzakjp/generational-cache": 1.0.1 + "@csstools/css-calc": 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + "@csstools/css-color-parser": 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + "@csstools/css-parser-algorithms": 4.0.0(@csstools/css-tokenizer@4.0.0) + "@csstools/css-tokenizer": 4.0.0 + + "@asamuzakjp/dom-selector@7.1.1": + dependencies: + "@asamuzakjp/generational-cache": 1.0.1 + "@asamuzakjp/nwsapi": 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + + "@asamuzakjp/generational-cache@1.0.1": {} + + "@asamuzakjp/nwsapi@2.3.9": {} + "@babel/code-frame@7.29.0": dependencies: "@babel/helper-validator-identifier": 7.28.5 @@ -8998,6 +9354,10 @@ snapshots: "@better-fetch/fetch@1.1.21": {} + "@bramus/specificity@2.4.2": + dependencies: + css-tree: 3.2.1 + "@chevrotain/cst-dts-gen@10.5.0": dependencies: "@chevrotain/gast": 10.5.0 @@ -9032,6 +9392,30 @@ snapshots: conventional-commits-parser: 6.4.0 picocolors: 1.1.1 + "@csstools/color-helpers@6.0.2": {} + + "@csstools/css-calc@3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)": + dependencies: + "@csstools/css-parser-algorithms": 4.0.0(@csstools/css-tokenizer@4.0.0) + "@csstools/css-tokenizer": 4.0.0 + + "@csstools/css-color-parser@4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)": + dependencies: + "@csstools/color-helpers": 6.0.2 + "@csstools/css-calc": 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + "@csstools/css-parser-algorithms": 4.0.0(@csstools/css-tokenizer@4.0.0) + "@csstools/css-tokenizer": 4.0.0 + + "@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)": + dependencies: + "@csstools/css-tokenizer": 4.0.0 + + "@csstools/css-syntax-patches-for-csstree@1.1.3(css-tree@3.2.1)": + optionalDependencies: + css-tree: 3.2.1 + + "@csstools/css-tokenizer@4.0.0": {} + "@darraghor/eslint-plugin-nestjs-typed@7.1.27(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(class-validator@0.15.1)(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)": dependencies: "@typescript-eslint/parser": 8.56.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) @@ -9461,6 +9845,10 @@ snapshots: "@eslint/core": 0.17.0 levn: 0.4.1 + "@exodus/bytes@1.15.0(@noble/hashes@2.0.1)": + optionalDependencies: + "@noble/hashes": 2.0.1 + "@floating-ui/core@1.7.5": dependencies: "@floating-ui/utils": 0.2.11 @@ -10662,6 +11050,11 @@ snapshots: optionalDependencies: typescript: 5.9.3 + "@tailwindcss/typography@0.5.19(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2))": + dependencies: + postcss-selector-parser: 6.0.10 + tailwindcss: 3.4.19(tsx@4.21.0)(yaml@2.8.2) + "@tanstack/eslint-plugin-query@5.91.4(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)": dependencies: "@typescript-eslint/utils": 8.56.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) @@ -10737,6 +11130,9 @@ snapshots: dependencies: csstype: 3.2.3 + "@types/trusted-types@2.0.7": + optional: true + "@types/uuid@10.0.0": {} "@types/validator@13.15.10": {} @@ -11046,6 +11442,10 @@ snapshots: optionalDependencies: zod: 4.3.6 + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + binary-extensions@2.3.0: {} brace-expansion@1.1.12: @@ -11294,6 +11694,13 @@ snapshots: damerau-levenshtein@1.0.8: {} + data-urls@7.0.0(@noble/hashes@2.0.1): + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1(@noble/hashes@2.0.1) + transitivePeerDependencies: + - "@noble/hashes" + data-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 @@ -11328,6 +11735,8 @@ snapshots: dependencies: ms: 2.1.3 + decimal.js@10.6.0: {} + deep-is@0.1.4: {} deepmerge-ts@7.1.5: {} @@ -11380,6 +11789,10 @@ snapshots: dependencies: domelementtype: 2.3.0 + dompurify@3.4.2: + optionalDependencies: + "@types/trusted-types": 2.0.7 + domutils@3.2.2: dependencies: dom-serializer: 2.0.0 @@ -11462,6 +11875,8 @@ snapshots: entities@4.5.0: {} + entities@8.0.0: {} + env-paths@3.0.0: {} error-stack-parser@2.1.4: @@ -12235,6 +12650,12 @@ snapshots: dependencies: lru-cache: 11.2.6 + html-encoding-sniffer@6.0.0(@noble/hashes@2.0.1): + dependencies: + "@exodus/bytes": 1.15.0(@noble/hashes@2.0.1) + transitivePeerDependencies: + - "@noble/hashes" + html-entities@2.6.0: {} html-to-image@1.11.13: {} @@ -12393,6 +12814,8 @@ snapshots: is-plain-obj@4.1.0: {} + is-potential-custom-element-name@1.0.1: {} + is-property@1.0.2: {} is-regex@1.2.1: @@ -12442,6 +12865,14 @@ snapshots: isexe@2.0.0: {} + isomorphic-dompurify@3.12.0(@noble/hashes@2.0.1): + dependencies: + dompurify: 3.4.2 + jsdom: 29.1.1(@noble/hashes@2.0.1) + transitivePeerDependencies: + - "@noble/hashes" + - canvas + iterator.prototype@1.1.5: dependencies: define-data-property: 1.1.4 @@ -12482,6 +12913,32 @@ snapshots: jsdoc-type-pratt-parser@7.1.1: {} + jsdom@29.1.1(@noble/hashes@2.0.1): + dependencies: + "@asamuzakjp/css-color": 5.1.11 + "@asamuzakjp/dom-selector": 7.1.1 + "@bramus/specificity": 2.4.2 + "@csstools/css-syntax-patches-for-csstree": 1.1.3(css-tree@3.2.1) + "@exodus/bytes": 1.15.0(@noble/hashes@2.0.1) + css-tree: 3.2.1 + data-urls: 7.0.0(@noble/hashes@2.0.1) + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0(@noble/hashes@2.0.1) + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.3.6 + parse5: 8.0.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.1 + undici: 7.25.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1(@noble/hashes@2.0.1) + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - "@noble/hashes" + jsesc@3.1.0: {} json-buffer@3.0.1: {} @@ -12587,6 +13044,8 @@ snapshots: lru-cache@11.2.6: {} + lru-cache@11.3.6: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -12924,6 +13383,10 @@ snapshots: parse-statements@1.0.11: {} + parse5@8.0.1: + dependencies: + entities: 8.0.0 + parseley@0.12.1: dependencies: leac: 0.6.0 @@ -13037,6 +13500,11 @@ snapshots: postcss: 8.5.8 postcss-selector-parser: 6.1.2 + postcss-selector-parser@6.0.10: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + postcss-selector-parser@6.1.2: dependencies: cssesc: 3.0.0 @@ -13361,6 +13829,10 @@ snapshots: safer-buffer@2.1.2: {} + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + scheduler@0.27.0: {} screenfull@5.2.0: {} @@ -13707,6 +14179,8 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + symbol-tree@3.2.4: {} + synckit@0.11.12: dependencies: "@pkgr/core": 0.2.9 @@ -13772,6 +14246,12 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tldts-core@7.0.30: {} + + tldts@7.0.30: + dependencies: + tldts-core: 7.0.30 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -13783,10 +14263,18 @@ snapshots: toggle-selection@1.0.6: {} + tough-cookie@6.0.1: + dependencies: + tldts: 7.0.30 + tr46@5.1.1: dependencies: punycode: 2.3.1 + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + ts-api-utils@2.4.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -13896,6 +14384,8 @@ snapshots: undici-types@6.21.0: {} + undici@7.25.0: {} + unicorn-magic@0.3.0: {} unicorn-magic@0.4.0: {} @@ -13955,15 +14445,31 @@ snapshots: - "@types/react" - "@types/react-dom" + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + walk-up-path@4.0.0: {} webidl-conversions@7.0.0: {} + webidl-conversions@8.0.1: {} + + whatwg-mimetype@5.0.0: {} + whatwg-url@14.2.0: dependencies: tr46: 5.1.1 webidl-conversions: 7.0.0 + whatwg-url@16.0.1(@noble/hashes@2.0.1): + dependencies: + "@exodus/bytes": 1.15.0(@noble/hashes@2.0.1) + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - "@noble/hashes" + when-exit@2.1.5: {} which-boxed-primitive@1.1.1: @@ -14015,6 +14521,10 @@ snapshots: ws@8.18.3: {} + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + xtend@4.0.2: {} yallist@3.1.1: {} diff --git a/src/app/(homepage)/page.tsx b/src/app/(homepage)/page.tsx index 9ef822d..8c05cfc 100644 --- a/src/app/(homepage)/page.tsx +++ b/src/app/(homepage)/page.tsx @@ -4,6 +4,7 @@ import Link from "next/link"; import { Suspense } from "react"; import SolvroLogo from "@/../public/assets/logo/logo_solvro_color.png"; +import { Alerts } from "@/components/alerts"; import { Icons } from "@/components/icons"; import { AnimatedGradientText } from "@/components/magicui/animated-text"; import { BorderBeam } from "@/components/magicui/border-beam"; @@ -111,8 +112,14 @@ export default function Home() {
+
+ +
diff --git a/src/app/plans/edit/[id]/_components/app-sidebar.tsx b/src/app/plans/edit/[id]/_components/app-sidebar.tsx index 9a0a09a..cef4704 100644 --- a/src/app/plans/edit/[id]/_components/app-sidebar.tsx +++ b/src/app/plans/edit/[id]/_components/app-sidebar.tsx @@ -1,12 +1,13 @@ "use client"; -import { useQuery } from "@tanstack/react-query"; import type { UseMutationResult } from "@tanstack/react-query"; +import { useQuery } from "@tanstack/react-query"; import { isEqual } from "date-fns"; import { format } from "date-fns/format"; import React from "react"; import { getFaculties } from "@/actions/get-faculties"; +import { Alerts } from "@/components/alerts"; import { AlgorithmDialog } from "@/components/algo-dialog"; import { GroupsAccordionItem } from "@/components/groups-accordion"; import { PlanDisplayLink } from "@/components/plan-display-link"; @@ -277,6 +278,7 @@ export function AppSidebar({
+ ); } diff --git a/src/app/plans/preview/[id]/page.client.tsx b/src/app/plans/preview/[id]/page.client.tsx index 3a0fc4d..57f6b86 100644 --- a/src/app/plans/preview/[id]/page.client.tsx +++ b/src/app/plans/preview/[id]/page.client.tsx @@ -8,6 +8,7 @@ import { v4 as uuidv4 } from "uuid"; import { planFamily } from "@/atoms/plan-family"; import { plansIds } from "@/atoms/plans-ids"; +import { Alerts } from "@/components/alerts"; import { ClassSchedule } from "@/components/class-schedule"; import { Icons } from "@/components/icons"; import { Button } from "@/components/ui/button"; @@ -47,7 +48,8 @@ export function SharePlanPage({ plan }: { plan: SharedPlan["plan"] }) { }; return ( -
+
+

{plan.name}

diff --git a/src/components/alerts.tsx b/src/components/alerts.tsx new file mode 100644 index 0000000..82bd41a --- /dev/null +++ b/src/components/alerts.tsx @@ -0,0 +1,349 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { sanitize } from "isomorphic-dompurify"; +import { AlertCircle, AlertTriangle, Info, X } from "lucide-react"; +import { useEffect, useState } from "react"; + +import { env } from "@/env.mjs"; +import { cn } from "@/lib/utils"; + +const ALERTS_ENDPOINT = "https://alerts.solvro.pl/api/v1/alerts/"; +const STORAGE_KEY = "solvro-alerts-dismissed"; +const STALE_TIME_MS = 60 * 1000; + +const ALLOWED_TAGS = [ + "a", + "b", + "blockquote", + "br", + "code", + "del", + "div", + "em", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "hr", + "i", + "li", + "ol", + "p", + "pre", + "s", + "span", + "strong", + "sub", + "sup", + "u", + "ul", +]; +const ALLOWED_ATTR = ["href", "title", "target"]; + +type AlertType = "info" | "warning" | "critical"; + +interface Alert { + id: string; + title: string; + content: string; + alert_type: AlertType; + link: string; + open_in_new_tab: boolean; + is_global: boolean; + is_dismissable: boolean; + start_at: string | null; + end_at: string | null; +} + +interface AlertsProps { + className?: string; + variant?: "banner" | "pill"; +} + +function readDismissed(): string[] { + if (typeof window === "undefined") { + return []; + } + try { + const raw = window.localStorage.getItem(STORAGE_KEY); + if (raw === null) { + return []; + } + const parsed = JSON.parse(raw) as unknown; + if (!Array.isArray(parsed)) { + return []; + } + return parsed.filter((value): value is string => typeof value === "string"); + } catch { + return []; + } +} + +function writeDismissed(ids: string[]) { + if (typeof window === "undefined") { + return; + } + try { + window.localStorage.setItem(STORAGE_KEY, JSON.stringify(ids)); + } catch { + // ignore storage errors (private mode, quota, etc.) + } +} + +async function fetchAlerts(appCode: string): Promise { + const url = new URL(ALERTS_ENDPOINT); + url.searchParams.set("app", appCode); + const response = await fetch(url.toString()); + if (response.status === 400) { + throw new Error( + `Solvro Alerts: unknown app code "${appCode}". Check NEXT_PUBLIC_ALERTS_APP_CODE.`, + ); + } + if (!response.ok) { + throw new Error( + `Solvro Alerts: request failed with ${String(response.status)}`, + ); + } + const data = (await response.json()) as Alert[]; + return Array.isArray(data) ? data : []; +} + +const VARIANT_STYLES: Record< + AlertType, + { + container: string; + pillContainer: string; + pillText: string; + pillSeparator: string; + icon: typeof Info; + iconClass: string; + } +> = { + info: { + container: + "border-blue-300 bg-blue-50 text-blue-900 dark:border-blue-800 dark:bg-blue-950/60 dark:text-blue-100", + pillContainer: + "border-blue-400/60 bg-blue-50/30 dark:border-blue-400/50 dark:bg-blue-950/20 backdrop-blur-md", + pillText: "text-blue-900 dark:text-blue-100", + pillSeparator: "bg-blue-400/40", + icon: Info, + iconClass: "text-blue-600 dark:text-blue-300", + }, + warning: { + container: + "border-amber-300 bg-amber-50 text-amber-900 dark:border-amber-800 dark:bg-amber-950/60 dark:text-amber-100", + pillContainer: + "border-amber-400/60 bg-amber-50/30 dark:border-amber-400/40 dark:bg-amber-950/20 backdrop-blur-md", + pillText: "text-amber-900 dark:text-amber-100", + pillSeparator: "bg-amber-400/40", + icon: AlertTriangle, + iconClass: "text-amber-600 dark:text-amber-300", + }, + critical: { + container: + "border-red-300 bg-red-50 text-red-900 dark:border-red-800 dark:bg-red-950/60 dark:text-red-100", + pillContainer: + "border-red-400/60 bg-red-50/30 dark:border-red-400/50 dark:bg-red-950/20 backdrop-blur-md", + pillText: "text-red-900 dark:text-red-100", + pillSeparator: "bg-red-400/50", + icon: AlertCircle, + iconClass: "text-red-600 dark:text-red-300", + }, +}; + +export function Alerts({ className, variant = "banner" }: AlertsProps = {}) { + const appCode = env.NEXT_PUBLIC_ALERTS_APP_CODE; + const [dismissed, setDismissed] = useState([]); + const [hydrated, setHydrated] = useState(false); + + useEffect(() => { + // eslint-disable-next-line react-you-might-not-need-an-effect/no-initialize-state + setDismissed(readDismissed()); + // eslint-disable-next-line react-you-might-not-need-an-effect/no-initialize-state + setHydrated(true); + }, []); + + const { data } = useQuery({ + queryKey: ["solvro-alerts", appCode], + queryFn: async () => fetchAlerts(appCode), + staleTime: STALE_TIME_MS, + gcTime: STALE_TIME_MS, + refetchOnWindowFocus: false, + }); + + if (!hydrated || data === undefined) { + return null; + } + + const dismissedSet = new Set(dismissed); + const visible = data.filter((alert) => !dismissedSet.has(alert.id)); + + if (visible.length === 0) { + return null; + } + + const handleDismiss = (id: string) => { + setDismissed((previous) => { + if (previous.includes(id)) { + return previous; + } + const next = [...previous, id]; + writeDismissed(next); + return next; + }); + }; + + return ( +
+ {visible.map((alert) => { + const styles = VARIANT_STYLES[alert.alert_type]; + const Icon = styles.icon; + const hasLink = alert.link !== ""; + + if (variant === "pill") { + const sanitizedPill = sanitize(alert.content, { + ALLOWED_TAGS, + ALLOWED_ATTR, + }); + const hasTitle = alert.title.trim() !== ""; + const hasContent = sanitizedPill.trim() !== ""; + + const pillInner = ( +
+