From 2e5f221f887c397e44773ef4f88758a0353a1f27 Mon Sep 17 00:00:00 2001 From: Beast Date: Mon, 12 Oct 2026 17:40:23 +0700 Subject: [PATCH 1/4] feat: remove privacy score wormhole --- src/__generated__/gql.ts | 8 +- src/__generated__/graphql.ts | 30 -------- src/api/wormhole.tsx | 6 -- .../features/wormhole/PrivacyScoreBadge.tsx | 28 ------- .../wormhole/WormholeOutputDetails.tsx | 76 +------------------ src/components/ui/badge.tsx | 4 +- src/schemas/wormhole.ts | 11 +-- 7 files changed, 7 insertions(+), 156 deletions(-) delete mode 100644 src/components/features/wormhole/PrivacyScoreBadge.tsx 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/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/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; From c3a04ceecf814930ca94c065846cde68c91998ad Mon Sep 17 00:00:00 2001 From: Beast Date: Mon, 12 Oct 2026 21:54:38 +0700 Subject: [PATCH 2/4] feat: add og image --- bun.lock | 49 ++++++ functions/api/og.ts | 73 ++++++++ index.html | 23 +++ package.json | 3 +- public/fonts/geist-latin-400-normal.woff | Bin 0 -> 17208 bytes public/fonts/geist-latin-500-normal.woff | Bin 0 -> 17484 bytes public/fonts/geist-mono-latin-500-normal.woff | Bin 0 -> 17380 bytes .../network-provider/network-provider.tsx | 4 +- .../landing/hero/chain-stats/ChainStats.tsx | 22 ++- .../landing/hero/chain-stats/use-qtc-price.ts | 43 +++++ src/config/metadata.ts | 8 +- src/lib/generate-og-response.ts | 20 +++ src/lib/og-fonts.ts | 40 +++++ src/lib/og-template.tsx | 115 +++++++++++++ src/lib/qtc-market.test.ts | 159 ++++++++++++++++++ src/lib/qtc-market.ts | 133 +++++++++++++++ src/types/cloudflare-cache.ts | 7 + 17 files changed, 689 insertions(+), 10 deletions(-) create mode 100644 functions/api/og.ts create mode 100644 public/fonts/geist-latin-400-normal.woff create mode 100644 public/fonts/geist-latin-500-normal.woff create mode 100644 public/fonts/geist-mono-latin-500-normal.woff create mode 100644 src/components/features/landing/hero/chain-stats/use-qtc-price.ts create mode 100644 src/lib/generate-og-response.ts create mode 100644 src/lib/og-fonts.ts create mode 100644 src/lib/og-template.tsx create mode 100644 src/lib/qtc-market.test.ts create mode 100644 src/lib/qtc-market.ts create mode 100644 src/types/cloudflare-cache.ts 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 0000000000000000000000000000000000000000..6fc3b7ae35a2e037b0bcf7f1aefc8995a89e3700 GIT binary patch literal 17208 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 WT>jTVp#)mn4+Q_`FaXc3zyttG_JnT$ literal 0 HcmV?d00001 diff --git a/public/fonts/geist-latin-500-normal.woff b/public/fonts/geist-latin-500-normal.woff new file mode 100644 index 0000000000000000000000000000000000000000..e96fcc002e5324e0a8a8735ff500ed315288f159 GIT binary patch literal 17484 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 q=bUSU{~o&ISjxvHqgQnC;$MTyEQEU literal 0 HcmV?d00001 diff --git a/src/components/common/network-provider/network-provider.tsx b/src/components/common/network-provider/network-provider.tsx index ea42523..8a464f9 100644 --- a/src/components/common/network-provider/network-provider.tsx +++ b/src/components/common/network-provider/network-provider.tsx @@ -4,8 +4,8 @@ const ENABLE_LOCAL_NETWORK = import.meta.env.VITE_ENABLE_LOCAL_NETWORK === 'true'; const BASE_NETWORKS = { - mainnet: 'https://sqm.quantus.com/v1/graphql', - planck: 'https://sub2.quantus.com/v1/graphql' + mainnet: 'http://127.0.0.1:8080/https://sqm.quantus.com/v1/graphql', + planck: 'http://127.0.0.1:8080/https://sub2.quantus.com/v1/graphql' } as const; const LOCAL_NETWORK = { 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/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..c5a6a5a --- /dev/null +++ b/src/lib/og-fonts.ts @@ -0,0 +1,40 @@ +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 + }; + }) + ); + } + + return fontsPromise; +} diff --git a/src/lib/og-template.tsx b/src/lib/og-template.tsx new file mode 100644 index 0000000..6a31628 --- /dev/null +++ b/src/lib/og-template.tsx @@ -0,0 +1,115 @@ +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; + +export function buildOgElement(quote: QtcMarketQuote) { + const price = formatUsdPrice(quote.priceUsd); + const marketCap = formatUsdMarketCap(quote.marketCapUsd); + + return ( +
+
+ Quantus Explorer +
+ +
+
+ QTC Price +
+
+ {price} +
+
+ +
+
+ Market Cap +
+
+ {marketCap} +
+
+
+ ); +} diff --git a/src/lib/qtc-market.test.ts b/src/lib/qtc-market.test.ts new file mode 100644 index 0000000..68514d3 --- /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.tsx'), + '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/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; + } +} From c17bec7f0f35174604227480fb5c6f9fe8e8ec79 Mon Sep 17 00:00:00 2001 From: Beast Date: Mon, 12 Oct 2026 23:34:02 +0700 Subject: [PATCH 3/4] fix: issues --- src/components/common/network-provider/network-provider.tsx | 4 ++-- src/lib/og-fonts.ts | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/common/network-provider/network-provider.tsx b/src/components/common/network-provider/network-provider.tsx index 8a464f9..ea42523 100644 --- a/src/components/common/network-provider/network-provider.tsx +++ b/src/components/common/network-provider/network-provider.tsx @@ -4,8 +4,8 @@ const ENABLE_LOCAL_NETWORK = import.meta.env.VITE_ENABLE_LOCAL_NETWORK === 'true'; const BASE_NETWORKS = { - mainnet: 'http://127.0.0.1:8080/https://sqm.quantus.com/v1/graphql', - planck: 'http://127.0.0.1:8080/https://sub2.quantus.com/v1/graphql' + mainnet: 'https://sqm.quantus.com/v1/graphql', + planck: 'https://sub2.quantus.com/v1/graphql' } as const; const LOCAL_NETWORK = { diff --git a/src/lib/og-fonts.ts b/src/lib/og-fonts.ts index c5a6a5a..4ac65dd 100644 --- a/src/lib/og-fonts.ts +++ b/src/lib/og-fonts.ts @@ -33,7 +33,10 @@ export function loadOgFonts(origin: string): Promise { style: 'normal' as const }; }) - ); + ).catch((error: unknown) => { + fontsPromise = null; + throw error; + }); } return fontsPromise; From 9261981bf1ce5093a90601fbca36f95fce102ff2 Mon Sep 17 00:00:00 2001 From: Beast Date: Tue, 13 Oct 2026 00:10:53 +0700 Subject: [PATCH 4/4] fix: issue cf function --- src/lib/og-template.ts | 112 ++++++++++++++++++++++++++++++++++++ src/lib/og-template.tsx | 115 ------------------------------------- src/lib/qtc-market.test.ts | 2 +- 3 files changed, 113 insertions(+), 116 deletions(-) create mode 100644 src/lib/og-template.ts delete mode 100644 src/lib/og-template.tsx 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/og-template.tsx b/src/lib/og-template.tsx deleted file mode 100644 index 6a31628..0000000 --- a/src/lib/og-template.tsx +++ /dev/null @@ -1,115 +0,0 @@ -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; - -export function buildOgElement(quote: QtcMarketQuote) { - const price = formatUsdPrice(quote.priceUsd); - const marketCap = formatUsdMarketCap(quote.marketCapUsd); - - return ( -
-
- Quantus Explorer -
- -
-
- QTC Price -
-
- {price} -
-
- -
-
- Market Cap -
-
- {marketCap} -
-
-
- ); -} diff --git a/src/lib/qtc-market.test.ts b/src/lib/qtc-market.test.ts index 68514d3..4678efc 100644 --- a/src/lib/qtc-market.test.ts +++ b/src/lib/qtc-market.test.ts @@ -147,7 +147,7 @@ describe('Open Graph tags', () => { it('draws the card with the explorer flare, not the tracker accent', () => { const template = fs.readFileSync( - path.join(process.cwd(), 'src/lib/og-template.tsx'), + path.join(process.cwd(), 'src/lib/og-template.ts'), 'utf8' );