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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions backend/src/config/env.validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,12 @@ export const envValidationSchema = Joi.object({
BACKUP_S3_REGION: Joi.string().optional(),
BACKUP_AWS_ACCESS_KEY_ID: Joi.string().optional(),
BACKUP_AWS_SECRET_ACCESS_KEY: Joi.string().optional(),
BACKUP_ENCRYPTION_KEY: Joi.string()
.length(64)
.hex()
.optional(),
BACKUP_ENCRYPTION_KEY: Joi.string().length(64).hex().optional(),
BACKUP_RETENTION_DAYS: Joi.number().integer().min(1).default(30).optional(),
BACKUP_TMP_DIR: Joi.string().optional(),
BACKUP_TEST_DB_HOST: Joi.string().hostname().optional(),
BACKUP_TEST_DB_PORT: Joi.number().port().default(5432).optional(),
BACKUP_TEST_DB_USER: Joi.string().optional(),
BACKUP_TEST_DB_PASSWORD: Joi.string().optional(),
BACKUP_TEST_DB_NAME: Joi.string().default('nestera_restore_test').optional(),}).or('DATABASE_URL', 'DB_HOST'); // enforce at least one DB connection strategy
BACKUP_TEST_DB_NAME: Joi.string().default('nestera_restore_test').optional(),
}).or('DATABASE_URL', 'DB_HOST'); // enforce at least one DB connection strategy
3 changes: 2 additions & 1 deletion frontend/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,5 @@
}
}
]
}
}

57 changes: 57 additions & 0 deletions frontend/app/components/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"use client";

import React, { Component, ErrorInfo, ReactNode } from "react";

interface Props {
children: ReactNode;
fallback?: ReactNode;
}

interface State {
hasError: boolean;
}

/**
* React Error Boundary — catches render-time errors in the component tree.
* Note: does NOT catch errors in event handlers (use try/catch there).
*/
class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false };

static getDerivedStateFromError(): State {
return { hasError: true };
}

componentDidCatch(error: Error, info: ErrorInfo) {
console.error("[ErrorBoundary]", error, info.componentStack);
}

handleReset = () => this.setState({ hasError: false });

render() {
if (this.state.hasError) {
if (this.props.fallback) return this.props.fallback;
return (
<div
role="alert"
className="flex min-h-[200px] flex-col items-center justify-center gap-4 rounded-2xl border border-red-500/20 bg-red-500/5 p-8 text-center"
>
<p className="text-lg font-semibold text-red-400">Something went wrong</p>
<p className="max-w-sm text-sm text-[var(--color-text-muted)]">
An unexpected error occurred. You can try again or refresh the page.
</p>
<button
type="button"
onClick={this.handleReset}
className="rounded-full bg-[var(--color-accent)] px-5 py-2 text-sm font-semibold text-[#061a1a] hover:brightness-105"
>
Try Again
</button>
</div>
);
}
return this.props.children;
}
}

export default ErrorBoundary;
65 changes: 61 additions & 4 deletions frontend/app/components/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
"use client";

import React, { useEffect, useRef, useState } from "react";
import React, { useCallback, useEffect, useRef, useState } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { Loader2, Wallet } from "lucide-react";
import ThemeToggle from "./ThemeToggle";
import { useWallet } from "../context/WalletContext";
import { useToast } from "../context/ToastContext";
import { useFocusTrap } from "../hooks/useFocusTrap";

interface NavLink {
label: string;
Expand Down Expand Up @@ -36,6 +37,10 @@ const Navbar: React.FC = () => {
const { address, network, isConnected, isLoading, error, connect } = useWallet();
const toast = useToast();
const previousConnectedRef = useRef(isConnected);
const menuRef = useRef<HTMLDivElement>(null);
const menuButtonRef = useRef<HTMLButtonElement>(null);

const closeMenu = useCallback(() => setIsMobileMenuOpen(false), []);

const isActiveLink = (href: string): boolean => {
return pathname === href || pathname?.startsWith(href + "/") || false;
Expand All @@ -45,6 +50,45 @@ const Navbar: React.FC = () => {
? `${address.slice(0, 4)}...${address.slice(-4)}`
: null;

// Focus trap inside mobile menu
useFocusTrap({
isOpen: isMobileMenuOpen,
containerRef: menuRef,
onEscape: closeMenu,
});

// Body scroll lock when menu is open
useEffect(() => {
if (isMobileMenuOpen) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
return () => {
document.body.style.overflow = "";
};
}, [isMobileMenuOpen]);

// Click-outside to close
useEffect(() => {
if (!isMobileMenuOpen) return;

const handleClickOutside = (event: MouseEvent) => {
const target = event.target as Node;
if (
menuRef.current &&
!menuRef.current.contains(target) &&
menuButtonRef.current &&
!menuButtonRef.current.contains(target)
) {
closeMenu();
}
};

document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, [isMobileMenuOpen, closeMenu]);

useEffect(() => {
if (error) {
toast.error("Wallet connection failed", error);
Expand Down Expand Up @@ -90,7 +134,10 @@ const Navbar: React.FC = () => {
};

return (
<nav className="sticky top-0 z-50 border-b border-[var(--color-border)] bg-[var(--color-nav)]/95 backdrop-blur-xl">
<nav
className="sticky top-0 z-50 border-b border-[var(--color-border)] bg-[var(--color-nav)]/95 backdrop-blur-xl"
aria-label="Main navigation"
>
<div className="w-full">
<div className="flex h-16 items-center justify-between px-4 sm:px-6 md:px-[30px]">
<div className="shrink-0">
Expand Down Expand Up @@ -124,18 +171,21 @@ const Navbar: React.FC = () => {
<WalletButton />

<button
ref={menuButtonRef}
type="button"
onClick={() => setIsMobileMenuOpen((open) => !open)}
className="inline-flex items-center justify-center rounded-xl border border-[var(--color-border)] bg-[var(--color-surface)] p-2 text-[var(--color-text-muted)] hover:text-[var(--color-text)] md:hidden"
aria-expanded={isMobileMenuOpen}
aria-label="Toggle navigation menu"
aria-controls="mobile-menu"
aria-label={isMobileMenuOpen ? "Close navigation menu" : "Open navigation menu"}
>
{isMobileMenuOpen ? (
<svg
className="h-6 w-6 fill-none stroke-current stroke-2"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
strokeLinecap="round"
Expand All @@ -149,6 +199,7 @@ const Navbar: React.FC = () => {
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
strokeLinecap="round"
Expand All @@ -162,7 +213,13 @@ const Navbar: React.FC = () => {
</div>
</div>

{/* Mobile menu */}
<div
id="mobile-menu"
ref={menuRef}
role="dialog"
aria-modal="true"
aria-label="Navigation menu"
className={`border-t border-[var(--color-border)] bg-[var(--color-nav)] shadow-lg md:hidden ${isMobileMenuOpen ? "block" : "hidden"}`}
>
<div className="flex flex-col gap-2 p-3 pb-4">
Expand All @@ -172,7 +229,7 @@ const Navbar: React.FC = () => {
key={link.href}
href={link.href}
className={isActiveLink(link.href) ? `${mobileLinkBase} ${mobileLinkActive}` : mobileLinkBase}
onClick={() => setIsMobileMenuOpen(false)}
onClick={closeMenu}
>
{link.label}
</Link>
Expand Down
25 changes: 23 additions & 2 deletions frontend/app/components/dashboard/ActivePoolList.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"use client";

import React from "react";
import React, { useState } from "react";
import ActivePoolCard, { PoolItem } from "./ActivePoolCard";
import Skeleton from "../ui/Skeleton";

const MOCK_POOLS: PoolItem[] = [
{
Expand All @@ -22,7 +23,27 @@ const MOCK_POOLS: PoolItem[] = [
},
];

const ActivePoolList: React.FC = () => {
const ActivePoolListSkeleton: React.FC = () => (
<section className="bg-linear-to-b from-[rgba(6,18,20,0.45)] to-[rgba(4,12,14,0.35)] border border-[rgba(8,120,120,0.06)] rounded-2xl p-[18px]">
<div className="flex justify-between items-center mb-3">
<Skeleton className="h-5 w-40" />
<Skeleton className="h-4 w-14" />
</div>
<div className="flex flex-col gap-3">
{[1, 2].map((i) => (
<Skeleton key={i} className="h-20 rounded-xl" />
))}
</div>
</section>
);

interface ActivePoolListProps {
isLoading?: boolean;
}

const ActivePoolList: React.FC<ActivePoolListProps> = ({ isLoading = false }) => {
if (isLoading) return <ActivePoolListSkeleton />;

return (
<section className="bg-linear-to-b from-[rgba(6,18,20,0.45)] to-[rgba(4,12,14,0.35)] border border-[rgba(8,120,120,0.06)] rounded-2xl p-[18px] text-[#dff]">
<div className="flex justify-between items-center mb-3">
Expand Down
33 changes: 33 additions & 0 deletions frontend/app/components/dashboard/RecentTransactionsWidget.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import React from "react";
import Skeleton from "../ui/Skeleton";

interface Transaction {
id: number;
Expand Down Expand Up @@ -110,6 +111,38 @@ const TransactionRow: React.FC<{ transaction: Transaction }> = ({
};

const RecentTransactionsWidget: React.FC = () => {
const isLoading = false; // replace with real loading state when data is fetched

const widgetStyle = {
background: "linear-gradient(180deg, rgba(4,20,22,0.85), rgba(6,18,20,0.75))",
border: "1px solid rgba(6,110,110,0.15)",
borderRadius: "18px",
padding: "24px",
color: "#e6ffff",
boxShadow: "0 10px 30px rgba(2,12,14,0.6)",
backdropFilter: "blur(6px)",
};

if (isLoading) {
return (
<div style={widgetStyle}>
<Skeleton className="h-6 w-44 mb-4" />
<div className="flex flex-col gap-3">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="flex items-center gap-3 py-2">
<Skeleton className="h-9 w-9 rounded-lg shrink-0" />
<div className="flex-1 space-y-2">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-3 w-24" />
</div>
<Skeleton className="h-4 w-16" />
</div>
))}
</div>
</div>
);
}

const mockTransactions: Transaction[] = [
{
id: 1,
Expand Down
9 changes: 3 additions & 6 deletions frontend/app/components/dashboard/WalletBalanceCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import React from "react";
import { useWallet } from "../../context/WalletContext";
import { Loader2, Wallet, ArrowUpRight } from "lucide-react";
import Skeleton from "../ui/Skeleton";

const WalletBalanceCard: React.FC = () => {
const {
Expand All @@ -26,14 +27,10 @@ const WalletBalanceCard: React.FC = () => {
if ((isLoading || isBalancesLoading) && balances.length === 0) {
return (
<div className="bg-[#0e2330] border border-white/5 rounded-2xl p-6 min-h-[300px]">
<div className="mb-4 inline-flex items-center gap-2 text-xs text-[#6a9fae]">
<Loader2 size={14} className="animate-spin text-[#08c1c1]" />
Loading wallet balances...
</div>
<div className="h-6 w-32 bg-white/5 rounded mb-6"></div>
<Skeleton className="h-5 w-32 mb-6" />
<div className="space-y-4">
{[1, 2, 3].map((i) => (
<div key={i} className="h-16 bg-white/5 rounded-xl animate-pulse"></div>
<Skeleton key={i} className="h-16 rounded-xl" />
))}
</div>
</div>
Expand Down
21 changes: 21 additions & 0 deletions frontend/app/components/ui/Skeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"use client";

import React from "react";
import { clsx } from "clsx";

interface SkeletonProps {
className?: string;
}

/**
* Reusable skeleton placeholder. Shimmer is disabled automatically
* when the user has prefers-reduced-motion: reduce set.
*/
const Skeleton: React.FC<SkeletonProps> = ({ className }) => (
<div
aria-hidden="true"
className={clsx("rounded bg-white/[0.06] skeleton-shimmer", className)}
/>
);

export default Skeleton;
1 change: 1 addition & 0 deletions frontend/app/dashboard/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from "react";
import Sidebar from "../components/dashboard/Sidebar";
import TopNav from "../components/dashboard/TopNav";
import ErrorBoundary from "../components/ErrorBoundary";

export const metadata = {
title: "Dashboard - Nestera",
Expand Down
23 changes: 23 additions & 0 deletions frontend/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -218,3 +218,26 @@ body {
scroll-behavior: auto !important;
}
}

/* Skeleton shimmer animation */
@keyframes skeleton-shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}

.skeleton-shimmer {
background: linear-gradient(
90deg,
rgba(255,255,255,0.04) 25%,
rgba(255,255,255,0.10) 50%,
rgba(255,255,255,0.04) 75%
);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
.skeleton-shimmer {
animation: none;
}
}
Loading
Loading