From c12e65b1ee57691693e489fd59ade5bd0d70eaeb Mon Sep 17 00:00:00 2001 From: Od-hunter Date: Thu, 28 May 2026 14:35:43 +0100 Subject: [PATCH] feat: reduce frontend bundle size with tree shaking and dependency audit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add @next/bundle-analyzer with ANALYZE=true script for bundle visibility - Configure next.config.ts: transpilePackages for Web3 libs, optimizePackageImports for lucide-react/framer-motion/all Radix UI packages, Node.js polyfill fallbacks - Lazy-init @web3auth/modal via getWeb3Auth() async fn — removes ~500KB from initial bundle; SDK only loads on first social login click - Scope WagmiProvider + QueryClientProvider to dashboard routes only via DashboardProviders — landing page no longer loads wagmi/viem - Convert landing page (app/page.tsx) to Server Component; extract animated sections into minimal client islands (HeroCTA, HeroAnimations) - Dynamically import Sidebar, Header, PWAWrapper in dashboard layout - Dynamically import SocialLogin and WalletConnect on auth page with skeleton fallbacks so Web3 SDK is deferred until tab renders - Replace framer-motion motion.div entrance animations in all dashboard pages with zero-JS CSS FadeIn component (animate-fade-in-up keyframe) - Fix web3auth logout: use provider check instead of non-existent .connected prop - Fix auth page provider race: DashboardProviders is static import so WagmiProvider is mounted before WalletConnect calls useConnect() - Remove unused DashboardInvoice import and dead handleSubmitWork function --- backend/frontend/app/auth/page.tsx | 82 +- .../frontend/app/dashboard/invoices/page.tsx | 13 +- backend/frontend/app/dashboard/layout.tsx | 44 +- backend/frontend/app/dashboard/page.tsx | 42 +- .../frontend/app/dashboard/payments/page.tsx | 11 +- .../app/dashboard/projects/[id]/page.tsx | 92 +-- .../frontend/app/dashboard/projects/page.tsx | 11 +- backend/frontend/app/globals.css | 16 + backend/frontend/app/layout.tsx | 29 +- backend/frontend/app/page.tsx | 197 ++--- .../frontend/components/auth/SocialLogin.tsx | 32 +- .../components/landing/HeroAnimations.tsx | 27 + .../frontend/components/landing/HeroCTA.tsx | 76 ++ backend/frontend/components/layout/Header.tsx | 28 +- .../components/providers-dashboard.tsx | 24 + backend/frontend/components/providers.tsx | 24 +- backend/frontend/components/ui/fade-in.tsx | 30 + backend/frontend/lib/web3auth.ts | 103 ++- backend/frontend/next.config.ts | 71 +- backend/frontend/package-lock.json | 751 +++++------------- backend/frontend/package.json | 2 + 21 files changed, 775 insertions(+), 930 deletions(-) create mode 100644 backend/frontend/components/landing/HeroAnimations.tsx create mode 100644 backend/frontend/components/landing/HeroCTA.tsx create mode 100644 backend/frontend/components/providers-dashboard.tsx create mode 100644 backend/frontend/components/ui/fade-in.tsx diff --git a/backend/frontend/app/auth/page.tsx b/backend/frontend/app/auth/page.tsx index 3849ca77..ae03389f 100644 --- a/backend/frontend/app/auth/page.tsx +++ b/backend/frontend/app/auth/page.tsx @@ -1,10 +1,44 @@ 'use client'; import { motion } from 'framer-motion'; -import { SocialLogin } from '@/components/auth/SocialLogin'; -import { WalletConnect } from '@/components/auth/WalletConnect'; +import dynamic from 'next/dynamic'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Wallet, Users } from 'lucide-react'; +import { Skeleton } from '@/components/ui/skeleton'; +// DashboardProviders must be a static import so WagmiProvider is available +// synchronously before any child component calls useAccount() or useConnect(). +// Dynamically importing it would cause a race where WalletConnect renders +// before the provider mounts, throwing a wagmi context error. +import { DashboardProviders } from '@/components/providers-dashboard'; + +// Dynamically import the heavy auth components — they pull in @web3auth/modal +// and wagmi connectors. Loading them on-demand keeps the auth page's initial +// JS small while the provider is already in place. +const SocialLogin = dynamic( + () => import('@/components/auth/SocialLogin').then((m) => m.SocialLogin), + { + loading: () => ( +
+ {[1, 2, 3].map((i) => ( + + ))} +
+ ), + ssr: false, + } +); + +const WalletConnect = dynamic( + () => import('@/components/auth/WalletConnect').then((m) => m.WalletConnect), + { + loading: () => ( +
+ +
+ ), + ssr: false, + } +); export default function AuthPage() { return ( @@ -28,31 +62,32 @@ export default function AuthPage() {

Welcome to AgenticPay

-

- Get paid instantly for your work -

+

Get paid instantly for your work

- - - - - Social Login - - - - Web3 Wallet - - + {/* Provider must wrap the tabs so WalletConnect can call useConnect() */} + + + + + + Social Login + + + + Web3 Wallet + + - - - + + + - - - - + + + + +

By continuing, you agree to our Terms of Service and Privacy Policy @@ -62,4 +97,3 @@ export default function AuthPage() { ); } - diff --git a/backend/frontend/app/dashboard/invoices/page.tsx b/backend/frontend/app/dashboard/invoices/page.tsx index a134e285..f014b27d 100644 --- a/backend/frontend/app/dashboard/invoices/page.tsx +++ b/backend/frontend/app/dashboard/invoices/page.tsx @@ -1,11 +1,11 @@ 'use client'; import { useState } from 'react'; -import { useDashboardData, DashboardInvoice } from '@/lib/hooks/useDashboardData'; +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 } from 'lucide-react'; -import { motion } from 'framer-motion'; +import { FadeIn } from '@/components/ui/fade-in'; import Link from 'next/link'; import { InvoiceCardSkeleton } from '@/components/ui/loading-skeletons'; import { EmptyState } from '@/components/empty/EmptyState'; @@ -88,12 +88,7 @@ export default function InvoicesPage() {

{filteredInvoices.map((invoice, index) => ( - + @@ -124,7 +119,7 @@ export default function InvoicesPage() { - + ))}
diff --git a/backend/frontend/app/dashboard/layout.tsx b/backend/frontend/app/dashboard/layout.tsx index e5e41485..3d9f312d 100644 --- a/backend/frontend/app/dashboard/layout.tsx +++ b/backend/frontend/app/dashboard/layout.tsx @@ -3,10 +3,26 @@ import { useAuthStore } from '@/store/useAuthStore'; import { useRouter } from 'next/navigation'; import { useEffect } from 'react'; -import { Sidebar } from '@/components/layout/Sidebar'; -import { Header } from '@/components/layout/Header'; +import dynamic from 'next/dynamic'; +import { DashboardProviders } from '@/components/providers-dashboard'; import { ErrorBoundary } from '@/components/errors/ErrorBoundary'; +// Sidebar and Header are dashboard-only — load them as part of the dashboard +// chunk rather than the root bundle. +const Sidebar = dynamic( + () => import('@/components/layout/Sidebar').then((m) => m.Sidebar), + { ssr: false } +); +const Header = dynamic( + () => import('@/components/layout/Header').then((m) => m.Header), + { ssr: false } +); + +// PWA components are non-critical — load after the dashboard is interactive. +const PWAWrapper = dynamic(() => import('@/components/PWAWrapper'), { + ssr: false, +}); + export default function DashboardLayout({ children, }: { @@ -26,17 +42,19 @@ export default function DashboardLayout({ } return ( - -
- -
-
-
- {children} -
+ + +
+ +
+
+
+ {children} +
+
-
- + + + ); } - diff --git a/backend/frontend/app/dashboard/page.tsx b/backend/frontend/app/dashboard/page.tsx index a20453d0..77780d0b 100644 --- a/backend/frontend/app/dashboard/page.tsx +++ b/backend/frontend/app/dashboard/page.tsx @@ -3,7 +3,7 @@ import { useDashboardData } from '@/lib/hooks/useDashboardData'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { DollarSign, Clock, Folder, CheckCircle2, TrendingUp } from 'lucide-react'; -import { motion } from 'framer-motion'; +import { FadeIn } from '@/components/ui/fade-in'; import { DashboardStatsSkeleton } from '@/components/ui/loading-skeletons'; export default function DashboardPage() { @@ -43,11 +43,7 @@ export default function DashboardPage() { {/* Stats Grid */}
- + @@ -63,13 +59,9 @@ export default function DashboardPage() {

-
+ - + @@ -82,13 +74,9 @@ export default function DashboardPage() {

Awaiting approval

-
+ - + @@ -101,13 +89,9 @@ export default function DashboardPage() {

In progress

-
+ - + @@ -120,15 +104,11 @@ export default function DashboardPage() {

Projects done

-
+
{/* Recent Activity */} - + Recent Activity @@ -152,7 +132,7 @@ export default function DashboardPage() { )} - +
); } diff --git a/backend/frontend/app/dashboard/payments/page.tsx b/backend/frontend/app/dashboard/payments/page.tsx index 8e34cff1..07a6c51c 100644 --- a/backend/frontend/app/dashboard/payments/page.tsx +++ b/backend/frontend/app/dashboard/payments/page.tsx @@ -3,7 +3,7 @@ import { useDashboardData } from '@/lib/hooks/useDashboardData'; import { Card, CardContent } from '@/components/ui/card'; import { CheckCircle2, Clock, XCircle, ExternalLink, Wallet } from 'lucide-react'; -import { motion } from 'framer-motion'; +import { FadeIn } from '@/components/ui/fade-in'; import { PaymentCardSkeleton } from '@/components/ui/loading-skeletons'; import { EmptyState } from '@/components/empty/EmptyState'; @@ -48,12 +48,7 @@ export default function PaymentsPage() {
{payments.map((payment, index) => ( - +
@@ -95,7 +90,7 @@ export default function PaymentsPage() { )} - + ))}
diff --git a/backend/frontend/app/dashboard/projects/[id]/page.tsx b/backend/frontend/app/dashboard/projects/[id]/page.tsx index 507ee4d6..d888f8f6 100644 --- a/backend/frontend/app/dashboard/projects/[id]/page.tsx +++ b/backend/frontend/app/dashboard/projects/[id]/page.tsx @@ -8,7 +8,7 @@ import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { ArrowLeft, ExternalLink, CheckCircle2, Clock, Circle, Loader2 } from 'lucide-react'; import Link from 'next/link'; -import { motion } from 'framer-motion'; +import { FadeIn } from '@/components/ui/fade-in'; import { ProjectDetailSkeleton } from '@/components/ui/loading-skeletons'; import { useAgenticPay } from '@/lib/hooks/useAgenticPay'; import { useAccount } from 'wagmi'; @@ -70,19 +70,6 @@ export default function ProjectDetailPage() { } }; - const handleSubmitWork = async () => { - if (!repoLink) { - toast.error('Please enter a GitHub repository link'); - return; - } - try { - await submitWork(project.id, repoLink); - toast.info('Submission transaction submitted...'); - } catch (e) { - console.error(e); - } - }; - const handleApprove = async () => { try { await approveWork(project.id); @@ -272,52 +259,49 @@ export default function ProjectDetailPage() {
{project.milestones.map((milestone, index) => ( - -
-
- {getStatusIcon(milestone.status)} -
-

{milestone.title}

- {milestone.description && ( -

{milestone.description}

- )} + +
+
+
+ {getStatusIcon(milestone.status)} +
+

{milestone.title}

+ {milestone.description && ( +

{milestone.description}

+ )} +
-
-
-

- {milestone.amount} {project.currency} -

- {milestone.dueDate && ( -

- Due: {new Date(milestone.dueDate).toLocaleDateString()} +

+

+ {milestone.amount} {project.currency}

- )} -
-
-
-
- Progress - {milestone.completionPercentage}% + {milestone.dueDate && ( +

+ Due: {new Date(milestone.dueDate).toLocaleDateString()} +

+ )} +
-
-
+
+ Progress + {milestone.completionPercentage}% +
+
+
+ style={{ width: `${milestone.completionPercentage}%` }} + /> +
- + ))}
diff --git a/backend/frontend/app/dashboard/projects/page.tsx b/backend/frontend/app/dashboard/projects/page.tsx index 52a4ed83..190249a8 100644 --- a/backend/frontend/app/dashboard/projects/page.tsx +++ b/backend/frontend/app/dashboard/projects/page.tsx @@ -4,7 +4,7 @@ import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Plus, ExternalLink, Clock, Folder } from 'lucide-react'; import Link from 'next/link'; -import { motion } from 'framer-motion'; +import { FadeIn } from '@/components/ui/fade-in'; import { ProjectCardSkeleton } from '@/components/ui/loading-skeletons'; import { Skeleton } from '@/components/ui/skeleton'; import { EmptyState } from '@/components/empty/EmptyState'; @@ -101,12 +101,7 @@ export default function ProjectsPage() { : 0; return ( - +
@@ -162,7 +157,7 @@ export default function ProjectsPage() { - + ); })}
diff --git a/backend/frontend/app/globals.css b/backend/frontend/app/globals.css index 7602c03d..08a68bc5 100644 --- a/backend/frontend/app/globals.css +++ b/backend/frontend/app/globals.css @@ -130,6 +130,22 @@ } } +/* Lightweight CSS fade-in-up — replaces framer-motion for simple list animations */ +@keyframes fade-in-up { + from { + opacity: 0; + transform: translateY(16px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.animate-fade-in-up { + animation: fade-in-up 0.4s ease-out both; +} + .bg-grid-pattern { background-image: linear-gradient(to right, rgba(0, 0, 0, 0.1) 1px, transparent 1px), diff --git a/backend/frontend/app/layout.tsx b/backend/frontend/app/layout.tsx index 5349258e..b04b187f 100644 --- a/backend/frontend/app/layout.tsx +++ b/backend/frontend/app/layout.tsx @@ -2,7 +2,6 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; import { Providers } from "@/components/providers"; -import PWAWrapper from "@/components/PWAWrapper"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -16,19 +15,30 @@ const geistMono = Geist_Mono({ export const metadata: Metadata = { title: "AgenticPay - Get Paid Instantly for Your Work", - description: "Secure, fast, and transparent payments for freelancers powered by blockchain technology.", + description: + "Secure, fast, and transparent payments for freelancers powered by blockchain technology.", manifest: "/manifest.webmanifest", - keywords: ["freelancer", "payments", "blockchain", "crypto", "web3", "escrow", "milestones"], + keywords: [ + "freelancer", + "payments", + "blockchain", + "crypto", + "web3", + "escrow", + "milestones", + ], authors: [{ name: "AgenticPay" }], openGraph: { title: "AgenticPay - Get Paid Instantly for Your Work", - description: "Secure, fast, and transparent payments for freelancers powered by blockchain technology.", + description: + "Secure, fast, and transparent payments for freelancers powered by blockchain technology.", type: "website", }, twitter: { card: "summary_large_image", title: "AgenticPay - Get Paid Instantly for Your Work", - description: "Secure, fast, and transparent payments for freelancers powered by blockchain technology.", + description: + "Secure, fast, and transparent payments for freelancers powered by blockchain technology.", }, }; @@ -39,13 +49,8 @@ export default function RootLayout({ }>) { return ( - - - {children} - - + + {children} ); diff --git a/backend/frontend/app/page.tsx b/backend/frontend/app/page.tsx index 7f25c6da..6ce784f7 100644 --- a/backend/frontend/app/page.tsx +++ b/backend/frontend/app/page.tsx @@ -1,147 +1,93 @@ -'use client'; +/** + * Landing page — Server Component. + * Only the animated sub-components (HeroCTA, HeroAnimations, Navbar) are + * client components, keeping framer-motion out of the initial HTML payload + * for the static sections. + */ import Link from 'next/link'; -import { motion } from 'framer-motion'; import { ArrowRight, Shield, Zap, Wallet, CheckCircle2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Navbar } from '@/components/landing/Navbar'; +import { HeroCTA } from '@/components/landing/HeroCTA'; +import { HeroAnimations } from '@/components/landing/HeroAnimations'; + +const features = [ + { + icon: Zap, + title: 'Instant Payments', + description: + 'Receive payments instantly upon milestone completion. No waiting, no delays.', + }, + { + icon: Shield, + title: 'Secure & Transparent', + description: + 'Blockchain-powered escrow ensures your funds are safe and transactions are transparent.', + }, + { + icon: Wallet, + title: 'Multiple Payment Methods', + description: + 'Connect with social login or your Web3 wallet. Choose what works for you.', + }, + { + icon: CheckCircle2, + title: 'Milestone Tracking', + description: + 'Track project progress with clear milestones and automated invoicing.', + }, +]; export default function Home() { return (
+ {/* Hero Section */}
-
+
- {/* Features Section */} + {/* Features Section — fully static, no JS needed */}
- +

Why Choose AgenticPay?

Everything you need to get paid faster and more securely

- +
- {features.map((feature, index) => ( - ( +
- +
-

- {feature.title} -

-

- {feature.description} -

- +

{feature.title}

+

{feature.description}

+
))}
- {/* CTA Section */} + {/* CTA Section — static */}
- +

Ready to Get Started?

@@ -157,11 +103,11 @@ export default function Home() {
- {/* Footer */} + {/* Footer — static */}
@@ -170,15 +116,9 @@ export default function Home() {

Secure payments for freelancers

@@ -189,26 +129,3 @@ export default function Home() {
); } - -const features = [ - { - icon: Zap, - title: 'Instant Payments', - description: 'Receive payments instantly upon milestone completion. No waiting, no delays.', - }, - { - icon: Shield, - title: 'Secure & Transparent', - description: 'Blockchain-powered escrow ensures your funds are safe and transactions are transparent.', - }, - { - icon: Wallet, - title: 'Multiple Payment Methods', - description: 'Connect with social login or your Web3 wallet. Choose what works for you.', - }, - { - icon: CheckCircle2, - title: 'Milestone Tracking', - description: 'Track project progress with clear milestones and automated invoicing.', - }, -]; diff --git a/backend/frontend/components/auth/SocialLogin.tsx b/backend/frontend/components/auth/SocialLogin.tsx index 190334f8..c35e880e 100644 --- a/backend/frontend/components/auth/SocialLogin.tsx +++ b/backend/frontend/components/auth/SocialLogin.tsx @@ -1,9 +1,7 @@ 'use client'; import { useState } from 'react'; -import { web3auth } from '@/lib/web3auth'; -import { WALLET_ADAPTERS } from "@web3auth/base"; - +import { getWeb3Auth } from '@/lib/web3auth'; import { Button } from '@/components/ui/button'; import { Mail, Chrome, Twitter } from 'lucide-react'; import { useRouter } from 'next/navigation'; @@ -16,27 +14,29 @@ export function SocialLogin() { const setAuth = useAuthStore((state) => state.setAuth); const handleLogin = async (loginProvider: string) => { - if (!web3auth) { - 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 as any, { + // Dynamically load Web3Auth only when the user actually clicks login + const { WALLET_ADAPTERS } = await import('@web3auth/base'); + const web3auth = await getWeb3Auth(); + + if (!web3auth) { + toast.error( + 'Web3Auth is not configured. Please add NEXT_PUBLIC_WEB3AUTH_CLIENT_ID to your .env.local file.' + ); + return; + } + + await (web3auth as any).initModal(); + const web3authProvider = await web3auth.connectTo((WALLET_ADAPTERS as any).AUTH, { loginProvider, }); if (web3authProvider) { - // Get user info const user = await web3auth.getUserInfo(); - const accounts = await web3authProvider.request({ - method: 'eth_accounts', - }); + const accounts = await web3authProvider.request({ method: 'eth_accounts' }); - // Save to store setAuth({ address: (accounts as string[])[0], email: user.email, @@ -46,7 +46,6 @@ export function SocialLogin() { }); toast.success('Login successful!'); - // Redirect to dashboard router.push('/dashboard'); } } catch (error) { @@ -89,4 +88,3 @@ export function SocialLogin() {
); } - diff --git a/backend/frontend/components/landing/HeroAnimations.tsx b/backend/frontend/components/landing/HeroAnimations.tsx new file mode 100644 index 00000000..c958ebb1 --- /dev/null +++ b/backend/frontend/components/landing/HeroAnimations.tsx @@ -0,0 +1,27 @@ +'use client'; + +import { motion } from 'framer-motion'; + +/** + * Purely decorative floating blobs on the hero section. + * Isolated into a tiny client component so the rest of the landing page + * can remain a Server Component. + */ +export function HeroAnimations() { + return ( + <> +