diff --git a/frontend/app/dashboard/invoices/[id]/page.tsx b/frontend/app/dashboard/invoices/[id]/page.tsx
index 4716728c..f3f4e614 100644
--- a/frontend/app/dashboard/invoices/[id]/page.tsx
+++ b/frontend/app/dashboard/invoices/[id]/page.tsx
@@ -13,6 +13,7 @@ import {
formatTimeInTimeZone,
} from '@/lib/utils';
import { useAuthStore } from '@/store/useAuthStore';
+import { PageBreadcrumb } from '@/components/layout/PageBreadcrumb';
export default function InvoiceDetailPage() {
const params = useParams();
diff --git a/frontend/app/dashboard/invoices/page.tsx b/frontend/app/dashboard/invoices/page.tsx
index 6d0c1676..8fe26db4 100644
--- a/frontend/app/dashboard/invoices/page.tsx
+++ b/frontend/app/dashboard/invoices/page.tsx
@@ -4,22 +4,35 @@ import { useState } from 'react';
import { useDashboardData } from '@/lib/hooks/useDashboardData';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
-import { CheckCircle2, Clock, AlertCircle, Filter, FileText, Loader2 } from 'lucide-react';
+import {
+ CheckCircle2,
+ Clock,
+ AlertCircle,
+ Filter,
+ FileText,
+ Loader2,
+} from 'lucide-react';
import { motion } from 'framer-motion';
import Link from 'next/link';
import { InvoiceCardSkeleton } from '@/components/ui/loading-skeletons';
import { EmptyState } from '@/components/empty/EmptyState';
import { formatDateInTimeZone } from '@/lib/utils';
import { useAuthStore } from '@/store/useAuthStore';
+import { useRouter } from 'next/navigation';
export default function InvoicesPage() {
const router = useRouter();
const { invoices, loading } = useDashboardData();
const timezone = useAuthStore((state) => state.timezone);
- const [filter, setFilter] = useState<'all' | 'paid' | 'pending' | 'overdue'>('all');
+
+ const [filter, setFilter] = useState<
+ 'all' | 'paid' | 'pending' | 'overdue'
+ >('all');
const filteredInvoices =
- filter === 'all' ? invoices : invoices.filter((inv) => inv.status === filter);
+ filter === 'all'
+ ? invoices
+ : invoices.filter((inv) => inv.status === filter);
const getStatusIcon = (status: string) => {
switch (status) {
@@ -52,12 +65,15 @@ export default function InvoicesPage() {
Invoices
-
View and manage your invoices
+
+ View and manage your invoices
+
Loading invoices...
+
{[1, 2, 3, 4].map((i) => (
@@ -69,83 +85,55 @@ export default function InvoicesPage() {
return (
+ {/* Header */}
Invoices
-
View and manage your invoices
+
+ View and manage your invoices
+
+ {/* Filters */}
- {(['all', 'paid', 'pending', 'overdue'] as const).map((status) => (
-
- ))}
+ {(['all', 'paid', 'pending', 'overdue'] as const).map(
+ (status) => (
+
+ )
+ )}
-
- {filteredInvoices.map((invoice, index) => (
-
-
-
-
-
-
- {getStatusIcon(invoice.status)}
-
-
{invoice.projectTitle}
-
{invoice.milestoneTitle}
-
- Ref #{invoice.id} • {formatDateInTimeZone(invoice.generatedAt, timezone)}
-
-
-
-
-
- {invoice.amount} {invoice.currency}
-
-
- {invoice.status}
-
-
-
-
-
-
-
- ))}
-
-
- {filteredInvoices.length === 0 && (
+ {/* Content */}
+ {filteredInvoices.length === 0 ? (
{
if (filter === 'all') {
router.push('/dashboard/projects');
@@ -166,25 +154,39 @@ export default function InvoicesPage() {
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
>
-
+
{getStatusIcon(invoice.status)}
+
-
{invoice.projectTitle}
-
{invoice.milestoneTitle}
+
+ {invoice.projectTitle}
+
+
+
+ {invoice.milestoneTitle}
+
+
Ref #{invoice.id} •{' '}
- {new Date(invoice.generatedAt).toLocaleDateString()}
+ {formatDateInTimeZone(
+ invoice.generatedAt,
+ timezone
+ )}
+
{invoice.amount} {invoice.currency}
+
(null);
+ const scrollPositions = React.useRef>({});
+
useEffect(() => {
if (!isAuthenticated) {
router.push('/auth');
diff --git a/frontend/app/dashboard/payments/page.tsx b/frontend/app/dashboard/payments/page.tsx
index 3671c645..21ecd600 100644
--- a/frontend/app/dashboard/payments/page.tsx
+++ b/frontend/app/dashboard/payments/page.tsx
@@ -17,13 +17,18 @@ import { motion } from 'framer-motion';
import { PaymentCardSkeleton } from '@/components/ui/loading-skeletons';
import { EmptyState } from '@/components/empty/EmptyState';
import { formatDateTimeInTimeZone } from '@/lib/utils';
-import { useAuthStore } from '@/store/useAuthStore';
+import { useRouter } from 'next/navigation';
+import { Button } from '@/components/ui/button';
+import { PaymentQRModal } from '@/components/payment/QRCode';
export default function PaymentsPage() {
const router = useRouter();
const { payments, loading } = useDashboardData();
const timezone = useAuthStore((state) => state.timezone);
+ const [isQrModalOpen, setIsQrModalOpen] = useState(false);
+ const address = useAuthStore((state) => state.address);
+
const getStatusIcon = (status: string) => {
switch (status) {
case 'completed':
@@ -41,13 +46,18 @@ export default function PaymentsPage() {
return (
-
Payment History
-
View all your payment transactions
+
+ Payment History
+
+
+ View all your payment transactions
+
Loading payments...
+
{[1, 2, 3, 4].map((i) => (
@@ -59,75 +69,30 @@ export default function PaymentsPage() {
return (
+ {/* Header */}
-
Payment History
-
View all your payment transactions
+
+ Payment History
+
+
+ View all your payment transactions
+
{address && (
-
- {/* --- PAYMENT LIST --- */}
-
- {payments.map((payment, index) => (
-
-
-
-
-
- {getStatusIcon(payment.status)}
-
-
{payment.projectTitle}
-
- {payment.type === 'milestone_payment' ? 'Milestone Payment' : 'Full Payment'}
-
-
- {formatDateTimeInTimeZone(payment.timestamp, timezone)}
-
-
-
-
-
- {payment.transactionHash && (
-
-
- {payment.transactionHash}
-
-
- )}
-
-
-
- ))}
-
-
- {payments.length === 0 && (
+ {/* Content */}
+ {payments.length === 0 ? (
{getStatusIcon(payment.status)}
+
-
{payment.projectTitle}
+
+ {payment.projectTitle}
+
+
{payment.type === 'milestone_payment'
? 'Milestone Payment'
: 'Full Payment'}
+
- {new Date(payment.timestamp).toLocaleString()}
+ {formatDateTimeInTimeZone(
+ payment.timestamp,
+ timezone
+ )}
+
{payment.amount} {payment.currency}
+
{payment.transactionHash && (
+
{payment.transactionHash && (
@@ -198,6 +174,7 @@ export default function PaymentsPage() {
)}
+ {/* QR Modal */}
{address && (
{
if (!web3auth) {
- toast.error('Web3Auth is not configured. Please add NEXT_PUBLIC_WEB3AUTH_CLIENT_ID to your .env.local file.');
+ toast.error(
+ 'Web3Auth is not configured. Please add NEXT_PUBLIC_WEB3AUTH_CLIENT_ID to your .env.local file.'
+ );
return;
}
try {
setLoading(true);
- await web3auth.initModal();
- const web3authProvider = await web3auth.connectTo(WALLET_ADAPTERS.AUTH, {
- loginProvider,
- });
+ // Cast to any to access initModal safely
+ const web3authAny = web3auth as any;
+ await web3authAny.initModal();
+
+ // Latest API: connect() accepts loginProvider directly
+ const web3authProvider = await web3auth.connect();
if (web3authProvider) {
// Get user info
@@ -46,7 +49,6 @@ export function SocialLogin() {
});
toast.success('Login successful!');
- // Redirect to dashboard
router.push('/dashboard');
}
} catch (error) {
@@ -88,4 +90,4 @@ export function SocialLogin() {
);
-}
+}
\ No newline at end of file
diff --git a/frontend/components/landing/Navbar.tsx b/frontend/components/landing/Navbar.tsx
index 5f9be6cc..12975de7 100644
--- a/frontend/components/landing/Navbar.tsx
+++ b/frontend/components/landing/Navbar.tsx
@@ -5,11 +5,13 @@ import Link from 'next/link';
import { motion, AnimatePresence } from 'framer-motion';
import { Menu, X, Wallet } from 'lucide-react';
import { Button } from '@/components/ui/button';
-import { LanguageSwitcher } from '@/components/language/LanguageSwitcher';
+import { usePathname } from 'next/navigation';
+import { LanguageSwitcher } from '../language/LanguageSwitcher';
export function Navbar() {
const [isScrolled, setIsScrolled] = useState(false);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
+ const pathname = usePathname();
useEffect(() => {
const handleScroll = () => {
@@ -36,16 +38,18 @@ export function Navbar() {
? 'bg-white/80 backdrop-blur-md shadow-lg border-b border-gray-200/50'
: 'bg-transparent'
}`}
+ role="navigation"
+ aria-label="Main Navigation"
>
{/* Logo */}
-
+
-
+
AgenticPay
@@ -58,7 +62,8 @@ export function Navbar() {
{link.name}
@@ -70,7 +75,8 @@ export function Navbar() {
Get Started
@@ -79,8 +85,9 @@ export function Navbar() {
{/* Mobile Menu Button */}
setIsMobileMenuOpen(!isMobileMenuOpen)}
- className="md:hidden p-2 rounded-lg hover:bg-gray-100 transition-colors"
+ className="md:hidden p-2 rounded-lg hover:bg-gray-100 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
aria-label="Toggle menu"
+ aria-expanded={isMobileMenuOpen}
>
{isMobileMenuOpen ? (
@@ -113,7 +120,8 @@ export function Navbar() {
setIsMobileMenuOpen(false)}
- className="block py-2 text-gray-700 font-medium hover:text-blue-600 transition-colors"
+ className="block py-2 text-gray-700 font-medium hover:text-blue-600 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded"
+ aria-current={pathname === link.href ? 'page' : undefined}
>
{link.name}
@@ -127,7 +135,10 @@ export function Navbar() {
>
setIsMobileMenuOpen(false)}>
-
+
Get Started
@@ -138,5 +149,4 @@ export function Navbar() {
);
-}
-
+}
\ No newline at end of file
diff --git a/frontend/components/layout/Header.tsx b/frontend/components/layout/Header.tsx
index f434211b..fc4d159e 100644
--- a/frontend/components/layout/Header.tsx
+++ b/frontend/components/layout/Header.tsx
@@ -4,7 +4,6 @@ import { useEffect, useState } from 'react';
import { useAuthStore } from '@/store/useAuthStore';
import { useRouter, usePathname } from 'next/navigation';
import { useThemeStore } from '@/store/useThemeStore';
-import { useRouter } from 'next/navigation';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
@@ -15,10 +14,15 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
-import { Bell, LogOut, User, Settings, Sun, Moon, Clock, CloudOff, RefreshCw } from 'lucide-react';
+import {
+ Bell,
+ LogOut,
+ User,
+ Settings,
+ Sun,
+ Moon,
+} from 'lucide-react';
import { toast } from 'sonner';
-import { LanguageSwitcher } from '@/components/language/LanguageSwitcher';
-import { useEffect, useState } from 'react';
import { useDisconnect, useAccount } from 'wagmi';
import { web3auth } from '@/lib/web3auth';
import {
@@ -26,7 +30,6 @@ import {
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
- BreadcrumbPage,
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb';
import { getDashboardBreadcrumbs } from '@/lib/breadcrumbs';
@@ -34,6 +37,13 @@ import { ThemeSettingsModal } from '@/components/theme/ThemeSettingsModal';
import { TimezoneSettingsModal } from '@/components/settings/TimezoneSettingsModal';
import { getBrowserTimeZone, isValidTimeZone } from '@/lib/utils';
+/* ---------------- TYPES ---------------- */
+type BreadcrumbItemType = {
+ label: string;
+ href: string;
+};
+
+/* ---------------- NETWORK INDICATOR ---------------- */
const NetworkIndicator = () => {
const { chain, isConnected } = useAccount();
@@ -42,69 +52,81 @@ const NetworkIndicator = () => {
if (!chain) {
return (
-
+
Wrong Network
);
}
const isTestnet = chain.testnet === true;
- const bgColor = isTestnet
- ? 'bg-yellow-100 text-yellow-800 border-yellow-200'
- : 'bg-green-100 text-green-800 border-green-200';
- const dotColor = isTestnet ? 'bg-yellow-500' : 'bg-green-500';
return (
-
-
+
+
{chain.name}
);
};
+/* ---------------- HEADER ---------------- */
export function Header() {
- const { name, email, address, timezone, logout, setTimezone } = useAuthStore();
+ const { name, email, address, timezone, logout, setTimezone } =
+ useAuthStore();
const { isDark, mode, setIsDark } = useThemeStore();
const { disconnect } = useDisconnect();
- const { isOnline, queueLength, isSyncing } = useOfflineStatus();
+
const router = useRouter();
const pathname = usePathname();
- const [breadcrumbs, setBreadcrumbs] = useState
([]);
- useEffect(() => {
- const items = getDashboardBreadcrumbs(pathname);
- setBreadcrumbs(items);
- }, [pathname]);
+ const [breadcrumbs, setBreadcrumbs] = useState([]);
const [themeSettingsOpen, setThemeSettingsOpen] = useState(false);
const [timezoneSettingsOpen, setTimezoneSettingsOpen] = useState(false);
+ /* -------- Breadcrumbs -------- */
useEffect(() => {
- if (timezone) {
- return;
- }
+ // Wrap setBreadcrumbs in requestAnimationFrame to avoid synchronous state update warning
+ const items = getDashboardBreadcrumbs(pathname);
+ requestAnimationFrame(() => setBreadcrumbs(items));
+ }, [pathname]);
- const detectedTimeZone = getBrowserTimeZone();
- if (detectedTimeZone && isValidTimeZone(detectedTimeZone)) {
- setTimezone(detectedTimeZone);
+ /* -------- Timezone detection (SAFE) -------- */
+ useEffect(() => {
+ if (!timezone) {
+ const detected = getBrowserTimeZone();
+ if (detected && isValidTimeZone(detected)) {
+ setTimezone(detected);
+ }
}
- }, [setTimezone, timezone]);
+ }, [timezone, setTimezone]);
+ /* -------- Logout -------- */
const handleLogout = async () => {
disconnect();
- if (web3auth) {
- await web3auth.logout();
- }
+ if (web3auth) await web3auth.logout();
+
logout();
toast.success('Logged out successfully');
router.push('/auth');
};
+ /* -------- Theme toggle -------- */
const handleManualToggle = () => {
const next = !isDark;
setIsDark(next);
document.documentElement.classList.toggle('dark', next);
};
+ /* -------- Helpers -------- */
const initials =
name
?.split(' ')
@@ -117,148 +139,120 @@ export function Header() {
? `${address.slice(0, 6)}...${address.slice(-4)}`
: 'Not connected';
+ /* ---------------- UI ---------------- */
return (
-