diff --git a/bun.lock b/bun.lock index 135ba6f..6aa4b3c 100644 --- a/bun.lock +++ b/bun.lock @@ -34,6 +34,7 @@ "tailwind-merge": "^3.3.1", "tailwindcss-animate": "^1.0.7", "usehooks-ts": "^3.1.1", + "workers-og": "0.0.27", }, "devDependencies": { "@graphql-codegen/cli": "^5.0.7", @@ -537,6 +538,8 @@ "@repeaterjs/repeater": ["@repeaterjs/repeater@3.0.6", "", {}, "sha512-Javneu5lsuhwNCryN+pXH93VPQ8g0dBX7wItHFgYiwQmzE1sVdg5tWHiOgHywzL2W21XQopa7IwIEnNbmeUJYA=="], + "@resvg/resvg-wasm": ["@resvg/resvg-wasm@2.4.0", "", {}, "sha512-C7c51Nn4yTxXFKvgh2txJFNweaVcfUPQxwEUFw4aWsCmfiBDJsTSwviIF8EcwjQ6k8bPyMWCl1vw4BdxE569Cg=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], "@rollup/plugin-inject": ["@rollup/plugin-inject@5.0.5", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "estree-walker": "^2.0.2", "magic-string": "^0.30.3" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg=="], @@ -595,6 +598,8 @@ "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], + "@shuding/opentype.js": ["@shuding/opentype.js@1.4.0-beta.0", "", { "dependencies": { "fflate": "^0.7.3", "string.prototype.codepointat": "^0.2.1" }, "bin": { "ot": "bin/ot" } }, "sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA=="], + "@sideway/address": ["@sideway/address@4.1.5", "", { "dependencies": { "@hapi/hoek": "^9.0.0" } }, "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q=="], "@sideway/formula": ["@sideway/formula@3.0.1", "", {}, "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg=="], @@ -989,6 +994,8 @@ "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], + "camelize": ["camelize@1.0.1", "", {}, "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ=="], + "caniuse-api": ["caniuse-api@3.0.0", "", { "dependencies": { "browserslist": "^4.0.0", "caniuse-lite": "^1.0.0", "lodash.memoize": "^4.1.2", "lodash.uniq": "^4.5.0" } }, "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw=="], "caniuse-lite": ["caniuse-lite@1.0.30001793", "", {}, "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA=="], @@ -1099,10 +1106,20 @@ "crypto-browserify": ["crypto-browserify@3.12.1", "", { "dependencies": { "browserify-cipher": "^1.0.1", "browserify-sign": "^4.2.3", "create-ecdh": "^4.0.4", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", "diffie-hellman": "^5.0.3", "hash-base": "~3.0.4", "inherits": "^2.0.4", "pbkdf2": "^3.1.2", "public-encrypt": "^4.0.3", "randombytes": "^2.1.0", "randomfill": "^1.0.4" } }, "sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ=="], + "css-background-parser": ["css-background-parser@0.1.0", "", {}, "sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA=="], + + "css-box-shadow": ["css-box-shadow@1.0.0-3", "", {}, "sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg=="], + + "css-color-keywords": ["css-color-keywords@1.0.0", "", {}, "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg=="], + "css-declaration-sorter": ["css-declaration-sorter@7.4.0", "", { "peerDependencies": { "postcss": "^8.0.9" } }, "sha512-LTuzjPoyA2vMGKKcaOqKSp7Ub2eGrNfKiZH4LpezxpNrsICGCSFvsQOI29psISxNZtaXibkC2CXzrQ5enMeGGw=="], + "css-gradient-parser": ["css-gradient-parser@0.0.16", "", {}, "sha512-3O5QdqgFRUbXvK1x5INf1YkBz1UKSWqrd63vWsum8MNHDBYD5urm3QtxZbKU259OrEXNM26lP/MPY3d1IGkBgA=="], + "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], + "css-to-react-native": ["css-to-react-native@3.2.0", "", { "dependencies": { "camelize": "^1.0.0", "css-color-keywords": "^1.0.0", "postcss-value-parser": "^4.0.2" } }, "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ=="], + "css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="], "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], @@ -1233,6 +1250,8 @@ "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + "emoji-regex-xs": ["emoji-regex-xs@2.0.1", "", {}, "sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g=="], + "enhanced-resolve": ["enhanced-resolve@5.22.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww=="], "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], @@ -1265,6 +1284,8 @@ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], "escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="], @@ -1369,6 +1390,8 @@ "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + "fflate": ["fflate@0.7.5", "", {}, "sha512-QieYf//cis6ywHNi5qW1+PXPQ4bC+XVJAtS4AXIML8P76GroEiOxm/oQtn1f02UkJY1+KsXMJcC+R2v/Eg4G3g=="], + "figures": ["figures@3.2.0", "", { "dependencies": { "escape-string-regexp": "^1.0.5" } }, "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg=="], "file-entry-cache": ["file-entry-cache@6.0.1", "", { "dependencies": { "flat-cache": "^3.0.4" } }, "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg=="], @@ -1495,6 +1518,8 @@ "headers-polyfill": ["headers-polyfill@4.0.3", "", {}, "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ=="], + "hex-rgb": ["hex-rgb@4.3.0", "", {}, "sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw=="], + "hmac-drbg": ["hmac-drbg@1.0.1", "", { "dependencies": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", "minimalistic-crypto-utils": "^1.0.1" } }, "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg=="], "hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="], @@ -1773,6 +1798,8 @@ "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], + "just-camel-case": ["just-camel-case@6.2.0", "", {}, "sha512-ICenRLXwkQYLk3UyvLQZ+uKuwFVJ3JHFYFn7F2782G2Mv2hW8WPePqgdhpnjGaqkYtSVWnyCESZhGXUmY3/bEg=="], + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], @@ -1789,6 +1816,8 @@ "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + "linebreak": ["linebreak@1.1.0", "", { "dependencies": { "base64-js": "0.0.8", "unicode-trie": "^2.0.0" } }, "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ=="], + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], "lint-staged": ["lint-staged@15.5.2", "", { "dependencies": { "chalk": "^5.4.1", "commander": "^13.1.0", "debug": "^4.4.0", "execa": "^8.0.1", "lilconfig": "^3.1.3", "listr2": "^8.2.5", "micromatch": "^4.0.8", "pidtree": "^0.6.0", "string-argv": "^0.3.2", "yaml": "^2.7.0" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-YUSOLq9VeRNAo/CTaVmhGDKG+LBtA8KF1X4K5+ykMSwWST1vDxJRB2kv2COgLb1fvpCo+A/y9A0G0znNVmdx4w=="], @@ -2003,6 +2032,8 @@ "parse-asn1": ["parse-asn1@5.1.9", "", { "dependencies": { "asn1.js": "^4.10.1", "browserify-aes": "^1.2.0", "evp_bytestokey": "^1.0.3", "pbkdf2": "^3.1.5", "safe-buffer": "^5.2.1" } }, "sha512-fIYNuZ/HastSb80baGOuPRo1O9cf4baWw5WsAp7dBuUzeTD/BoaG8sVTdlPFksBE2lF21dN+A1AnrpIjSWqHHg=="], + "parse-css-color": ["parse-css-color@0.2.1", "", { "dependencies": { "color-name": "^1.1.4", "hex-rgb": "^4.1.0" } }, "sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg=="], + "parse-filepath": ["parse-filepath@1.0.2", "", { "dependencies": { "is-absolute": "^1.0.0", "map-cache": "^0.2.0", "path-root": "^0.1.1" } }, "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q=="], "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], @@ -2273,6 +2304,8 @@ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "satori": ["satori@0.15.2", "", { "dependencies": { "@shuding/opentype.js": "1.4.0-beta.0", "css-background-parser": "^0.1.0", "css-box-shadow": "1.0.0-3", "css-gradient-parser": "^0.0.16", "css-to-react-native": "^3.0.0", "emoji-regex-xs": "^2.0.1", "escape-html": "^1.0.3", "linebreak": "^1.1.0", "parse-css-color": "^0.2.1", "postcss-value-parser": "^4.2.0", "yoga-wasm-web": "^0.3.3" } }, "sha512-vu/49vdc8MzV5jUchs3TIRDCOkOvMc1iJ11MrZvhg9tE4ziKIEIBjBZvies6a9sfM2vQ2gc3dXeu6rCK7AztHA=="], + "sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="], "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], @@ -2383,6 +2416,8 @@ "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "string.prototype.codepointat": ["string.prototype.codepointat@0.2.1", "", {}, "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg=="], + "string.prototype.includes": ["string.prototype.includes@2.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3" } }, "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg=="], "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="], @@ -2455,6 +2490,8 @@ "timers-browserify": ["timers-browserify@2.0.12", "", { "dependencies": { "setimmediate": "^1.0.4" } }, "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ=="], + "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="], + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], "tiny-warning": ["tiny-warning@1.0.3", "", {}, "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="], @@ -2525,6 +2562,8 @@ "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "unicode-trie": ["unicode-trie@2.0.0", "", { "dependencies": { "pako": "^0.2.5", "tiny-inflate": "^1.0.0" } }, "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ=="], + "unicorn-magic": ["unicorn-magic@0.1.0", "", {}, "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ=="], "union": ["union@0.5.0", "", { "dependencies": { "qs": "^6.4.0" } }, "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA=="], @@ -2611,6 +2650,8 @@ "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + "workers-og": ["workers-og@0.0.27", "", { "dependencies": { "@resvg/resvg-wasm": "2.4.0", "just-camel-case": "^6.2.0", "satori": "^0.15.2", "yoga-wasm-web": "0.3.3" } }, "sha512-QvwptQ0twmouQHiITUi3kYxEPCLdueC/U4msQ2xMz2iktd+iseSs7zlREw3T1dAsPxPw73FQlw8cXFsfANZPlw=="], + "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], @@ -2645,6 +2686,8 @@ "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="], + "yoga-wasm-web": ["yoga-wasm-web@0.3.3", "", {}, "sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA=="], + "zen-observable": ["zen-observable@0.8.15", "", {}, "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ=="], "zen-observable-ts": ["zen-observable-ts@1.2.5", "", { "dependencies": { "zen-observable": "0.8.15" } }, "sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg=="], @@ -2963,6 +3006,8 @@ "jsdom/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], + "linebreak/base64-js": ["base64-js@0.0.8", "", {}, "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw=="], + "lint-staged/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], "lint-staged/listr2": ["listr2@8.3.3", "", { "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ=="], @@ -3015,6 +3060,8 @@ "parse-asn1/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "parse-css-color/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "pbkdf2/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], @@ -3099,6 +3146,8 @@ "tsx/esbuild": ["esbuild@0.28.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.0", "@esbuild/android-arm": "0.28.0", "@esbuild/android-arm64": "0.28.0", "@esbuild/android-x64": "0.28.0", "@esbuild/darwin-arm64": "0.28.0", "@esbuild/darwin-x64": "0.28.0", "@esbuild/freebsd-arm64": "0.28.0", "@esbuild/freebsd-x64": "0.28.0", "@esbuild/linux-arm": "0.28.0", "@esbuild/linux-arm64": "0.28.0", "@esbuild/linux-ia32": "0.28.0", "@esbuild/linux-loong64": "0.28.0", "@esbuild/linux-mips64el": "0.28.0", "@esbuild/linux-ppc64": "0.28.0", "@esbuild/linux-riscv64": "0.28.0", "@esbuild/linux-s390x": "0.28.0", "@esbuild/linux-x64": "0.28.0", "@esbuild/netbsd-arm64": "0.28.0", "@esbuild/netbsd-x64": "0.28.0", "@esbuild/openbsd-arm64": "0.28.0", "@esbuild/openbsd-x64": "0.28.0", "@esbuild/openharmony-arm64": "0.28.0", "@esbuild/sunos-x64": "0.28.0", "@esbuild/win32-arm64": "0.28.0", "@esbuild/win32-ia32": "0.28.0", "@esbuild/win32-x64": "0.28.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw=="], + "unicode-trie/pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="], + "unixify/normalize-path": ["normalize-path@2.1.1", "", { "dependencies": { "remove-trailing-separator": "^1.0.1" } }, "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w=="], "uri-js/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], diff --git a/functions/api/og.ts b/functions/api/og.ts new file mode 100644 index 0000000..47da21f --- /dev/null +++ b/functions/api/og.ts @@ -0,0 +1,73 @@ +import { generateOgImageResponse } from '../../src/lib/generate-og-response'; +import { + isOgImageFresh, + OBSERVED_AT_HEADER, + OG_RETENTION_SECONDS, + ogBrowserMaxAgeSeconds, + qtcMarketQuote, + TRACKER_URL +} from '../../src/lib/qtc-market'; + +function ogCacheRequest(pageUrl: string): Request { + const url = new URL(pageUrl); + url.pathname = '/api/og'; + url.search = ''; + url.hash = ''; + return new Request(url.toString(), { method: 'GET' }); +} + +function browserImage( + response: Response, + options: { stale: boolean; now: number } +): Response { + const observedAt = response.headers.get(OBSERVED_AT_HEADER) ?? ''; + const headers = new Headers(response.headers); + headers.set( + 'Cache-Control', + `public, max-age=${ogBrowserMaxAgeSeconds(observedAt, options.now, options.stale)}` + ); + return new Response(response.body, { + status: response.status, + headers + }); +} + +export async function onRequestGet(context: { + request: Request; + waitUntil: (promise: Promise) => void; +}): Promise { + const now = Date.now(); + const cacheKey = ogCacheRequest(context.request.url); + const cached = await caches.default.match(cacheKey); + + if (cached && isOgImageFresh(cached.headers.get(OBSERVED_AT_HEADER), now)) { + return browserImage(cached, { stale: false, now }); + } + + try { + const trackerResponse = await fetch(TRACKER_URL); + if (!trackerResponse.ok) { + throw new Error(`Tracker request failed: ${trackerResponse.status}`); + } + const quote = qtcMarketQuote(await trackerResponse.json()); + const image = await generateOgImageResponse(context.request.url, quote); + const headers = new Headers(image.headers); + headers.set('Content-Type', 'image/png'); + headers.set(OBSERVED_AT_HEADER, quote.updatedAt); + headers.set('Cache-Control', `public, max-age=${OG_RETENTION_SECONDS}`); + const stored = new Response(image.body, { + status: image.status, + headers + }); + context.waitUntil(caches.default.put(cacheKey, stored.clone())); + return browserImage(stored, { stale: false, now }); + } catch (error) { + if (cached) return browserImage(cached, { stale: true, now }); + const message = + error instanceof Error ? error.message : 'Failed to generate OG image'; + return new Response(message, { + status: 500, + headers: { 'Cache-Control': 'no-store' } + }); + } +} diff --git a/index.html b/index.html index c2aa2b9..c4d635d 100644 --- a/index.html +++ b/index.html @@ -5,6 +5,29 @@ + Quantus Explorer + + + + + + + + + + + + + diff --git a/package.json b/package.json index 1acf9ae..7f2cd98 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,8 @@ "sonner": "^2.0.5", "tailwind-merge": "^3.3.1", "tailwindcss-animate": "^1.0.7", - "usehooks-ts": "^3.1.1" + "usehooks-ts": "^3.1.1", + "workers-og": "0.0.27" }, "devDependencies": { "@graphql-codegen/cli": "^5.0.7", diff --git a/public/fonts/geist-latin-400-normal.woff b/public/fonts/geist-latin-400-normal.woff new file mode 100644 index 0000000..6fc3b7a Binary files /dev/null and b/public/fonts/geist-latin-400-normal.woff differ diff --git a/public/fonts/geist-latin-500-normal.woff b/public/fonts/geist-latin-500-normal.woff new file mode 100644 index 0000000..e96fcc0 Binary files /dev/null and b/public/fonts/geist-latin-500-normal.woff differ diff --git a/public/fonts/geist-mono-latin-500-normal.woff b/public/fonts/geist-mono-latin-500-normal.woff new file mode 100644 index 0000000..4c25f5c Binary files /dev/null and b/public/fonts/geist-mono-latin-500-normal.woff differ diff --git a/src/__generated__/gql.ts b/src/__generated__/gql.ts index f8dbaa4..7337e4b 100644 --- a/src/__generated__/gql.ts +++ b/src/__generated__/gql.ts @@ -83,7 +83,7 @@ type Documents = { '\n query GetVestingScheduleStats {\n meta: vesting_schedule_aggregate {\n aggregate {\n count\n sum {\n total\n claimed\n }\n }\n }\n }\n': typeof types.GetVestingScheduleStatsDocument; '\n query GetVestingScheduleChart($limit: Int!) {\n schedules: vesting_schedule(limit: $limit) {\n id\n start\n cliff\n end\n total\n }\n }\n': typeof types.GetVestingScheduleChartDocument; '\n query GetVestingSchedules(\n $limit: Int\n $offset: Int\n $orderBy: [vesting_schedule_order_by!]\n ) {\n schedules: vesting_schedule(\n limit: $limit\n offset: $offset\n order_by: $orderBy\n ) {\n id\n beneficiary\n total\n claimed\n start\n cliff\n end\n last_claim_at\n }\n meta: vesting_schedule_aggregate {\n aggregate {\n totalCount: count\n }\n }\n }\n': typeof types.GetVestingSchedulesDocument; - '\n query GetWormholeExtrinsicById($id: String!) {\n wormholeExtrinsicById: wormhole_extrinsic_by_pk(id: $id) {\n id\n extrinsic {\n id\n pallet\n call\n }\n total_amount\n output_count\n timestamp\n privacy_score\n privacy_score01_pct\n privacy_score1_pct\n privacy_score5_pct\n privacy_label\n pool_snapshot\n block {\n id\n height\n hash\n timestamp\n }\n outputs {\n id\n exitAccount {\n id\n }\n amount\n }\n }\n wormholeNullifiers: wormhole_nullifier(\n where: { wormholeExtrinsic: { id: { _eq: $id } } }\n ) {\n nullifier\n nullifier_hash\n }\n }\n': typeof types.GetWormholeExtrinsicByIdDocument; + '\n query GetWormholeExtrinsicById($id: String!) {\n wormholeExtrinsicById: wormhole_extrinsic_by_pk(id: $id) {\n id\n extrinsic {\n id\n pallet\n call\n }\n total_amount\n output_count\n timestamp\n block {\n id\n height\n hash\n timestamp\n }\n outputs {\n id\n exitAccount {\n id\n }\n amount\n }\n }\n wormholeNullifiers: wormhole_nullifier(\n where: { wormholeExtrinsic: { id: { _eq: $id } } }\n ) {\n nullifier\n nullifier_hash\n }\n }\n': typeof types.GetWormholeExtrinsicByIdDocument; '\n query AccountPartySamples {\n busiest: account_stats(\n limit: 1\n order_by: { total_immediate_transfers: desc }\n ) {\n id\n total_immediate_transfers\n }\n quietest: account_stats(\n limit: 1\n where: { total_immediate_transfers: { _gt: 0 } }\n order_by: [{ total_immediate_transfers: asc }, { id: asc }]\n ) {\n id\n }\n topMiner: account_stats(\n limit: 1\n order_by: { total_mined_blocks: desc }\n ) {\n id\n total_mined_blocks\n }\n }\n ': typeof types.AccountPartySamplesDocument; '\n query Accounts($limit: Int) {\n account(limit: $limit) {\n id\n }\n }\n': typeof types.AccountsDocument; '\n query BusyAccounts($limit: Int!) {\n account_stats(\n limit: $limit\n order_by: { total_immediate_transfers: desc }\n ) {\n id\n total_immediate_transfers\n total_mined_blocks\n }\n }\n ': typeof types.BusyAccountsDocument; @@ -250,7 +250,7 @@ const documents: Documents = { types.GetVestingScheduleChartDocument, '\n query GetVestingSchedules(\n $limit: Int\n $offset: Int\n $orderBy: [vesting_schedule_order_by!]\n ) {\n schedules: vesting_schedule(\n limit: $limit\n offset: $offset\n order_by: $orderBy\n ) {\n id\n beneficiary\n total\n claimed\n start\n cliff\n end\n last_claim_at\n }\n meta: vesting_schedule_aggregate {\n aggregate {\n totalCount: count\n }\n }\n }\n': types.GetVestingSchedulesDocument, - '\n query GetWormholeExtrinsicById($id: String!) {\n wormholeExtrinsicById: wormhole_extrinsic_by_pk(id: $id) {\n id\n extrinsic {\n id\n pallet\n call\n }\n total_amount\n output_count\n timestamp\n privacy_score\n privacy_score01_pct\n privacy_score1_pct\n privacy_score5_pct\n privacy_label\n pool_snapshot\n block {\n id\n height\n hash\n timestamp\n }\n outputs {\n id\n exitAccount {\n id\n }\n amount\n }\n }\n wormholeNullifiers: wormhole_nullifier(\n where: { wormholeExtrinsic: { id: { _eq: $id } } }\n ) {\n nullifier\n nullifier_hash\n }\n }\n': + '\n query GetWormholeExtrinsicById($id: String!) {\n wormholeExtrinsicById: wormhole_extrinsic_by_pk(id: $id) {\n id\n extrinsic {\n id\n pallet\n call\n }\n total_amount\n output_count\n timestamp\n block {\n id\n height\n hash\n timestamp\n }\n outputs {\n id\n exitAccount {\n id\n }\n amount\n }\n }\n wormholeNullifiers: wormhole_nullifier(\n where: { wormholeExtrinsic: { id: { _eq: $id } } }\n ) {\n nullifier\n nullifier_hash\n }\n }\n': types.GetWormholeExtrinsicByIdDocument, '\n query AccountPartySamples {\n busiest: account_stats(\n limit: 1\n order_by: { total_immediate_transfers: desc }\n ) {\n id\n total_immediate_transfers\n }\n quietest: account_stats(\n limit: 1\n where: { total_immediate_transfers: { _gt: 0 } }\n order_by: [{ total_immediate_transfers: asc }, { id: asc }]\n ) {\n id\n }\n topMiner: account_stats(\n limit: 1\n order_by: { total_mined_blocks: desc }\n ) {\n id\n total_mined_blocks\n }\n }\n ': types.AccountPartySamplesDocument, @@ -737,8 +737,8 @@ export function gql( * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function gql( - source: '\n query GetWormholeExtrinsicById($id: String!) {\n wormholeExtrinsicById: wormhole_extrinsic_by_pk(id: $id) {\n id\n extrinsic {\n id\n pallet\n call\n }\n total_amount\n output_count\n timestamp\n privacy_score\n privacy_score01_pct\n privacy_score1_pct\n privacy_score5_pct\n privacy_label\n pool_snapshot\n block {\n id\n height\n hash\n timestamp\n }\n outputs {\n id\n exitAccount {\n id\n }\n amount\n }\n }\n wormholeNullifiers: wormhole_nullifier(\n where: { wormholeExtrinsic: { id: { _eq: $id } } }\n ) {\n nullifier\n nullifier_hash\n }\n }\n' -): (typeof documents)['\n query GetWormholeExtrinsicById($id: String!) {\n wormholeExtrinsicById: wormhole_extrinsic_by_pk(id: $id) {\n id\n extrinsic {\n id\n pallet\n call\n }\n total_amount\n output_count\n timestamp\n privacy_score\n privacy_score01_pct\n privacy_score1_pct\n privacy_score5_pct\n privacy_label\n pool_snapshot\n block {\n id\n height\n hash\n timestamp\n }\n outputs {\n id\n exitAccount {\n id\n }\n amount\n }\n }\n wormholeNullifiers: wormhole_nullifier(\n where: { wormholeExtrinsic: { id: { _eq: $id } } }\n ) {\n nullifier\n nullifier_hash\n }\n }\n']; + source: '\n query GetWormholeExtrinsicById($id: String!) {\n wormholeExtrinsicById: wormhole_extrinsic_by_pk(id: $id) {\n id\n extrinsic {\n id\n pallet\n call\n }\n total_amount\n output_count\n timestamp\n block {\n id\n height\n hash\n timestamp\n }\n outputs {\n id\n exitAccount {\n id\n }\n amount\n }\n }\n wormholeNullifiers: wormhole_nullifier(\n where: { wormholeExtrinsic: { id: { _eq: $id } } }\n ) {\n nullifier\n nullifier_hash\n }\n }\n' +): (typeof documents)['\n query GetWormholeExtrinsicById($id: String!) {\n wormholeExtrinsicById: wormhole_extrinsic_by_pk(id: $id) {\n id\n extrinsic {\n id\n pallet\n call\n }\n total_amount\n output_count\n timestamp\n block {\n id\n height\n hash\n timestamp\n }\n outputs {\n id\n exitAccount {\n id\n }\n amount\n }\n }\n wormholeNullifiers: wormhole_nullifier(\n where: { wormholeExtrinsic: { id: { _eq: $id } } }\n ) {\n nullifier\n nullifier_hash\n }\n }\n']; /** * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/src/__generated__/graphql.ts b/src/__generated__/graphql.ts index 2e41790..338f7b0 100644 --- a/src/__generated__/graphql.ts +++ b/src/__generated__/graphql.ts @@ -11888,12 +11888,6 @@ export type GetWormholeExtrinsicByIdQuery = { total_amount: any; output_count: number; timestamp: any; - privacy_score: any; - privacy_score01_pct: any; - privacy_score1_pct: any; - privacy_score5_pct: any; - privacy_label: string; - pool_snapshot: string; extrinsic?: { __typename?: 'extrinsic'; id: string; @@ -22830,30 +22824,6 @@ export const GetWormholeExtrinsicByIdDocument = { name: { kind: 'Name', value: 'output_count' } }, { kind: 'Field', name: { kind: 'Name', value: 'timestamp' } }, - { - kind: 'Field', - name: { kind: 'Name', value: 'privacy_score' } - }, - { - kind: 'Field', - name: { kind: 'Name', value: 'privacy_score01_pct' } - }, - { - kind: 'Field', - name: { kind: 'Name', value: 'privacy_score1_pct' } - }, - { - kind: 'Field', - name: { kind: 'Name', value: 'privacy_score5_pct' } - }, - { - kind: 'Field', - name: { kind: 'Name', value: 'privacy_label' } - }, - { - kind: 'Field', - name: { kind: 'Name', value: 'pool_snapshot' } - }, { kind: 'Field', name: { kind: 'Name', value: 'block' }, diff --git a/src/api/wormhole.tsx b/src/api/wormhole.tsx index a4b1a05..cef690a 100644 --- a/src/api/wormhole.tsx +++ b/src/api/wormhole.tsx @@ -14,12 +14,6 @@ const GET_WORMHOLE_EXTRINSIC_BY_ID = gql` total_amount output_count timestamp - privacy_score - privacy_score01_pct - privacy_score1_pct - privacy_score5_pct - privacy_label - pool_snapshot block { id height diff --git a/src/components/features/landing/hero/chain-stats/ChainStats.tsx b/src/components/features/landing/hero/chain-stats/ChainStats.tsx index d5623ef..a68dbb9 100644 --- a/src/components/features/landing/hero/chain-stats/ChainStats.tsx +++ b/src/components/features/landing/hero/chain-stats/ChainStats.tsx @@ -10,9 +10,11 @@ import { import { InlineFetchError } from '@/components/ui/composites/fetch-error/FetchError'; import { StatSparklineCard } from '@/components/ui/composites/stat-sparkline-card'; import { Skeleton } from '@/components/ui/skeleton'; +import { formatUsdPrice } from '@/lib/qtc-market'; import { formatChainSupply } from './format-chain-supply'; import { useChainStats } from './hook'; +import { useQtcPrice } from './use-qtc-price'; const STROKE_FLARE = 'var(--flare)'; const STROKE_GLACIER = 'var(--glacier)'; @@ -24,14 +26,19 @@ export interface ChainStatsProps {} const SupplyStatCard = ({ label, value, + formatted, loading, error }: { label: string; value?: string; + formatted?: string; loading: boolean; error: string | null; }) => { + const text = + formatted ?? (value != null ? formatChainSupply(value) : undefined); + return ( @@ -42,9 +49,7 @@ const SupplyStatCard = ({ {loading && } {!loading && error && } - {!loading && !error && value != null && ( -

{formatChainSupply(value)}

- )} + {!loading && !error && text != null &&

{text}

}
); @@ -67,6 +72,7 @@ export const ChainStats: React.FC = () => { maxSupply, circulatingSupply } = useChainStats(); + const price = useQtcPrice(); const errorMessage = error?.message ?? null; @@ -121,7 +127,15 @@ export const ChainStats: React.FC = () => { /> - + + (() => + shouldFetchQtcPrice(isDev) + ? { loading: true, error: null, priceUsd: null } + : { loading: false, error: null, priceUsd: DEV_QTC_PRICE_USD } + ); + + useEffect(() => { + if (!shouldFetchQtcPrice(isDev)) return undefined; + + const controller = new AbortController(); + + loadQtcPrice(false, (url) => fetch(url, { signal: controller.signal })) + .then((priceUsd) => { + setState({ loading: false, error: null, priceUsd }); + }) + .catch((error: unknown) => { + if (controller.signal.aborted) return; + const message = + error instanceof Error ? error.message : 'Failed to load QTC price'; + setState({ loading: false, error: message, priceUsd: null }); + }); + + return () => controller.abort(); + }, [isDev]); + + return state; +} diff --git a/src/components/features/wormhole/PrivacyScoreBadge.tsx b/src/components/features/wormhole/PrivacyScoreBadge.tsx deleted file mode 100644 index b8e6b13..0000000 --- a/src/components/features/wormhole/PrivacyScoreBadge.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import type { VariantProps } from 'class-variance-authority'; - -import { Badge, type badgeVariants } from '@/components/ui/badge'; - -interface PrivacyScoreBadgeProps { - score: number; - label: string; -} - -type BadgeVariant = NonNullable['variant']>; - -const labelVariants: Record = { - Critical: 'error', - Weak: 'weak', - Moderate: 'moderate', - Strong: 'strong', - 'Very Strong': 'strong' -}; - -export const PrivacyScoreBadge = ({ score, label }: PrivacyScoreBadgeProps) => { - const variant = labelVariants[label] ?? 'miner'; - - return ( - - {score.toFixed(1)} bits ({label}) - - ); -}; diff --git a/src/components/features/wormhole/WormholeOutputDetails.tsx b/src/components/features/wormhole/WormholeOutputDetails.tsx index f3af70c..dbe5b31 100644 --- a/src/components/features/wormhole/WormholeOutputDetails.tsx +++ b/src/components/features/wormhole/WormholeOutputDetails.tsx @@ -9,8 +9,6 @@ import { TimestampDisplay } from '@/components/ui/timestamp-display'; import { RESOURCES } from '@/constants/resources'; import { formatBlockHeight, formatMonetaryValue } from '@/utils/formatter'; -import { PrivacyScoreBadge } from './PrivacyScoreBadge'; - interface WormholeOutputDetailsProps { id: string; } @@ -21,12 +19,6 @@ interface ExtrinsicInfo { outputCount: number; block: { height: number; hash: string }; timestamp: string; - privacyScore: string; - privacyLabel: string; - privacyScore01Pct: string; - privacyScore1Pct: string; - privacyScore5Pct: string; - poolSnapshot: string; } interface ExitOutputInfo { @@ -57,13 +49,7 @@ export const WormholeOutputInformation = ({ totalAmount: extrinsic?.total_amount, outputCount: extrinsic?.output_count, block: extrinsic?.block, - timestamp: extrinsic?.timestamp, - privacyScore: extrinsic?.privacy_score, - privacyLabel: extrinsic?.privacy_label, - privacyScore01Pct: extrinsic?.privacy_score01_pct, - privacyScore1Pct: extrinsic?.privacy_score1_pct, - privacyScore5Pct: extrinsic?.privacy_score5_pct, - poolSnapshot: extrinsic?.pool_snapshot + timestamp: extrinsic?.timestamp } ]; @@ -130,66 +116,6 @@ export const WormholeOutputInformation = ({ /> -
-

Privacy Analysis

- > - loading={loading} - data={extrinsicInfo} - fields={[ - { - label: 'Privacy Score', - key: 'privacyScore', - render: (value, item) => ( - - ), - tooltip: - 'Estimated bits of anonymity at 0.01 DEV precision. Based on how many deposit subsets could produce this total.' - }, - { - label: 'With 0.1% sacrifice', - key: 'privacyScore01Pct', - render: (value) => `${Number(value).toFixed(1)} bits`, - tooltip: - 'Score if the user had sacrificed 0.1% of the output for privacy.' - }, - { - label: 'With 1% sacrifice', - key: 'privacyScore1Pct', - render: (value) => `${Number(value).toFixed(1)} bits`, - tooltip: - 'Score if the user had sacrificed 1% of the output for privacy.' - }, - { - label: 'With 5% sacrifice', - key: 'privacyScore5Pct', - render: (value) => `${Number(value).toFixed(1)} bits`, - tooltip: - 'Score if the user had sacrificed 5% of the output for privacy.' - }, - { - label: 'Pool Snapshot', - key: 'poolSnapshot', - render: (value) => { - try { - const buckets = JSON.parse(value as string); - const nonEmpty = buckets.filter( - (b: { count: number }) => b.count > 0 - ); - return `${nonEmpty.length} active buckets`; - } catch { - return '-'; - } - }, - tooltip: - 'Deposit pool bucket distribution at time of proof verification.' - } - ]} - /> -
- {showExitOutputs && (

Exit Outputs

diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx index 27d3cc8..f0b77c0 100644 --- a/src/components/ui/badge.tsx +++ b/src/components/ui/badge.tsx @@ -13,9 +13,7 @@ const badgeVariants = cva( immediate: 'border-gamboge/30 text-gamboge', reversible: 'border-glacier/30 text-glacier', miner: 'border-gamboge/20 text-gamboge/70', - weak: 'border-ember/30 text-ember', - moderate: 'border-gamboge/30 text-gamboge', - strong: 'border-sage/30 text-sage' + weak: 'border-ember/30 text-ember' } }, defaultVariants: { diff --git a/src/config/metadata.ts b/src/config/metadata.ts index 48263ea..7002052 100644 --- a/src/config/metadata.ts +++ b/src/config/metadata.ts @@ -1,4 +1,6 @@ // src/lib/metadata.ts +import { OG_HEIGHT, OG_IMAGE_URL, OG_WIDTH } from '@/lib/qtc-market'; + import env from './env'; const siteName = env.SITE_NAME; @@ -16,10 +18,10 @@ export const defaultMetadata = { baseUrl, canonical: baseUrl, images: { - url: `${baseUrl}/images/next.svg`, + url: OG_IMAGE_URL, alt: siteName, - width: 226, - height: 100 + width: OG_WIDTH, + height: OG_HEIGHT }, openGraph: { title: siteName, diff --git a/src/lib/generate-og-response.ts b/src/lib/generate-og-response.ts new file mode 100644 index 0000000..2e686a2 --- /dev/null +++ b/src/lib/generate-og-response.ts @@ -0,0 +1,20 @@ +import { ImageResponse } from 'workers-og'; + +import { loadOgFonts } from './og-fonts'; +import { buildOgElement } from './og-template'; +import type { QtcMarketQuote } from './qtc-market'; +import { OG_HEIGHT, OG_WIDTH } from './qtc-market'; + +export async function generateOgImageResponse( + requestUrl: string, + quote: QtcMarketQuote +): Promise { + const fonts = await loadOgFonts(requestUrl); + const element = buildOgElement(quote); + + return new ImageResponse(element, { + width: OG_WIDTH, + height: OG_HEIGHT, + fonts + }); +} diff --git a/src/lib/og-fonts.ts b/src/lib/og-fonts.ts new file mode 100644 index 0000000..4ac65dd --- /dev/null +++ b/src/lib/og-fonts.ts @@ -0,0 +1,43 @@ +interface OgFont { + name: string; + data: ArrayBuffer; + weight: number; + style: 'normal'; +} + +const FONT_PATHS = [ + { name: 'Geist', path: '/fonts/geist-latin-400-normal.woff', weight: 400 }, + { name: 'Geist', path: '/fonts/geist-latin-500-normal.woff', weight: 500 }, + { + name: 'Geist Mono', + path: '/fonts/geist-mono-latin-500-normal.woff', + weight: 500 + } +] as const; + +let fontsPromise: Promise | null = null; + +export function loadOgFonts(origin: string): Promise { + if (!fontsPromise) { + fontsPromise = Promise.all( + FONT_PATHS.map(async ({ name, path, weight }) => { + const response = await fetch(new URL(path, origin)); + if (!response.ok) { + throw new Error(`Failed to load font ${path}: ${response.status}`); + } + + return { + name, + data: await response.arrayBuffer(), + weight, + style: 'normal' as const + }; + }) + ).catch((error: unknown) => { + fontsPromise = null; + throw error; + }); + } + + return fontsPromise; +} diff --git a/src/lib/og-template.ts b/src/lib/og-template.ts new file mode 100644 index 0000000..5b1bf45 --- /dev/null +++ b/src/lib/og-template.ts @@ -0,0 +1,112 @@ +import React from 'react'; + +import type { QtcMarketQuote } from './qtc-market'; +import { + formatUsdMarketCap, + formatUsdPrice, + OG_HEIGHT, + OG_WIDTH +} from './qtc-market'; + +const colors = { + void: '#0e0e0e', + flare: '#ff6b35', + content: '#e8e6e0', + muted: '#8a8784', + border: '#262626' +} as const; + +function div( + style: React.CSSProperties, + ...children: React.ReactNode[] +): React.ReactElement { + return React.createElement('div', { style }, ...children); +} + +export function buildOgElement(quote: QtcMarketQuote) { + const price = formatUsdPrice(quote.priceUsd); + const marketCap = formatUsdMarketCap(quote.marketCapUsd); + + return div( + { + display: 'flex', + flexDirection: 'column', + justifyContent: 'space-between', + width: OG_WIDTH, + height: OG_HEIGHT, + padding: '64px 72px', + background: colors.void, + color: colors.content, + fontFamily: 'Geist' + }, + div( + { + display: 'flex', + fontFamily: 'Geist Mono', + fontSize: 18, + fontWeight: 500, + letterSpacing: '0.12em', + textTransform: 'uppercase', + color: colors.muted + }, + 'Quantus Explorer' + ), + div( + { display: 'flex', flexDirection: 'column' }, + div( + { + display: 'flex', + marginBottom: 16, + fontFamily: 'Geist Mono', + fontSize: 20, + fontWeight: 500, + letterSpacing: '0.08em', + textTransform: 'uppercase', + color: colors.muted + }, + 'QTC Price' + ), + div( + { + display: 'flex', + fontSize: 112, + fontWeight: 500, + letterSpacing: '-0.04em', + lineHeight: 1, + color: colors.flare + }, + price + ) + ), + div( + { + display: 'flex', + alignItems: 'baseline', + paddingTop: 28, + borderTop: `1px solid ${colors.border}` + }, + div( + { + display: 'flex', + fontFamily: 'Geist Mono', + fontSize: 20, + fontWeight: 500, + letterSpacing: '0.08em', + textTransform: 'uppercase', + color: colors.muted + }, + 'Market Cap' + ), + div( + { + display: 'flex', + marginLeft: 24, + fontSize: 42, + fontWeight: 500, + letterSpacing: '-0.02em' + }, + marketCap + ) + ) + ); +} diff --git a/src/lib/qtc-market.test.ts b/src/lib/qtc-market.test.ts new file mode 100644 index 0000000..4678efc --- /dev/null +++ b/src/lib/qtc-market.test.ts @@ -0,0 +1,159 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +import { defaultMetadata } from '@/config/metadata'; + +import { + DEV_QTC_PRICE_USD, + formatUsdMarketCap, + formatUsdPrice, + isOgImageFresh, + loadQtcPrice, + OBSERVED_AT_HEADER, + OG_FRESHNESS_MS, + OG_HEIGHT, + OG_IMAGE_URL, + OG_RETENTION_SECONDS, + OG_STALE_BROWSER_MAX_AGE_SECONDS, + OG_WIDTH, + ogBrowserMaxAgeSeconds, + qtcMarketQuote, + TRACKER_URL +} from './qtc-market'; + +const UPDATED_AT = '2026-10-07T11:30:00.000Z'; +const NOW = Date.parse('2026-10-07T12:00:00.000Z'); + +const snapshot = { + updatedAt: UPDATED_AT, + coins: [ + { + id: 'quantus', + symbol: 'QTC', + current_price: 101.84, + market_cap: 27373239 + } + ] +}; + +describe('qtcMarketQuote', () => { + it('reads the Quantus price, market cap, and observation time', () => { + expect(qtcMarketQuote(snapshot)).toEqual({ + priceUsd: 101.84, + marketCapUsd: 27373239, + updatedAt: UPDATED_AT + }); + }); + + it('rejects a snapshot that has no Quantus price', () => { + expect(() => qtcMarketQuote({ updatedAt: UPDATED_AT, coins: [] })).toThrow( + /Quantus/ + ); + expect(() => + qtcMarketQuote({ + updatedAt: UPDATED_AT, + coins: [{ id: 'quantus', current_price: null, market_cap: 1 }] + }) + ).toThrow(/price/); + expect(() => + qtcMarketQuote({ + updatedAt: UPDATED_AT, + coins: [{ id: 'quantus', current_price: 1, market_cap: null }] + }) + ).toThrow(/market cap/); + }); +}); + +describe('USD formatting', () => { + it('formats the spot price in dollars and cents', () => { + expect(formatUsdPrice(101.84)).toBe('$101.84'); + expect(formatUsdPrice(DEV_QTC_PRICE_USD)).toBe('$100.00'); + }); + + it('formats market cap in millions', () => { + expect(formatUsdMarketCap(27373239)).toBe('$27.37M'); + }); +}); + +describe('loadQtcPrice', () => { + it('returns the fixed local price without calling the tracker', async () => { + const fetchImpl = jest.fn(); + + await expect(loadQtcPrice(true, fetchImpl)).resolves.toBe( + DEV_QTC_PRICE_USD + ); + expect(fetchImpl).not.toHaveBeenCalled(); + }); + + it('loads the live Quantus price from the tracker', async () => { + const fetchImpl = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + json: async () => snapshot + }); + + await expect(loadQtcPrice(false, fetchImpl)).resolves.toBe(101.84); + expect(fetchImpl).toHaveBeenCalledWith(TRACKER_URL); + }); + + it('fails when the tracker request fails', async () => { + const fetchImpl = jest.fn().mockResolvedValue({ + ok: false, + status: 503, + json: async () => ({}) + }); + + await expect(loadQtcPrice(false, fetchImpl)).rejects.toThrow( + 'Tracker request failed: 503' + ); + }); +}); + +describe('OG cache timing', () => { + it('treats an image as fresh for one hour after the tracker observation', () => { + expect(isOgImageFresh(UPDATED_AT, NOW)).toBe(true); + expect(isOgImageFresh(UPDATED_AT, NOW + OG_FRESHNESS_MS)).toBe(false); + expect(isOgImageFresh(null, NOW)).toBe(false); + expect(isOgImageFresh('not-a-date', NOW)).toBe(false); + }); + + it('keeps a short browser cache and a week-long stored copy', () => { + expect(ogBrowserMaxAgeSeconds(UPDATED_AT, NOW, false)).toBe(1800); + expect(ogBrowserMaxAgeSeconds(UPDATED_AT, NOW, true)).toBe( + OG_STALE_BROWSER_MAX_AGE_SECONDS + ); + expect(OG_RETENTION_SECONDS).toBe(7 * 24 * 60 * 60); + expect(OBSERVED_AT_HEADER).toBe('X-Observed-At'); + }); +}); + +describe('Open Graph tags', () => { + it('points the static page and client metadata at the same 1200x630 image', () => { + const html = fs.readFileSync( + path.join(process.cwd(), 'index.html'), + 'utf8' + ); + + expect(OG_IMAGE_URL).toBe('https://explorer.quantus.com/api/og'); + expect(OG_WIDTH).toBe(1200); + expect(OG_HEIGHT).toBe(630); + expect(html).toContain(`content="${OG_IMAGE_URL}"`); + expect(html).toContain('content="1200"'); + expect(html).toContain('content="630"'); + expect(defaultMetadata.images.url).toBe(OG_IMAGE_URL); + expect(defaultMetadata.images.width).toBe(OG_WIDTH); + expect(defaultMetadata.images.height).toBe(OG_HEIGHT); + }); + + it('draws the card with the explorer flare, not the tracker accent', () => { + const template = fs.readFileSync( + path.join(process.cwd(), 'src/lib/og-template.ts'), + 'utf8' + ); + + expect(template).toContain('#ff6b35'); + expect(template).toContain('formatUsdPrice'); + expect(template).toContain('formatUsdMarketCap'); + expect(template).not.toContain('#ff2d55'); + }); +}); diff --git a/src/lib/qtc-market.ts b/src/lib/qtc-market.ts new file mode 100644 index 0000000..7c9858f --- /dev/null +++ b/src/lib/qtc-market.ts @@ -0,0 +1,133 @@ +export const TRACKER_URL = 'https://migration.fail/api/tracker'; + +export const EXPLORER_ORIGIN = 'https://explorer.quantus.com'; + +export const OG_IMAGE_URL = `${EXPLORER_ORIGIN}/api/og`; + +export const OG_WIDTH = 1200; + +export const OG_HEIGHT = 630; + +/** Shown on the Vite dev server, which is not the explorer origin. */ +export const DEV_QTC_PRICE_USD = 100; + +export const OBSERVED_AT_HEADER = 'X-Observed-At'; + +export const OG_FRESHNESS_MS = 60 * 60 * 1000; + +export const OG_RETENTION_SECONDS = 7 * 24 * 60 * 60; + +export const OG_STALE_BROWSER_MAX_AGE_SECONDS = 60; + +const QTC_COIN_ID = 'quantus'; + +export interface QtcMarketQuote { + priceUsd: number; + marketCapUsd: number; + updatedAt: string; +} + +type TrackerFetch = (url: string, init?: RequestInit) => Promise; + +export function shouldFetchQtcPrice(isDev: boolean): boolean { + return !isDev; +} + +export function qtcMarketQuote(body: unknown): QtcMarketQuote { + if (!body || typeof body !== 'object') { + throw new Error('Tracker snapshot is missing'); + } + + const record = body as Record; + if ( + typeof record.updatedAt !== 'string' || + Number.isNaN(Date.parse(record.updatedAt)) + ) { + throw new Error('Tracker snapshot is missing updatedAt'); + } + if (!Array.isArray(record.coins)) { + throw new Error('Tracker snapshot is missing coins'); + } + + const coin = record.coins.find( + (entry): entry is Record => + !!entry && + typeof entry === 'object' && + (entry as { id?: unknown }).id === QTC_COIN_ID + ); + if (!coin) { + throw new Error('Tracker snapshot is missing Quantus'); + } + if ( + typeof coin.current_price !== 'number' || + !Number.isFinite(coin.current_price) + ) { + throw new Error('Quantus price is missing'); + } + if ( + typeof coin.market_cap !== 'number' || + !Number.isFinite(coin.market_cap) + ) { + throw new Error('Quantus market cap is missing'); + } + + return { + priceUsd: coin.current_price, + marketCapUsd: coin.market_cap, + updatedAt: record.updatedAt + }; +} + +export function formatUsdPrice(value: number): string { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 2, + maximumFractionDigits: 2 + }).format(value); +} + +export function formatUsdMarketCap(value: number): string { + if (value >= 1e12) return `$${(value / 1e12).toFixed(2)}T`; + if (value >= 1e9) return `$${(value / 1e9).toFixed(2)}B`; + if (value >= 1e6) return `$${(value / 1e6).toFixed(2)}M`; + if (value >= 1e3) return `$${(value / 1e3).toFixed(2)}K`; + return `$${value.toFixed(2)}`; +} + +export function isOgImageFresh( + observedAt: string | null, + now: number +): boolean { + if (!observedAt) return false; + const updated = Date.parse(observedAt); + if (Number.isNaN(updated)) return false; + const age = now - updated; + return age >= 0 && age < OG_FRESHNESS_MS; +} + +export function ogBrowserMaxAgeSeconds( + observedAt: string, + now: number, + stale: boolean +): number { + if (stale) return OG_STALE_BROWSER_MAX_AGE_SECONDS; + const updated = Date.parse(observedAt); + if (Number.isNaN(updated)) return OG_STALE_BROWSER_MAX_AGE_SECONDS; + const remaining = Math.floor((OG_FRESHNESS_MS - (now - updated)) / 1000); + return Math.max(0, remaining); +} + +export async function loadQtcPrice( + isDev: boolean, + fetchImpl: TrackerFetch = fetch +): Promise { + if (!shouldFetchQtcPrice(isDev)) return DEV_QTC_PRICE_USD; + + const response = await fetchImpl(TRACKER_URL); + if (!response.ok) { + throw new Error(`Tracker request failed: ${response.status}`); + } + + return qtcMarketQuote(await response.json()).priceUsd; +} diff --git a/src/schemas/wormhole.ts b/src/schemas/wormhole.ts index 347dd79..f4698f6 100644 --- a/src/schemas/wormhole.ts +++ b/src/schemas/wormhole.ts @@ -4,16 +4,7 @@ import type * as gql from '../__generated__/graphql'; export interface WormholeExtrinsicDetail extends Pick< gql.Wormhole_Extrinsic, - | 'id' - | 'total_amount' - | 'output_count' - | 'timestamp' - | 'privacy_score' - | 'privacy_score01_pct' - | 'privacy_score1_pct' - | 'privacy_score5_pct' - | 'privacy_label' - | 'pool_snapshot' + 'id' | 'total_amount' | 'output_count' | 'timestamp' > { extrinsic: Pick | null; block: Pick; diff --git a/src/types/cloudflare-cache.ts b/src/types/cloudflare-cache.ts new file mode 100644 index 0000000..ebf53c8 --- /dev/null +++ b/src/types/cloudflare-cache.ts @@ -0,0 +1,7 @@ +export {}; + +declare global { + interface CacheStorage { + readonly default: Cache; + } +}