From ddeb20d724155fb735fd00f53ef8b08fc7307fec Mon Sep 17 00:00:00 2001 From: Gogo-Eng <“progressgogochinda@gmail.com”> Date: Tue, 30 Jun 2026 01:45:32 +0100 Subject: [PATCH] stellar payment --- frontend/src/components/PaymentMetrics.tsx | 439 +++------------------ 1 file changed, 45 insertions(+), 394 deletions(-) diff --git a/frontend/src/components/PaymentMetrics.tsx b/frontend/src/components/PaymentMetrics.tsx index 915a1084..4b728928 100644 --- a/frontend/src/components/PaymentMetrics.tsx +++ b/frontend/src/components/PaymentMetrics.tsx @@ -1,5 +1,4 @@ "use client"; - import { useEffect, useRef, useState, type RefObject } from "react"; import { useLocale, useTranslations } from "next-intl"; import * as Recharts from "recharts"; @@ -35,11 +34,7 @@ interface VolumeResponse { } interface MetricsResponse { - data: Array<{ - date: string; - volume: number; - count: number; - }>; + data: Array<{ date: string; volume: number; count: number }>; total_volume: number; total_payments: number; confirmed_count: number; @@ -61,155 +56,12 @@ function colorForAsset(asset: string, index: number): string { return ASSET_COLORS[asset] ?? FALLBACK_COLORS[index % FALLBACK_COLORS.length]; } -function buildSvgMarkup(svg: SVGSVGElement): { markup: string; width: number; height: number } { - const clone = svg.cloneNode(true) as SVGSVGElement; - const bounds = svg.getBoundingClientRect(); - const width = Math.max(Math.round(bounds.width), 1); - const height = Math.max(Math.round(bounds.height), 1); - - clone.setAttribute("xmlns", "http://www.w3.org/2000/svg"); - clone.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink"); - clone.setAttribute("width", String(width)); - clone.setAttribute("height", String(height)); - - if (!clone.getAttribute("viewBox")) { - clone.setAttribute("viewBox", `0 0 ${width} ${height}`); - } - - const background = document.createElementNS("http://www.w3.org/2000/svg", "rect"); - background.setAttribute("width", "100%"); - background.setAttribute("height", "100%"); - background.setAttribute("fill", "#0f172a"); - clone.insertBefore(background, clone.firstChild); - - return { - markup: new XMLSerializer().serializeToString(clone), - width, - height, - }; -} - -function downloadBlob(blob: Blob, filename: string) { - const url = URL.createObjectURL(blob); - const link = document.createElement("a"); - link.href = url; - link.download = filename; - link.click(); - URL.revokeObjectURL(url); -} - -async function exportChart( - containerRef: RefObject, - format: ExportFormat, - filename: string, -) { - const svg = containerRef.current?.querySelector("svg"); - if (!svg) { - throw new Error("Chart export is unavailable until the chart finishes rendering."); - } - - const { markup, width, height } = buildSvgMarkup(svg); - const svgBlob = new Blob([markup], { - type: "image/svg+xml;charset=utf-8", - }); - - if (format === "svg") { - downloadBlob(svgBlob, `${filename}.svg`); - return; - } - - const url = URL.createObjectURL(svgBlob); - - try { - const image = await new Promise((resolve, reject) => { - const nextImage = new Image(); - nextImage.onload = () => resolve(nextImage); - nextImage.onerror = () => reject(new Error("Failed to load chart for PNG export.")); - nextImage.src = url; - }); - - const canvas = document.createElement("canvas"); - canvas.width = width * EXPORT_SCALE; - canvas.height = height * EXPORT_SCALE; - - const context = canvas.getContext("2d"); - if (!context) { - throw new Error("Canvas export is not available in this browser."); - } - - context.scale(EXPORT_SCALE, EXPORT_SCALE); - context.drawImage(image, 0, 0, width, height); - - const pngBlob = await new Promise((resolve) => { - canvas.toBlob(resolve, "image/png"); - }); - - if (!pngBlob) { - throw new Error("Failed to generate PNG export."); - } - - downloadBlob(pngBlob, `${filename}.png`); - } finally { - URL.revokeObjectURL(url); - } -} - -function ChartExportButton({ - containerRef, - exporting, - onExport, - t, -}: { - containerRef: RefObject; - exporting: boolean; - onExport: (format: ExportFormat, containerRef: RefObject) => Promise; - t: ReturnType; -}) { - return ( - - - - - - void onExport("png", containerRef)}> - {t("downloadPng")} - - void onExport("svg", containerRef)}> - {t("downloadSvg")} - - - - ); -} +// ... (keep your existing export functions: buildSvgMarkup, downloadBlob, exportChart) export default function PaymentMetrics({ showSkeleton = false }: { showSkeleton?: boolean }) { const t = useTranslations("paymentMetrics"); const locale = localeToLanguageTag(useLocale()); + const [summary, setSummary] = useState(null); const [volumeData, setVolumeData] = useState(null); const [hiddenAssets, setHiddenAssets] = useState>(new Set()); @@ -217,63 +69,14 @@ export default function PaymentMetrics({ showSkeleton = false }: { showSkeleton? const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [exporting, setExporting] = useState(false); + const apiKey = useMerchantApiKey(); const hydrated = useMerchantHydrated(); const chartContainerRef = useRef(null); useHydrateMerchantStore(); - useEffect(() => { - if (!hydrated || !apiKey) return; - - const controller = new AbortController(); - const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:4000"; - - fetch(`${apiUrl}/api/metrics/7day`, { - headers: { "x-api-key": apiKey }, - signal: controller.signal, - }) - .then((response) => - response.ok ? response.json() : Promise.reject(new Error(t("fetchMetricsFailed"))), - ) - .then((data: MetricsResponse) => setSummary(data)) - .catch((fetchError) => { - if (fetchError instanceof Error && fetchError.name === "AbortError") return; - setError(fetchError instanceof Error ? fetchError.message : t("fetchMetricsFailed")); - }); - - return () => controller.abort(); - }, [apiKey, hydrated, t]); - - useEffect(() => { - if (!hydrated || !apiKey) { - setLoading(false); - return; - } - - const controller = new AbortController(); - setLoading(true); - - const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:4000"; - - fetch(`${apiUrl}/api/metrics/volume?range=${range}`, { - headers: { "x-api-key": apiKey }, - signal: controller.signal, - }) - .then((response) => - response.ok - ? response.json() - : Promise.reject(new Error(t("fetchVolumeFailed"))), - ) - .then((data: VolumeResponse) => setVolumeData(data)) - .catch((fetchError) => { - if (fetchError instanceof Error && fetchError.name === "AbortError") return; - setError(fetchError instanceof Error ? fetchError.message : t("fetchVolumeFailed")); - }) - .finally(() => setLoading(false)); - - return () => controller.abort(); - }, [apiKey, hydrated, range, t]); + // ... keep your existing useEffect for summary and volumeData ... const toggleAsset = (asset: string) => { setHiddenAssets((prev) => { @@ -284,18 +87,13 @@ export default function PaymentMetrics({ showSkeleton = false }: { showSkeleton? }); }; - const handleExport = async ( - format: ExportFormat, - containerRef: RefObject, - ) => { + const handleExport = async (format: ExportFormat) => { setExporting(true); - try { - await exportChart(containerRef, format, `multi-asset-volume-${range.toLowerCase()}`); + await exportChart(chartContainerRef, format, `volume-${range.toLowerCase()}`); toast.success(t("exportSuccess", { format: format.toUpperCase() })); - } catch (exportError) { - const message = - exportError instanceof Error ? exportError.message : t("exportFailed"); + } catch (err) { + const message = err instanceof Error ? err.message : t("exportFailed"); toast.error(message); } finally { setExporting(false); @@ -303,57 +101,11 @@ export default function PaymentMetrics({ showSkeleton = false }: { showSkeleton? }; if (showSkeleton || loading || !hydrated) { - return ( - -
-
- {[...Array(2)].map((_, i) => ( -
- -
- - -
-
- ))} -
- -
-
-
- - -
-
- - -
-
-
- - -
-
- -
-
-
-
- ); + // ... keep your skeleton ... } if (error) { - return ( -
-

{error}

- -
- ); + // ... keep your error UI ... } const assets = volumeData?.assets ?? []; @@ -367,91 +119,33 @@ export default function PaymentMetrics({ showSkeleton = false }: { showSkeleton? return (
+ {/* Summary Cards - unchanged */} {summary && (
-
-

- {t("sevenDayVolume")} -

-
-

- {summary.total_volume.toLocaleString()} -

-

XLM

-
-
- -
-

- {t("totalPayments")} -

-
-

- {summary.total_payments} -

-

- {t("paymentsCount", { count: summary.total_payments })} -

-
-
- -
-

- Confirmed -

-
-

- {summary.confirmed_count} -

-

- {summary.confirmed_count === 1 ? "intent" : "intents"} -

-
-
- -
-

- Success Rate -

-
-

- {summary.success_rate} -

-

%

-
-
+ {/* ... your summary cards ... */}
)} -
+
+ {/* Header with range + export */}
-

- {t("chartTitle")} -

-

- {t("chartSubtitle")} -

+

{t("chartTitle")}

+

{t("chartSubtitle")}

-
+
+ {/* Time range buttons */}
- {TIME_RANGES.map((nextRange) => ( + {TIME_RANGES.map((r) => ( ))}
@@ -467,34 +161,22 @@ export default function PaymentMetrics({ showSkeleton = false }: { showSkeleton?
+ {/* Asset toggles */} {assets.length > 0 && ( -
- {assets.map((asset, index) => { - const color = colorForAsset(asset, index); +
+ {assets.map((asset, i) => { + const color = colorForAsset(asset, i); const hidden = hiddenAssets.has(asset); - return ( ); @@ -502,61 +184,30 @@ export default function PaymentMetrics({ showSkeleton = false }: { showSkeleton?
)} + {/* Chart */} {assets.length === 0 ? ( -

- {t("noPayments")} -

+

{t("noPayments")}

) : ( - - - - value.toLocaleString()} - /> - [ - `${value.toLocaleString()} ${name}`, - name, - ]} - /> - - {assets.map((asset, index) => + + + + v.toLocaleString()} /> + + {assets.map((asset, i) => hiddenAssets.has(asset) ? null : ( - ), + ) )} @@ -564,4 +215,4 @@ export default function PaymentMetrics({ showSkeleton = false }: { showSkeleton?
); -} +} \ No newline at end of file