Skip to content
13 changes: 3 additions & 10 deletions components/NavHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"use client";
import { useState, useEffect, useCallback, useRef } from "react";
import { useState, useEffect } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import NetworkSelector from "@/components/NetworkSelector";
Expand All @@ -8,10 +8,10 @@ import ThemeToggle from "@/components/ThemeToggle";
import ChangelogModal, { useChangelogUnread } from "@/components/ChangelogModal";
import NotificationBadge from "@/components/NotificationBadge";
import GlobalSearch from "@/components/GlobalSearch";
import WalletBalanceDisplay from "@/components/WalletBalanceDisplay";
import { useNotifications } from "@/src/context/NotificationContext";
import { useSettings } from "@/src/context/SettingsContext";
import { useWallet } from "@/src/context/WalletContext";
import { APP_NETWORK } from "@/src/lib/freighter";
import { useTranslations } from "@/src/lib/i18n";
import { useGlobalShortcuts } from "@/components/GlobalShortcuts";
import RpcHealthIndicator from "@/components/RpcHealthIndicator";
Expand All @@ -28,19 +28,12 @@ const NAV_LINKS = [
{ href: "/settings", key: "settings" },
] as const;

const HORIZON_URL =
APP_NETWORK === "public" || APP_NETWORK === "mainnet"
? "https://horizon.stellar.org"
: APP_NETWORK === "futurenet"
? "https://horizon-futurenet.stellar.org"
: "https://horizon-testnet.stellar.org";

export default function NavHeader() {
const t = useTranslations("nav");
const pathname = usePathname();
const [scrolled, setScrolled] = useState(false);
const { countFor, clearSection } = useNotifications();
const { showUsd, toggleShowUsd, language } = useSettings();
const { showUsd, toggleShowUsd } = useSettings();
const { address, balanceRefreshTrigger } = useWallet();
const { network } = useNetwork();
const [xlmBalance, setXlmBalance] = useState<string | null>(null);
Expand Down
95 changes: 95 additions & 0 deletions components/PollingIndicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"use client";

import { useState, useEffect } from "react";
import { useTranslations } from "@/src/lib/i18n";

interface PollingIndicatorProps {
lastRefreshTime: number | null;
isLoading?: boolean;
onManualRefresh?: () => void;
pollIntervalMs?: number;
}

export default function PollingIndicator({
lastRefreshTime,
isLoading = false,
onManualRefresh,
pollIntervalMs = 30000,
}: PollingIndicatorProps) {
const t = useTranslations("dashboard");
const [secondsUntilNext, setSecondsUntilNext] = useState(0);
const [now, setNow] = useState<number>(Date.now());

useEffect(() => {
const timer = setInterval(() => setNow(Date.now()), 1000);
return () => clearInterval(timer);
}, []);

useEffect(() => {
if (!lastRefreshTime) {
setSecondsUntilNext(0);
return;
}

const timeUntilNext = Math.max(
0,
Math.ceil((pollIntervalMs - (now - lastRefreshTime)) / 1000)
);
setSecondsUntilNext(timeUntilNext);
}, [now, lastRefreshTime, pollIntervalMs]);

const formatTime = (timestamp: number): string => {
const date = new Date(timestamp);
const hours = date.getHours().toString().padStart(2, "0");
const minutes = date.getMinutes().toString().padStart(2, "0");
const seconds = date.getSeconds().toString().padStart(2, "0");
return `${hours}:${minutes}:${seconds}`;
};

return (
<div className="flex items-center gap-3 text-xs text-gray-600 dark:text-gray-400">
{lastRefreshTime && (
<>
<span className="hidden sm:inline" title={new Date(lastRefreshTime).toLocaleString()}>
Last update: {formatTime(lastRefreshTime)}
</span>
<span className="text-gray-400 dark:text-gray-500 hidden sm:inline">•</span>
<span className="flex items-center gap-1" title={`Next automatic refresh in ${secondsUntilNext} seconds`}>
<span className={`inline-block w-2 h-2 rounded-full ${
isLoading ? "bg-yellow-400 animate-pulse" : "bg-green-400"
}`} aria-hidden="true" />
Next in {secondsUntilNext}s
</span>
</>
)}
{onManualRefresh && (
<button
onClick={onManualRefresh}
disabled={isLoading}
className="ml-1 px-2 py-1 rounded text-gray-600 dark:text-gray-400 border border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-green-500"
aria-label={t("manual_refresh") || "Refresh now"}
title={t("manual_refresh") || "Refresh now"}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={isLoading ? "animate-spin" : ""}
aria-hidden="true"
>
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" />
<path d="M3 21v-5h5" />
</svg>
</button>
)}
</div>
);
}
146 changes: 146 additions & 0 deletions components/StreamRateCalculator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
"use client";

import { useState, useEffect } from "react";
import { useTranslations } from "@/src/lib/i18n";

interface StreamRateCalculatorProps {
amount?: string;
duration?: number;
onRateCalculated?: (rate: number) => void;
}

const STROOPS_PER_UNIT = 10_000_000;

export default function StreamRateCalculator({
amount: initialAmount = "",
duration: initialDuration = 0,
onRateCalculated,
}: StreamRateCalculatorProps) {
const t = useTranslations("stream_new");
const [amount, setAmount] = useState(initialAmount);
const [duration, setDuration] = useState(initialDuration);
const [calculatedRate, setCalculatedRate] = useState<number | null>(null);
const [ratePerDay, setRatePerDay] = useState<number | null>(null);

// Recalculate rate whenever amount or duration changes
useEffect(() => {
if (amount && duration > 0) {
const amountNum = parseFloat(amount);
if (!isNaN(amountNum) && amountNum > 0) {
const totalStroops = amountNum * STROOPS_PER_UNIT;
const rate = totalStroops / duration;
setCalculatedRate(rate);
setRatePerDay(rate * 86400); // 86400 seconds in a day
onRateCalculated?.(rate);
return;
}
}
setCalculatedRate(null);
setRatePerDay(null);
onRateCalculated?.(0);
}, [amount, duration, onRateCalculated]);

const formatDuration = (seconds: number): string => {
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const mins = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;

const parts = [];
if (days > 0) parts.push(`${days}d`);
if (hours > 0) parts.push(`${hours}h`);
if (mins > 0) parts.push(`${mins}m`);
if (secs > 0 || parts.length === 0) parts.push(`${secs}s`);

return parts.join(" ");
};

return (
<div className="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4 space-y-4">
<div className="flex items-center gap-2 mb-3">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-green-600 dark:text-green-400"
aria-hidden="true"
>
<path d="M12 2v20M2 12h20" />
</svg>
<h3 className="text-sm font-semibold text-gray-900 dark:text-white">
Stream Rate Calculator
</h3>
</div>

{/* Calculator display - read-only summary */}
{calculatedRate !== null && amount && duration > 0 && (
<div className="bg-white dark:bg-gray-700 rounded-lg p-3 space-y-2 border border-green-200 dark:border-green-800">
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<div className="text-gray-600 dark:text-gray-400 text-xs font-medium">
Total Amount
</div>
<div className="text-base font-semibold text-gray-900 dark:text-white">
{parseFloat(amount).toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 7,
})}{" "}
units
</div>
</div>
<div>
<div className="text-gray-600 dark:text-gray-400 text-xs font-medium">
Duration
</div>
<div className="text-base font-semibold text-gray-900 dark:text-white">
{formatDuration(duration)}
</div>
</div>
</div>

<div className="border-t border-gray-200 dark:border-gray-600 pt-2 mt-2">
<div className="mb-2">
<div className="text-gray-600 dark:text-gray-400 text-xs font-medium">
Flow Rate
</div>
<div className="text-lg font-bold text-green-600 dark:text-green-400 font-mono">
{calculatedRate.toFixed(0)} stroops/second
</div>
</div>

<div>
<div className="text-gray-600 dark:text-gray-400 text-xs font-medium">
Flow Rate Per Day
</div>
<div className="text-sm font-mono text-gray-700 dark:text-gray-300">
{ratePerDay?.toLocaleString(undefined, {
minimumFractionDigits: 0,
maximumFractionDigits: 0,
})}{" "}
stroops/day
</div>
</div>
</div>

<div className="text-xs text-gray-500 dark:text-gray-400 pt-2">
This calculation helps you set up your stream with the correct amount. The flow rate
above is automatically calculated based on your total amount and duration.
</div>
</div>
)}

{/* Empty state when no values */}
{(!calculatedRate || !amount || duration <= 0) && (
<div className="bg-white dark:bg-gray-700 rounded-lg p-4 text-center text-sm text-gray-500 dark:text-gray-400">
<p>Enter amount and duration to calculate the stream rate</p>
</div>
)}
</div>
);
}
Loading