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
12 changes: 11 additions & 1 deletion frontend/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -218,11 +218,21 @@
},
"walletSelector": {
"chooseWallet": "Choose a wallet",
"description": "Connect your Stellar wallet to complete this payment.",
"connecting": "Connecting...",
"walletConnectWaiting": "Waiting for wallet...",
"noProjectId": "(no project ID)",
"notInstalled": "(not installed)",
"tapToConnect": "Click to connect",
"scanTitle": "Scan with your compatible wallet app",
"pairingFailed": "WalletConnect pairing failed"
"scanDescription": "Scan with Freighter mobile or any WalletConnect-compatible wallet",
"pairingFailed": "WalletConnect pairing failed",
"userRejected": "Connection request was canceled. Please approve it in your wallet to continue.",
"walletConnectUnavailable": "WalletConnect is unavailable right now. Try Freighter or enable a valid WalletConnect project ID.",
"extensionNotFound": "We couldn't find that wallet on this device. Install the extension and try again.",
"noAccountFound": "Your wallet connected, but no Stellar account was returned. Open a Stellar account in the wallet and retry.",
"walletConnectFailed": "We couldn't finish the WalletConnect handshake. Please try again.",
"connectionFailed": "We couldn't connect to that wallet right now. Please try again."
},
"checkout": {
"paymentRequest": "Payment Request",
Expand Down
12 changes: 11 additions & 1 deletion frontend/messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -218,11 +218,21 @@
},
"walletSelector": {
"chooseWallet": "Elige una billetera",
"description": "Conecta tu billetera Stellar para completar este pago.",
"connecting": "Conectando...",
"walletConnectWaiting": "Esperando la billetera...",
"noProjectId": "(sin project ID)",
"notInstalled": "(no instalada)",
"tapToConnect": "Haz clic para conectar",
"scanTitle": "Escanea con tu app de billetera Stellar",
"pairingFailed": "La vinculacion de WalletConnect fallo"
"scanDescription": "Escanea con Freighter móvil o cualquier billetera compatible con WalletConnect",
"pairingFailed": "La vinculacion de WalletConnect fallo",
"userRejected": "La solicitud de conexión fue cancelada. Apruébala en tu billetera para continuar.",
"walletConnectUnavailable": "WalletConnect no está disponible en este momento. Prueba con Freighter o habilita un WalletConnect project ID válido.",
"extensionNotFound": "No pudimos encontrar esa billetera en este dispositivo. Instala la extensión e inténtalo de nuevo.",
"noAccountFound": "La billetera se conectó, pero no devolvió ninguna cuenta Stellar. Abre una cuenta Stellar en la billetera y vuelve a intentarlo.",
"walletConnectFailed": "No pudimos completar la conexión con WalletConnect. Inténtalo de nuevo.",
"connectionFailed": "No pudimos conectar esa billetera en este momento. Inténtalo de nuevo."
},
"checkout": {
"paymentRequest": "Solicitud de pago",
Expand Down
12 changes: 11 additions & 1 deletion frontend/messages/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -218,11 +218,21 @@
},
"walletSelector": {
"chooseWallet": "Escolha uma carteira",
"description": "Conecte sua carteira Stellar para concluir este pagamento.",
"connecting": "Conectando...",
"walletConnectWaiting": "Aguardando a carteira...",
"noProjectId": "(sem project ID)",
"notInstalled": "(nao instalada)",
"tapToConnect": "Clique para conectar",
"scanTitle": "Escaneie com seu app de carteira Stellar",
"pairingFailed": "Falha no pareamento do WalletConnect"
"scanDescription": "Escaneie com o Freighter mobile ou qualquer carteira compatível com WalletConnect",
"pairingFailed": "Falha no pareamento do WalletConnect",
"userRejected": "A solicitação de conexão foi cancelada. Aprove-a na carteira para continuar.",
"walletConnectUnavailable": "O WalletConnect não está disponível agora. Tente usar o Freighter ou habilite um WalletConnect project ID válido.",
"extensionNotFound": "Não encontramos essa carteira neste dispositivo. Instale a extensão e tente novamente.",
"noAccountFound": "A carteira conectou, mas não retornou nenhuma conta Stellar. Abra uma conta Stellar na carteira e tente novamente.",
"walletConnectFailed": "Não conseguimos concluir a conexão com o WalletConnect. Tente novamente.",
"connectionFailed": "Não conseguimos conectar essa carteira agora. Tente novamente."
},
"checkout": {
"paymentRequest": "Solicitacao de pagamento",
Expand Down
27 changes: 18 additions & 9 deletions frontend/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,24 @@ const createNextIntlPlugin = require("next-intl/plugin");
const withSentryConfig = (config) => config;
const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts");

const withPWA = require("@ducanh2912/next-pwa").default({
dest: "public",
disable: process.env.NODE_ENV === "development",
register: true,
skipWaiting: true,
fallbacks: {
offline: "/offline",
},
});
let withPWA = (config) => config;

try {
withPWA = require("@ducanh2912/next-pwa").default({
dest: "public",
disable: process.env.NODE_ENV === "development",
register: true,
skipWaiting: true,
fallbacks: {
offline: "/offline",
},
});
} catch (error) {
console.warn(
"next-pwa is unavailable; continuing without PWA support.",
error?.message ?? error,
);
}

const nextConfig = {
reactStrictMode: true,
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/app/pay/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,14 @@ export default function PaymentPage() {
{/* CTA */}
{!isSettled && !isFailed && (
<div className="flex flex-col gap-3">
<div className="rounded-xl border border-[#E8E8E8] bg-[#F9F9F9] p-4">
<p className="text-[10px] font-bold uppercase tracking-widest text-[#6B6B6B] mb-2">
{t("completePayment")}
</p>
<p className="text-sm text-[#6B6B6B]">
{payment.description ?? t("paymentRequest")}
</p>
</div>
{activeProvider ? (
<>
<p className="text-center text-[10px] text-[#6B6B6B] font-medium">{t("connectedVia", { provider: activeProvider.name ?? "" })}</p>
Expand Down
22 changes: 11 additions & 11 deletions frontend/src/components/AnalyticsCards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export default function AnalyticsCards() {

if (loading || !hydrated) {
return (
<div className="grid gap-6 sm:grid-cols-3">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3">
{[1, 2, 3].map((i) => (
<div key={i} className="h-32 rounded-lg bg-[#F5F5F5] animate-pulse" />
))}
Expand All @@ -93,11 +93,11 @@ export default function AnalyticsCards() {
}

return (
<div className="grid gap-6 sm:grid-cols-3">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3">
{/* Total Volume */}
<div className="rounded-lg border border-[#E8E8E8] bg-white p-6 transition-all hover:bg-[#F9F9F9]">
<div className="flex flex-col gap-1">
<p className="text-[clamp(28px,4vw,48px)] font-bold tracking-tight text-[#0A0A0A]">
<div className="min-w-0 overflow-hidden rounded-lg border border-[#E8E8E8] bg-white p-5 transition-all hover:bg-[#F9F9F9] sm:p-6">
<div className="flex min-w-0 flex-col gap-1">
<p className="break-words text-[clamp(26px,8vw,48px)] font-bold leading-none tracking-tight text-[#0A0A0A]">
{formatAmount(totalVolume, locale, hideCents)}
</p>
<p className="text-xs font-medium text-[#6B6B6B] uppercase tracking-wider">
Expand All @@ -107,9 +107,9 @@ export default function AnalyticsCards() {
</div>

{/* Success Rate */}
<div className="rounded-lg border border-[#E8E8E8] bg-white p-6 transition-all hover:bg-[#F9F9F9]">
<div className="flex flex-col gap-1">
<p className="text-[clamp(28px,4vw,48px)] font-bold tracking-tight text-[#0A0A0A]">
<div className="min-w-0 overflow-hidden rounded-lg border border-[#E8E8E8] bg-white p-5 transition-all hover:bg-[#F9F9F9] sm:p-6">
<div className="flex min-w-0 flex-col gap-1">
<p className="break-words text-[clamp(26px,8vw,48px)] font-bold leading-none tracking-tight text-[#0A0A0A]">
{successRate.toFixed(1)}%
</p>
<p className="text-xs font-medium text-[#6B6B6B] uppercase tracking-wider">
Expand All @@ -119,9 +119,9 @@ export default function AnalyticsCards() {
</div>

{/* Active Intents */}
<div className="rounded-lg border border-[#E8E8E8] bg-white p-6 transition-all hover:bg-[#F9F9F9]">
<div className="flex flex-col gap-1">
<p className="text-[clamp(28px,4vw,48px)] font-bold tracking-tight text-[#0A0A0A]">
<div className="min-w-0 overflow-hidden rounded-lg border border-[#E8E8E8] bg-white p-5 transition-all hover:bg-[#F9F9F9] sm:p-6">
<div className="flex min-w-0 flex-col gap-1">
<p className="break-words text-[clamp(26px,8vw,48px)] font-bold leading-none tracking-tight text-[#0A0A0A]">
{activeIntents}
</p>
<p className="text-xs font-medium text-[#6B6B6B] uppercase tracking-wider">
Expand Down
72 changes: 63 additions & 9 deletions frontend/src/components/ApiUsageChart.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { useEffect, useState } from "react";
import { useLocale } from "next-intl";
import {
BarChart,
Bar,
Expand All @@ -10,6 +11,7 @@ import {
Tooltip,
ResponsiveContainer,
} from "recharts";
import { localeToLanguageTag } from "@/i18n/config";
import { useMerchantApiKey } from "@/lib/merchant-store";

const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000";
Expand All @@ -21,9 +23,24 @@ interface DailyUsage {

export default function ApiUsageChart() {
const apiKey = useMerchantApiKey();
const locale = localeToLanguageTag(useLocale());
const [data, setData] = useState<DailyUsage[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [isCompact, setIsCompact] = useState(false);

useEffect(() => {
const mediaQuery = window.matchMedia("(max-width: 640px)");
const syncCompactState = (event?: MediaQueryList | MediaQueryListEvent) => {
setIsCompact(event?.matches ?? mediaQuery.matches);
};

syncCompactState(mediaQuery);

const listener = (event: MediaQueryListEvent) => syncCompactState(event);
mediaQuery.addEventListener("change", listener);
return () => mediaQuery.removeEventListener("change", listener);
}, []);

useEffect(() => {
if (!apiKey) {
Expand Down Expand Up @@ -86,10 +103,21 @@ export default function ApiUsageChart() {
}

const totalRequests = data.reduce((sum, day) => sum + day.requests, 0);
const formatTickLabel = (value: string) =>
new Intl.DateTimeFormat(locale, {
month: "short",
day: isCompact ? undefined : "numeric",
}).format(new Date(value));
const formatTooltipLabel = (value: string) =>
new Intl.DateTimeFormat(locale, {
month: "short",
day: "numeric",
year: "numeric",
}).format(new Date(value));

return (
<div className="rounded-xl border border-white/10 bg-white/5 p-6">
<div className="mb-6 flex items-center justify-between">
<div className="min-w-0 rounded-xl border border-white/10 bg-white/5 p-4 sm:p-6">
<div className="mb-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<h3 className="text-lg font-semibold text-white">
API Usage - Last 7 Days
Expand All @@ -105,30 +133,56 @@ export default function ApiUsageChart() {
<p className="text-sm text-slate-400">No API usage data available</p>
</div>
) : (
<ResponsiveContainer width="100%" height={300}>
<BarChart data={data}>
<div className="-mx-2 sm:mx-0">
<ResponsiveContainer width="100%" height={isCompact ? 240 : 300}>
<BarChart
data={data}
margin={{
top: 8,
right: isCompact ? 8 : 16,
left: isCompact ? -20 : -8,
bottom: isCompact ? 0 : 8,
}}
>
<CartesianGrid
strokeDasharray="3 3"
stroke="rgba(255,255,255,0.1)"
/>
<XAxis
dataKey="date"
stroke="#94a3b8"
style={{ fontSize: "12px" }}
axisLine={false}
tickLine={false}
tickMargin={10}
interval="preserveStartEnd"
minTickGap={isCompact ? 28 : 16}
tickFormatter={formatTickLabel}
style={{ fontSize: isCompact ? "10px" : "12px" }}
/>
<YAxis
stroke="#94a3b8"
axisLine={false}
tickLine={false}
width={isCompact ? 32 : 44}
allowDecimals={false}
tickFormatter={(value: number) => value.toLocaleString(locale, { notation: "compact" })}
style={{ fontSize: isCompact ? "10px" : "12px" }}
/>
<YAxis stroke="#94a3b8" style={{ fontSize: "12px" }} />
<Tooltip
contentStyle={{
backgroundColor: "rgba(0, 0, 0, 0.9)",
border: "1px solid rgba(255, 255, 255, 0.1)",
borderRadius: "8px",
color: "#fff",
}}
formatter={(value: number) => [value.toLocaleString(locale), "Requests"]}
labelFormatter={formatTooltipLabel}
labelStyle={{ color: "#94a3b8" }}
/>
<Bar dataKey="requests" fill="#5ef2c0" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
<Bar dataKey="requests" fill="#5ef2c0" radius={[4, 4, 0, 0]} maxBarSize={isCompact ? 22 : 32} />
</BarChart>
</ResponsiveContainer>
</div>
)}
</div>
);
Expand Down
20 changes: 14 additions & 6 deletions frontend/src/components/LocaleSwitcher.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,44 @@
"use client";

import { useTransition } from "react";
import { useRouter } from "next/navigation";
import { useSearchParams } from "next/navigation";
import { useLocale, useTranslations } from "next-intl";
import {
localeCookieMaxAge,
localeCookieName,
type AppLocale,
locales,
resolveAppLocale,
type AppLocale,
} from "@/i18n/config";
import { usePathname, useRouter } from "@/i18n/navigation";

interface LocaleSwitcherProps {
className?: string;
}

const COOKIE_TTL_SECONDS = 60 * 60 * 24 * 365;

export default function LocaleSwitcher({
className = "",
}: LocaleSwitcherProps) {
const t = useTranslations("localeSwitcher");
const locale = resolveAppLocale(useLocale());
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const [isPending, startTransition] = useTransition();

const handleChange = (nextLocale: AppLocale) => {
if (nextLocale === locale) return;

document.cookie = `${localeCookieName}=${nextLocale}; path=/; max-age=${COOKIE_TTL_SECONDS}; samesite=lax`;
document.cookie = `${localeCookieName}=${nextLocale}; path=/; max-age=${localeCookieMaxAge}; samesite=lax`;

const query = searchParams.toString();
const href = query ? `${pathname}?${query}` : pathname;

startTransition(() => {
router.refresh();
router.replace(href, {
locale: nextLocale,
scroll: false,
});
});
};

Expand Down
Loading
Loading