Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions bun.lock

Large diffs are not rendered by default.

73 changes: 73 additions & 0 deletions functions/api/og.ts
Original file line number Diff line number Diff line change
@@ -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<unknown>) => void;
}): Promise<Response> {
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' }
});
}
}
23 changes: 23 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,29 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/favicon.ico?v=2" />
<meta name="theme-color" content="#0E0E0E" />
<title>Quantus Explorer</title>
<meta
name="description"
content="Quantus Explorer: Discover, analyze, and track real-time transactions, blocks, and accounts on the Quantus blockchain network. Stay informed with our powerful explorer."
/>
<meta property="og:type" content="website" />
<meta property="og:title" content="Quantus Explorer" />
<meta
property="og:description"
content="Quantus Explorer: Discover, analyze, and track real-time transactions, blocks, and accounts on the Quantus blockchain network. Stay informed with our powerful explorer."
/>
<meta property="og:url" content="https://explorer.quantus.com" />
<meta property="og:image" content="https://explorer.quantus.com/api/og" />
<meta property="og:image:alt" content="Quantus Explorer" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Quantus Explorer" />
<meta
name="twitter:description"
content="Quantus Explorer: Discover, analyze, and track real-time transactions, blocks, and accounts on the Quantus blockchain network. Stay informed with our powerful explorer."
/>
<meta name="twitter:image" content="https://explorer.quantus.com/api/og" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="manifest" href="/manifest.json" />
</head>
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Binary file added public/fonts/geist-latin-400-normal.woff
Binary file not shown.
Binary file added public/fonts/geist-latin-500-normal.woff
Binary file not shown.
Binary file added public/fonts/geist-mono-latin-500-normal.woff
Binary file not shown.
8 changes: 4 additions & 4 deletions src/__generated__/gql.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 0 additions & 30 deletions src/__generated__/graphql.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 0 additions & 6 deletions src/api/wormhole.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 18 additions & 4 deletions src/components/features/landing/hero/chain-stats/ChainStats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)';
Expand All @@ -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 (
<Card>
<CardHeader>
Expand All @@ -42,9 +49,7 @@ const SupplyStatCard = ({
<CardContent>
{loading && <Skeleton className="h-7 w-28" />}
{!loading && error && <InlineFetchError error={error} />}
{!loading && !error && value != null && (
<p>{formatChainSupply(value)}</p>
)}
{!loading && !error && text != null && <p>{text}</p>}
</CardContent>
</Card>
);
Expand All @@ -67,6 +72,7 @@ export const ChainStats: React.FC<ChainStatsProps> = () => {
maxSupply,
circulatingSupply
} = useChainStats();
const price = useQtcPrice();

const errorMessage = error?.message ?? null;

Expand Down Expand Up @@ -121,7 +127,15 @@ export const ChainStats: React.FC<ChainStatsProps> = () => {
/>
</CardGroup>

<CardGroup className="grid-cols-1 sm:grid-cols-2">
<CardGroup className="grid-cols-1 sm:grid-cols-3">
<SupplyStatCard
label="QTC Price"
loading={price.loading}
error={price.error}
formatted={
price.priceUsd != null ? formatUsdPrice(price.priceUsd) : undefined
}
/>
<SupplyStatCard
label="Max Supply"
loading={loading}
Expand Down
43 changes: 43 additions & 0 deletions src/components/features/landing/hero/chain-stats/use-qtc-price.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useEffect, useState } from 'react';

import {
DEV_QTC_PRICE_USD,
loadQtcPrice,
shouldFetchQtcPrice
} from '@/lib/qtc-market';

type QtcPriceState = {
loading: boolean;
error: string | null;
priceUsd: number | null;
};

export function useQtcPrice(): QtcPriceState {
const isDev = import.meta.env.DEV;
const [state, setState] = useState<QtcPriceState>(() =>
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;
}
28 changes: 0 additions & 28 deletions src/components/features/wormhole/PrivacyScoreBadge.tsx

This file was deleted.

Loading
Loading