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.amount} {payment.currency} -

- {payment.transactionHash && ( - - View on Explorer - - - )} -
-
- {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() { @@ -79,8 +85,9 @@ export function Navbar() { {/* Mobile Menu Button */} @@ -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 ( -
+ <> +
-
-

- Dashboard -

-
+ {/* LEFT */} +

+ Dashboard +

-
- - {/* 3. I dropped the new component right here! */} - - -
- {(!isOnline || queueLength > 0 || isSyncing) && ( -
- {isSyncing ? ( - - ) : ( - - )} - - {isSyncing - ? `Syncing ${queueLength}` - : !isOnline - ? `Offline${queueLength > 0 ? ` - ${queueLength} queued` : ''}` - : `${queueLength} queued`} - -
- )} + {/* RIGHT */} +
+ {/* Notifications */} - - {/* Dark mode toggle — only interactive label when manual */} + {/* Theme */} - {/* Theme schedule settings */} - - - + {/* USER MENU */} + - + -
-

{name || 'User'}

-

{email || 'No email'}

-

{shortAddress}

-
+

{name}

+

{email}

+ + Profile - setTimezoneSettingsOpen(true)}> + + - Timezone Settings + Settings + - + + Logout
-
+
+
-
- {/* Breadcrumb Navigation */} - {breadcrumbs.length > 0 && ( -
- - - {breadcrumbs.map((item, index) => ( -
- - - {item.label} - - - {index < breadcrumbs.length - 1 && } -
- ))} -
-
-
- )} + {/* BREADCRUMBS */} + {breadcrumbs.length > 0 && ( +
+ + + {breadcrumbs.map((item, index) => ( +
+ + + {item.label} + + + + {index < breadcrumbs.length - 1 && ( + + )} +
+ ))} +
+
+
+ )}
- setThemeSettingsOpen(false)} /> + + {/* MODALS */} + setThemeSettingsOpen(false)} + /> + setTimezoneSettingsOpen(false)} /> ); -} +} \ No newline at end of file diff --git a/frontend/components/layout/Sidebar.tsx b/frontend/components/layout/Sidebar.tsx index 03d50622..191ffe37 100644 --- a/frontend/components/layout/Sidebar.tsx +++ b/frontend/components/layout/Sidebar.tsx @@ -27,14 +27,19 @@ export function Sidebar() { size="icon" onClick={() => setIsMobileOpen(!isMobileOpen)} className="bg-white shadow-lg" - aria-label="Toggle menu" + aria-label={isMobileOpen ? 'Close menu' : 'Open menu'} + aria-expanded={isMobileOpen} + aria-controls="sidebar-navigation" > - {isMobileOpen ? : } + {isMobileOpen ?
{/* Sidebar */} ); -} - +} \ No newline at end of file diff --git a/frontend/components/settings/TimezoneSettingsModal.tsx b/frontend/components/settings/TimezoneSettingsModal.tsx index ecb5b1cb..cf4f3a2f 100644 --- a/frontend/components/settings/TimezoneSettingsModal.tsx +++ b/frontend/components/settings/TimezoneSettingsModal.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; import { Clock3, LocateFixed } from 'lucide-react'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; @@ -37,19 +37,28 @@ function getSupportedTimeZones() { return Intl.supportedValuesOf('timeZone'); } -export function TimezoneSettingsModal({ open, onClose }: TimezoneSettingsModalProps) { +export function TimezoneSettingsModal({ + open, + onClose, +}: TimezoneSettingsModalProps) { const storedTimeZone = useAuthStore((state) => state.timezone); const setTimezone = useAuthStore((state) => state.setTimezone); const browserTimeZone = getBrowserTimeZone(); - const [localTimeZone, setLocalTimeZone] = useState(resolveTimeZone(storedTimeZone)); - useEffect(() => { - if (open) { - setLocalTimeZone(resolveTimeZone(storedTimeZone)); - } - }, [open, storedTimeZone]); + // ✅ derive initial value safely + const initialTimeZone = useMemo( + () => resolveTimeZone(storedTimeZone), + [storedTimeZone] + ); + + const [localTimeZone, setLocalTimeZone] = + useState(initialTimeZone); + + const supportedTimeZones = useMemo( + () => getSupportedTimeZones(), + [] + ); - const supportedTimeZones = useMemo(() => getSupportedTimeZones(), []); const previewTimeZone = resolveTimeZone(localTimeZone); const handleDetectTimezone = () => { @@ -60,7 +69,9 @@ export function TimezoneSettingsModal({ open, onClose }: TimezoneSettingsModalPr const handleSave = () => { if (!isValidTimeZone(localTimeZone)) { - toast.error('Enter a valid IANA timezone such as Europe/London'); + toast.error( + 'Enter a valid IANA timezone such as Europe/London' + ); return; } @@ -70,7 +81,12 @@ export function TimezoneSettingsModal({ open, onClose }: TimezoneSettingsModalPr }; return ( - !nextOpen && onClose()}> + + !nextOpen && onClose() + } + > @@ -78,25 +94,33 @@ export function TimezoneSettingsModal({ open, onClose }: TimezoneSettingsModalPr Timezone Settings - Choose how dates and times are shown across your dashboard. + Choose how dates and times are shown across your + dashboard.
-

Detected timezone

+

+ Detected timezone +

- {browserTimeZone ?? 'Unavailable in this browser'} + {browserTimeZone ?? + 'Unavailable in this browser'}

- + setLocalTimeZone(event.target.value)} + onChange={(event) => + setLocalTimeZone(event.target.value) + } placeholder="e.g. America/New_York" /> @@ -105,28 +129,46 @@ export function TimezoneSettingsModal({ open, onClose }: TimezoneSettingsModalPr ))}

- Use a valid IANA timezone, like `Africa/Lagos` or `America/Los_Angeles`. + Use a valid IANA timezone, like `Africa/Lagos` + or `America/Los_Angeles`.

-

Preview

+

+ Preview +

- {formatDateTimeInTimeZone(new Date(), previewTimeZone)} + {formatDateTimeInTimeZone( + new Date(), + previewTimeZone + )} +

+

+ {previewTimeZone}

-

{previewTimeZone}

- +
- + @@ -135,4 +177,4 @@ export function TimezoneSettingsModal({ open, onClose }: TimezoneSettingsModalPr
); -} +} \ No newline at end of file diff --git a/frontend/components/ui/breadcrumb.tsx b/frontend/components/ui/breadcrumb.tsx index d9d55031..e601f654 100644 --- a/frontend/components/ui/breadcrumb.tsx +++ b/frontend/components/ui/breadcrumb.tsx @@ -1,15 +1,16 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" -import { ChevronRight } from "lucide-react" -import { cn } from "@/lib/utils" -import Link from "next/link" +'use client'; -const breadcrumbVariants = cva("flex items-center gap-1.5") +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { cva, type VariantProps } from 'class-variance-authority'; +import { ChevronRight } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import Link from 'next/link'; -interface BreadcrumbProps - extends React.HTMLAttributes, - VariantProps {} +const breadcrumbVariants = cva('flex items-center gap-1.5'); + +type BreadcrumbProps = React.HTMLAttributes & + VariantProps; const Breadcrumb = React.forwardRef( ({ className, ...props }, ref) => ( @@ -20,90 +21,105 @@ const Breadcrumb = React.forwardRef( {...props} /> ) -) -Breadcrumb.displayName = "Breadcrumb" +); +Breadcrumb.displayName = 'Breadcrumb'; -interface BreadcrumbListProps extends React.HTMLAttributes {} +type BreadcrumbListProps = React.HTMLAttributes; -const BreadcrumbList = React.forwardRef( - ({ className, ...props }, ref) => ( -
    - ) -) -BreadcrumbList.displayName = "BreadcrumbList" +const BreadcrumbList = React.forwardRef< + HTMLOListElement, + BreadcrumbListProps +>(({ className, ...props }, ref) => ( +
      +)); +BreadcrumbList.displayName = 'BreadcrumbList'; -interface BreadcrumbItemProps extends React.HTMLAttributes {} +type BreadcrumbItemProps = React.HTMLAttributes; -const BreadcrumbItem = React.forwardRef( - ({ className, ...props }, ref) => ( -
    1. - ) -) -BreadcrumbItem.displayName = "BreadcrumbItem" - -interface BreadcrumbLinkProps - extends React.AnchorHTMLAttributes { - asChild?: boolean - href: string -} - -const BreadcrumbLink = React.forwardRef( - ({ className, href, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : Link - +const BreadcrumbItem = React.forwardRef< + HTMLLIElement, + BreadcrumbItemProps +>(({ className, ...props }, ref) => ( +
    2. +)); +BreadcrumbItem.displayName = 'BreadcrumbItem'; + +/* ✅ FIXED TYPES HERE */ +type BreadcrumbLinkProps = { + asChild?: boolean; + href: string; +} & Omit, 'href'>; + +const BreadcrumbLink = React.forwardRef< + HTMLAnchorElement, + BreadcrumbLinkProps +>(({ className, href, asChild = false, ...props }, ref) => { + if (asChild) { return ( - - ) + ); } -) -BreadcrumbLink.displayName = "BreadcrumbLink" -interface BreadcrumbPageProps - extends React.HTMLAttributes {} - -const BreadcrumbPage = React.forwardRef( - ({ className, ...props }, ref) => ( - - ) -) -BreadcrumbPage.displayName = "BreadcrumbPage" + ); +}); +BreadcrumbLink.displayName = 'BreadcrumbLink'; + +type BreadcrumbPageProps = React.HTMLAttributes; + +const BreadcrumbPage = React.forwardRef< + HTMLSpanElement, + BreadcrumbPageProps +>(({ className, ...props }, ref) => ( + +)); +BreadcrumbPage.displayName = 'BreadcrumbPage'; -interface BreadcrumbSeparatorProps - extends React.HTMLAttributes { - icon?: React.ReactNode -} +type BreadcrumbSeparatorProps = + React.HTMLAttributes & { + icon?: React.ReactNode; + }; -const BreadcrumbSeparator = ({ className, icon, ...props }: BreadcrumbSeparatorProps) => +const BreadcrumbSeparator = ({ + className, + icon, + ...props +}: BreadcrumbSeparatorProps) => icon ? (