diff --git a/package.json b/package.json index 027c86b3..aa509bee 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "bcryptjs": "^3.0.2", "croner": "^10.0.1", "hash-wasm": "^4.12.0", + "jszip": "^3.10.1", "lucide-react": "^1.23.0", "next": "^16.2.10", "next-auth": "5.0.0-beta.31", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 72d3889b..c906643f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: hash-wasm: specifier: ^4.12.0 version: 4.12.0 + jszip: + specifier: ^3.10.1 + version: 3.10.1 lucide-react: specifier: ^1.23.0 version: 1.23.0(react@19.2.7) @@ -1697,6 +1700,9 @@ packages: confbox@0.2.4: resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + croner@10.0.1: resolution: {integrity: sha512-ixNtAJndqh173VQ4KodSdJEI6nuioBWI0V1ITNKhZZsO0pEMoDxz539T4FTTbSZ/xIOSuDnzxLVRqBVSvPNE2g==} engines: {node: '>=18.0'} @@ -2003,6 +2009,12 @@ packages: icu-minify@4.13.1: resolution: {integrity: sha512-nFYW2im0WJ3RUVZwabd71J8QTZRtkK1xxZBY7klg7a6KS/os17LZSj9q1VhbRSnk3S8Mv2I7F1izr/aEJ6cUsw==} + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + intl-messageformat@11.2.9: resolution: {integrity: sha512-cGzymZerpDhVXRKjKLgXKda9gI29TU2o88L7gwNMHp3WZVxA/0c5tX52udXbW9JklDApolvMXZG6Dhhdz5eirA==} @@ -2021,6 +2033,9 @@ packages: is-property@1.0.2: resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -2038,9 +2053,15 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + launder@1.7.1: resolution: {integrity: sha512-mU6WRz5EusL9ZZuiZ5SO4Y6C0P9PAUR9iwdb6bzj4KDihm28DiHFw+/yk9DBH4f+Pv1wuzQ4e2jV3oQ7mkIqvw==} + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + lightningcss-android-arm64@1.32.0: resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} engines: {node: '>= 12.0.0'} @@ -2236,6 +2257,9 @@ packages: otplib@12.0.1: resolution: {integrity: sha512-xDGvUOQjop7RDgxTQ+o4pOol0/3xSZzawTiPKRrHnQWAy0WjhNs/5HdIDJCrqC4MBynmjXgULc6YfioaxZeFgg==} + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + parse-srcset@1.0.2: resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==} @@ -2310,6 +2334,9 @@ packages: typescript: optional: true + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + proper-lockfile@4.1.2: resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} @@ -2328,6 +2355,9 @@ packages: resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} engines: {node: '>=0.10.0'} + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readdirp@5.0.0: resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} engines: {node: '>= 20.19.0'} @@ -2351,6 +2381,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -2368,6 +2401,9 @@ packages: seq-queue@0.0.5: resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + sharp@0.34.5: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -2419,6 +2455,9 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + styled-jsx@5.1.6: resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} engines: {node: '>= 12.0.0'} @@ -3657,6 +3696,8 @@ snapshots: confbox@0.2.4: {} + core-util-is@1.0.3: {} + croner@10.0.1: {} cross-spawn@7.0.6: @@ -3938,6 +3979,10 @@ snapshots: dependencies: '@formatjs/icu-messageformat-parser': 3.5.12 + immediate@3.0.6: {} + + inherits@2.0.4: {} + intl-messageformat@11.2.9: dependencies: '@formatjs/fast-memoize': 3.1.6 @@ -3953,6 +3998,8 @@ snapshots: is-property@1.0.2: {} + isarray@1.0.0: {} + isexe@2.0.0: {} isexe@3.1.5: {} @@ -3963,10 +4010,21 @@ snapshots: json-schema-traverse@1.0.0: {} + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + launder@1.7.1: dependencies: dayjs: 1.11.21 + lie@3.3.0: + dependencies: + immediate: 3.0.6 + lightningcss-android-arm64@1.32.0: optional: true @@ -4141,6 +4199,8 @@ snapshots: '@otplib/preset-default': 12.0.1 '@otplib/preset-v11': 12.0.1 + pako@1.0.11: {} + parse-srcset@1.0.2: {} path-key@3.1.1: {} @@ -4213,6 +4273,8 @@ snapshots: - react - react-dom + process-nextick-args@2.0.1: {} + proper-lockfile@4.1.2: dependencies: graceful-fs: 4.2.11 @@ -4233,6 +4295,16 @@ snapshots: react@19.2.7: {} + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + readdirp@5.0.0: {} remeda@2.33.4: {} @@ -4274,6 +4346,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.62.2 fsevents: 2.3.3 + safe-buffer@5.1.2: {} + safer-buffer@2.1.2: {} sanitize-html@2.17.5: @@ -4292,6 +4366,8 @@ snapshots: seq-queue@0.0.5: {} + setimmediate@1.0.5: {} + sharp@0.34.5: dependencies: '@img/colour': 1.1.0 @@ -4355,6 +4431,10 @@ snapshots: std-env@3.10.0: {} + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + styled-jsx@5.1.6(react@19.2.7): dependencies: client-only: 0.0.1 diff --git a/public/assets/fonts/cache/habbo-atom.gif b/public/assets/fonts/cache/habbo-atom.gif new file mode 100644 index 00000000..fd83c084 Binary files /dev/null and b/public/assets/fonts/cache/habbo-atom.gif differ diff --git a/src/components/public/logo-generator.tsx b/src/components/public/logo-generator.tsx index ffd2e6ee..7b175867 100644 --- a/src/components/public/logo-generator.tsx +++ b/src/components/public/logo-generator.tsx @@ -52,6 +52,47 @@ export default function LogoGenerator() { }); }, [gifUrl, startTransition]); + const [zipping, setZipping] = useState(false); + const allFonts = useMemo( + () => HABBO_FONT_GROUPS.flatMap((g) => g.fonts), + [], + ); + + const handleDownloadAll = useCallback(async () => { + if (zipping) return; + setZipping(true); + try { + const JSZip = (await import("jszip")).default; + const zip = new JSZip(); + + const folder = zip.folder(safeText.replace(/[^a-z0-9]+/gi, "_") || "logo")!; + const results = await Promise.allSettled( + allFonts.map(async (font) => { + const url = buildFontUrl(font.styleName, safeText); + const res = await fetch(url); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const blob = await res.blob(); + folder.file(`${font.styleName}.gif`, blob); + }), + ); + + const failed = results.filter((r) => r.status === "rejected").length; + const content = await zip.generateAsync({ type: "blob" }); + const a = document.createElement("a"); + a.href = URL.createObjectURL(content); + a.download = `${safeText.replace(/[^a-z0-9]+/gi, "_") || "logo"}-all-fonts.zip`; + a.click(); + URL.revokeObjectURL(a.href); + if (failed > 0) { + alert(`${allFonts.length - failed}/${allFonts.length} fonts downloaded. ${failed} failed.`); + } + } catch { + alert("Failed to create ZIP."); + } finally { + setZipping(false); + } + }, [safeText, allFonts, zipping]); + return (
@@ -122,6 +163,14 @@ export default function LogoGenerator() { ? "❌ Error" : "💾 Save as site logo"} +
@@ -146,11 +195,11 @@ export default function LogoGenerator() { />

- Generated via{" "} + Fonts rendered via{" "} habbofont.net - . The GIF is loaded from their server. + {" "}and served locally.