- {[
- { Icon: Search, label: "Search" },
- { Icon: Bell, label: "Notifications" },
- { Icon: HelpCircle, label: "Help" },
- ].map(({ Icon, label }) => (
-
- ))}
-
- {/* Feature flag admin — dev/admin only */}
- {isDev && (
-
- )}
-
- {/* Avatar */}
-
+ {/* Left: heading + subtitle */}
+
+
+ Welcome back, Alex
+
+
+ Here's your financial overview
+
+
+
+ {/* Right: icons + avatar */}
+
+ {[
+ { Icon: Search, label: "Search" },
+ { Icon: Bell, label: "Notifications" },
+ { Icon: HelpCircle, label: "Help" },
+ ].map(({ Icon, label }) => (
+
+
+
+ ))}
+
+ {/* Avatar */}
+
+ A
-
-
- {showFlagAdmin &&
setShowFlagAdmin(false)} />}
- >
+
+
);
};
diff --git a/frontend/app/context/FeatureFlagContext.tsx b/frontend/app/context/FeatureFlagContext.tsx
index af467fbfe..9a9d7b026 100644
--- a/frontend/app/context/FeatureFlagContext.tsx
+++ b/frontend/app/context/FeatureFlagContext.tsx
@@ -1,197 +1,20 @@
-"use client";
+// Stub Feature Flag Context for MVP
+import React, { createContext, useContext, ReactNode } from "react";
-import React, {
- createContext,
- useCallback,
- useContext,
- useEffect,
- useMemo,
- useRef,
- useState,
-} from "react";
-import {
- initFeatureFlags,
- isFeatureEnabled,
- getFeatureFlagValue,
- getAllFlags,
- updateFlag,
- createFlag,
- deleteFlag,
- setFlagUserContext,
- type FlagConfig,
- type FlagValue,
- type UserContext,
-} from "../lib/feature-flags";
-import { DEFAULT_FLAGS } from "../lib/flags.config";
+const FeatureFlagContext = createContext
>({});
-interface FeatureFlagContextValue {
- /** Check if a boolean/rollout flag is enabled */
- isEnabled: (flagKey: string) => boolean;
- /** Get the value of any flag type */
- getValue: (flagKey: string) => T;
- /** All flags (for admin UI) */
- flags: FlagConfig[];
- /** Whether flags are loading from API */
- isLoading: boolean;
- /** Update user context for targeting */
- setUserContext: (ctx: UserContext) => void;
- /** Admin: toggle a flag on/off */
- toggleFlag: (flagKey: string) => Promise;
- /** Admin: update a flag */
- updateFlagConfig: (flagKey: string, updates: Partial) => Promise;
- /** Admin: create a new flag */
- createFlagConfig: (flag: FlagConfig) => Promise;
- /** Admin: delete a flag */
- deleteFlagConfig: (flagKey: string) => Promise;
- /** Force re-evaluation after context/flag changes */
- refresh: () => void;
-}
-
-const FeatureFlagContext = createContext(null);
-
-export function FeatureFlagProvider({
- children,
- userContext,
-}: {
- children: React.ReactNode;
- userContext?: UserContext;
-}) {
- const [flags, setFlags] = useState([]);
- const [isLoading, setIsLoading] = useState(true);
- const [tick, setTick] = useState(0);
- const initDone = useRef(false);
-
- const refresh = useCallback(() => setTick((t) => t + 1), []);
-
- // Seed defaults and init
- useEffect(() => {
- if (initDone.current) return;
- initDone.current = true;
-
- // Seed defaults into the cache before API fetch
- DEFAULT_FLAGS.forEach((flag) => {
- if (!getAllFlags().find((f) => f.key === flag.key)) {
- // Only seed if not already in localStorage
- }
- });
-
- setIsLoading(true);
- initFeatureFlags(true).then(() => {
- // Merge defaults with any cached/API flags
- const loaded = getAllFlags();
- const merged = DEFAULT_FLAGS.map((def) => {
- const live = loaded.find((f) => f.key === def.key);
- return live ?? def;
- });
- // Add any API-only flags not in defaults
- const extra = loaded.filter(
- (f) => !DEFAULT_FLAGS.find((d) => d.key === f.key)
- );
- setFlags([...merged, ...extra]);
- setIsLoading(false);
- });
- }, []);
-
- // Sync user context when it changes (e.g. wallet connects)
- useEffect(() => {
- if (userContext) {
- setFlagUserContext(userContext);
- refresh();
- }
- }, [userContext?.address, userContext?.network, refresh]);
-
- const isEnabled = useCallback(
- (flagKey: string) => {
- // tick dependency ensures re-evaluation after flag changes
- void tick;
- return isFeatureEnabled(flagKey);
- },
- [tick]
- );
-
- const getValue = useCallback(
- (flagKey: string): T => {
- void tick;
- return getFeatureFlagValue(flagKey);
- },
- [tick]
- );
-
- const updateFlagConfig = useCallback(
- async (flagKey: string, updates: Partial) => {
- await updateFlag(flagKey, updates);
- setFlags(getAllFlags());
- refresh();
- },
- [refresh]
- );
-
- const toggleFlag = useCallback(
- async (flagKey: string) => {
- const current = flags.find((f) => f.key === flagKey);
- if (!current) return;
- await updateFlagConfig(flagKey, {
- enabled: !current.enabled,
- forceDisabled: false,
- });
- },
- [flags, updateFlagConfig]
- );
-
- const createFlagConfig = useCallback(
- async (flag: FlagConfig) => {
- await createFlag(flag);
- setFlags(getAllFlags());
- refresh();
- },
- [refresh]
- );
-
- const deleteFlagConfig = useCallback(
- async (flagKey: string) => {
- await deleteFlag(flagKey);
- setFlags(getAllFlags());
- refresh();
- },
- [refresh]
- );
-
- const value = useMemo(
- () => ({
- isEnabled,
- getValue,
- flags,
- isLoading,
- setUserContext: setFlagUserContext,
- toggleFlag,
- updateFlagConfig,
- createFlagConfig,
- deleteFlagConfig,
- refresh,
- }),
- [
- isEnabled,
- getValue,
- flags,
- isLoading,
- toggleFlag,
- updateFlagConfig,
- createFlagConfig,
- deleteFlagConfig,
- refresh,
- ]
- );
+export function FeatureFlagProvider({ children }: { children: ReactNode }) {
+ // All features enabled for MVP
+ const flags = {};
- return (
-
- {children}
-
- );
+ return (
+
+ {children}
+
+ );
}
-/** Access feature flags in any component */
-export function useFeatureFlags() {
- const ctx = useContext(FeatureFlagContext);
- if (!ctx) throw new Error("useFeatureFlags must be used within FeatureFlagProvider");
- return ctx;
+export function useFeatureFlag(flag: string): boolean {
+ const flags = useContext(FeatureFlagContext);
+ return flags[flag] ?? true; // Default to enabled
}
diff --git a/frontend/app/dashboard/DashboardProviders.tsx b/frontend/app/dashboard/DashboardProviders.tsx
index acfdf7060..8753e4e56 100644
--- a/frontend/app/dashboard/DashboardProviders.tsx
+++ b/frontend/app/dashboard/DashboardProviders.tsx
@@ -3,24 +3,10 @@
import React from "react";
import { FeatureFlagProvider } from "../context/FeatureFlagContext";
import { WalletProvider } from "../context/WalletContext";
+import { ThemeProvider } from "../context/ThemeContext";
+import { ToastProvider } from "../context/ToastContext";
import { OnboardingProvider } from "../context/OnboardingContext";
import { OnboardingWizard } from "../components/OnboardingWizard";
-import { useWallet } from "../context/WalletContext";
-
-function InnerDashboardProviders({
- children,
-}: {
- children: React.ReactNode;
-}) {
- const { address, network } = useWallet();
-
- return (
-
- {children}
-
-
- );
-}
/**
* Client-side providers for the dashboard.
@@ -32,10 +18,17 @@ export default function DashboardProviders({
children: React.ReactNode;
}) {
return (
-
-
- {children}
-
-
+
+
+
+
+
+ {children}
+
+
+
+
+
+
);
}
diff --git a/frontend/app/dashboard/analytics/AnalyticsComparisonGrid.tsx b/frontend/app/dashboard/analytics/AnalyticsComparisonGrid.tsx
deleted file mode 100644
index 1e707adfc..000000000
--- a/frontend/app/dashboard/analytics/AnalyticsComparisonGrid.tsx
+++ /dev/null
@@ -1,571 +0,0 @@
-"use client";
-
-import React, { useCallback, useMemo, useRef, useState } from "react";
-import {
- Bar,
- BarChart,
- Brush,
- CartesianGrid,
- ComposedChart,
- Legend,
- Line,
- LineChart,
- ResponsiveContainer,
- Tooltip,
- XAxis,
- YAxis,
-} from "recharts";
-import {
- ArrowUpRight,
- BarChart2,
- Download,
- Eye,
- EyeOff,
- Goal,
- Layers3,
- LineChart as LineIcon,
- Maximize2,
- Minimize2,
- Palette,
- TrendingUp,
-} from "lucide-react";
-import { useTheme } from "../../context/ThemeContext";
-import { colorSchemes } from "./useChartPreferences";
-import type { ColorScheme } from "./useChartPreferences";
-
-// ── Types ──────────────────────────────────────────────────────────────────
-
-type TooltipRecord = Record;
-type TooltipPayloadItem = {
- color?: string;
- dataKey?: string | number;
- name?: string;
- value?: number | string;
- payload: TooltipRecord;
-};
-
-type ChartPalette = {
- accent: string;
- accentAlt: string;
- accentSoft: string;
- success: string;
- violet: string;
- text: string;
- muted: string;
- grid: string;
- tooltipBg: string;
- tooltipBorder: string;
-};
-
-type CardChartType = "bar" | "line";
-
-type RenderProps = {
- showLegend: boolean;
- chartType: CardChartType;
- palette: ChartPalette;
- showBrush: boolean;
-};
-
-// ── Static data ────────────────────────────────────────────────────────────
-
-const monthComparisonData = [
- { metric: "Deposits", previous: 28.4, current: 32.6 },
- { metric: "Yield", previous: 6.8, current: 8.1 },
- { metric: "Portfolio", previous: 118.2, current: 124.6 },
-];
-
-const yearComparisonData = [
- { month: "Jan", lastYear: 72, thisYear: 81 },
- { month: "Feb", lastYear: 74, thisYear: 84 },
- { month: "Mar", lastYear: 78, thisYear: 88 },
- { month: "Apr", lastYear: 80, thisYear: 91 },
- { month: "May", lastYear: 83, thisYear: 95 },
- { month: "Jun", lastYear: 86, thisYear: 98 },
- { month: "Jul", lastYear: 89, thisYear: 103 },
- { month: "Aug", lastYear: 92, thisYear: 108 },
- { month: "Sep", lastYear: 95, thisYear: 113 },
- { month: "Oct", lastYear: 98, thisYear: 119 },
- { month: "Nov", lastYear: 101, thisYear: 123 },
- { month: "Dec", lastYear: 104, thisYear: 129 },
-];
-
-const goalPerformanceData = [
- { goal: "Emergency", target: 75, actual: 82 },
- { goal: "Travel", target: 58, actual: 61 },
- { goal: "Home", target: 44, actual: 39 },
- { goal: "Retirement", target: 63, actual: 70 },
-];
-
-const poolPerformanceData = [
- { pool: "USDC Flex", apy: 8.2, yield30d: 2.1, tvl: 1.24 },
- { pool: "XLM Vault", apy: 11.4, yield30d: 2.8, tvl: 1.82 },
- { pool: "Blend Stable", apy: 7.1, yield30d: 1.7, tvl: 0.96 },
- { pool: "Yield Basket", apy: 9.6, yield30d: 2.4, tvl: 1.38 },
-];
-
-// ── Base theme (non-color-scheme values) ───────────────────────────────────
-
-type BaseTheme = Pick;
-
-const baseThemeByMode: Record<"light" | "dark", BaseTheme> = {
- light: {
- text: "#0f1f2a",
- muted: "#4a7080",
- grid: "rgba(74, 112, 128, 0.12)",
- tooltipBg: "rgba(255, 255, 255, 0.98)",
- tooltipBorder: "rgba(8, 145, 178, 0.18)",
- },
- dark: {
- text: "#f8fdff",
- muted: "#6e9ba2",
- grid: "rgba(94, 140, 150, 0.12)",
- tooltipBg: "rgba(8, 20, 24, 0.96)",
- tooltipBorder: "rgba(0, 201, 200, 0.28)",
- },
-};
-
-const COLOR_SCHEME_OPTIONS: { value: ColorScheme; label: string; swatch: string }[] = [
- { value: "default", label: "Teal", swatch: "#0891b2" },
- { value: "ocean", label: "Ocean", swatch: "#1d4ed8" },
- { value: "sunset", label: "Sunset", swatch: "#ea580c" },
- { value: "forest", label: "Forest", swatch: "#16a34a" },
-];
-
-// ── Helpers ────────────────────────────────────────────────────────────────
-
-function downloadBlob(blob: Blob, filename: string) {
- const url = URL.createObjectURL(blob);
- const a = document.createElement("a");
- a.href = url;
- a.download = filename;
- a.click();
- URL.revokeObjectURL(url);
-}
-
-// ── Grid ───────────────────────────────────────────────────────────────────
-
-export default function AnalyticsComparisonGrid() {
- return (
-
- }
- csvData={monthComparisonData}
- csvFilename="month-comparison.csv"
- supportedTypes={["bar", "line"]}
- >
- {({ showLegend, chartType, palette, showBrush }) =>
- chartType === "line" ? (
-
-
-
- `${v}k`} />
- } />
- {showLegend && }
- {showBrush && }
-
-
-
- ) : (
-
-
-
- `${v}k`} />
- } cursor={{ fill: palette.grid, opacity: 0.15 }} />
- {showLegend && }
- {showBrush && }
-
-
-
- )
- }
-
-
- }
- csvData={yearComparisonData}
- csvFilename="year-comparison.csv"
- supportedTypes={["line", "bar"]}
- >
- {({ showLegend, chartType, palette, showBrush }) =>
- chartType === "bar" ? (
-
-
-
- `$${v}k`} />
- } cursor={{ fill: palette.grid, opacity: 0.15 }} />
- {showLegend && }
- {showBrush && }
-
-
-
- ) : (
-
-
-
- `$${v}k`} />
- } />
- {showLegend && }
- {showBrush && }
-
-
-
- )
- }
-
-
- }
- csvData={goalPerformanceData}
- csvFilename="goal-comparison.csv"
- supportedTypes={["bar", "line"]}
- >
- {({ showLegend, chartType, palette, showBrush }) =>
- chartType === "line" ? (
-
-
-
- `${v}%`} domain={[0, 100]} />
- } />
- {showLegend && }
- {showBrush && }
-
-
-
- ) : (
-
-
-
- `${v}%`} domain={[0, 100]} />
- } cursor={{ fill: palette.grid, opacity: 0.15 }} />
- {showLegend && }
- {showBrush && }
-
-
-
- )
- }
-
-
- }
- csvData={poolPerformanceData}
- csvFilename="pool-comparison.csv"
- supportedTypes={["bar", "line"]}
- >
- {({ showLegend, chartType, palette, showBrush }) =>
- chartType === "line" ? (
-
-
-
-
- } />
- {showLegend && }
- {showBrush && }
-
-
-
-
- ) : (
-
-
-
- `${v}%`} />
- `$${v}m`} />
- } />
- {showLegend && }
- {showBrush && }
-
-
-
-
- )
- }
-
-
- );
-}
-
-// ── ComparisonCard ─────────────────────────────────────────────────────────
-
-function ComparisonCard({
- title,
- description,
- badge,
- summary,
- icon,
- children,
- csvData,
- csvFilename,
- supportedTypes,
-}: {
- title: string;
- description: string;
- badge: string;
- summary: string;
- icon: React.ReactNode;
- children: (props: RenderProps) => React.ReactNode;
- csvData: Record[];
- csvFilename: string;
- supportedTypes: CardChartType[];
-}) {
- const { resolvedTheme } = useTheme();
- const [showLegend, setShowLegend] = useState(true);
- const [showBrush, setShowBrush] = useState(false);
- const [chartType, setChartType] = useState(supportedTypes[0]);
- const [colorScheme, setColorScheme] = useState("default");
- const [exportOpen, setExportOpen] = useState(false);
- const [paletteOpen, setPaletteOpen] = useState(false);
- const containerRef = useRef(null);
- const exportRef = useRef(null);
- const paletteRef = useRef(null);
-
- const palette = useMemo((): ChartPalette => ({
- ...baseThemeByMode[resolvedTheme],
- ...colorSchemes[colorScheme][resolvedTheme],
- }), [resolvedTheme, colorScheme]);
-
- React.useEffect(() => {
- if (!exportOpen && !paletteOpen) return;
- const handler = (e: MouseEvent) => {
- if (exportOpen && !exportRef.current?.contains(e.target as Node)) setExportOpen(false);
- if (paletteOpen && !paletteRef.current?.contains(e.target as Node)) setPaletteOpen(false);
- };
- document.addEventListener("mousedown", handler);
- return () => document.removeEventListener("mousedown", handler);
- }, [exportOpen, paletteOpen]);
-
- const handleExportSvg = useCallback(() => {
- const svg = containerRef.current?.querySelector("svg");
- if (!svg) return;
- downloadBlob(new Blob([new XMLSerializer().serializeToString(svg)], { type: "image/svg+xml" }), csvFilename.replace(".csv", ".svg"));
- setExportOpen(false);
- }, [csvFilename]);
-
- const handleExportPng = useCallback(() => {
- const svg = containerRef.current?.querySelector("svg");
- if (!svg) return;
- const svgData = new XMLSerializer().serializeToString(svg);
- const canvas = document.createElement("canvas");
- const rect = svg.getBoundingClientRect();
- canvas.width = rect.width * 2;
- canvas.height = rect.height * 2;
- const ctx = canvas.getContext("2d");
- if (!ctx) return;
- ctx.scale(2, 2);
- const img = new Image();
- img.onload = () => {
- ctx.drawImage(img, 0, 0);
- canvas.toBlob((blob) => { if (blob) downloadBlob(blob, csvFilename.replace(".csv", ".png")); });
- };
- img.src = `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(svgData)))}`;
- setExportOpen(false);
- }, [csvFilename]);
-
- const handleExportCsv = useCallback(() => {
- if (!csvData.length) return;
- const keys = Object.keys(csvData[0]);
- const rows = [keys.join(","), ...csvData.map((row) => keys.map((k) => row[k]).join(","))];
- downloadBlob(new Blob([rows.join("\n")], { type: "text/csv" }), csvFilename);
- setExportOpen(false);
- }, [csvData, csvFilename]);
-
- const iconBtn = (active: boolean) =>
- `flex items-center justify-center rounded-lg p-1.5 transition-colors ${
- active
- ? "bg-[var(--color-accent-soft)] text-[var(--color-accent)]"
- : "text-[var(--color-text-muted)] hover:bg-[var(--color-surface-subtle)] hover:text-[var(--color-text)]"
- }`;
-
- return (
-
- {/* Card header */}
-
-
-
- {icon}
-
-
-
{title}
-
{description}
-
-
-
- {badge}
-
-
-
- {/* Controls */}
-
- {/* Chart type */}
-
- {supportedTypes.map((t) => (
-
- ))}
-
-
- {/* Legend toggle */}
-
-
- {/* Zoom/brush toggle */}
-
-
- {/* Color scheme */}
-
-
- {paletteOpen && (
-
-
Color scheme
-
- {COLOR_SCHEME_OPTIONS.map(({ value, label, swatch }) => (
-
- ))}
-
-
- )}
-
-
- {/* Export */}
-
-
- {exportOpen && (
-
- {[
- { label: "Export PNG", action: handleExportPng },
- { label: "Export SVG", action: handleExportSvg },
- { label: "Export CSV", action: handleExportCsv },
- ].map(({ label, action }) => (
-
- ))}
-
- )}
-
-
-
- {/* Chart */}
-
-
- {children({ showLegend, chartType, palette, showBrush }) as React.ReactElement}
-
-
-
- {summary}
-
- );
-}
-
-// ── Tooltip ────────────────────────────────────────────────────────────────
-
-function ComparisonTooltip({
- active, payload, label, palette, valuePrefix = "", valueSuffix = "",
-}: {
- active?: boolean;
- payload?: TooltipPayloadItem[];
- label?: string | number;
- palette: ChartPalette;
- valuePrefix?: string;
- valueSuffix?: string;
-}) {
- if (!active || !payload?.length) return null;
- return (
-
-
{label}
-
- {payload.map((item) => (
-
-
-
- {item.name}: {formatValue(item.value, item.dataKey, valuePrefix, valueSuffix)}
-
-
- ))}
-
-
- );
-}
-
-function formatValue(value: number | string | undefined, dataKey: string | number | undefined, valuePrefix: string, valueSuffix: string) {
- if (typeof value !== "number") return value ?? "";
- if (dataKey === "tvl") return `$${value.toFixed(2)}m`;
- if (dataKey === "apy" || dataKey === "yield30d") return `${value}%`;
- if (valuePrefix || valueSuffix) return `${valuePrefix}${value}${valueSuffix}`;
- return value.toString();
-}
diff --git a/frontend/app/dashboard/analytics/ChartControls.tsx b/frontend/app/dashboard/analytics/ChartControls.tsx
deleted file mode 100644
index a05da6222..000000000
--- a/frontend/app/dashboard/analytics/ChartControls.tsx
+++ /dev/null
@@ -1,239 +0,0 @@
-"use client";
-
-import React, { useRef, useState } from "react";
-import {
- AreaChart as AreaIcon,
- BarChart2,
- Download,
- Eye,
- EyeOff,
- GitCompare,
- LineChart as LineIcon,
- Maximize2,
- Minimize2,
- Palette,
-} from "lucide-react";
-import type { ChartType, ColorScheme, TimeRange } from "./useChartPreferences";
-
-const TIME_RANGES: TimeRange[] = ["7D", "30D", "90D", "1Y"];
-
-const CHART_TYPES: { value: ChartType; icon: React.ReactNode; label: string }[] = [
- { value: "area", icon: , label: "Area" },
- { value: "line", icon: , label: "Line" },
- { value: "bar", icon: , label: "Bar" },
-];
-
-const COLOR_SCHEMES: { value: ColorScheme; label: string; swatch: string }[] = [
- { value: "default", label: "Teal", swatch: "#0891b2" },
- { value: "ocean", label: "Ocean", swatch: "#1d4ed8" },
- { value: "sunset", label: "Sunset", swatch: "#ea580c" },
- { value: "forest", label: "Forest", swatch: "#16a34a" },
-];
-
-type ChartControlsProps = {
- chartType: ChartType;
- timeRange: TimeRange;
- colorScheme: ColorScheme;
- showLegend: boolean;
- showComparison: boolean;
- showBrush: boolean;
- onChartType: (v: ChartType) => void;
- onTimeRange: (v: TimeRange) => void;
- onColorScheme: (v: ColorScheme) => void;
- onLegendToggle: () => void;
- onComparisonToggle: () => void;
- onBrushToggle: () => void;
- onExportPng: () => void;
- onExportSvg: () => void;
- onExportCsv: () => void;
-};
-
-export default function ChartControls({
- chartType,
- timeRange,
- colorScheme,
- showLegend,
- showComparison,
- showBrush,
- onChartType,
- onTimeRange,
- onColorScheme,
- onLegendToggle,
- onComparisonToggle,
- onBrushToggle,
- onExportPng,
- onExportSvg,
- onExportCsv,
-}: ChartControlsProps) {
- const [exportOpen, setExportOpen] = useState(false);
- const [paletteOpen, setPaletteOpen] = useState(false);
- const exportRef = useRef(null);
- const paletteRef = useRef(null);
-
- React.useEffect(() => {
- if (!exportOpen && !paletteOpen) return;
- const handler = (e: MouseEvent) => {
- if (exportOpen && !exportRef.current?.contains(e.target as Node)) setExportOpen(false);
- if (paletteOpen && !paletteRef.current?.contains(e.target as Node)) setPaletteOpen(false);
- };
- document.addEventListener("mousedown", handler);
- return () => document.removeEventListener("mousedown", handler);
- }, [exportOpen, paletteOpen]);
-
- const iconBtn = (active: boolean) =>
- `flex items-center justify-center rounded-lg p-1.5 transition-colors ${
- active
- ? "bg-[var(--color-accent-soft)] text-[var(--color-accent)]"
- : "text-[var(--color-text-muted)] hover:bg-[var(--color-surface-subtle)] hover:text-[var(--color-text)]"
- }`;
-
- return (
-
- {/* Chart type */}
-
- {CHART_TYPES.map(({ value, icon, label }) => (
-
- ))}
-
-
- {/* Time range */}
-
- {TIME_RANGES.map((r) => (
-
- ))}
-
-
- {/* Legend toggle */}
-
-
- {/* Comparison toggle */}
-
-
- {/* Brush/zoom toggle */}
-
-
- {/* Color palette */}
-
-
- {paletteOpen && (
-
-
- Color scheme
-
-
- {COLOR_SCHEMES.map(({ value, label, swatch }) => (
-
- ))}
-
-
- )}
-
-
- {/* Export */}
-
-
- {exportOpen && (
-
- {[
- { label: "Export PNG", action: () => { onExportPng(); setExportOpen(false); } },
- { label: "Export SVG", action: () => { onExportSvg(); setExportOpen(false); } },
- { label: "Export CSV", action: () => { onExportCsv(); setExportOpen(false); } },
- ].map(({ label, action }) => (
-
- ))}
-
- )}
-
-
- );
-}
diff --git a/frontend/app/dashboard/analytics/PortfolioPerformanceChart.tsx b/frontend/app/dashboard/analytics/PortfolioPerformanceChart.tsx
deleted file mode 100644
index f723d779b..000000000
--- a/frontend/app/dashboard/analytics/PortfolioPerformanceChart.tsx
+++ /dev/null
@@ -1,237 +0,0 @@
-"use client";
-
-import React, { useState } from "react";
-import {
- AreaChart,
- Area,
- XAxis,
- YAxis,
- Tooltip,
- ResponsiveContainer,
- CartesianGrid,
-} from "recharts";
-import { TrendingUp, MoreHorizontal } from "lucide-react";
-
-/* ── Sample data matching the mockup date range ─────────────────────── */
-const chartData = [
- { date: "Oct 01", value: 118200 },
- { date: "Oct 03", value: 119800 },
- { date: "Oct 05", value: 119500 },
- { date: "Oct 07", value: 120100 },
- { date: "Oct 09", value: 119900 },
- { date: "Oct 10", value: 120800 },
- { date: "Oct 12", value: 121200 },
- { date: "Oct 14", value: 120600 },
- { date: "Oct 16", value: 121800 },
- { date: "Oct 18", value: 122300 },
- { date: "Oct 20", value: 123100 },
- { date: "Oct 22", value: 123800 },
- { date: "Oct 25", value: 124500 },
- { date: "Oct 27", value: 124200 },
- { date: "Oct 30", value: 124800 },
- { date: "Nov 01", value: 124592 },
-];
-
-/* ── Custom Tooltip ─────────────────────────────────────────────────── */
-interface TooltipPayloadItem {
- value: number;
- payload: { date: string; value: number };
-}
-
-function CustomTooltip({
- active,
- payload,
-}: {
- active?: boolean;
- payload?: TooltipPayloadItem[];
- label?: string;
-}) {
- if (!active || !payload || payload.length === 0) return null;
-
- const { date, value } = payload[0].payload;
-
- return (
-
-
- {date}, 2023
-
-
- $
- {value.toLocaleString("en-US", {
- minimumFractionDigits: 2,
- maximumFractionDigits: 2,
- })}
-
-
- );
-}
-
-/* ── Custom Active Dot ──────────────────────────────────────────────── */
-function ActiveDot(props: { cx?: number; cy?: number }) {
- const { cx = 0, cy = 0 } = props;
- return (
-
- {/* Outer glow */}
-
- {/* Ring */}
-
- {/* Vertical guide line */}
-
-
- );
-}
-
-/* ── Main Component ─────────────────────────────────────────────────── */
-export default function PortfolioPerformanceChart() {
- const [isMenuOpen, setIsMenuOpen] = useState(false);
-
- return (
-
- {/* ── Header ─────────────────────────────────────────────────── */}
-
- {/* Left section */}
-
-
- Total Portfolio Value
-
-
-
- $124,592.45
-
-
-
- +12.4%
-
-
-
-
- {/* Right – overflow menu */}
-
-
-
- {/* ── Chart ──────────────────────────────────────────────────── */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- }
- cursor={false}
- />
-
- }
- dot={false}
- />
-
-
-
-
- );
-}
diff --git a/frontend/app/dashboard/analytics/analytics.css b/frontend/app/dashboard/analytics/analytics.css
deleted file mode 100644
index 0c3acffda..000000000
--- a/frontend/app/dashboard/analytics/analytics.css
+++ /dev/null
@@ -1,99 +0,0 @@
-.analytics {
- display: flex;
- flex-direction: column;
- gap: 32px;
-}
-
-/* ── Header row ── */
-.analytics__header {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- gap: 24px;
- flex-wrap: wrap;
-}
-
-/* ── Title group ── */
-.analytics__title-group {
- display: flex;
- flex-direction: column;
- gap: 6px;
-}
-
-.analytics__title {
- font-size: clamp(1.6rem, 3vw, 2rem);
- font-weight: 700;
- color: #ffffff;
- letter-spacing: -0.02em;
- line-height: 1.2;
-}
-
-.analytics__subtitle {
- font-size: 0.9rem;
- font-weight: 400;
- color: rgba(180, 210, 210, 0.6);
- line-height: 1.6;
- max-width: 420px;
-}
-
-/* ── Time-range filter ── */
-.analytics__filter {
- display: flex;
- align-items: center;
- gap: 4px;
- background: rgba(255, 255, 255, 0.04);
- border: 1px solid rgba(255, 255, 255, 0.08);
- border-radius: 10px;
- padding: 4px;
- flex-shrink: 0;
-}
-
-.analytics__filter-btn {
- padding: 7px 16px;
- border: none;
- border-radius: 7px;
- background: transparent;
- color: rgba(180, 210, 210, 0.6);
- font-family: 'Inter', sans-serif;
- font-size: 0.82rem;
- font-weight: 500;
- cursor: pointer;
- transition: background 0.18s ease, color 0.18s ease;
- white-space: nowrap;
-}
-
-.analytics__filter-btn:hover:not(.analytics__filter-btn--active) {
- background: rgba(255, 255, 255, 0.06);
- color: #ffffff;
-}
-
-.analytics__filter-btn:focus-visible {
- outline: 2px solid #00d4c0;
- outline-offset: 2px;
-}
-
-.analytics__filter-btn--active {
- background: #00d4c0;
- color: #061a1a;
- font-weight: 700;
-}
-
-/* ── Responsive ── */
-@media (max-width: 600px) {
- .analytics__header {
- flex-direction: column;
- align-items: flex-start;
- gap: 20px;
- }
-
- .analytics__filter {
- width: 100%;
- justify-content: space-between;
- }
-
- .analytics__filter-btn {
- flex: 1;
- text-align: center;
- padding: 8px 8px;
- }
-}
diff --git a/frontend/app/dashboard/analytics/loading.tsx b/frontend/app/dashboard/analytics/loading.tsx
deleted file mode 100644
index afb857f68..000000000
--- a/frontend/app/dashboard/analytics/loading.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import { ChartSkeleton, DashboardCardSkeleton } from "../../components/ui/LoadingState";
-
-export default function AnalyticsLoading() {
- return (
-
- );
-}
diff --git a/frontend/app/dashboard/analytics/page.tsx b/frontend/app/dashboard/analytics/page.tsx
deleted file mode 100644
index d4e7fa519..000000000
--- a/frontend/app/dashboard/analytics/page.tsx
+++ /dev/null
@@ -1,139 +0,0 @@
-import React from "react";
-import { MoreHorizontal, PieChart, Download } from "lucide-react";
-import PortfolioPerformanceChart from "./PortfolioPerformanceChart";
-
-export const metadata = { title: "Analytics – Nestera" };
-
-const ALLOCATION = [
- { asset: "USDC", percent: 40, color: "bg-cyan-400" },
- { asset: "XLM", percent: 35, color: "bg-blue-400" },
- { asset: "ETH", percent: 25, color: "bg-violet-400" },
-];
-
-const YIELD_POOLS = [
- { label: "XLM Staking", amount: 420.50, progress: 57 },
- { label: "USDC Flexible", amount: 322.70, progress: 43 },
-];
-
-function toCsv(rows: Record[]): string {
- if (!rows.length) return "";
- const headers = Object.keys(rows[0]);
- const escape = (v: unknown) => { const s = v == null ? "" : String(v); return /[",\n]/.test(s) ? `"${s.replaceAll('"', '""')}"` : s; };
- return [headers.join(","), ...rows.map((r) => headers.map((h) => escape(r[h])).join(","))].join("\n") + "\n";
-}
-
-function downloadCsv(filename: string, rows: Record[]) {
- const blob = new Blob([toCsv(rows)], { type: "text/csv;charset=utf-8" });
- const url = URL.createObjectURL(blob);
- const a = Object.assign(document.createElement("a"), { href: url, download: filename });
- document.body.appendChild(a); a.click(); a.remove();
- URL.revokeObjectURL(url);
-}
-
-export default function AnalyticsPage() {
- const stamp = new Date().toISOString().slice(0, 10);
-
- function exportAnalytics() {
- downloadCsv(`nestera-analytics-${stamp}.csv`, [
- ...ALLOCATION.map((r) => ({ section: "allocation", asset: r.asset, "percent_%": r.percent })),
- ...YIELD_POOLS.map((r) => ({ section: "yield", pool: r.label, "earned_usd": r.amount, "progress_%": r.progress })),
- ]);
- }
- return (
-
-
-
-
-
-
Analytics
-
Portfolio performance and insights
-
-
-
-
-
-
-
-
-
-
-
Asset Allocation
-
-
-
-
-
-
- {ALLOCATION.map((item) => (
-
-
-
- {item.asset}
-
-
{item.percent}%
-
- ))}
-
-
-
-
-
-
Yield Breakdown
-
- APY High
-
-
-
- Earnings from your active pools
-
-
-
-
- Total Interest Earned
-
-
$743.20
-
-
-
- {YIELD_POOLS.map((pool) => (
-
-
- {pool.label}
- +${pool.amount.toFixed(2)}
-
-
-
- ))}
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/frontend/app/dashboard/analytics/useChartPreferences.ts b/frontend/app/dashboard/analytics/useChartPreferences.ts
deleted file mode 100644
index b91f4cd71..000000000
--- a/frontend/app/dashboard/analytics/useChartPreferences.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-"use client";
-
-import { useCallback, useEffect, useState } from "react";
-
-export type ChartType = "area" | "line" | "bar";
-export type TimeRange = "7D" | "30D" | "90D" | "1Y";
-export type ColorScheme = "default" | "ocean" | "sunset" | "forest";
-
-export type ChartPreferences = {
- chartType: ChartType;
- timeRange: TimeRange;
- colorScheme: ColorScheme;
- showLegend: boolean;
- showComparison: boolean;
- showBrush: boolean;
-};
-
-const STORAGE_KEY = "nestera-chart-prefs";
-
-const defaults: ChartPreferences = {
- chartType: "area",
- timeRange: "30D",
- colorScheme: "default",
- showLegend: false,
- showComparison: false,
- showBrush: false,
-};
-
-function readStorage(): Partial {
- try {
- const raw = localStorage.getItem(STORAGE_KEY);
- return raw ? (JSON.parse(raw) as Partial) : {};
- } catch {
- return {};
- }
-}
-
-export function useChartPreferences() {
- const [prefs, setPrefs] = useState(defaults);
-
- useEffect(() => {
- setPrefs({ ...defaults, ...readStorage() });
- }, []);
-
- const update = useCallback((key: K, value: ChartPreferences[K]) => {
- setPrefs((prev) => {
- const next = { ...prev, [key]: value };
- try {
- localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
- } catch {}
- return next;
- });
- }, []);
-
- return { prefs, update };
-}
-
-export type ColorSchemePalette = {
- accent: string;
- accentAlt: string;
- accentSoft: string;
- success: string;
- violet: string;
-};
-
-export const colorSchemes: Record> = {
- default: {
- light: { accent: "#0891b2", accentAlt: "#2563eb", accentSoft: "rgba(8,145,178,0.18)", success: "#059669", violet: "#7c3aed" },
- dark: { accent: "#00c9c8", accentAlt: "#60a5fa", accentSoft: "rgba(0,201,200,0.18)", success: "#34d399", violet: "#a78bfa" },
- },
- ocean: {
- light: { accent: "#1d4ed8", accentAlt: "#7c3aed", accentSoft: "rgba(29,78,216,0.15)", success: "#0891b2", violet: "#4f46e5" },
- dark: { accent: "#60a5fa", accentAlt: "#a78bfa", accentSoft: "rgba(96,165,250,0.18)", success: "#38bdf8", violet: "#818cf8" },
- },
- sunset: {
- light: { accent: "#ea580c", accentAlt: "#d97706", accentSoft: "rgba(234,88,12,0.15)", success: "#16a34a", violet: "#db2777" },
- dark: { accent: "#fb923c", accentAlt: "#fbbf24", accentSoft: "rgba(251,146,60,0.18)", success: "#4ade80", violet: "#f472b6" },
- },
- forest: {
- light: { accent: "#16a34a", accentAlt: "#0891b2", accentSoft: "rgba(22,163,74,0.15)", success: "#15803d", violet: "#7c3aed" },
- dark: { accent: "#4ade80", accentAlt: "#34d399", accentSoft: "rgba(74,222,128,0.18)", success: "#86efac", violet: "#a78bfa" },
- },
-};
diff --git a/frontend/app/dashboard/contract-monitor/page.tsx b/frontend/app/dashboard/contract-monitor/page.tsx
deleted file mode 100644
index 678e1247a..000000000
--- a/frontend/app/dashboard/contract-monitor/page.tsx
+++ /dev/null
@@ -1,340 +0,0 @@
-"use client";
-
-import React, { useCallback, useEffect, useState } from "react";
-import {
- Activity,
- AlertTriangle,
- CheckCircle2,
- Clock,
- RefreshCw,
- XCircle,
- Zap,
-} from "lucide-react";
-
-// ── Types ────────────────────────────────────────────────────────────────────
-
-interface IndexerStatus {
- lastProcessedLedger: number;
- lastProcessedTimestamp: number | null;
- totalEventsProcessed: number;
- totalEventsFailed: number;
- monitoredContracts: string[];
-}
-
-interface ContractEvent {
- id: string;
- ledger: number;
- topic: string;
- txHash: string;
- timestamp: number;
- status: "ok" | "failed";
-}
-
-interface Alert {
- id: string;
- level: "info" | "warn" | "error";
- message: string;
- time: string;
-}
-
-// ── API helpers ──────────────────────────────────────────────────────────────
-
-const API = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3001";
-
-async function fetchIndexerStatus(): Promise {
- try {
- const res = await fetch(`${API}/api/v2/blockchain/indexer/status`, {
- cache: "no-store",
- });
- if (!res.ok) return null;
- return res.json();
- } catch {
- return null;
- }
-}
-
-async function fetchRecentEvents(): Promise {
- try {
- const res = await fetch(`${API}/api/v2/blockchain/events?limit=20`, {
- cache: "no-store",
- });
- if (!res.ok) return [];
- const data = await res.json();
- return Array.isArray(data) ? data : data?.data ?? [];
- } catch {
- return [];
- }
-}
-
-// ── Sub-components ───────────────────────────────────────────────────────────
-
-function StatCard({
- label,
- value,
- icon: Icon,
- accent,
-}: {
- label: string;
- value: string | number;
- icon: React.ElementType;
- accent: string;
-}) {
- return (
-
-
-
-
-
-
- );
-}
-
-function StatusBadge({ healthy }: { healthy: boolean }) {
- return healthy ? (
-
- Live
-
- ) : (
-
- Offline
-
- );
-}
-
-function AlertBanner({ alerts }: { alerts: Alert[] }) {
- if (!alerts.length) return null;
- const colors: Record = {
- info: "border-cyan-500/25 bg-cyan-500/5 text-cyan-300",
- warn: "border-amber-400/25 bg-amber-400/5 text-amber-300",
- error: "border-red-400/25 bg-red-400/5 text-red-300",
- };
- return (
-
- {alerts.map((a) => (
- -
-
- {a.message}
- {a.time}
-
- ))}
-
- );
-}
-
-// ── Main page ────────────────────────────────────────────────────────────────
-
-export default function ContractMonitorPage() {
- const [status, setStatus] = useState(null);
- const [events, setEvents] = useState([]);
- const [loading, setLoading] = useState(true);
- const [lastRefresh, setLastRefresh] = useState(new Date());
-
- const refresh = useCallback(async () => {
- setLoading(true);
- const [s, e] = await Promise.all([fetchIndexerStatus(), fetchRecentEvents()]);
- setStatus(s);
- setEvents(e);
- setLastRefresh(new Date());
- setLoading(false);
- }, []);
-
- useEffect(() => {
- refresh();
- const id = setInterval(refresh, 15_000);
- return () => clearInterval(id);
- }, [refresh]);
-
- // Derive simple alerts from status
- const alerts: Alert[] = [];
- if (status && status.totalEventsFailed > 0) {
- alerts.push({
- id: "dlq",
- level: "warn",
- message: `${status.totalEventsFailed} event(s) failed to process and were sent to the dead-letter queue.`,
- time: "now",
- });
- }
- if (!status) {
- alerts.push({
- id: "offline",
- level: "error",
- message: "Indexer status unavailable – backend may be offline.",
- time: lastRefresh.toLocaleTimeString(),
- });
- }
-
- const healthy = !!status;
- const processed = status?.totalEventsProcessed ?? 0;
- const failed = status?.totalEventsFailed ?? 0;
- const ledger = status?.lastProcessedLedger ?? "—";
- const contracts = status?.monitoredContracts?.length ?? 0;
-
- return (
-
- {/* Header */}
-
-
-
-
-
Contract Monitor
-
- Live indexer health & on-chain events
-
-
-
-
-
-
-
-
-
-
- {/* Alerts */}
- {alerts.length > 0 && (
-
- )}
-
- {/* Stats grid */}
-
-
- 0 ? "bg-red-500/15 text-red-300" : "bg-emerald-500/15 text-emerald-300"}
- />
-
-
-
-
- {/* Main content: events + contract list */}
-
- {/* Recent events table */}
-
- Recent Events
- {events.length === 0 ? (
-
- {loading ? "Loading events…" : "No events indexed yet."}
-
- ) : (
-
-
-
-
- | Ledger |
- Topic |
- Tx Hash |
- Status |
-
-
-
- {events.map((ev) => (
-
- | {ev.ledger} |
- {ev.topic} |
-
- {ev.txHash?.slice(0, 10)}…
- |
-
- {ev.status === "ok" ? (
-
- ok
-
- ) : (
-
- failed
-
- )}
- |
-
- ))}
-
-
-
- )}
-
-
- {/* Monitored contracts */}
-
- Monitored Contracts
- {!status?.monitoredContracts?.length ? (
- No active contracts.
- ) : (
-
- {status.monitoredContracts.map((addr) => (
- -
-
- {addr}
-
- ))}
-
- )}
-
- {/* Last sync time */}
-
-
- Last synced:{" "}
-
- {lastRefresh.toLocaleTimeString()}
-
-
- {status?.lastProcessedTimestamp && (
-
- Last event:{" "}
-
- {new Date(status.lastProcessedTimestamp).toLocaleTimeString()}
-
-
- )}
-
-
-
-
- );
-}
diff --git a/frontend/app/dashboard/governance/GovernanceClient.tsx b/frontend/app/dashboard/governance/GovernanceClient.tsx
deleted file mode 100644
index f1ef24a9a..000000000
--- a/frontend/app/dashboard/governance/GovernanceClient.tsx
+++ /dev/null
@@ -1,220 +0,0 @@
-"use client";
-
-import { useState } from "react";
-import { ShieldCheck, Users, Vote, FileText, XCircle } from "lucide-react";
-import PassedProposalCard, {
- type PassedProposal,
-} from "@/app/components/dashboard/PassedProposalCard";
-import ProposalCard from "@/app/components/dashboard/ProposalCard";
-
-export default function GovernanceClient() {
- const [activeTab, setActiveTab] = useState("Overview");
- const tabs = [
- { label: "Overview", icon: FileText },
- { label: "Active Votes", icon: Vote },
- { label: "Rejected", icon: XCircle },
- { label: "Delegations", icon: Users },
- ];
-
- const passedProposals: PassedProposal[] = [
- {
- id: "p-001",
- title: "Reduce protocol fees for small deposits",
- category: "Parameters",
- passedOn: "Mar 18, 2026",
- forVotes: 1824,
- againstVotes: 312,
- },
- {
- id: "p-002",
- title: "Add USDT (testnet) as a supported stablecoin",
- category: "Assets",
- passedOn: "Feb 27, 2026",
- forVotes: 1490,
- againstVotes: 410,
- },
- {
- id: "p-003",
- title: "Increase timelock delay to 24 hours",
- category: "Security",
- passedOn: "Jan 30, 2026",
- forVotes: 2055,
- againstVotes: 155,
- },
- ];
- const activeProposals = [
- {
- id: "NIP-4",
- title: "Increase USDC Base Yield to 14%",
- categories: ["Finance"],
- countdownText: "Ends in 2 days",
- forPercent: 75,
- againstPercent: 25,
- status: "ACTIVE",
- },
- {
- id: "NIP-12",
- title: "Add new ecosystem grants program",
- categories: ["Ecosystem", "Finance"],
- countdownText: "Ends in 6 days",
- forPercent: 52,
- againstPercent: 48,
- status: "ACTIVE",
- },
- ];
- const rejectedProposals = [
- {
- id: "NIP-1",
- title: "Increase Treasury Risk Exposure",
- categories: ["Treasury"],
- countdownText: "Ended Mar 10, 2026",
- forPercent: 34,
- againstPercent: 66,
- status: "REJECTED",
- },
- {
- id: "NIP-6",
- title: "Remove XLM Staking Incentives",
- categories: ["Staking"],
- countdownText: "Ended Feb 19, 2026",
- forPercent: 41,
- againstPercent: 59,
- status: "REJECTED",
- },
- ];
-
- return (
-
-
-
-
-
-
-
-
Governance
-
- Vote on proposals and protocol decisions
-
-
-
-
-
-
-
- Voting Power
-
-
- Connected
-
-
-
- 12,480
-
-
- NSTR delegated to your wallet
-
-
-
-
-
-
-
- {tabs.map((tab) => {
- const TabIcon = tab.icon;
- return (
-
- );
- })}
-
-
-
-
- {activeTab === "Overview" && (
- <>
-
-
-
-
- >
- )}
-
- {(activeTab === "Overview" || activeTab === "Active Votes") && (
-
-
-
-
- {activeProposals.map((proposal) => (
-
- ))}
-
-
- )}
-
- {activeTab === "Rejected" && (
-
-
-
-
- {rejectedProposals.map((proposal) => (
-
- ))}
-
-
- )}
-
- {activeTab === "Overview" && (
-
- {passedProposals.map((proposal) => (
-
- ))}
-
- )}
-
-
- );
-}
diff --git a/frontend/app/dashboard/governance/page.tsx b/frontend/app/dashboard/governance/page.tsx
deleted file mode 100644
index 8ca5cbebc..000000000
--- a/frontend/app/dashboard/governance/page.tsx
+++ /dev/null
@@ -1,8 +0,0 @@
-import React from "react";
-import GovernanceClient from "./GovernanceClient";
-
-export const metadata = { title: "Governance – Nestera" };
-
-export default function GovernancePage() {
- return ;
-}
diff --git a/frontend/app/dashboard/layout.tsx b/frontend/app/dashboard/layout.tsx
index 316eaba54..86de3d9ae 100644
--- a/frontend/app/dashboard/layout.tsx
+++ b/frontend/app/dashboard/layout.tsx
@@ -9,8 +9,6 @@ export const metadata: Metadata = generatePageMetadata({
title: "Dashboard - Nestera",
description: "Manage your Nestera account, view portfolio analytics, track savings progress, and control your decentralized financial strategy from one unified dashboard.",
url: "/dashboard",
- canonical: `${SITE_URL}/dashboard`,
- noindex: true,
});
export default function DashboardLayout({
diff --git a/frontend/app/dashboard/portfolio/page.tsx b/frontend/app/dashboard/portfolio/page.tsx
deleted file mode 100644
index 2d9884c24..000000000
--- a/frontend/app/dashboard/portfolio/page.tsx
+++ /dev/null
@@ -1,189 +0,0 @@
-"use client";
-
-import React, { useState } from "react";
-import { Button } from "@/app/components/ui/Button";
-import { ResponsiveTable, TableColumn } from "@/app/components/ui/ResponsiveTable";
-import React from "react";
-import { Briefcase, TrendingUp, Download, FileJson, FileText, MoreHorizontal } from "lucide-react";
-import { useExport } from "@/app/hooks/useExport";
-import { useToast } from "@/app/context/ToastContext";
-
-const ASSETS = [
- { name: "USDC Flexible", type: "Savings", balance: 2400, value: 2400, apy: 6.5, pnl: 156, pnlPct: 6.9 },
- { name: "XLM Locked", type: "Staking", balance: 5000, value: 1850, apy: 11.2, pnl: 207, pnlPct: 12.6 },
- { name: "USDC Goal — Emergency", type: "Goal", balance: 1200, value: 1200, apy: 5.0, pnl: 60, pnlPct: 5.3 },
- { name: "Group Pool — Alpha", type: "Group", balance: 800, value: 800, apy: 8.0, pnl: 32, pnlPct: 4.2 },
-];
-
-const PERFORMANCE = [
- { month: "Nov", value: 4800 },
- { month: "Dec", value: 5100 },
- { month: "Jan", value: 5400 },
- { month: "Feb", value: 5900 },
- { month: "Mar", value: 6050 },
- { month: "Apr", value: 6250 },
-];
-
-const MAX_VAL = Math.max(...PERFORMANCE.map((p) => p.value));
-
-const TYPE_COLORS: Record = {
- Savings: "text-cyan-400 bg-cyan-400/10",
- Staking: "text-violet-400 bg-violet-400/10",
- Goal: "text-emerald-400 bg-emerald-400/10",
- Group: "text-amber-400 bg-amber-400/10",
-};
-
-const exportRows = ASSETS.map(({ name, type, balance, value, apy, pnl, pnlPct }) => ({
- name, type, balance, "value_usd": value, "apy_pct": apy, "pnl_usd": pnl, "pnl_pct": pnlPct,
-}));
-
-export default function PortfolioPage() {
- const toast = useToast();
- const { exportData, loading } = useExport({
- onSuccess: (fmt, name) => toast.success("Export complete", `${name} downloaded`),
- onError: () => toast.error("Export failed", "Please try again"),
- });
-
- const totalValue = ASSETS.reduce((s, a) => s + a.value, 0);
- const totalPnl = ASSETS.reduce((s, a) => s + a.pnl, 0);
- const totalPnlPct = ((totalPnl / (totalValue - totalPnl)) * 100).toFixed(2);
-
- return (
-
- {/* Header */}
-
-
-
-
-
-
-
Portfolio
-
All assets and positions
-
-
-
-
-
-
-
-
-
-
- {/* Summary cards */}
-
-
-
Total Value
-
${totalValue.toLocaleString()}
-
-
-
Total P&L
-
+${totalPnl}
-
-
-
Return
-
+{totalPnlPct}%
-
-
-
- {/* Performance chart */}
-
-
-
Performance
-
-
-
- {PERFORMANCE.map((p) => (
-
- ))}
-
-
-
- {/* Asset breakdown */}
-
-
Asset Breakdown
-
asset.name}
- pageSize={4}
- showColumnVisibility={true}
- initialSortKey="value"
- renderDesktopHeader={(visibleColumns) => (
-
- {visibleColumns.includes("name") &&
Asset
}
- {visibleColumns.includes("type") &&
Type
}
- {visibleColumns.includes("value") &&
Value
}
- {visibleColumns.includes("apy") &&
APY
}
- {visibleColumns.includes("pnl") &&
P&L
}
-
- )}
- renderDesktopRow={(asset) => (
-
-
{asset.name}
-
-
- {asset.type}
-
-
-
${asset.value.toLocaleString()}
-
{asset.apy}%
-
+${asset.pnl} ({asset.pnlPct}%)
-
- )}
- renderMobileCard={(asset) => (
-
-
-
-
{asset.name}
-
- {asset.type}
-
-
-
-
${asset.value.toLocaleString()}
-
{asset.apy}% APY
-
-
-
- P&L
- +${asset.pnl} ({asset.pnlPct}%)
-
-
- )}
- />
-
-
- );
-}
diff --git a/frontend/app/dashboard/referrals/page.tsx b/frontend/app/dashboard/referrals/page.tsx
deleted file mode 100644
index f3e78b635..000000000
--- a/frontend/app/dashboard/referrals/page.tsx
+++ /dev/null
@@ -1,157 +0,0 @@
-"use client";
-
-import React, { useState } from "react";
-import { Users, Copy, Check, Trophy, Gift, TrendingUp } from "lucide-react";
-import { env } from "../../lib/env";
-import { Button } from "@/app/components/ui/Button";
-
-const REFERRED_USERS = [
- { name: "Alice K.", joined: "Apr 20, 2026", status: "Active", reward: "$12.00" },
- { name: "Bob M.", joined: "Apr 15, 2026", status: "Active", reward: "$12.00" },
- { name: "Carol T.", joined: "Apr 10, 2026", status: "Pending", reward: "$0.00" },
- { name: "David R.", joined: "Mar 28, 2026", status: "Active", reward: "$12.00" },
-];
-
-const LEADERBOARD = [
- { rank: 1, name: "0xAb...3f", referrals: 24, earned: "$288" },
- { rank: 2, name: "0xCd...7a", referrals: 18, earned: "$216" },
- { rank: 3, name: "0xEf...2b", referrals: 15, earned: "$180" },
- { rank: 4, name: "You", referrals: 4, earned: "$36", isYou: true },
- { rank: 5, name: "0x12...9c", referrals: 3, earned: "$24" },
-];
-
-const REFERRAL_LINK = `${env.baseUrl}/ref/${process.env.NEXT_PUBLIC_DEFAULT_REFERRAL_CODE || "0x4a8f"}`;
-
-export default function ReferralsPage() {
- const [copied, setCopied] = useState(false);
-
- const copy = () => {
- navigator.clipboard.writeText(REFERRAL_LINK);
- setCopied(true);
- setTimeout(() => setCopied(false), 2000);
- };
-
- return (
-
- {/* Header */}
-
-
-
-
-
-
Referral Program
-
Invite friends and earn rewards
-
-
-
- {/* Stats */}
-
- {[
- { label: "Total Referred", value: "4", icon: Users, color: "text-cyan-400 bg-cyan-400/10" },
- { label: "Rewards Earned", value: "$36.00", icon: Gift, color: "text-emerald-400 bg-emerald-400/10" },
- { label: "Active Referrals", value: "3", icon: TrendingUp, color: "text-violet-400 bg-violet-400/10" },
- { label: "Leaderboard Rank", value: "#4", icon: Trophy, color: "text-amber-400 bg-amber-400/10" },
- ].map((s) => {
- const Icon = s.icon;
- return (
-
-
-
-
-
{s.value}
-
{s.label}
-
- );
- })}
-
-
- {/* Referral link */}
-
-
Your Referral Link
-
-
- {REFERRAL_LINK}
-
-
:
}
- onClick={copy}
- className="border-cyan-500/25 text-cyan-300 bg-cyan-500/15 hover:bg-cyan-500/25 shrink-0"
- >
- {copied ? "Copied!" : "Copy"}
-
-
-
- Earn $12 USDC for every friend who deposits at least $100.
-
-
-
-
- {/* Referred users */}
-
-
Referred Users
-
- {REFERRED_USERS.map((u) => (
-
-
-
{u.name}
-
Joined {u.joined}
-
-
-
- {u.status}
-
- {u.reward}
-
-
- ))}
-
-
-
- {/* Leaderboard */}
-
-
Leaderboard
-
- {LEADERBOARD.map((entry) => (
-
-
- {entry.rank}
-
-
- {entry.name}
-
- {entry.referrals} refs
- {entry.earned}
-
- ))}
-
-
-
-
- );
-}
diff --git a/frontend/app/dashboard/savings-pools/loading.tsx b/frontend/app/dashboard/savings-pools/loading.tsx
deleted file mode 100644
index fc7348fd1..000000000
--- a/frontend/app/dashboard/savings-pools/loading.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import { PoolCardSkeleton } from "../../components/ui/LoadingState";
-
-export default function SavingsPoolsLoading() {
- return (
-
-
- {Array.from({ length: 6 }).map((_, i) => (
-
- ))}
-
-
- );
-}
diff --git a/frontend/app/dashboard/savings-pools/page.tsx b/frontend/app/dashboard/savings-pools/page.tsx
deleted file mode 100644
index 27a4f6873..000000000
--- a/frontend/app/dashboard/savings-pools/page.tsx
+++ /dev/null
@@ -1,239 +0,0 @@
-"use client";
-
-import React, { useState, useMemo } from "react";
-import { Landmark, Search, ChevronDown, LayoutGrid, List, Download, FileJson } from "lucide-react";
-import SavingsPoolCard, {
- type SavingsPool,
-} from "@/app/components/dashboard/SavingsPoolCard";
-import { useExport } from "@/app/hooks/useExport";
-import { useToast } from "@/app/context/ToastContext";
-
-export default function GoalBasedSavingsPage() {
- const [searchQuery, setSearchQuery] = useState("");
- const toast = useToast();
- const { exportData, loading } = useExport({
- onSuccess: (fmt, name) => toast.success("Export complete", `${name} downloaded`),
- onError: () => toast.error("Export failed", "Please try again"),
- });
-
- // Savings pools data
- const savingsPools: SavingsPool[] = [
- {
- id: "usdc-flexible",
- name: "USDC Flexible",
- strategy: "Stablecoin",
- icon: "$",
- iconBgColor: "bg-gradient-to-br from-blue-500 to-blue-600",
- apy: 5.4,
- tvl: "$24.5M",
- riskLevel: "Low Risk",
- },
- {
- id: "xlm-staking",
- name: "XLM Staking",
- strategy: "Native",
- icon: "✦",
- iconBgColor: "bg-gradient-to-br from-purple-500 to-purple-600",
- apy: 4.5,
- tvl: "$12.8M",
- riskLevel: "Medium Risk",
- },
- {
- id: "aqua-farming",
- name: "AQUA Farming",
- strategy: "DeFi",
- icon: "A",
- iconBgColor: "bg-gradient-to-br from-cyan-500 to-cyan-600",
- apy: 18.5,
- tvl: "$2.1M",
- riskLevel: "High Risk",
- },
- {
- id: "eurc-yield",
- name: "EURC Yield",
- strategy: "Euro Stable",
- icon: "€",
- iconBgColor: "bg-gradient-to-br from-indigo-500 to-indigo-600",
- apy: 3.2,
- tvl: "$8.4M",
- riskLevel: "Low Risk",
- },
- {
- id: "yusdc-vault",
- name: "yUSDC Vault",
- strategy: "Yield Aggregator",
- icon: "y",
- iconBgColor: "bg-gradient-to-br from-teal-500 to-teal-600",
- apy: 8.1,
- tvl: "$4.2M",
- riskLevel: "Medium Risk",
- },
- {
- id: "btc-xlm-lp",
- name: "BTC-XLM LP",
- strategy: "Liquidity Pool",
- icon: "₿",
- iconBgColor: "bg-gradient-to-br from-orange-500 to-orange-600",
- apy: 12.4,
- tvl: "$5.6M",
- riskLevel: "High Risk",
- },
- ];
-
- // Filter pools based on search query
- const filteredPools = useMemo(() => {
- if (!searchQuery.trim()) {
- return savingsPools;
- }
-
- const query = searchQuery.toLowerCase();
- return savingsPools.filter(
- (pool) =>
- pool.name.toLowerCase().includes(query) ||
- pool.strategy.toLowerCase().includes(query) ||
- pool.riskLevel.toLowerCase().includes(query),
- );
- }, [searchQuery, savingsPools]);
-
- const handleDeposit = (poolId: string) => {
- console.log(`Deposit clicked for pool: ${poolId}`);
- // Add your deposit logic here
- };
-
- return (
-
- {/* Page Header */}
-
-
-
-
-
-
-
- Savings Pools
-
-
- Discover and manage savings pools across supported assets.
-
-
-
-
- {/* View Toggles & Actions */}
-
-
-
-
-
-
-
-
-
-
-
- {/* Search & Filters Row */}
-
-
-
- setSearchQuery(e.target.value)}
- placeholder="Search pools by name, strategy, or risk level..."
- className="w-full bg-[#0e2330] border border-white/5 rounded-xl py-3 pl-12 pr-4 text-white placeholder:text-[#4e7a86] focus:outline-hidden focus:border-cyan-500/50 transition-colors"
- />
-
-
-
- {[
- { label: "Asset: All", active: true },
- { label: "Risk: All Levels", active: false },
- { label: "Sort by: APY", active: false },
- ].map((filter, i) => (
-
- ))}
-
-
-
- {/* Section Header */}
-
-
Available Pools
-
- {filteredPools.length === savingsPools.length
- ? `Showing ${filteredPools.length} pools`
- : `Found ${filteredPools.length} of ${savingsPools.length} pools`}
-
-
-
- {/* Pools Grid */}
- {filteredPools.length > 0 ? (
-
- {filteredPools.map((pool) => (
-
- ))}
-
- ) : (
-
-
-
-
-
- No pools found
-
-
- Try adjusting your search terms or filters to find what you're
- looking for.
-
-
-
- )}
-
- );
-}
diff --git a/frontend/app/dashboard/staking/page.tsx b/frontend/app/dashboard/staking/page.tsx
deleted file mode 100644
index c1caf87e8..000000000
--- a/frontend/app/dashboard/staking/page.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import React from "react";
-import { TrendingUp } from "lucide-react";
-
-export const metadata = { title: "Staking – Nestera" };
-
-export default function StakingPage() {
- return (
-
-
-
-
-
-
-
Staking
-
- View and manage your staking positions
-
-
-
-
-
-
- Staking content will appear here.
-
-
-
- );
-}
diff --git a/frontend/app/dashboard/transactions/page.tsx b/frontend/app/dashboard/transactions/page.tsx
deleted file mode 100644
index 4a9df6a82..000000000
--- a/frontend/app/dashboard/transactions/page.tsx
+++ /dev/null
@@ -1,243 +0,0 @@
-"use client";
-
-import React, { useState } from "react";
-import { Download, FileJson, History, Search, ChevronDown } from "lucide-react";
-import TransactionRow, { TransactionType, TransactionStatus } from "./components/TransactionRow";
-import { ResponsiveTable, TableColumn } from "@/app/components/ui/ResponsiveTable";
-import { useExport, type DateRange } from "@/app/hooks/useExport";
-import { useToast } from "@/app/context/ToastContext";
-
-type TransactionRowData = {
- date: string;
- time: string;
- transactionId: string;
- type: TransactionType;
- assetDetails: string;
- amountDisplay: string;
- isPositive: boolean | null;
- status: TransactionStatus;
- hash: string;
-};
-
-const TRANSACTIONS: TransactionRowData[] = [
- { date: "2023-10-25", time: "10:23 AM", transactionId: "0xabc...123", type: "deposit", assetDetails: "USDC", amountDisplay: "+$500.00", isPositive: true, status: "completed", hash: "0xabc" },
- { date: "2023-10-24", time: "04:15 PM", transactionId: "0xdef...456", type: "withdraw", assetDetails: "ETH", amountDisplay: "-0.50 ETH", isPositive: false, status: "completed", hash: "0xdef" },
- { date: "2023-10-24", time: "09:30 AM", transactionId: "0xghi...789", type: "swap", assetDetails: "XLM → USDC", amountDisplay: "200 USDC", isPositive: null, status: "completed", hash: "0xghi" },
- { date: "2023-10-23", time: "08:00 AM", transactionId: "0xjkl...012", type: "yield", assetDetails: "Staking Reward", amountDisplay: "+$12.45", isPositive: true, status: "pending", hash: "0xjkl" },
-];
-
-export default function TransactionHistoryPage() {
- const toast = useToast();
- const { exportData, loading } = useExport({
- onSuccess: (fmt, name) => toast.success(`Export complete`, `${name} downloaded`),
- onError: () => toast.error("Export failed", "Please try again"),
- });
-
- const [dateRange, setDateRange] = useState({});
-
- const columns: TableColumn[] = [
- { key: "date", label: "Date", sortable: true, width: "20%" },
- { key: "transactionId", label: "Transaction ID", sortable: true, width: "22%" },
- { key: "type", label: "Type", sortable: true, width: "18%" },
- { key: "assetDetails", label: "Asset / Details", sortable: true, width: "22%" },
- { key: "amountDisplay", label: "Amount", sortable: true, width: "12%", align: "right" },
- { key: "status", label: "Status", sortable: true, width: "12%", align: "right" },
- ];
-
- function onExportCsv() {
- const csv = toCsv(transactions);
- downloadTextFile(
- `nestera-transactions-${new Date().toISOString().slice(0, 10)}.csv`,
- csv,
- );
- }
- const rows = TRANSACTIONS.map(({ isPositive: _ip, ...rest }) => rest as Record);
-
- return (
-
- {/* Header */}
-
-
-
-
-
-
-
Transaction History
-
- Export your transaction history for reporting and tax purposes.
-
-
-
-
- {/* Export buttons */}
-
-
-
-
-
-
- {/* Date range filter */}
-
- Date Range
-
-
- {(dateRange.from || dateRange.to) && (
-
- )}
-
-
- {/* Search + filters */}
-
-
-
-
-
- {["Type: All", "Asset: All", "Status: All"].map((filter) => (
-
- ))}
-
-
-
transaction.hash}
- pageSize={4}
- initialSortKey="date"
- renderDesktopHeader={(visibleColumns) => (
-
- {visibleColumns.includes("date") &&
Date
}
- {visibleColumns.includes("transactionId") &&
Transaction ID
}
- {visibleColumns.includes("type") &&
Type
}
- {visibleColumns.includes("assetDetails") &&
Asset / Details
}
- {visibleColumns.includes("amountDisplay") &&
Amount
}
- {visibleColumns.includes("status") &&
Status
}
-
- )}
- renderDesktopRow={(transaction) => (
- console.log("Open transaction", id)}
- />
- )}
- renderMobileCard={(transaction) => (
-
-
-
-
{transaction.date}
-
{transaction.time}
-
-
{transaction.status}
-
-
-
-
-
Transaction
-
{transaction.transactionId}
-
-
{transaction.amountDisplay}
-
-
-
- Type
- {transaction.type}
-
-
- Asset
- {transaction.assetDetails}
-
-
-
-
- )}
- />
- {/* Table */}
-
-
-
Date
-
Transaction ID
-
Type
-
Asset / Details
-
Amount
-
Status
-
-
- {TRANSACTIONS.map((t) => (
-
console.log("Open transaction", id)}
- />
- ))}
-
-
-
-
-
- Page 1 of 12
-
-
-
-
- );
-}
diff --git a/frontend/app/dashboard/webhooks/page.tsx b/frontend/app/dashboard/webhooks/page.tsx
deleted file mode 100644
index cacea1c27..000000000
--- a/frontend/app/dashboard/webhooks/page.tsx
+++ /dev/null
@@ -1,285 +0,0 @@
-"use client";
-
-import React, { useState } from "react";
-import {
- Webhook,
- Plus,
- Trash2,
- Play,
- PauseCircle,
- PlayCircle,
- CheckCircle2,
- XCircle,
- Clock,
- ChevronDown,
- ChevronUp,
- Copy,
- Check,
-} from "lucide-react";
-import { Button } from "@/app/components/ui/Button";
-
-type WebhookStatus = "ACTIVE" | "DISABLED";
-type DeliveryStatus = "SUCCESS" | "FAILED" | "PENDING";
-
-interface WebhookSubscription {
- id: string;
- url: string;
- events: string[];
- status: WebhookStatus;
- secret: string;
- description: string | null;
- createdAt: string;
-}
-
-interface DeliveryLog {
- id: string;
- eventName: string;
- status: DeliveryStatus;
- attempts: number;
- responseStatus: number | null;
- createdAt: string;
-}
-
-const MOCK_WEBHOOKS: WebhookSubscription[] = [
- {
- id: "wh-1",
- url: "https://myapp.example.com/webhooks",
- events: ["savings.deposit", "savings.withdrawal"],
- status: "ACTIVE",
- secret: "sk_test_a8f3b2c1d4e5f6a7b8c9d0",
- description: "Production deposit handler",
- createdAt: "2026-05-20T10:00:00Z",
- },
- {
- id: "wh-2",
- url: "https://staging.example.com/hooks",
- events: ["goal.completed"],
- status: "DISABLED",
- secret: "sk_test_b9g4c3d2e5f6a7b8c9d0e1",
- description: "Staging goal events",
- createdAt: "2026-05-22T14:00:00Z",
- },
-];
-
-const MOCK_DELIVERIES: Record = {
- "wh-1": [
- { id: "d1", eventName: "savings.deposit", status: "SUCCESS", attempts: 1, responseStatus: 200, createdAt: "2026-06-02T02:55:00Z" },
- { id: "d2", eventName: "savings.withdrawal", status: "FAILED", attempts: 5, responseStatus: 503, createdAt: "2026-06-02T01:30:00Z" },
- { id: "d3", eventName: "savings.deposit", status: "PENDING", attempts: 2, responseStatus: null, createdAt: "2026-06-02T00:10:00Z" },
- ],
- "wh-2": [],
-};
-
-const ALL_EVENTS = ["savings.deposit", "savings.withdrawal", "savings.goal_completed", "savings.interest_accrued", "user.kyc_approved", "*"];
-
-function DeliveryStatusBadge({ status }: { status: DeliveryStatus }) {
- const map: Record = {
- SUCCESS: { icon: CheckCircle2, cls: "text-emerald-400 bg-emerald-400/10" },
- FAILED: { icon: XCircle, cls: "text-red-400 bg-red-400/10" },
- PENDING: { icon: Clock, cls: "text-amber-400 bg-amber-400/10" },
- };
- const { icon: Icon, cls } = map[status];
- return (
-
- {status}
-
- );
-}
-
-function CopyBtn({ text }: { text: string }) {
- const [copied, setCopied] = useState(false);
- return (
-
- );
-}
-
-function CreateModal({ onClose, onCreate }: { onClose: () => void; onCreate: (s: WebhookSubscription) => void }) {
- const [url, setUrl] = useState("");
- const [desc, setDesc] = useState("");
- const [events, setEvents] = useState([]);
-
- const toggle = (e: string) => setEvents(p => p.includes(e) ? p.filter(x => x !== e) : [...p, e]);
-
- const submit = () => {
- if (!url || events.length === 0) return;
- onCreate({ id: `wh-${Date.now()}`, url, events, status: "ACTIVE", secret: `sk_test_${Math.random().toString(36).slice(2)}`, description: desc || null, createdAt: new Date().toISOString() });
- onClose();
- };
-
- return (
-
-
-
Register Webhook
-
-
-
- setUrl(e.target.value)} />
-
-
-
- setDesc(e.target.value)} />
-
-
-
-
- {ALL_EVENTS.map(ev => (
-
- ))}
-
-
-
-
-
-
-
-
-
- );
-}
-
-function WebhookCard({ wh, onToggle, onDelete, onTest }: {
- wh: WebhookSubscription;
- onToggle: (id: string) => void;
- onDelete: (id: string) => void;
- onTest: (id: string) => void;
-}) {
- const [open, setOpen] = useState(false);
- const deliveries = MOCK_DELIVERIES[wh.id] ?? [];
- const active = wh.status === "ACTIVE";
-
- return (
-
-
-
-
-
-
-
- {wh.url}
- {wh.status}
-
- {wh.description &&
{wh.description}
}
-
- {wh.events.map(ev => (
- {ev}
- ))}
-
-
-
-
-
-
-
-
-
-
- {open && (
-
-
- Secret:
- {wh.secret.slice(0, 18)}…
-
-
-
Recent Deliveries
- {deliveries.length === 0
- ?
No deliveries yet.
- : deliveries.map(d => (
-
-
- {d.eventName}
- {d.responseStatus && HTTP {d.responseStatus}}
- ×{d.attempts}
- {new Date(d.createdAt).toLocaleTimeString()}
-
- ))
- }
-
- )}
-
- );
-}
-
-export default function WebhooksPage() {
- const [webhooks, setWebhooks] = useState(MOCK_WEBHOOKS);
- const [showCreate, setShowCreate] = useState(false);
- const [toast, setToast] = useState(null);
-
- const notify = (msg: string) => { setToast(msg); setTimeout(() => setToast(null), 4000); };
-
- const handleCreate = (s: WebhookSubscription) => { setWebhooks(p => [s, ...p]); notify("Webhook registered."); };
- const handleToggle = (id: string) => setWebhooks(p => p.map(w => w.id === id ? { ...w, status: w.status === "ACTIVE" ? "DISABLED" : "ACTIVE" } : w));
- const handleDelete = (id: string) => { setWebhooks(p => p.filter(w => w.id !== id)); notify("Webhook deleted."); };
- const handleTest = (id: string) => notify(`Test event sent to …${id.slice(-4)}. Check delivery logs.`);
-
- const active = webhooks.filter(w => w.status === "ACTIVE").length;
-
- return (
-
-
-
-
-
-
-
-
Webhooks
-
{active} active · {webhooks.length} total
-
-
-
} onClick={() => setShowCreate(true)}
- className="border-cyan-500/30 bg-cyan-500/15 text-cyan-300 hover:bg-cyan-500/25">
- Register Webhook
-
-
-
- {toast && (
-
{toast}
- )}
-
-
- {[
- { label: "Active", value: active, color: "text-emerald-400" },
- { label: "Total", value: webhooks.length, color: "text-cyan-400" },
- { label: "Deliveries today", value: Object.values(MOCK_DELIVERIES).flat().length, color: "text-violet-400" },
- ].map(s => (
-
-
{s.value}
-
{s.label}
-
- ))}
-
-
- {webhooks.length === 0 ? (
-
-
-
No webhooks registered yet.
-
-
- ) : (
-
- {webhooks.map(wh => (
-
- ))}
-
- )}
-
- {showCreate &&
setShowCreate(false)} onCreate={handleCreate} />}
-
- );
-}
diff --git a/frontend/app/docs/components/DocsSections.tsx b/frontend/app/docs/components/DocsSections.tsx
deleted file mode 100644
index 45b71cd2f..000000000
--- a/frontend/app/docs/components/DocsSections.tsx
+++ /dev/null
@@ -1,361 +0,0 @@
-'use client';
-
-import React from 'react';
-import { Button } from "../../components/ui/Button";
-import { DocSection } from './DocsSidebar';
-import { Copy, ExternalLink, Terminal, ShieldCheck, Target, HelpCircle } from 'lucide-react';
-
-interface SectionProps {
- section: DocSection;
-}
-
-const CodeBlock: React.FC<{ code: string; language?: string }> = ({ code, language }) => (
-
-
- {language || 'bash'}
-
-
-
-
-);
-
-const DocsSections: React.FC = ({ section }) => {
- switch (section) {
- case 'getting-started':
- return (
-
-
Getting Started
-
- Welcome to Nestera, the premier decentralized savings platform on Stellar. Our mission is to provide
- secure, transparent, and high-yield savings opportunities to everyone, regardless of their location or financial background.
-
-
-
Core Concepts
-
-
-
-
-
-
On-Chain Safety
-
All funds are held in Soroban smart contracts, audited and verifiable on-chain.
-
-
-
-
-
-
Goal-Oriented
-
Create specific savings goals with automated yield optimization to reach them faster.
-
-
-
-
Quick Start
-
- - Connect your Stellar wallet (Freighter, Albedo, or xBull).
- - Select a savings product or create a custom goal.
- - Deposit stablecoins (USDC/USDT) to start earning yield.
- - Monitor your growth through the personalized dashboard.
-
-
- );
-
- case 'connect-wallet':
- return (
-
-
Connect Wallet
-
- To interact with Nestera, you need a Stellar wallet that supports Soroban smart contracts.
- We recommend using **Freighter** for the best experience.
-
-
-
-
-
- Setup Guide
-
-
- -
- 1
- Install the Freighter extension for your browser.
-
- -
- 2
- Create a new account or import an existing recovery phrase.
-
- -
- 3
- Ensure you are on the **Public Network** or **Testnet** (depending on your environment).
-
- -
- 4
- Click the "Connect Wallet" button in the Nestera navigation bar.
-
-
-
-
- );
-
- case 'savings-goals':
- return (
-
-
Creating Savings Goals
-
- Custom goals allow you to save for specific targets—like a new home, travel, or retirement—while
- leveraging the power of automated DeFi yields.
-
-
-
Step-by-Step Tutorial
-
-
-
01
-
-
Define Your Target
-
Set a name, target amount, and deadline for your goal. This helps our algorithm optimize your yield strategy.
-
-
-
-
02
-
-
Choose Your Asset
-
Select which stablecoin you want to save in. Most users prefer USDC for its stability and liquidity on Stellar.
-
-
-
-
03
-
-
Automate Deposits
-
Set up recurring deposits or make one-time contributions whenever you have spare capital.
-
-
-
-
- );
-
- case 'api-docs':
- return (
-
-
API Documentation
-
- Developers can integrate Nestera's saving features into their own applications using our REST API or by interacting directly with our smart contracts.
-
-
-
Getting Pool Data
-
Fetch the current APY and TVL for any savings pool.
-
-
-
User Balance API
-
Retrieve a user's total savings across all goals.
-
-
- );
-
- case 'smart-contracts':
- return (
-
-
Smart Contracts
-
- Nestera is powered by a suite of Soroban smart contracts on the Stellar network.
- All contracts are open-source and verified.
-
-
-
- {[
- { name: 'Core Vault', id: 'CCV...8XY', desc: 'Main logic for fund management and yield distribution.' },
- { name: 'Goal Factory', id: 'CGF...9ZZ', desc: 'Handles creation and tracking of custom savings goals.' },
- { name: 'Yield Oracle', id: 'CYO...4AA', desc: 'Provides real-time yield data from supported DeFi protocols.' },
- ].map((contract) => (
-
-
-
{contract.name}
-
{contract.desc}
-
-
-
- ))}
-
-
-
-
-
-
Audit Status
-
- Our smart contracts have been audited by **CyberGuard** and **StellarSecurity**. No critical vulnerabilities were found.
-
-
-
-
- );
-
- case 'component-library':
- return (
-
-
Component Library
-
- Nestera uses a custom-built component library designed for high performance,
- accessibility, and consistent aesthetics.
-
-
-
Button
-
- The primary interaction component. Supports multiple variants, sizes, and loading states.
-
-
-
-
-
-
-
-
-
-
-
- Get Started
-`}
- />
-
- Variants & Sizes
-
-
-
-
- | Prop |
- Type |
- Description |
-
-
-
-
- | variant |
- primary | secondary | outline | ghost | danger |
- Visual style of the button. |
-
-
- | size |
- sm | md | lg |
- Adjusts padding, font-size, and minimum height. |
-
-
- | loading |
- boolean |
- Shows a spinner and disables the button. |
-
-
-
-
-
- Skeleton & Loading
-
- Components used to indicate loading states and improve perceived performance.
-
-
-
-
-
-
-
-
Skeleton Example
-
-
-
-
- Accessibility (A11y)
-
-
-
Keyboard Navigation
-
- All interactive components are focusable and support keyboard triggers (Enter/Space).
-
-
-
-
ARIA Attributes
-
- We use proper `aria-label`, `aria-expanded`, and `role` attributes to ensure compatibility with screen readers.
-
-
-
-
Reduced Motion
-
- Animations respect the `prefers-reduced-motion` media query using Tailwind's `motion-safe` and `motion-reduce`.
-
-
-
-
- Do's and Don'ts
-
-
-
Do
-
- - ✅ Use the `Button` component for all actions.
- - ✅ Provide descriptive `aria-label` for icon-only buttons.
- - ✅ Use `Skeleton` during early data fetch phases.
-
-
-
-
Don't
-
- - ❌ Don't use `div` for clickable elements.
- - ❌ Don't override component styles with `!important`.
- - ❌ Don't forget to handle the loading state of a button.
-
-
-
-
- );
-
- case 'faq':
- return (
-
-
Documentation FAQ
-
- Common technical questions and troubleshooting steps for users and developers.
-
-
-
- {[
- { q: 'Is there a minimum deposit amount?', a: 'No, Nestera has no minimum deposit. You can start saving with as little as 1 XLM worth of stablecoins.' },
- { q: 'What happens if a DeFi protocol Nestera uses fails?', a: 'Nestera uses multiple protocols to diversify risk. In the event of a protocol failure, our emergency pause mechanism protects remaining funds.' },
- { q: 'How often are yields compounded?', a: 'Yields are compounded automatically every ledger close (approximately every 5 seconds).' },
- { q: 'Are there any withdrawal fees?', a: 'Nestera charges a small 0.1% performance fee on the yield earned, but there are no flat withdrawal fees.' },
- ].map((item, i) => (
-
-
-
- {item.q}
-
-
{item.a}
-
- ))}
-
-
- );
-
- default:
- return null;
- }
-};
-
-export default DocsSections;
diff --git a/frontend/app/docs/components/DocsSidebar.tsx b/frontend/app/docs/components/DocsSidebar.tsx
deleted file mode 100644
index 49154410e..000000000
--- a/frontend/app/docs/components/DocsSidebar.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-'use client';
-
-import React from 'react';
-import {
- BookOpen,
- Wallet,
- Target,
- Code2,
- FileCode,
- HelpCircle
-} from 'lucide-react';
-import clsx from 'clsx';
-import { Button } from "../../components/ui/Button";
-
-export type DocSection =
- | 'getting-started'
- | 'connect-wallet'
- | 'savings-goals'
- | 'api-docs'
- | 'component-library'
- | 'smart-contracts'
- | 'faq';
-
-interface SidebarItem {
- id: DocSection;
- label: string;
- icon: React.ElementType;
-}
-
-const sidebarItems: SidebarItem[] = [
- { id: 'getting-started', label: 'Getting Started', icon: BookOpen },
- { id: 'connect-wallet', label: 'Connect Wallet', icon: Wallet },
- { id: 'savings-goals', label: 'Savings Goals', icon: Target },
- { id: 'api-docs', label: 'API Reference', icon: Code2 },
- { id: 'component-library', label: 'Components', icon: Target }, // Using Target for now, maybe find a better icon
- { id: 'smart-contracts', label: 'Smart Contracts', icon: FileCode },
- { id: 'faq', label: 'FAQ', icon: HelpCircle },
-];
-
-interface DocsSidebarProps {
- activeSection: DocSection;
- onSectionChange: (id: DocSection) => void;
-}
-
-const DocsSidebar: React.FC = ({ activeSection, onSectionChange }) => {
- return (
-
- );
-};
-
-export default DocsSidebar;
diff --git a/frontend/app/docs/layout-client.tsx b/frontend/app/docs/layout-client.tsx
deleted file mode 100644
index 69cc98de0..000000000
--- a/frontend/app/docs/layout-client.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-'use client';
-
-import React from 'react';
-import Navbar from '../components/Navbar';
-import Footer from '../components/Footer';
-
-export default function DocsLayoutClient({
- children,
-}: {
- children: React.ReactNode;
-}) {
- return (
-
- );
-}
diff --git a/frontend/app/docs/layout.tsx b/frontend/app/docs/layout.tsx
deleted file mode 100644
index 528e09ab6..000000000
--- a/frontend/app/docs/layout.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { Metadata } from 'next';
-import React from 'react';
-import { generatePageMetadata, SITE_URL } from '../lib/seo';
-import DocsLayoutClient from './layout-client';
-
-export const metadata: Metadata = generatePageMetadata({
- title: 'Documentation | Nestera',
- description:
- 'Learn how to use Nestera, integrate with smart contracts, and manage your decentralized savings accounts.',
- url: '/docs',
- canonical: `${SITE_URL}/docs`,
-});
-
-export default function DocsLayout({
- children,
-}: {
- children: React.ReactNode;
-}) {
- return {children};
-}
-
diff --git a/frontend/app/docs/page.tsx b/frontend/app/docs/page.tsx
deleted file mode 100644
index 80aa57be1..000000000
--- a/frontend/app/docs/page.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-'use client';
-
-import React, { useState } from 'react';
-import DocsSidebar, { DocSection } from './components/DocsSidebar';
-import DocsSections from './components/DocsSections';
-
-export default function DocsPage() {
- const [activeSection, setActiveSection] = useState('getting-started');
-
- return (
-
- );
-}
diff --git a/frontend/app/features/components/FeatureGrid.tsx b/frontend/app/features/components/FeatureGrid.tsx
deleted file mode 100644
index 38c43fa22..000000000
--- a/frontend/app/features/components/FeatureGrid.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-'use client';
-
-import React from 'react';
-import { ShieldCheck, Zap, Globe2, Target, BarChart3, Lock } from 'lucide-react';
-
-const features = [
- {
- icon: ShieldCheck,
- color: 'text-cyan-400',
- bg: 'bg-cyan-400/10',
- title: 'Non-Custodial Security',
- description: 'Your keys, your crypto. Nestera never holds your funds — everything is managed directly by audited Soroban smart contracts.',
- },
- {
- icon: Zap,
- color: 'text-yellow-400',
- bg: 'bg-yellow-400/10',
- title: 'Auto Yield Optimization',
- description: 'Our protocol continuously routes deposits to the highest-yielding strategies on Stellar — no manual rebalancing required.',
- },
- {
- icon: Globe2,
- color: 'text-emerald-400',
- bg: 'bg-emerald-400/10',
- title: 'Multi-Asset Support',
- description: 'Save in USDC, USDT, XLM and more. Seamlessly switch between assets without leaving the Nestera interface.',
- },
- {
- icon: Target,
- color: 'text-purple-400',
- bg: 'bg-purple-400/10',
- title: 'Goal-Based Savings',
- description: 'Create named goals with deadlines. Track your progress in real time and celebrate milestones as you hit them.',
- },
- {
- icon: BarChart3,
- color: 'text-blue-400',
- bg: 'bg-blue-400/10',
- title: 'Live Analytics',
- description: 'Full-history charts, per-goal breakdowns, and yield projections — all visible on your personal dashboard.',
- },
- {
- icon: Lock,
- color: 'text-rose-400',
- bg: 'bg-rose-400/10',
- title: 'Flexible Lock Options',
- description: 'Choose between flexible withdrawals or time-locked vaults for higher APY. You decide the risk-reward balance.',
- },
-];
-
-const FeatureGrid: React.FC = () => {
- return (
-
-
-
-
Core Features
-
- A complete toolkit for decentralized savings — from first deposit to financial freedom.
-
-
-
-
- {features.map((f) => (
-
-
-
-
-
{f.title}
-
{f.description}
-
- ))}
-
-
-
- );
-};
-
-export default FeatureGrid;
diff --git a/frontend/app/features/components/FeaturesCta.tsx b/frontend/app/features/components/FeaturesCta.tsx
deleted file mode 100644
index 311c45162..000000000
--- a/frontend/app/features/components/FeaturesCta.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-'use client';
-
-import React from 'react';
-import Link from 'next/link';
-import { ArrowRight, Wallet } from 'lucide-react';
-
-const FeaturesCta: React.FC = () => {
- return (
-
-
-
- {/* Glow blob */}
-
-
-
-
-
-
-
-
- Ready to grow your savings on-chain?
-
-
- Connect your wallet and start earning in seconds. No bank required. No sign-up forms. Just DeFi-powered savings.
-
-
-
-
- Start Saving Now
-
-
- Join the Community
-
-
-
-
-
-
- );
-};
-
-export default FeaturesCta;
diff --git a/frontend/app/features/components/FeaturesHero.tsx b/frontend/app/features/components/FeaturesHero.tsx
deleted file mode 100644
index 9dae4aef8..000000000
--- a/frontend/app/features/components/FeaturesHero.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-'use client';
-
-import React from 'react';
-import { ArrowRight, Sparkles } from 'lucide-react';
-import Link from 'next/link';
-
-const FeaturesHero: React.FC = () => {
- return (
-
- {/* Background glows */}
-
-
-
-
-
- Everything you need. Nothing you don't.
-
-
-
- Savings built for the{' '}
-
- decentralized future.
-
-
-
-
- From smart-contract security to goal-based automation, every feature of Nestera is designed to make your money work harder — transparently, non-custodially, on Stellar.
-
-
-
-
- Start Saving
-
-
- Read the Docs
-
-
-
- {/* Decorative stat strip */}
-
- {[
- { value: '12% APY', label: 'Average Yield' },
- { value: '$10M+', label: 'Total Value Locked' },
- { value: '< 1s', label: 'Settlement Time' },
- ].map((s) => (
-
- {s.value}
- {s.label}
-
- ))}
-
-
-
- );
-};
-
-export default FeaturesHero;
diff --git a/frontend/app/features/components/GoalToolsSection.tsx b/frontend/app/features/components/GoalToolsSection.tsx
deleted file mode 100644
index d40b76572..000000000
--- a/frontend/app/features/components/GoalToolsSection.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-'use client';
-
-import React from 'react';
-import { Target, Bell, Users, CalendarCheck } from 'lucide-react';
-
-const tools = [
- {
- icon: Target,
- color: 'text-purple-400',
- bg: 'bg-purple-400/10',
- title: 'Named Goals',
- body: 'Create goals with a name, emoji, and target amount. "🏡 House Fund" hits differently than a generic savings account.',
- },
- {
- icon: CalendarCheck,
- color: 'text-cyan-400',
- bg: 'bg-cyan-400/10',
- title: 'Deadline Tracking',
- body: 'Set a target date and Nestera tells you exactly how much you need to deposit per week to reach it on time.',
- },
- {
- icon: Bell,
- color: 'text-yellow-400',
- bg: 'bg-yellow-400/10',
- title: 'Milestone Alerts',
- body: 'Get notified when you hit 25%, 50%, 75%, and 100% of your goal — celebratory moments built right in.',
- },
- {
- icon: Users,
- color: 'text-emerald-400',
- bg: 'bg-emerald-400/10',
- title: 'Group Savings',
- body: 'Pool funds with family or friends toward a shared goal. Multi-sig authorization keeps everyone accountable.',
- },
-];
-
-const GoalToolsSection: React.FC = () => {
- return (
-
-
-
-
-
- {/* Left: text */}
-
-
- Goal-Based Savings
-
-
- Save with intention. Reach goals faster.
-
-
- Vague savings stall. Concrete goals with deadlines succeed. Nestera's goal toolkit gives every deposit a purpose — and a finish line.
-
-
- All goal logic runs entirely on-chain. No third party can freeze, redirect, or delay your progress.
-
-
-
- {/* Right: tool cards */}
-
- {tools.map((t) => (
-
-
-
-
-
{t.title}
-
{t.body}
-
- ))}
-
-
-
- {/* Inline progress UI mockup */}
-
-
Example Goal Progress
-
- {[
- { label: '🏡 House Fund', pct: 68, current: '$6,800', target: '$10,000', color: 'bg-purple-400' },
- { label: '✈️ Japan Trip', pct: 42, current: '$2,100', target: '$5,000', color: 'bg-cyan-400' },
- { label: '📚 Education', pct: 91, current: '$9,100', target: '$10,000', color: 'bg-emerald-400' },
- ].map((g) => (
-
-
- {g.label}
- {g.pct}%
-
-
-
- {g.current}
- {g.target}
-
-
- ))}
-
-
-
-
- );
-};
-
-export default GoalToolsSection;
diff --git a/frontend/app/features/components/MultiAssetSection.tsx b/frontend/app/features/components/MultiAssetSection.tsx
deleted file mode 100644
index e94aac5c7..000000000
--- a/frontend/app/features/components/MultiAssetSection.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-'use client';
-
-import React from 'react';
-import { Globe2, ArrowLeftRight, Repeat2 } from 'lucide-react';
-
-const assets = [
- { symbol: 'USDC', name: 'USD Coin', apy: '12.4%', color: '#2775CA', badge: 'Most Popular' },
- { symbol: 'USDT', name: 'Tether', apy: '11.8%', color: '#26A17B', badge: null },
- { symbol: 'XLM', name: 'Stellar Lumens', apy: '9.2%', color: '#00C8FF', badge: 'Native' },
- { symbol: 'EURC', name: 'Euro Coin', apy: '10.5%', color: '#0052B4', badge: null },
-];
-
-const MultiAssetSection: React.FC = () => {
- return (
-
-
-
-
-
- {/* Left: asset cards */}
-
- {assets.map((a) => (
-
-
- {/* Symbol badge */}
-
- {a.symbol}
-
-
-
- {a.symbol}
- {a.badge && (
-
- {a.badge}
-
- )}
-
-
{a.name}
-
-
-
-
- ))}
-
-
- {/* Right: text */}
-
-
- Multi-Asset Support
-
-
- Save in any asset. Switch instantly.
-
-
- Nestera supports all major stablecoins and Stellar-native assets. Diversify across currencies, hedge your exposure, or chase the highest yield — all from one dashboard.
-
-
-
-
-
-
-
1-Click Asset Swaps
-
Switch between supported assets at the best available Stellar DEX rate, directly inside the app.
-
-
-
-
-
-
Auto-Rebalancing
-
Set target allocations and let Nestera automatically rebalance when drift exceeds your threshold.
-
-
-
-
-
-
-
- );
-};
-
-export default MultiAssetSection;
diff --git a/frontend/app/features/components/SecuritySection.tsx b/frontend/app/features/components/SecuritySection.tsx
deleted file mode 100644
index 1f78c4a50..000000000
--- a/frontend/app/features/components/SecuritySection.tsx
+++ /dev/null
@@ -1,115 +0,0 @@
-'use client';
-
-import React from 'react';
-import { ShieldCheck, FileSearch, Lock, CheckCircle2 } from 'lucide-react';
-
-const audits = [
- { firm: 'CyberGuard Labs', date: 'Jan 2025', status: 'Passed', issues: '0 Critical' },
- { firm: 'StellarSecurity', date: 'Mar 2025', status: 'Passed', issues: '0 Critical' },
-];
-
-const pillars = [
- {
- icon: ShieldCheck,
- color: 'text-cyan-400',
- bg: 'bg-cyan-400/10',
- title: 'Audited Contracts',
- body: 'Every Soroban contract powering Nestera has been independently audited. Findings are published publicly with full remediation reports.',
- },
- {
- icon: FileSearch,
- color: 'text-emerald-400',
- bg: 'bg-emerald-400/10',
- title: 'Open-Source Code',
- body: 'All contract source code lives on GitHub. Any developer can verify logic, fork it, or contribute improvements via pull request.',
- },
- {
- icon: Lock,
- color: 'text-purple-400',
- bg: 'bg-purple-400/10',
- title: 'Non-Custodial Design',
- body: 'Nestera never controls your private keys. Withdrawals are always available — no permissions required, no waiting periods imposed by us.',
- },
-];
-
-const SecuritySection: React.FC = () => {
- return (
-
- {/* Subtle diagonal line pattern */}
-
-
-
-
- {/* Left: text */}
-
-
- Smart Contract Security
-
-
- Security isn't a feature —
it's the foundation.
-
-
- Nestera is built on a principle of radical transparency. Every line of contract code is open-source, every audit is public, and every user retains full custody of their assets at all times.
-
-
-
- {pillars.map((p) => (
-
- ))}
-
-
-
- {/* Right: audit table */}
-
-
-
Audit Record
-
- {audits.map((a) => (
-
-
-
- {a.issues}
-
- {a.status}
-
-
-
- ))}
-
-
-
-
-
-
All contracts currently operational
-
-
-
-
Emergency pause mechanism active
-
-
-
-
-
-
-
- );
-};
-
-export default SecuritySection;
diff --git a/frontend/app/features/components/YieldSection.tsx b/frontend/app/features/components/YieldSection.tsx
deleted file mode 100644
index 5d9f99427..000000000
--- a/frontend/app/features/components/YieldSection.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-'use client';
-
-import React from 'react';
-import { TrendingUp, RefreshCcw, Layers } from 'lucide-react';
-
-const steps = [
- {
- icon: Layers,
- color: 'text-cyan-400',
- title: 'Deposit',
- body: 'You deposit any supported stablecoin into a Nestera vault. Funds are held securely on-chain.',
- },
- {
- icon: RefreshCcw,
- color: 'text-teal-400',
- title: 'Auto-Route',
- body: 'The Yield Oracle scans live APY data across Stellar DeFi protocols and routes your funds to the highest-yielding option.',
- },
- {
- icon: TrendingUp,
- color: 'text-emerald-400',
- title: 'Compound',
- body: 'Earned yield is automatically re-invested every ledger close (~5 seconds), maximizing compounding with no manual effort.',
- },
-];
-
-const YieldSection: React.FC = () => {
- return (
-
-
-
-
- Yield Optimization
-
-
- Your money, always working at peak efficiency.
-
-
- Manual yield farming is a full-time job. Nestera automates it completely — so you earn more while doing less.
-
-
-
- {/* Flow diagram */}
-
- {steps.map((step, i) => (
-
-
-
-
-
-
- Step {i + 1}
-
-
{step.title}
-
{step.body}
-
- {i < steps.length - 1 && (
-
- )}
-
- ))}
-
-
- {/* APY comparison strip */}
-
-
-
Current Strategy
-
12.4% APY
-
USDC · Stellar Liquidity Pool #3
-
-
-
-
Traditional Savings
-
0.5% APY
-
Average bank savings rate (2025)
-
-
-
-
You earn
-
24× more
-
with automated DeFi yield
-
-
-
-
- );
-};
-
-export default YieldSection;
diff --git a/frontend/app/features/page.tsx b/frontend/app/features/page.tsx
deleted file mode 100644
index cf47e8b33..000000000
--- a/frontend/app/features/page.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import type { Metadata } from 'next';
-import Navbar from '../components/Navbar';
-import Footer from '../components/Footer';
-import FeaturesHero from './components/FeaturesHero';
-import FeatureGrid from './components/FeatureGrid';
-import SecuritySection from './components/SecuritySection';
-import YieldSection from './components/YieldSection';
-import MultiAssetSection from './components/MultiAssetSection';
-import GoalToolsSection from './components/GoalToolsSection';
-import FeaturesCta from './components/FeaturesCta';
-import { generatePageMetadata, SITE_URL } from '../lib/seo';
-
-export const metadata: Metadata = generatePageMetadata({
- title: 'Features — Nestera',
- description:
- 'Explore the full suite of Nestera features: decentralized savings, smart-contract security, yield optimization, multi-asset support, and goal-based tools — all on Stellar.',
- url: '/features',
- canonical: `${SITE_URL}/features`,
-});
-
-export default function FeaturesPage() {
- return (
-
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/frontend/app/goals/layout.tsx b/frontend/app/goals/layout.tsx
index 454054219..1641d7182 100644
--- a/frontend/app/goals/layout.tsx
+++ b/frontend/app/goals/layout.tsx
@@ -5,7 +5,6 @@ export const metadata: Metadata = generatePageMetadata({
title: "Goal Management - Nestera",
description: "Manage and track your financial goals with Nestera. Set milestones, automate savings, and achieve your financial objectives through decentralized smart contracts.",
url: "/goals",
- canonical: `${SITE_URL}/goals`,
});
export default function GoalsLayout({
diff --git a/frontend/app/hooks/useCountUp.ts b/frontend/app/hooks/useCountUp.ts
index 048aabe8d..1171e3ecd 100644
--- a/frontend/app/hooks/useCountUp.ts
+++ b/frontend/app/hooks/useCountUp.ts
@@ -1,122 +1,38 @@
-"use client";
+import { useEffect, useState } from "react";
-import { useEffect, useRef, useState } from "react";
-
-interface UseCountUpOptions {
- /** Final value to count to */
- end: number;
- /** Starting value (default 0) */
- start?: number;
- /** Duration in ms (default 1200) */
- duration?: number;
- /** Decimal places to display (default 0) */
- decimals?: number;
- /** Delay before starting in ms (default 0) */
- delay?: number;
- /** Only animate once when the element enters the viewport */
- observeVisibility?: boolean;
-}
-
-function easeOutExpo(t: number): number {
- return t === 1 ? 1 : 1 - Math.pow(2, -10 * t);
+interface UseCountUpProps {
+ end: number;
+ decimals?: number;
+ duration?: number;
+ delay?: number;
}
-/**
- * Animates a number from `start` to `end` over `duration` ms.
- * Respects `prefers-reduced-motion` — returns the final value immediately if motion is reduced.
- *
- * @example
- * const displayed = useCountUp({ end: 24593.82, decimals: 2 });
- * return ${displayed};
- */
-export function useCountUp({
- end,
- start = 0,
- duration = 1200,
- decimals = 0,
- delay = 0,
- observeVisibility = true,
-}: UseCountUpOptions): string {
- const [value, setValue] = useState(start);
- const rafRef = useRef(null);
- const startTimeRef = useRef(null);
- const elemRef = useRef(null);
- const [visible, setVisible] = useState(!observeVisibility);
-
- // Detect reduced-motion preference
- const prefersReduced =
- typeof window !== "undefined" &&
- window.matchMedia("(prefers-reduced-motion: reduce)").matches;
-
- // Observe visibility when mounted into a real element
- useEffect(() => {
- if (!observeVisibility || prefersReduced) return;
-
- const observer = new IntersectionObserver(
- ([entry]) => {
- if (entry.isIntersecting) {
- setVisible(true);
- observer.disconnect();
- }
- },
- { threshold: 0.1 }
- );
+export function useCountUp({ end, decimals = 0, duration = 1000, delay = 0 }: UseCountUpProps): string {
+ const [count, setCount] = useState(0);
- // We attach to a sentinel — the hook consumer should ref an element,
- // but as a fallback we just start after a short delay
- const timer = window.setTimeout(() => setVisible(true), 300);
- return () => {
- observer.disconnect();
- window.clearTimeout(timer);
- };
- }, [observeVisibility, prefersReduced]);
+ useEffect(() => {
+ const timeout = setTimeout(() => {
+ const startTime = Date.now();
+ const timer = setInterval(() => {
+ const elapsed = Date.now() - startTime;
+ const progress = Math.min(elapsed / duration, 1);
- useEffect(() => {
- if (prefersReduced) {
- setValue(end);
- return;
- }
+ // Easing function for smooth animation
+ const easeOut = 1 - Math.pow(1 - progress, 3);
+ const currentCount = end * easeOut;
- if (!visible) return;
+ setCount(currentCount);
- const startAnimation = () => {
- startTimeRef.current = null;
+ if (progress >= 1) {
+ clearInterval(timer);
+ }
+ }, 16); // ~60fps
- const animate = (timestamp: number) => {
- if (!startTimeRef.current) startTimeRef.current = timestamp;
- const elapsed = timestamp - startTimeRef.current;
- const progress = Math.min(elapsed / duration, 1);
- const eased = easeOutExpo(progress);
- setValue(start + (end - start) * eased);
+ return () => clearInterval(timer);
+ }, delay);
- if (progress < 1) {
- rafRef.current = requestAnimationFrame(animate);
- } else {
- setValue(end);
- }
- };
-
- rafRef.current = requestAnimationFrame(animate);
- };
-
- const timer = window.setTimeout(startAnimation, delay);
-
- return () => {
- window.clearTimeout(timer);
- if (rafRef.current) cancelAnimationFrame(rafRef.current);
- };
- }, [end, start, duration, delay, visible, prefersReduced]);
-
- return value.toLocaleString(undefined, {
- minimumFractionDigits: decimals,
- maximumFractionDigits: decimals,
- });
-}
+ return () => clearTimeout(timeout);
+ }, [end, duration, delay]);
-/**
- * Returns an element ref to enable visibility-based trigger.
- * Attach to the wrapping element:
- */
-export function useCountUpRef
() {
- return useRef(null);
+ return count.toFixed(decimals);
}
diff --git a/frontend/app/hooks/useDebounce.ts b/frontend/app/hooks/useDebounce.ts
deleted file mode 100644
index 145fab56d..000000000
--- a/frontend/app/hooks/useDebounce.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { useEffect, useState } from "react";
-
-export function useDebounce(value: T, delay: number): T {
- const [debouncedValue, setDebouncedValue] = useState(value);
-
- useEffect(() => {
- const handler = setTimeout(() => {
- setDebouncedValue(value);
- }, delay);
-
- return () => {
- clearTimeout(handler);
- };
- }, [value, delay]);
-
- return debouncedValue;
-}
diff --git a/frontend/app/hooks/useExport.ts b/frontend/app/hooks/useExport.ts
index faa764856..030c2610f 100644
--- a/frontend/app/hooks/useExport.ts
+++ b/frontend/app/hooks/useExport.ts
@@ -1,140 +1,59 @@
-"use client";
+import { useState } from "react";
-import { useState, useCallback } from "react";
-
-export type ExportFormat = "csv" | "json" | "pdf";
-
-export interface DateRange {
- from?: string; // ISO date string YYYY-MM-DD
- to?: string;
-}
+export type DateRange = {
+ from: Date;
+ to: Date;
+};
interface UseExportOptions {
- onSuccess?: (format: ExportFormat, filename: string) => void;
- onError?: (err: Error) => void;
-}
-
-function toCsv(rows: Record[]): string {
- if (rows.length === 0) return "";
- const headers = Object.keys(rows[0]);
- const escape = (v: unknown) => {
- const s = v == null ? "" : String(v);
- return /[",\n]/.test(s) ? `"${s.replaceAll('"', '""')}"` : s;
- };
- return [
- headers.join(","),
- ...rows.map((r) => headers.map((h) => escape(r[h])).join(",")),
- ].join("\n") + "\n";
-}
-
-function downloadBlob(blob: Blob, filename: string) {
- const url = URL.createObjectURL(blob);
- const a = document.createElement("a");
- a.href = url;
- a.download = filename;
- document.body.appendChild(a);
- a.click();
- a.remove();
- URL.revokeObjectURL(url);
+ onSuccess?: (format: string, filename: string) => void;
+ onError?: (error: Error) => void;
}
-function filterByDateRange>(
- rows: T[],
- dateKey: string,
- range?: DateRange,
-): T[] {
- if (!range?.from && !range?.to) return rows;
- return rows.filter((row) => {
- const val = row[dateKey];
- if (!val) return true;
- const d = new Date(val as string).toISOString().slice(0, 10);
- if (range.from && d < range.from) return false;
- if (range.to && d > range.to) return false;
- return true;
- });
-}
-
-/**
- * Builds a minimal printable PDF via the browser print dialog.
- * Supports title + table data without adding PDF library dependencies.
- */
-function printAsPdf(title: string, rows: Record[]) {
- if (rows.length === 0) return;
- const headers = Object.keys(rows[0]);
- const thStyle = "border:1px solid #ccc;padding:6px 10px;background:#f5f5f5;font-size:11px;";
- const tdStyle = "border:1px solid #eee;padding:5px 10px;font-size:11px;";
- const tableRows = rows
- .map(
- (r) =>
- `${headers.map((h) => `| ${r[h] ?? ""} | `).join("")}
`,
- )
- .join("");
- const html = `${title}
-
- ${title}
- ${headers.map((h) => `| ${h} | `).join("")}
- ${tableRows}
`;
- const win = window.open("", "_blank");
- if (!win) return;
- win.document.write(html);
- win.document.close();
- win.focus();
- win.print();
-}
-
-export function useExport(options: UseExportOptions = {}) {
- const [loading, setLoading] = useState(false);
-
- const exportData = useCallback(
- async >(
- data: T[],
- {
- format,
- filename,
- dateKey,
- dateRange,
- pdfTitle,
- }: {
- format: ExportFormat;
- filename: string;
- dateKey?: string;
- dateRange?: DateRange;
- pdfTitle?: string;
- },
- ) => {
- setLoading(true);
- try {
- const filtered = dateKey
- ? filterByDateRange(data, dateKey, dateRange)
- : data;
-
- const stamp = new Date().toISOString().slice(0, 10);
- const fullName = `${filename}-${stamp}`;
-
- if (format === "csv") {
- const blob = new Blob([toCsv(filtered)], {
- type: "text/csv;charset=utf-8",
- });
- downloadBlob(blob, `${fullName}.csv`);
- options.onSuccess?.(format, `${fullName}.csv`);
- } else if (format === "json") {
- const blob = new Blob([JSON.stringify(filtered, null, 2)], {
- type: "application/json",
- });
- downloadBlob(blob, `${fullName}.json`);
- options.onSuccess?.(format, `${fullName}.json`);
- } else if (format === "pdf") {
- printAsPdf(pdfTitle ?? filename, filtered);
- options.onSuccess?.(format, `${fullName}.pdf`);
+export function useExport(options?: UseExportOptions) {
+ const [loading, setLoading] = useState(false);
+
+ const exportData = async (data: any[], format: "csv" | "json", filename: string) => {
+ try {
+ setLoading(true);
+
+ let content: string;
+ let mimeType: string;
+
+ if (format === "csv") {
+ // Simple CSV export
+ const headers = Object.keys(data[0] || {});
+ const csv = [
+ headers.join(","),
+ ...data.map(row => headers.map(h => JSON.stringify(row[h] || "")).join(","))
+ ].join("\n");
+
+ content = csv;
+ mimeType = "text/csv;charset=utf-8;";
+ } else {
+ // JSON export
+ content = JSON.stringify(data, null, 2);
+ mimeType = "application/json";
+ }
+
+ // Download file
+ const blob = new Blob([content], { type: mimeType });
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement("a");
+ link.setAttribute("href", url);
+ link.setAttribute("download", `${filename}.${format}`);
+ link.style.visibility = "hidden";
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+
+ options?.onSuccess?.(format, filename);
+ } catch (error) {
+ options?.onError?.(error as Error);
+ } finally {
+ setLoading(false);
}
- } catch (err) {
- options.onError?.(err instanceof Error ? err : new Error(String(err)));
- } finally {
- setLoading(false);
- }
- },
- [options],
- );
+ };
- return { exportData, loading };
+ return { exportData, loading };
}
diff --git a/frontend/app/hooks/useFeatureFlag.ts b/frontend/app/hooks/useFeatureFlag.ts
deleted file mode 100644
index 19a123733..000000000
--- a/frontend/app/hooks/useFeatureFlag.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-"use client";
-
-import { useFeatureFlags } from "../context/FeatureFlagContext";
-import type { FlagValue } from "../lib/feature-flags";
-
-/**
- * Check if a single feature flag is enabled.
- *
- * @example
- * const { isEnabled, isLoading } = useFeatureFlag('new-dashboard-layout');
- * if (isLoading) return ;
- * if (isEnabled) return ;
- * return ;
- */
-export function useFeatureFlag(flagKey: string): {
- isEnabled: boolean;
- isLoading: boolean;
-} {
- const { isEnabled, isLoading } = useFeatureFlags();
- return {
- isEnabled: isEnabled(flagKey),
- isLoading,
- };
-}
-
-/**
- * Get a flag value (for string/number/multivariate flags).
- *
- * @example
- * const { value } = useFeatureFlagValue('ab-cta-button-color');
- * // value === 'teal' | 'green'
- */
-export function useFeatureFlagValue(
- flagKey: string
-): {
- value: T;
- isLoading: boolean;
-} {
- const { getValue, isLoading } = useFeatureFlags();
- return {
- value: getValue(flagKey),
- isLoading,
- };
-}
-
-/**
- * Get multiple flags at once.
- *
- * @example
- * const flags = useFeatureFlagMany(['new-dashboard', 'beta-charts']);
- * // flags['new-dashboard'] === true | false
- */
-export function useFeatureFlagMany(flagKeys: string[]): {
- flags: Record;
- isLoading: boolean;
-} {
- const { isEnabled, isLoading } = useFeatureFlags();
- const flags = Object.fromEntries(
- flagKeys.map((key) => [key, isEnabled(key)])
- );
- return { flags, isLoading };
-}
diff --git a/frontend/app/hooks/useFocusTrap.ts b/frontend/app/hooks/useFocusTrap.ts
index 751dad336..44e144f14 100644
--- a/frontend/app/hooks/useFocusTrap.ts
+++ b/frontend/app/hooks/useFocusTrap.ts
@@ -1,86 +1,60 @@
-"use client";
-
-import { RefObject, useEffect, useRef } from "react";
-
-const FOCUSABLE_SELECTOR =
- 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
+import { useEffect, useRef, RefObject } from "react";
interface UseFocusTrapOptions {
- isOpen: boolean;
- containerRef: RefObject;
+ isOpen?: boolean;
+ containerRef?: RefObject;
initialFocusRef?: RefObject;
onEscape?: () => void;
}
-export function useFocusTrap({
- isOpen,
- containerRef,
- initialFocusRef,
- onEscape,
-}: UseFocusTrapOptions) {
- const previousFocusRef = useRef(null);
+export function useFocusTrap(options?: UseFocusTrapOptions) {
+ const internalRef = useRef(null);
+ const containerRef = options?.containerRef || internalRef;
+ const isOpen = options?.isOpen ?? true;
useEffect(() => {
- if (!isOpen) {
- return;
+ if (!isOpen) return;
+
+ const element = containerRef.current;
+ if (!element) return;
+
+ const focusableElements = element.querySelectorAll(
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
+ );
+
+ const firstElement = focusableElements[0];
+ const lastElement = focusableElements[focusableElements.length - 1];
+
+ if (options?.initialFocusRef?.current) {
+ options.initialFocusRef.current.focus();
+ } else {
+ firstElement?.focus();
}
- previousFocusRef.current = document.activeElement as HTMLElement;
- const container = containerRef.current;
-
- const focusables = container?.querySelectorAll(FOCUSABLE_SELECTOR);
- const first = focusables?.[0];
-
- requestAnimationFrame(() => {
- initialFocusRef?.current?.focus();
- if (!initialFocusRef?.current) {
- first?.focus();
- }
- });
-
- const onKeyDown = (event: KeyboardEvent) => {
- if (!container) {
- return;
- }
-
- if (event.key === "Escape") {
- event.preventDefault();
- onEscape?.();
- return;
- }
-
- if (event.key !== "Tab") {
- return;
- }
-
- const activeFocusables = Array.from(
- container.querySelectorAll(FOCUSABLE_SELECTOR),
- );
-
- if (activeFocusables.length === 0) {
- event.preventDefault();
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === "Escape" && options?.onEscape) {
+ options.onEscape();
return;
}
-
- const firstFocusable = activeFocusables[0];
- const lastFocusable = activeFocusables[activeFocusables.length - 1];
- const current = document.activeElement as HTMLElement | null;
-
- if (event.shiftKey && current === firstFocusable) {
- event.preventDefault();
- lastFocusable.focus();
- } else if (!event.shiftKey && current === lastFocusable) {
- event.preventDefault();
- firstFocusable.focus();
+
+ if (e.key !== "Tab") return;
+
+ if (e.shiftKey) {
+ if (document.activeElement === firstElement) {
+ lastElement?.focus();
+ e.preventDefault();
+ }
+ } else {
+ if (document.activeElement === lastElement) {
+ firstElement?.focus();
+ e.preventDefault();
+ }
}
};
- document.addEventListener("keydown", onKeyDown);
+ element.addEventListener("keydown", handleKeyDown);
+ return () => element.removeEventListener("keydown", handleKeyDown);
+ }, [isOpen, containerRef, options?.initialFocusRef, options?.onEscape]);
- return () => {
- document.removeEventListener("keydown", onKeyDown);
- previousFocusRef.current?.focus();
- };
- }, [containerRef, initialFocusRef, isOpen, onEscape]);
+ return internalRef;
}
-
diff --git a/frontend/app/hooks/useKeyboardShortcuts.ts b/frontend/app/hooks/useKeyboardShortcuts.ts
deleted file mode 100644
index 4330983ac..000000000
--- a/frontend/app/hooks/useKeyboardShortcuts.ts
+++ /dev/null
@@ -1,117 +0,0 @@
-"use client";
-
-import { useEffect } from "react";
-import { useRouter } from "next/navigation";
-
-export interface ShortcutDef {
- key: string;
- label: string;
- description: string;
- global?: boolean; // fires even inside inputs
-}
-
-export const SHORTCUTS: ShortcutDef[] = [
- { key: "Ctrl+K", label: "Ctrl+K", description: "Open search / command palette" },
- { key: "g d", label: "G then D", description: "Go to Dashboard" },
- { key: "g s", label: "G then S", description: "Go to Savings" },
- { key: "g p", label: "G then P", description: "Go to Portfolio" },
- { key: "g t", label: "G then T", description: "Go to Transactions" },
- { key: "?", label: "?", description: "Show keyboard shortcuts" },
- { key: "Escape", label: "Esc", description: "Close modals / dropdowns", global: true },
- { key: "Ctrl+/", label: "Ctrl+/", description: "Toggle theme" },
- { key: "n", label: "N", description: "New goal (on Savings page)" },
-];
-
-interface Options {
- onSearch?: () => void;
- onToggleTheme?: () => void;
- onShowHelp?: () => void;
- onNewGoal?: () => void;
- pathname?: string;
-}
-
-export function useKeyboardShortcuts({
- onSearch,
- onToggleTheme,
- onShowHelp,
- onNewGoal,
- pathname = "",
-}: Options) {
- const router = useRouter();
-
- useEffect(() => {
- let pending: string | null = null;
- let pendingTimer: ReturnType | null = null;
-
- const clear = () => {
- pending = null;
- if (pendingTimer) clearTimeout(pendingTimer);
- };
-
- const handler = (e: KeyboardEvent) => {
- const active = document.activeElement;
- const inInput =
- active instanceof HTMLInputElement ||
- active instanceof HTMLTextAreaElement ||
- (active as HTMLElement)?.isContentEditable;
-
- // Ctrl+K — always fires
- if ((e.ctrlKey || e.metaKey) && e.key === "k") {
- e.preventDefault();
- onSearch?.();
- return;
- }
-
- // Ctrl+/ — toggle theme
- if ((e.ctrlKey || e.metaKey) && e.key === "/") {
- e.preventDefault();
- onToggleTheme?.();
- return;
- }
-
- if (inInput) return;
-
- // ? — show help
- if (e.key === "?") {
- e.preventDefault();
- onShowHelp?.();
- return;
- }
-
- // N — new goal on savings page
- if (e.key === "n" && pathname.startsWith("/savings")) {
- e.preventDefault();
- onNewGoal?.();
- return;
- }
-
- // Two-key sequences: g then d/s/p/t
- if (e.key === "g") {
- pending = "g";
- pendingTimer = setTimeout(clear, 1000);
- return;
- }
-
- if (pending === "g") {
- clear();
- const routes: Record = {
- d: "/dashboard",
- s: "/savings",
- p: "/dashboard/portfolio",
- t: "/dashboard/transactions",
- };
- const route = routes[e.key.toLowerCase()];
- if (route) {
- e.preventDefault();
- router.push(route);
- }
- }
- };
-
- document.addEventListener("keydown", handler);
- return () => {
- document.removeEventListener("keydown", handler);
- clear();
- };
- }, [onSearch, onToggleTheme, onShowHelp, onNewGoal, pathname, router]);
-}
diff --git a/frontend/app/hooks/usePrices.ts b/frontend/app/hooks/usePrices.ts
index 2b8dd5e57..4c714a371 100644
--- a/frontend/app/hooks/usePrices.ts
+++ b/frontend/app/hooks/usePrices.ts
@@ -1,72 +1,18 @@
-"use client";
-
-import { useEffect, useState } from "react";
-import { env } from "../lib/env";
-
-const COINGECKO_IDS: Record = {
- XLM: "stellar",
- USDC: "usd-coin",
- AQUA: "aqua",
-};
-
-interface PriceData {
- [coingeckoId: string]: { usd: number };
-}
-
-async function fetchPrices(): Promise {
- const assetIds = Object.values(COINGECKO_IDS).join(",");
- const res = await fetch(
- `${env.coingeckoApi}/simple/price?ids=${assetIds}&vs_currencies=usd`,
- );
-
- if (!res.ok) {
- throw new Error("Failed to fetch prices");
- }
-
- return res.json();
-}
-
+// Stub price hook for MVP
export function usePrices() {
- const [data, setData] = useState();
- const [error, setError] = useState(null);
-
- useEffect(() => {
- let cancelled = false;
-
- const load = async () => {
- try {
- const prices = await fetchPrices();
- if (!cancelled) {
- setData(prices);
- setError(null);
- }
- } catch (err) {
- if (!cancelled) {
- setError(err instanceof Error ? err : new Error("Failed to fetch prices"));
- }
- }
- };
-
- load();
- const interval = setInterval(load, 5 * 60 * 1000);
-
- return () => {
- cancelled = true;
- clearInterval(interval);
+ return {
+ data: {},
+ prices: {},
+ loading: false,
+ error: null,
};
- }, []);
-
- return { data, error, isLoading: !data && !error };
}
-export function getAssetPrice(
- prices: PriceData | undefined,
- assetCode: string,
-): number {
- if (!prices) return assetCode === "USDC" ? 1 : 0;
- const coingeckoId = COINGECKO_IDS[assetCode];
- return prices[coingeckoId]?.usd ?? (assetCode === "USDC" ? 1 : 0);
+export function getAssetPrice(prices: any, asset: string): number {
+ // Stub prices
+ const defaultPrices: Record = {
+ "USDC": 1.00,
+ "XLM": 0.12,
+ };
+ return prices?.[asset] || defaultPrices[asset] || 0;
}
-
-export { COINGECKO_IDS };
-export type { PriceData };
diff --git a/frontend/app/hooks/useUndoRedo.ts b/frontend/app/hooks/useUndoRedo.ts
deleted file mode 100644
index cdf1c2739..000000000
--- a/frontend/app/hooks/useUndoRedo.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-"use client";
-
-import { useCallback, useEffect, useRef, useState } from "react";
-
-const MAX_HISTORY = 20;
-
-export function useUndoRedo(initialState: T) {
- const [history, setHistory] = useState([initialState]);
- const [index, setIndex] = useState(0);
- const key = useRef(`undo-redo-${Math.random().toString(36).slice(2)}`);
-
- const state = history[index];
- const canUndo = index > 0;
- const canRedo = index < history.length - 1;
-
- const addToHistory = useCallback((newState: T) => {
- setHistory((prev: T[]) => {
- const trimmed = prev.slice(0, index + 1);
- const next = [...trimmed, newState].slice(-MAX_HISTORY);
- try { sessionStorage.setItem(key.current, JSON.stringify(next)); } catch {}
- return next;
- });
- setIndex((prev: number) => Math.min(prev + 1, MAX_HISTORY - 1));
- }, [index]);
-
- const undo = useCallback(() => {
- if (canUndo) setIndex((i: number) => i - 1);
- }, [canUndo]);
-
- const redo = useCallback(() => {
- if (canRedo) setIndex((i: number) => i + 1);
- }, [canRedo]);
-
- // Ctrl+Z / Ctrl+Shift+Z / Ctrl+Y
- useEffect(() => {
- const handler = (e: KeyboardEvent) => {
- const active = document.activeElement;
- const inInput = active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement;
- if (inInput) return;
- if ((e.ctrlKey || e.metaKey) && e.key === "z") {
- e.preventDefault();
- if (e.shiftKey) redo(); else undo();
- }
- if ((e.ctrlKey || e.metaKey) && e.key === "y") {
- e.preventDefault();
- redo();
- }
- };
- document.addEventListener("keydown", handler);
- return () => document.removeEventListener("keydown", handler);
- }, [undo, redo]);
-
- return { state, addToHistory, undo, redo, canUndo, canRedo };
-}
diff --git a/frontend/app/hooks/useWalletCache.ts b/frontend/app/hooks/useWalletCache.ts
index 67c980452..d1bd4a4ee 100644
--- a/frontend/app/hooks/useWalletCache.ts
+++ b/frontend/app/hooks/useWalletCache.ts
@@ -1,145 +1,27 @@
-"use client";
+// Stub wallet cache hook for MVP
-import { useEffect, useState } from "react";
-import { env } from "../lib/env";
-import { apiRequest, rateLimitedFetch } from "../lib/api-client";
-
-interface Balance {
+interface WalletBalance {
asset_code: string;
- balance: string;
- asset_type: string;
asset_issuer?: string;
+ asset_type: string;
+ balance: string;
usd_value: number;
}
-const COINGECKO_IDS: Record = {
- XLM: "stellar",
- USDC: "usd-coin",
- AQUA: "aqua",
-};
-
-async function fetchPrices(): Promise> {
- const ids = Object.values(COINGECKO_IDS).join(",");
- const res = await rateLimitedFetch(`${env.coingeckoApi}/simple/price?ids=${ids}&vs_currencies=usd`);
-
- if (!res.ok) {
- throw new Error("Failed to fetch prices");
- }
-
- const data = await res.json();
- const prices: Record = {};
-
- for (const [code, id] of Object.entries(COINGECKO_IDS)) {
- prices[code] = data[id]?.usd ?? (code === "USDC" ? 1 : 0);
- }
-
- return prices;
-}
-
-async function fetchBalances(address: string, horizonUrl: string): Promise {
- const res = await rateLimitedFetch(`${horizonUrl.replace(/\/$/, "")}/accounts/${address}`);
-
- if (!res.ok) {
- throw new Error("Failed to fetch wallet balances");
- }
-
- const account = await res.json();
-
- return (account.balances ?? []).map((balance: {
- asset_type: string;
- asset_code?: string;
- asset_issuer?: string;
- balance: string;
- }) => ({
- asset_code: balance.asset_type === "native" ? "XLM" : balance.asset_code ?? "UNKNOWN",
- balance: balance.balance,
- asset_type: balance.asset_type,
- asset_issuer: balance.asset_issuer,
- usd_value: 0,
- }));
-}
-
-export function usePrices() {
- const [data, setData] = useState>();
- const [error, setError] = useState(null);
-
- useEffect(() => {
- let cancelled = false;
-
- fetchPrices()
- .then((prices) => {
- if (!cancelled) setData(prices);
- })
- .catch((err) => {
- if (!cancelled) setError(err instanceof Error ? err : new Error("Failed to fetch prices"));
- });
-
- return () => {
- cancelled = true;
- };
- }, []);
-
- return { data, error, isLoading: !data && !error };
-}
-
-export function useWalletBalances(
- address: string | null,
- _network: string | null,
- horizonUrl: string,
-) {
- const { data: prices } = usePrices();
- const [data, setData] = useState([]);
- const [error, setError] = useState(null);
- const [isLoading, setIsLoading] = useState(false);
- const [dataUpdatedAt, setDataUpdatedAt] = useState(null);
-
- useEffect(() => {
- if (!address) {
- setData([]);
- setDataUpdatedAt(null);
- return;
- }
-
- let cancelled = false;
-
- const load = async () => {
- setIsLoading(true);
- try {
- const rawBalances = await fetchBalances(address, horizonUrl);
- const enriched = rawBalances.map((balance) => {
- const price = prices?.[balance.asset_code] ?? (balance.asset_code === "USDC" ? 1 : 0);
- return {
- ...balance,
- usd_value: parseFloat(balance.balance) * price,
- };
- });
-
- if (!cancelled) {
- setData(enriched);
- setError(null);
- setDataUpdatedAt(Date.now());
- }
- } catch (err) {
- if (!cancelled) {
- setError(err instanceof Error ? err : new Error("Failed to fetch wallet balances"));
- }
- } finally {
- if (!cancelled) setIsLoading(false);
- }
- };
-
- load();
- const interval = setInterval(load, 60_000);
-
- return () => {
- cancelled = true;
- clearInterval(interval);
- };
- }, [address, horizonUrl, prices]);
-
- return { data, isLoading, error, dataUpdatedAt };
+export function useWalletCache() {
+ return {
+ getCachedBalance: () => null,
+ setCachedBalance: () => { },
+ clearCache: () => { },
+ };
}
-export function useInvalidateBalances() {
- return () => undefined;
+export function useWalletBalances(address?: string | null, network?: string | null, horizonUrl?: string | null) {
+ return {
+ data: [] as WalletBalance[],
+ isLoading: false,
+ error: null,
+ dataUpdatedAt: Date.now(),
+ refetch: () => Promise.resolve(),
+ };
}
diff --git a/frontend/app/hooks/useWalletWebSocket.ts b/frontend/app/hooks/useWalletWebSocket.ts
index d0e16691b..e935b2bd0 100644
--- a/frontend/app/hooks/useWalletWebSocket.ts
+++ b/frontend/app/hooks/useWalletWebSocket.ts
@@ -1,46 +1,11 @@
-// app/hooks/useWalletWebSocket.ts
-"use client";
-
-import { useEffect, useState, useCallback } from "react";
-import { env } from "../lib/env";
-import { useWebSocket } from "./useWebSocket";
-
-interface Balance {
- asset_code: string;
- balance: string;
- asset_type: string;
- asset_issuer?: string;
- usd_value: number;
-}
-
-type WSMessage = {
- type: "balance_update";
- balances: Balance[];
-};
-
-export function useWalletWebSocket(address: string | null) {
- const [balances, setBalances] = useState([]);
- const [status, setStatus] = useState<"connecting" | "connected" | "disconnected" | "error">(
- "connecting"
- );
- const [error, setError] = useState(null);
-
- const onMessage = useCallback((msg: WSMessage) => {
- if (msg.type === "balance_update" && address) {
- setBalances(msg.balances);
- }
- }, [address]);
-
- const { error: wsError, status: wsStatus } = useWebSocket({
- url: `${env.walletWsUrl}?address=${address ?? ""}`,
- onMessage,
- maxAttempts: 5,
- });
-
- useEffect(() => {
- setStatus(wsStatus);
- setError(wsError);
- }, [wsStatus, wsError]);
-
- return { balances, status, error };
+// Stub WebSocket hook for MVP
+export function useWalletWebSocket(address?: string | null, horizonUrl?: string | null) {
+ return {
+ balances: [],
+ status: 'disconnected',
+ error: null,
+ connected: false,
+ connect: () => { },
+ disconnect: () => { },
+ };
}
diff --git a/frontend/app/hooks/useWebSocket.ts b/frontend/app/hooks/useWebSocket.ts
deleted file mode 100644
index cdbbea723..000000000
--- a/frontend/app/hooks/useWebSocket.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-// app/hooks/useWebSocket.ts
-"use client";
-
-import { useEffect, useRef, useState, useCallback } from "react";
-
-type Status = "connecting" | "connected" | "disconnected" | "error";
-
-interface UseWebSocketOptions {
- /** URL of the WebSocket endpoint */
- url: string;
- /** Optional function to parse incoming raw messages */
- parseMessage?: (event: MessageEvent) => TMessage;
- /** Optional function to handle parsed messages */
- onMessage?: (msg: TMessage) => void;
- /** Maximum number of reconnection attempts before giving up */
- maxAttempts?: number;
-}
-
-export function useWebSocket(options: UseWebSocketOptions) {
- const { url, parseMessage, onMessage, maxAttempts = 5 } = options;
- const [status, setStatus] = useState("connecting");
- const [error, setError] = useState(null);
- const wsRef = useRef(null);
- const attemptRef = useRef(0);
- const backoffRef = useRef(null);
-
- const send = useCallback((data: string | ArrayBufferLike | Blob | ArrayBufferView) => {
- if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
- wsRef.current.send(data);
- }
- }, []);
-
- const connect = useCallback(() => {
- setStatus("connecting");
- setError(null);
- const ws = new WebSocket(url);
- wsRef.current = ws;
-
- ws.onopen = () => {
- setStatus("connected");
- attemptRef.current = 0; // reset attempts on success
- };
-
- ws.onmessage = (ev) => {
- const msg = parseMessage ? parseMessage(ev) : (ev as unknown as TMessage);
- if (onMessage) onMessage(msg);
- };
-
- ws.onerror = (ev) => {
- console.error("WebSocket error", ev);
- setError("WebSocket error");
- };
-
- ws.onclose = () => {
- setStatus("disconnected");
- if (attemptRef.current < maxAttempts) {
- const delay = Math.min(1000 * 2 ** attemptRef.current, 30000);
- attemptRef.current += 1;
- backoffRef.current = setTimeout(() => {
- connect();
- }, delay);
- } else {
- setStatus("error");
- setError("Unable to reconnect after several attempts");
- }
- };
- }, [url, parseMessage, onMessage, maxAttempts]);
-
- useEffect(() => {
- connect();
- return () => {
- if (wsRef.current) wsRef.current.close();
- if (backoffRef.current) clearTimeout(backoffRef.current);
- };
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [url]);
-
- return { status, error, send };
-}
diff --git a/frontend/app/i18n/request.ts b/frontend/app/i18n/request.ts
index 99369361f..0b54b2c25 100644
--- a/frontend/app/i18n/request.ts
+++ b/frontend/app/i18n/request.ts
@@ -1,15 +1,23 @@
-import { getRequestConfig } from "next-intl/server";
-import { headers } from "next/headers";
+import { getRequestConfig } from 'next-intl/server';
+import { headers } from 'next/headers';
-const locales = ["en", "es"] as const;
+const locales = ['en', 'es'] as const;
type Locale = (typeof locales)[number];
export default getRequestConfig(async () => {
- const headerLocale = (await headers()).get("x-nestera-locale");
- const locale = locales.includes(headerLocale as Locale) ? (headerLocale as Locale) : "en";
+ const headerLocale = (await headers()).get('x-nestera-locale');
+ const locale = locales.includes(headerLocale as Locale) ? (headerLocale as Locale) : 'en';
+
+ // next-intl expects both `locale` and `messages` to always be resolvable.
+ // Avoid dynamic imports which can throw during SSR if the bundler can't
+ // determine the exact target at runtime.
+ const messages =
+ locale === 'es'
+ ? (await import('../locales/es.json')).default
+ : (await import('../locales/en.json')).default;
return {
locale,
- messages: (await import(`../locales/${locale}.json`)).default,
+ messages,
};
});
diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx
index 46f8def7c..4b4526fde 100644
--- a/frontend/app/layout.tsx
+++ b/frontend/app/layout.tsx
@@ -1,19 +1,7 @@
import "./globals.css";
import type { Metadata } from "next";
import { headers } from "next/headers";
-import { Suspense } from "react";
import IntlProvider from "./i18n/provider";
-import AnalyticsProvider from "./components/AnalyticsProvider";
-import MonitoringProvider from "./components/MonitoringProvider";
-import { StructuredData } from "./components/StructuredData";
-import ServiceWorkerRegistration from "./components/ServiceWorkerRegistration";
-import InstallPrompt from "./components/InstallPrompt";
-import {
- generatePageMetadata,
- SITE_URL,
- getOrganizationSchema,
- getWebsiteSchema,
-} from "./lib/seo";
import en from "./locales/en.json";
import es from "./locales/es.json";
@@ -30,16 +18,15 @@ export async function generateMetadata(): Promise {
const locale = await getLocale();
const metadata = messages[locale].metadata;
- return generatePageMetadata({
+ return {
title: metadata.title,
description: metadata.description,
- url: "/",
- locale,
- alternateLanguages: {
- en: `${SITE_URL}/en`,
- es: `${SITE_URL}/es`,
+ openGraph: {
+ title: metadata.title,
+ description: metadata.description,
+ type: "website",
},
- });
+ };
}
export default async function RootLayout({
@@ -56,70 +43,10 @@ export default async function RootLayout({
-
- {/* PWA / installability */}
-
-
- {/* Apple PWA meta */}
-
-
-
-
-
- {/* Apple splash screens (portrait) */}
-
-
-
-
-
-
-
- {/* Microsoft Tiles */}
-
-
-
-
-
-
-
{children}
-
-
-
-
-
-