diff --git a/src/app/commitments/[id]/page.tsx b/src/app/commitments/[id]/page.tsx index 3150abb..b6a4ce3 100644 --- a/src/app/commitments/[id]/page.tsx +++ b/src/app/commitments/[id]/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import React from 'react'; +import React, { useState, useEffect } from 'react'; import Link from 'next/link'; import { notFound } from 'next/navigation'; import CommitmentHealthMetrics from '@/components/dashboard/CommitmentHealthMetrics'; @@ -119,6 +119,15 @@ export default function CommitmentDetailPage({ }: { params: { id: string }; }) { + const [isLoading, setIsLoading] = useState(true) + + useEffect(() => { + const timer = setTimeout(() => { + setIsLoading(false) + }, 1000) + return () => clearTimeout(timer) + }, []) + const commitment = getCommitmentById(params.id) if (!commitment) notFound() @@ -189,6 +198,7 @@ export default function CommitmentDetailPage({ feeGenerationData={MOCK_FEE_GENERATION_DATA} thresholdPercent={0.5} volatilityPercent={35} + isLoading={isLoading} /> (null) const [hasAcknowledged, setHasAcknowledged] = useState(false) const [commitmentsList, setCommitmentsList] = useState(mockCommitments) + const [isLoading, setIsLoading] = useState(true) + + useEffect(() => { + // Initial data load simulation + const timer = setTimeout(() => { + setIsLoading(false) + }, 800) + return () => clearTimeout(timer) + }, []) useEffect(() => { if (process.env.NEXT_PUBLIC_USE_MOCKS === 'true') { @@ -201,12 +211,16 @@ export default function MyCommitments() { />
- + {isLoading ? ( + + ) : ( + + )} - router.push(`/commitments/${id}`)} - onAttestations={(id) => console.log('Attestations for', id)} - onEarlyExit={openEarlyExitModal} - /> + {isLoading ? ( +
+ {[1, 2, 3].map((i) => ( + + ))} +
+ ) : ( + router.push(`/commitments/${id}`)} + onAttestations={(id) => console.log('Attestations for', id)} + onEarlyExit={openEarlyExitModal} + /> + )}
{commitmentForEarlyExit && earlyExitSummary && ( diff --git a/src/app/globals.css b/src/app/globals.css index a719bcc..dcd80f6 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -147,8 +147,36 @@ button { background-color: #51A2FF; } -/* Firefox */ +/* ... (existing custom-scrollbar styles) */ .custom-scrollbar { scrollbar-width: thin; scrollbar-color: #4A6B8A #0A0A0A; } + +/* Unified Loading Pattern: Shimmer */ +@keyframes shimmer { + 0% { + background-position: -200% 0; + } + 100% { + background-position: 200% 0; + } +} + +.animate-shimmer { + background: linear-gradient( + 90deg, + rgba(255, 255, 255, 0.03) 25%, + rgba(255, 255, 255, 0.08) 50%, + rgba(255, 255, 255, 0.03) 75% + ); + background-size: 200% 100%; + animation: shimmer 2s infinite linear; +} + +@media (prefers-reduced-motion: reduce) { + .animate-shimmer { + animation: none; + background: rgba(255, 255, 255, 0.05); + } +} diff --git a/src/app/marketplace/page.tsx b/src/app/marketplace/page.tsx index f438818..cc9f5ae 100644 --- a/src/app/marketplace/page.tsx +++ b/src/app/marketplace/page.tsx @@ -1,11 +1,12 @@ 'use client' import Link from 'next/link' -import { useMemo, useState } from 'react' +import { useMemo, useState, useEffect } from 'react' import { MarketplaceHeader } from '@/components/MarketplaceHeader/MarketplaceHeader' import { MarketplaceGrid } from '@/components/MarketplaceGrid' import { MarketplaceResultsLayout } from '@/components/MarketplaceResultsLayout' import MarketplaceFilters from '@/components/MarketplaceFilter/MarketplaceFilters' +import { MarketplaceCardSkeleton, MarketplaceRowSkeleton } from '@/components/MarketplaceSkeletons' // Interfaces matching the components interface Filters { @@ -335,6 +336,7 @@ export default function Marketplace() { const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid') const [currentPage, setCurrentPage] = useState(1) const [showMobileFilters, setShowMobileFilters] = useState(false) + const [isLoading, setIsLoading] = useState(true) const [filters, setFilters] = useState({ sortBy: 'price', commitmentType: ['balanced'], @@ -344,6 +346,14 @@ export default function Marketplace() { maxLoss: 100, }) + useEffect(() => { + // Initial data load simulation + const timer = setTimeout(() => { + setIsLoading(false) + }, 1200) + return () => clearTimeout(timer) + }, []) + // ... rest of the logic const itemsPerPage = 9 @@ -451,7 +461,25 @@ export default function Marketplace() { totalPages={totalPages} onPageChange={handlePageChange} > - {viewMode === 'grid' ? ( + {isLoading ? ( + viewMode === 'grid' ? ( +
+
    + {[1, 2, 3, 4, 5, 6].map((idx) => ( +
  • + +
  • + ))} +
+
+ ) : ( +
+ {[1, 2, 3, 4, 5].map((idx) => ( + + ))} +
+ ) + ) : viewMode === 'grid' ? ( ) : ( diff --git a/src/components/ChartSkeleton.tsx b/src/components/ChartSkeleton.tsx new file mode 100644 index 0000000..593011f --- /dev/null +++ b/src/components/ChartSkeleton.tsx @@ -0,0 +1,38 @@ +'use client'; + +import React from 'react'; +import { Skeleton } from './ui/Skeleton'; + +export function ChartSkeleton() { + return ( +
+
+ +
+ + +
+
+ + {/* Simulation of a chart axis and lines */} +
+ {/* Y Axis simulation */} +
+ {/* X Axis simulation */} +
+ + {/* Shimmering chart lines (simplified) */} +
+ {[40, 70, 50, 90, 60, 80, 45, 75].map((h, i) => ( + + ))} +
+
+ +
+ + +
+
+ ); +} diff --git a/src/components/MarketplaceSkeletons.tsx b/src/components/MarketplaceSkeletons.tsx new file mode 100644 index 0000000..fb533a7 --- /dev/null +++ b/src/components/MarketplaceSkeletons.tsx @@ -0,0 +1,69 @@ +'use client'; + +import React from 'react'; +import { Skeleton } from './ui/Skeleton'; + +export function MarketplaceCardSkeleton() { + return ( +
+ {/* Header */} +
+ +
+ + +
+
+ + {/* Body */} +
+ +
+ {[1, 2, 3, 4, 5].map((i) => ( +
+ + +
+ ))} +
+
+ + {/* Footer */} +
+ +
+ + +
+
+
+ ); +} + +export function MarketplaceRowSkeleton() { + return ( +
+
+ +
+ + +
+
+ +
+ {[1, 2, 3, 4, 5].map((i) => ( +
+ + +
+ ))} +
+ +
+ + +
+
+ ); +} diff --git a/src/components/MyCommitmentsSkeletons.tsx b/src/components/MyCommitmentsSkeletons.tsx new file mode 100644 index 0000000..882475c --- /dev/null +++ b/src/components/MyCommitmentsSkeletons.tsx @@ -0,0 +1,68 @@ +'use client'; + +import React from 'react'; +import { Skeleton } from './ui/Skeleton'; + +export function MyCommitmentCardSkeleton() { + return ( +
+
+ + +
+ + + +
+ + +
+ +
+ {[1, 2].map((i) => ( +
+ + +
+ ))} +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+ ); +} + +export function MyCommitmentsStatsSkeleton() { + return ( +
+ {[1, 2, 3, 4].map((i) => ( +
+ +
+ + +
+
+ ))} +
+ ); +} diff --git a/src/components/dashboard/CommitmentHealthMetrics.tsx b/src/components/dashboard/CommitmentHealthMetrics.tsx index 416ef63..86f89fe 100644 --- a/src/components/dashboard/CommitmentHealthMetrics.tsx +++ b/src/components/dashboard/CommitmentHealthMetrics.tsx @@ -8,6 +8,7 @@ import { HealthMetricsDrawdownChart } from './HealthMetricsDrawdownChart'; import { HealthMetricsValueHistoryChart } from './HealthMetricsValueHistoryChart'; import { HealthMetricsFeeGenerationChart } from './HealthMetricsFeeGenerationChart'; import { TrendingUp, TrendingDown, DollarSign, CheckCircle } from 'lucide-react'; +import { ChartSkeleton } from '../ChartSkeleton'; function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); @@ -29,6 +30,7 @@ interface CommitmentHealthMetricsProps { feeGenerationData: Array<{ date: string; feeAmount: number }>; thresholdPercent?: number; volatilityPercent?: number; + isLoading?: boolean; } export default function CommitmentHealthMetrics({ @@ -38,6 +40,7 @@ export default function CommitmentHealthMetrics({ feeGenerationData, thresholdPercent, volatilityPercent, + isLoading = false, }: CommitmentHealthMetricsProps) { const [activeTab, setActiveTab] = useState('value'); @@ -72,28 +75,34 @@ export default function CommitmentHealthMetrics({
-
- {activeTab === 'value' && ( - - )} - {activeTab === 'drawdown' && ( - - )} - {activeTab === 'fee' && ( - - )} - {activeTab === 'compliance' && ( - +
+ {isLoading ? ( + + ) : ( + <> + {activeTab === 'value' && ( + + )} + {activeTab === 'drawdown' && ( + + )} + {activeTab === 'fee' && ( + + )} + {activeTab === 'compliance' && ( + + )} + )}
diff --git a/src/components/ui/Skeleton.tsx b/src/components/ui/Skeleton.tsx new file mode 100644 index 0000000..9072dc4 --- /dev/null +++ b/src/components/ui/Skeleton.tsx @@ -0,0 +1,23 @@ +'use client'; + +import React from 'react'; + +interface SkeletonProps { + className?: string; + variant?: 'rect' | 'circle' | 'text'; +} + +export function Skeleton({ className = '', variant = 'rect' }: SkeletonProps) { + const baseClass = 'animate-shimmer rounded-md bg-white/5'; + const variantClass = + variant === 'circle' ? 'rounded-full' : + variant === 'text' ? 'h-4 w-full last:w-3/4' : + ''; + + return ( +