IFIF Project Management System
A comprehensive decentralized crowdfunding platform built with IFIF protocol, demonstrating project creation, investment management, NFT operations, and investor relations.
Overview
The IFIF Project Management System showcases how to implement comprehensive investment and crowdfunding functionality using the IFIF smart contracts. It provides a complete Web3 application with progressive data loading, advanced table management with TanStack, and multiple specialized interfaces for project lifecycle management, investor relations, and user portfolio tracking.
Key Features
- Project Portfolio Management: Browse, filter, and deploy investment projects with card/table views, advanced TanStack sorting, and deployment modals
- Investment Client Operations: Purchase, refund, claim, and deposit functionality through specialized client modals in project detail pages
- Project Detail & Investor Management: Comprehensive project pages with metrics grids, investor tracking tables, and transaction history
- NFT Operations & Management: Split, merge, and convert NFT operations with dedicated project NFT sub-pages and action modals
- Token Analytics Dashboard: Token deployment tracking, metrics visualization, and detailed token information pages
- Activity & Transaction Tracking: System-wide activity monitoring with TanStack tables, filtering, and transaction details
- User Portfolio Analytics: Individual user dashboards with investment tracking, activity history, and NFT portfolio management
- Progressive Data Loading: Real-time data synchronization with optimized caching, filtering, and progressive hooks integration
Smart Contract Integration
Project Management Hooks
The system provides type-safe React hooks for all project operations:
Project Deployment
import { useDeployProject } from '@/lib/ifif-project-management-hooks'
import { DeployProjectModal } from '@/components/ifif-project-management-modals'
import { shortenHash, shortenAddress } from '@/config/constants'
import { toast } from 'sonner'
function DeployProjectButton() {
const { deployProject, isLoading, isSuccess, isError, error, txHash, reset } = useDeployProject()
const handleDeployProject = async () => {
try {
const config = {
fundToken: '0x123...' as Address,
projectId: 1,
fundAmount: parseUnits('1000', 18),
privateSaleTime: Math.floor(Date.now() / 1000) + 86400, // 1 day from now
privateBonusPercent: 10,
publicSaleTime: Math.floor(Date.now() / 1000) + 172800, // 2 days from now
desiredEndEpoch: Math.floor(Date.now() / 1000) + 604800 // 1 week from now
}
const nextConfig = {
projectOwner: '0x456...' as Address,
distributor: '0x789...' as Address,
distributorFeePercent: 5,
projectOwnerFeePercent: 10,
platformFeePercent: 2,
pairPrice: parseUnits('1', 18)
}
const hash = await deployProject({
config,
nextConfig,
configSignature: '0xsignature1...' as `0x${string}`,
nextConfigSignature: '0xsignature2...' as `0x${string}`,
projectName: 'Test Project',
projectSymbol: 'TEST'
})
console.log('Transaction hash:', hash)
} catch (err) {
console.error('Failed to deploy project:', err)
// Error is automatically handled by the hook state
}
}
// Handle success state
useEffect(() => {
if (isSuccess && txHash) {
toast.success(`Project deployed successfully! TX: ${shortenHash(txHash)}`)
reset() // Reset hook state after success
}
}, [isSuccess, txHash, reset])
// Handle error state
useEffect(() => {
if (isError && error) {
toast.error(`Deployment failed: ${error}`)
}
}, [isError, error])
return (
<Button
onClick={handleDeployProject}
disabled={isLoading}
>
{isLoading ? 'Deploying...' : 'Deploy Project'}
</Button>
)
}Investment Operations
import { usePurchase, useRefund, useClaim, useDeposit } from '@/lib/ifif-project-management-hooks'
import { PurchaseModal, RefundModal, ClaimModal, DepositModal } from '@/components/ifif-project-client-modals'
function InvestmentControls({ projectAddress }: { projectAddress: Address }) {
const { purchase, isLoading: isPurchasing, isSuccess: purchaseSuccess, isError: purchaseError, error: purchaseErrorMessage, txHash: purchaseTxHash, reset: resetPurchase } = usePurchase()
const { refund, isLoading: isRefunding, isSuccess: refundSuccess, isError: refundError, error: refundErrorMessage, txHash: refundTxHash, reset: resetRefund } = useRefund()
const { claim, isLoading: isClaiming, isSuccess: claimSuccess, isError: claimError, error: claimErrorMessage, txHash: claimTxHash, reset: resetClaim } = useClaim()
const { deposit, isLoading: isDepositing, isSuccess: depositSuccess, isError: depositError, error: depositErrorMessage, txHash: depositTxHash, reset: resetDeposit } = useDeposit()
const handlePurchase = async (amount: string, fundTokenAddress: Address, merkleProof: string[] = []) => {
try {
await purchase({
projectAddress,
amount: parseUnits(amount, 18),
fundTokenAddress,
merkleProof
})
} catch (err) {
console.error('Purchase failed:', err)
// Error state automatically handled by hook
}
}
const handleRefund = async () => {
try {
await refund({ projectAddress })
} catch (err) {
console.error('Refund failed:', err)
// Error state automatically handled by hook
}
}
const handleClaim = async () => {
try {
await claim({ projectAddress })
} catch (err) {
console.error('Claim failed:', err)
// Error state automatically handled by hook
}
}
const handleDeposit = async (amount: string, fundTokenAddress: Address) => {
try {
await deposit({
projectAddress,
amount: parseUnits(amount, 18),
fundTokenAddress
})
} catch (err) {
console.error('Deposit failed:', err)
// Error state automatically handled by hook
}
}
// Handle success states with UI feedback
useEffect(() => {
if (purchaseSuccess && purchaseTxHash) {
console.log(`Purchase successful! TX: ${purchaseTxHash}`)
resetPurchase()
}
if (refundSuccess && refundTxHash) {
console.log(`Refund successful! TX: ${refundTxHash}`)
resetRefund()
}
if (claimSuccess && claimTxHash) {
console.log(`Claim successful! TX: ${claimTxHash}`)
resetClaim()
}
if (depositSuccess && depositTxHash) {
console.log(`Deposit successful! TX: ${depositTxHash}`)
resetDeposit()
}
}, [purchaseSuccess, refundSuccess, claimSuccess, depositSuccess])
// Handle error states with UI feedback
useEffect(() => {
if (purchaseError && purchaseErrorMessage) {
console.error(`Purchase failed: ${purchaseErrorMessage}`)
}
if (refundError && refundErrorMessage) {
console.error(`Refund failed: ${refundErrorMessage}`)
}
if (claimError && claimErrorMessage) {
console.error(`Claim failed: ${claimErrorMessage}`)
}
if (depositError && depositErrorMessage) {
console.error(`Deposit failed: ${depositErrorMessage}`)
}
}, [purchaseError, refundError, claimError, depositError])
return (
<div className="space-y-2">
<Button onClick={() => handlePurchase('100', '0x123...' as Address)} disabled={isPurchasing}>
{isPurchasing ? 'Purchasing...' : 'Purchase Tokens'}
</Button>
<Button onClick={handleRefund} disabled={isRefunding} variant="destructive">
{isRefunding ? 'Processing...' : 'Request Refund'}
</Button>
<Button onClick={handleClaim} disabled={isClaiming}>
{isClaiming ? 'Claiming...' : 'Claim Tokens'}
</Button>
<Button onClick={() => handleDeposit('50', '0x123...' as Address)} disabled={isDepositing}>
{isDepositing ? 'Depositing...' : 'Deposit Tokens'}
</Button>
</div>
)
}Project Configuration
import {
useUpdateProjectConfig,
useStartPrivateSale,
useStartPublicSale,
useEndSales,
usePublicEndSales
} from '@/lib/ifif-project-management-hooks'
import {
UpdateProjectConfigModal,
StartPrivateSaleModal,
StartPublicSaleModal,
EndSalesModal,
PublicEndSalesModal
} from '@/components/ifif-project-management-modals'
function ProjectConfigControls({ projectAddress }: { projectAddress: Address }) {
const { updateProjectConfig, isLoading: isUpdatingConfig, isSuccess: configUpdateSuccess, isError: configUpdateError, error: configError, txHash: configTxHash, reset: resetConfig } = useUpdateProjectConfig()
const { startPrivateSale, isLoading: isStartingPrivate, isSuccess: privateSaleSuccess, isError: privateSaleError, error: privateSaleErrorMessage, txHash: privateSaleTxHash, reset: resetPrivateSale } = useStartPrivateSale()
const { startPublicSale, isLoading: isStartingPublic, isSuccess: publicSaleSuccess, isError: publicSaleError, error: publicSaleErrorMessage, txHash: publicSaleTxHash, reset: resetPublicSale } = useStartPublicSale()
const { endSales, isLoading: isEndingSales, isSuccess: endSalesSuccess, isError: endSalesError, error: endSalesErrorMessage, txHash: endSalesTxHash, reset: resetEndSales } = useEndSales()
const { publicEndSales, isLoading: isPublicEndingSales, isSuccess: publicEndSalesSuccess, isError: publicEndSalesError, error: publicEndSalesErrorMessage, txHash: publicEndSalesTxHash, reset: resetPublicEndSales } = usePublicEndSales()
const handleUpdateConfig = async () => {
try {
const nextConfig = {
projectOwner: '0x456...' as Address,
distributor: '0x789...' as Address,
distributorFeePercent: 5,
projectOwnerFeePercent: 10,
platformFeePercent: 2,
pairPrice: parseUnits('1', 18)
}
await updateProjectConfig({
projectAddress,
nextConfig,
signatures: ['0xsig1', '0xsig2', '0xsig3'] // [manager, distributor, owner] signatures
})
} catch (err) {
console.error('Config update failed:', err)
// Error state automatically handled by hook
}
}
const handleStartPrivateSale = async () => {
try {
await startPrivateSale({ projectAddress })
} catch (err) {
console.error('Private sale start failed:', err)
// Error state automatically handled by hook
}
}
const handleStartPublicSale = async () => {
try {
await startPublicSale({ projectAddress })
} catch (err) {
console.error('Public sale start failed:', err)
// Error state automatically handled by hook
}
}
const handleEndSales = async () => {
try {
await endSales({ projectAddress })
} catch (err) {
console.error('End sales failed:', err)
// Error state automatically handled by hook
}
}
const handlePublicEndSales = async () => {
try {
await publicEndSales({ projectAddress })
} catch (err) {
console.error('Public end sales failed:', err)
// Error state automatically handled by hook
}
}
// Handle success states
useEffect(() => {
if (configUpdateSuccess && configTxHash) {
console.log(`Config updated successfully! TX: ${configTxHash}`)
resetConfig()
}
if (privateSaleSuccess && privateSaleTxHash) {
console.log(`Private sale started successfully! TX: ${privateSaleTxHash}`)
resetPrivateSale()
}
if (publicSaleSuccess && publicSaleTxHash) {
console.log(`Public sale started successfully! TX: ${publicSaleTxHash}`)
resetPublicSale()
}
if (endSalesSuccess && endSalesTxHash) {
console.log(`Sales ended successfully! TX: ${endSalesTxHash}`)
resetEndSales()
}
if (publicEndSalesSuccess && publicEndSalesTxHash) {
console.log(`Public end sales completed! TX: ${publicEndSalesTxHash}`)
resetPublicEndSales()
}
}, [configUpdateSuccess, privateSaleSuccess, publicSaleSuccess, endSalesSuccess, publicEndSalesSuccess])
// Handle error states
useEffect(() => {
if (configUpdateError && configError) {
console.error(`Config update failed: ${configError}`)
}
if (privateSaleError && privateSaleErrorMessage) {
console.error(`Private sale start failed: ${privateSaleErrorMessage}`)
}
if (publicSaleError && publicSaleErrorMessage) {
console.error(`Public sale start failed: ${publicSaleErrorMessage}`)
}
if (endSalesError && endSalesErrorMessage) {
console.error(`End sales failed: ${endSalesErrorMessage}`)
}
if (publicEndSalesError && publicEndSalesErrorMessage) {
console.error(`Public end sales failed: ${publicEndSalesErrorMessage}`)
}
}, [configUpdateError, privateSaleError, publicSaleError, endSalesError, publicEndSalesError])
return (
<div className="space-y-2">
<Button onClick={handleUpdateConfig} disabled={isUpdatingConfig}>
{isUpdatingConfig ? 'Updating...' : 'Update Project Config'}
</Button>
<Button onClick={handleStartPrivateSale} disabled={isStartingPrivate}>
{isStartingPrivate ? 'Starting...' : 'Start Private Sale'}
</Button>
<Button onClick={handleStartPublicSale} disabled={isStartingPublic}>
{isStartingPublic ? 'Starting...' : 'Start Public Sale'}
</Button>
<Button onClick={handleEndSales} disabled={isEndingSales} variant="destructive">
{isEndingSales ? 'Ending...' : 'End Sales'}
</Button>
<Button onClick={handlePublicEndSales} disabled={isPublicEndingSales} variant="destructive">
{isPublicEndingSales ? 'Ending...' : 'Public End Sales'}
</Button>
</div>
)
}High-Level Project Management
import { useProjectManagement } from '@/lib/ifif-project-management-hooks'
function ProjectManager({ projectAddress }: { projectAddress: Address }) {
const {
// Deployment operations
updateDeploymentDetails,
isUpdatingDeploymentDetails,
updateDeploymentDetailsSuccess,
updateDeploymentDetailsError,
updateDeploymentDetailsTxHash,
// Configuration operations
updateProjectConfig,
isUpdatingProjectConfig,
updateProjectConfigSuccess,
updateProjectConfigError,
updateProjectConfigTxHash,
// Sales lifecycle operations
startPrivateSale,
isStartingPrivateSale,
startPrivateSaleSuccess,
startPrivateSaleError,
startPrivateSaleTxHash,
startPublicSale,
isStartingPublicSale,
startPublicSaleSuccess,
startPublicSaleError,
startPublicSaleTxHash,
endSales,
isEndingSales,
endSalesSuccess,
endSalesError,
endSalesTxHash,
publicEndSales,
isPublicEndingSales,
publicEndSalesSuccess,
publicEndSalesError,
publicEndSalesTxHash,
// Client operations
purchase,
isPurchasing,
purchaseSuccess,
purchaseError,
purchaseTxHash,
refund,
isRefunding,
refundSuccess,
refundError,
refundTxHash,
claim,
isClaiming,
claimSuccess,
claimError,
claimTxHash,
claimNFT,
isClaimingNFT,
claimNFTSuccess,
claimNFTError,
claimNFTTxHash,
deposit,
isDepositing,
depositSuccess,
depositError,
depositTxHash,
convertNFT,
isConvertingNFT,
convertNFTSuccess,
convertNFTError,
convertNFTTxHash,
// Deployment operations
deployProject,
isDeploying,
deploySuccess,
deployError,
deployTxHash,
// Combined states for project lifecycle management
isAnyLoading,
hasAnyError,
lastError,
resetAll
} = useProjectManagement()
const handleDeploymentUpdate = async () => {
try {
await updateDeploymentDetails({
projectId: 1,
projectName: 'Updated Project Name',
projectSymbol: 'UPD'
})
} catch (err) {
console.error('Deployment update failed:', err)
// Error state automatically handled by hook
}
}
const handleConfigUpdate = async () => {
try {
const nextConfig = {
projectOwner: '0x456...' as Address,
distributor: '0x789...' as Address,
distributorFeePercent: 5,
projectOwnerFeePercent: 10,
platformFeePercent: 2,
pairPrice: parseUnits('1', 18)
}
await updateProjectConfig({
projectAddress,
nextConfig,
signatures: ['0xsig1', '0xsig2', '0xsig3']
})
} catch (err) {
console.error('Config update failed:', err)
// Error state automatically handled by hook
}
}
const handleStartPrivateSale = async () => {
try {
await startPrivateSale({ projectAddress })
} catch (err) {
console.error('Private sale start failed:', err)
// Error state automatically handled by hook
}
}
const handlePurchase = async () => {
try {
await purchase({
projectAddress,
amount: parseUnits('100', 18),
fundTokenAddress: '0x123...' as Address,
merkleProof: []
})
} catch (err) {
console.error('Purchase failed:', err)
// Error state automatically handled by hook
}
}
// Handle success states for project lifecycle
useEffect(() => {
if (updateDeploymentDetailsSuccess && updateDeploymentDetailsTxHash) {
console.log(`Deployment details updated! TX: ${updateDeploymentDetailsTxHash}`)
}
if (startPrivateSaleSuccess && startPrivateSaleTxHash) {
console.log(`Private sale started! TX: ${startPrivateSaleTxHash}`)
}
if (purchaseSuccess && purchaseTxHash) {
console.log(`Purchase completed! TX: ${purchaseTxHash}`)
}
// Additional success handlers for other operations...
}, [updateDeploymentDetailsSuccess, startPrivateSaleSuccess, purchaseSuccess])
// Handle error states for project lifecycle
useEffect(() => {
if (hasAnyError && lastError) {
console.error(`Project operation failed: ${lastError}`)
}
}, [hasAnyError, lastError])
return (
<div className="p-4 border rounded space-y-4">
<h3 className="text-lg font-semibold">Project Lifecycle Management</h3>
{hasAnyError && (
<Alert variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertTitle>Operation Failed</AlertTitle>
<AlertDescription>{lastError}</AlertDescription>
</Alert>
)}
{/* Project Management Operations */}
<div className="space-y-3">
<div className="grid grid-cols-2 gap-2">
<Button
onClick={handleDeploymentUpdate}
disabled={isAnyLoading}
variant="outline"
>
{isUpdatingDeploymentDetails ? 'Updating...' : 'Update Details'}
</Button>
<Button
onClick={handleConfigUpdate}
disabled={isAnyLoading}
variant="outline"
>
{isUpdatingProjectConfig ? 'Configuring...' : 'Update Config'}
</Button>
</div>
{/* Sales Lifecycle Management */}
<div className="grid grid-cols-2 gap-2">
<Button
onClick={handleStartPrivateSale}
disabled={isAnyLoading}
>
{isStartingPrivateSale ? 'Starting...' : 'Start Private Sale'}
</Button>
<Button
onClick={() => startPublicSale({ projectAddress })}
disabled={isAnyLoading}
>
{isStartingPublicSale ? 'Starting...' : 'Start Public Sale'}
</Button>
</div>
<div className="grid grid-cols-2 gap-2">
<Button
onClick={() => endSales({ projectAddress })}
disabled={isAnyLoading}
variant="destructive"
>
{isEndingSales ? 'Ending...' : 'End Sales'}
</Button>
<Button
onClick={() => publicEndSales({ projectAddress })}
disabled={isAnyLoading}
variant="destructive"
>
{isPublicEndingSales ? 'Emergency End' : 'Public End Sales'}
</Button>
</div>
{/* Client Operations */}
<div className="grid grid-cols-3 gap-2">
<Button
onClick={handlePurchase}
disabled={isAnyLoading}
size="sm"
>
{isPurchasing ? 'Buying...' : 'Purchase'}
</Button>
<Button
onClick={() => refund({ projectAddress })}
disabled={isAnyLoading}
size="sm"
variant="outline"
>
{isRefunding ? 'Processing...' : 'Refund'}
</Button>
<Button
onClick={() => claim({ projectAddress })}
disabled={isAnyLoading}
size="sm"
>
{isClaiming ? 'Claiming...' : 'Claim'}
</Button>
</div>
{/* Utility Actions */}
<div className="flex justify-between pt-4 border-t">
<Badge variant={isAnyLoading ? 'default' : 'outline'}>
{isAnyLoading ? 'Operation in Progress...' : 'Ready'}
</Badge>
<Button
onClick={resetAll}
variant="ghost"
size="sm"
disabled={isAnyLoading}
>
Reset All States
</Button>
</div>
</div>
</div>
)
}Data Layer Integration
Progressive Data Loading
The IFIF system uses progressive loading for optimal performance with large datasets:
import {
useProgressiveIFIFProjectsLoader,
useProgressiveNFTOperationsLoader,
useProgressiveIFIFDataLoader,
useCachedIFIFData
} from '@/lib/progressive-ifif-hooks'
function IFIFDashboard() {
// Individual progressive loaders
const {
projects,
isLoading: isLoadingProjects,
progress: projectsProgress,
total: totalProjects
} = useProgressiveIFIFProjectsLoader()
const {
operations,
isLoading: isLoadingOperations,
progress: operationsProgress,
total: totalOperations
} = useProgressiveNFTOperationsLoader()
// Main progressive data loader with all IFIF data types
const {
projects: allProjects,
operations: allOperations,
deployments,
claims,
purchases,
refunds,
deposits,
projectsProgress: detailedProjectsProgress,
operationsProgress: detailedOperationsProgress,
isAnyIFIFLoading
} = useProgressiveIFIFDataLoader()
return (
<div className="space-y-6">
<div>
<h3>Active Projects ({totalProjects})</h3>
{/* Progress indicator for projects */}
{isLoadingProjects && (
<div className="mb-4">
<div className="text-sm text-gray-600 mb-2">
Loading projects: {projectsProgress} / {totalProjects}
({detailedProjectsProgress.percentage}%)
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${detailedProjectsProgress.percentage}%` }}
/>
</div>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{projects.map((project) => (
<ProjectCard key={project.projectId} project={project} />
))}
</div>
</div>
<div>
<h3>NFT Operations ({totalOperations})</h3>
{/* Progress indicator for operations */}
{isLoadingOperations && (
<div className="mb-4">
<div className="text-sm text-gray-600 mb-2">
Loading operations: {operationsProgress} / {totalOperations}
({detailedOperationsProgress.percentage}%)
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-green-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${detailedOperationsProgress.percentage}%` }}
/>
</div>
</div>
)}
<div className="space-y-2">
{operations.map((operation) => (
<OperationCard key={operation.id} operation={operation} />
))}
</div>
</div>
{/* Overall loading indicator */}
{isAnyIFIFLoading && (
<div className="fixed bottom-4 right-4 bg-white border border-gray-200 rounded-lg p-4 shadow-lg">
<div className="flex items-center space-x-3">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600" />
<span className="text-sm text-gray-600">Loading IFIF data...</span>
</div>
</div>
)}
</div>
)
}Cached Data Management
import { useCachedIFIFData, useDataCacheStore } from '@/lib/progressive-ifif-hooks'
function ProjectAnalytics({ projectId }: { projectId: number }) {
// Use cached IFIF data from Zustand store
const {
projects,
deployments,
claims,
operations,
purchases,
refunds,
deposits,
isProjectsLoading,
isDeploymentsLoading,
isClaimsLoading,
isOperationsLoading,
isPurchasesLoading,
isRefundsLoading,
isDepositsLoading,
projectsProgress,
deploymentsProgress,
claimsProgress,
operationsProgress,
purchasesProgress,
refundsProgress,
depositsProgress,
isAnyIFIFLoading
} = useCachedIFIFData()
// Access specific cache actions for manual cache management
const updateProgress = useDataCacheStore((state) => state.updateProgress)
const clearCache = useDataCacheStore((state) => state.clearCache)
const isInitialized = useDataCacheStore((state) => state.isInitialized)
// Find specific project data from cached collections
const projectData = projects.find(p => p.projectId === projectId)
const projectOperations = operations.filter(op =>
op.projectAddress === projectData?.id
)
const projectClaims = claims.filter(c =>
c.projectAddress === projectData?.id
)
// Calculate analytics from cached data
const analytics = useMemo(() => {
if (!projectData) return null
const totalRaised = BigInt(projectData.totalPurchase)
const fundingProgress = BigInt(projectData.fundingTarget) > 0n
? Number((totalRaised * 100n) / BigInt(projectData.fundingTarget))
: 0
return {
totalRaised: totalRaised.toString(),
investorCount: purchases.filter(p => p.projectAddress === projectData.id).length,
nftCount: operations.filter(op =>
op.projectAddress === projectData.id && op.operationType === 1
).length,
completionPercent: Math.min(fundingProgress, 100),
recentActivity: operations
.filter(op => op.projectAddress === projectData.id)
.slice(0, 5)
}
}, [projectData, purchases, operations])
if (!isInitialized || isAnyIFIFLoading) {
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3>Project Analytics</h3>
<div className="flex items-center space-x-2">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600" />
<span className="text-sm text-gray-600">Loading data...</span>
</div>
</div>
{/* Progress indicators */}
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span>Projects: {projectsProgress}%</span>
<span>Operations: {operationsProgress}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${(projectsProgress + operationsProgress) / 2}%` }}
/>
</div>
</div>
<LoadingSpinner />
</div>
)
}
if (!analytics) {
return (
<Alert variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertTitle>Project Not Found</AlertTitle>
<AlertDescription>
Project with ID {projectId} was not found in cached data.
</AlertDescription>
</Alert>
)
}
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h3>Project Analytics</h3>
<div className="flex items-center space-x-2">
<Badge variant="outline" className="text-green-600">
Data Cached ({projects.length} projects)
</Badge>
<Button
onClick={clearCache}
variant="outline"
size="sm"
className="text-red-600 hover:text-red-700"
>
Clear Cache
</Button>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<MetricCard
title="Total Raised"
value={formatEther(analytics.totalRaised)}
suffix="ETH"
/>
<MetricCard
title="Investors"
value={analytics.investorCount}
/>
<MetricCard
title="NFTs Minted"
value={analytics.nftCount}
/>
<MetricCard
title="Completion"
value={`${analytics.completionPercent}%`}
/>
</div>
{/* Recent activity from cached data */}
<div className="space-y-2">
<h4 className="text-sm font-medium">Recent Activity</h4>
{analytics.recentActivity.map((activity) => (
<div key={activity.id} className="text-xs text-gray-600 p-2 bg-gray-50 rounded">
Operation {activity.operationType} - {activity.user.slice(0, 8)}...
</div>
))}
</div>
</div>
)
}Project History Tracking
import {
useCachedIFIFData,
useCachedFactoryData
} from '@/lib/progressive-ifif-hooks'
import {
getActivityType,
getActivityUser,
getActivityAmount,
getActivityProjectWithLookup,
getActivityProperty,
type ActivityData
} from '@/lib/activity-utils'
import { getActivityTypeIndicatorColor } from '@/config/constants'
import { useReactTable, getCoreRowModel, getPaginationRowModel, getSortedRowModel, getFilteredRowModel, createColumnHelper, flexRender } from '@tanstack/react-table'
function IFIFActivityTracker() {
// Get all cached IFIF data types
const {
deployments,
claims,
operations,
purchases,
refunds,
deposits,
isOperationsLoading,
isClaimsLoading
} = useCachedIFIFData()
// Get factory data for project name lookup
const { projectLookup } = useCachedFactoryData()
// State for filtering and table management
const [globalFilter, setGlobalFilter] = useState('')
const [activityFilters, setActivityFilters] = useState({
activityType: 'all' as string,
})
const [sorting, setSorting] = useState<SortingState>([])
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
})
// Combine all activities into unified structure
const allActivities = useMemo(() => {
const activities: ActivityData[] = []
if (deployments) activities.push(...deployments.map(d => ({ ...d, type: 'deployment' })))
if (claims) activities.push(...claims.map(c => ({ ...c, type: 'claim' })))
if (operations) activities.push(...operations.map(o => ({ ...o, type: 'operation' })))
if (purchases) activities.push(...purchases.map(p => ({ ...p, type: 'purchase' })))
if (refunds) activities.push(...refunds.map(r => ({ ...r, type: 'refund' })))
if (deposits) activities.push(...deposits.map(d => ({ ...d, type: 'deposit' })))
// Sort by timestamp descending (newest first)
return activities.sort((a, b) => {
const timestampA = typeof a.timestamp === 'string' ? parseFloat(a.timestamp) : Number(a.timestamp)
const timestampB = typeof b.timestamp === 'string' ? parseFloat(b.timestamp) : Number(b.timestamp)
return timestampB - timestampA
})
}, [deployments, claims, operations, purchases, refunds, deposits])
// Create table columns using activity utilities
const columns = useMemo(() => {
const columnHelper = createColumnHelper<ActivityData>()
return [
columnHelper.accessor('type', {
header: 'Activity Type',
cell: ({ row }) => {
const activity = row.original
const activityType = getActivityType(activity)
const typeColor = getActivityTypeIndicatorColor(activityType)
return (
<div className="flex items-center space-x-3">
<div className={`w-3 h-3 ${typeColor} rounded-full`} />
<div>
<p className="font-medium text-slate-900">{activityType}</p>
</div>
</div>
)
}
}),
columnHelper.accessor((row) => getActivityProjectWithLookup(row, projectLookup), {
id: 'project',
header: 'Project',
cell: ({ getValue }) => {
const projectInfo = getValue()
if (!projectInfo) {
return <span className="text-slate-400">-</span>
}
const { id, name, address } = projectInfo
return (
<div>
<p className="font-medium text-slate-900">#{id} - {name}</p>
{address && (
<p className="text-xs text-slate-500 font-mono">{shortenHash(address)}</p>
)}
</div>
)
}
}),
columnHelper.accessor((row) => getActivityUser(row), {
id: 'user',
header: 'User',
cell: ({ getValue }) => {
const user = getValue()
if (!user) {
return <span className="text-slate-400">System</span>
}
return (
<div>
<p className="font-medium text-slate-900">{shortenHash(user)}</p>
<p className="text-xs text-slate-500 font-mono">{user}</p>
</div>
)
}
}),
columnHelper.accessor((row) => getActivityAmount(row), {
id: 'amount',
header: 'Amount',
cell: ({ getValue }) => {
const amount = getValue()
return amount ? (
<Badge variant="outline" className="font-mono">
{Number(amount).toFixed(4)} ETH
</Badge>
) : (
<span className="text-slate-400">-</span>
)
}
}),
columnHelper.accessor('timestamp', {
header: 'Timestamp',
sortingFn: 'basic',
cell: ({ getValue }) => {
const timestamp = getValue()
const timestampValue = typeof timestamp === 'string' ? parseFloat(timestamp) : Number(timestamp)
const date = new Date(timestampValue * 1000)
return (
<div>
<p className="font-medium text-slate-900">
{format(date, 'MMM dd, yyyy')}
</p>
<p className="text-xs text-slate-500">
{format(date, 'HH:mm:ss')}
</p>
</div>
)
}
}),
columnHelper.display({
id: 'actions',
header: 'Actions',
cell: ({ row }) => (
<Button
variant="outline"
size="sm"
onClick={() => {
// Open activity details modal
setSelectedActivity(row.original)
}}
>
<Eye className="h-3 w-3 mr-1" />
View
</Button>
)
})
]
}, [projectLookup])
// Filter activities based on type selection
const filteredActivities = useMemo(() => {
if (activityFilters.activityType === 'all') {
return allActivities
}
return allActivities.filter(activity => activity.type === activityFilters.activityType)
}, [allActivities, activityFilters.activityType])
// Create table instance with comprehensive filtering
const table = useReactTable({
data: filteredActivities,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
globalFilterFn: (row, columnId, value) => {
if (!value) return true
const activity = row.original
const searchValue = String(value).toLowerCase()
const user = getActivityUser(activity)
const tokenAddress = getActivityProperty(activity, 'tokenAddress')
const activityType = getActivityType(activity)
const projectInfo = getActivityProjectWithLookup(activity, projectLookup)
return Boolean(
(user && user.toLowerCase().includes(searchValue)) ||
(activity.id && activity.id.toLowerCase().includes(searchValue)) ||
(typeof tokenAddress === 'string' && tokenAddress.toLowerCase().includes(searchValue)) ||
activityType.toLowerCase().includes(searchValue) ||
(projectInfo && (
(projectInfo.id && projectInfo.id.toString().toLowerCase().includes(searchValue)) ||
(projectInfo.name && projectInfo.name.toLowerCase().includes(searchValue))
))
)
},
onSortingChange: setSorting,
onGlobalFilterChange: setGlobalFilter,
onPaginationChange: setPagination,
state: {
sorting,
globalFilter,
pagination,
},
})
// Activity metrics from filtered data
const metrics = useMemo(() => {
const filteredData = table.getFilteredRowModel().rows.map(row => row.original)
const totalActivities = filteredData.length
const last24h = filteredData.filter(a => {
const now = Date.now() / 1000
const activityTimestamp = typeof a.timestamp === 'string' ? parseFloat(a.timestamp) : Number(a.timestamp)
return now - activityTimestamp <= 86400
}).length
const uniqueUsers = new Set(
filteredData
.map(a => getActivityUser(a))
.filter(u => u !== null)
).size
return {
totalActivities,
last24h,
uniqueUsers
}
}, [table.getFilteredRowModel().rows])
const isLoading = isOperationsLoading || isClaimsLoading
return (
<div className="space-y-6">
{/* Activity Metrics */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-white p-4 border border-gray-200 rounded-lg">
<div className="text-2xl font-bold text-gray-900">{metrics.totalActivities}</div>
<div className="text-sm text-gray-600">Total Activities</div>
</div>
<div className="bg-white p-4 border border-gray-200 rounded-lg">
<div className="text-2xl font-bold text-gray-900">{metrics.last24h}</div>
<div className="text-sm text-gray-600">Last 24 Hours</div>
</div>
<div className="bg-white p-4 border border-gray-200 rounded-lg">
<div className="text-2xl font-bold text-gray-900">{metrics.uniqueUsers}</div>
<div className="text-sm text-gray-600">Unique Users</div>
</div>
</div>
{/* Filters */}
<div className="bg-white border border-gray-200 p-6">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4 flex-1">
<div className="relative flex-1 max-w-md">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<Input
placeholder="Search activities..."
value={table.getState().globalFilter ?? ''}
onChange={(e) => table.setGlobalFilter(e.target.value)}
className="pl-10"
/>
</div>
<Select
value={activityFilters.activityType}
onValueChange={(value) => setActivityFilters(prev => ({ ...prev, activityType: value }))}
>
<SelectTrigger className="w-48">
<SelectValue placeholder="Filter by type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Types</SelectItem>
<SelectItem value="deployment">Token Deployments</SelectItem>
<SelectItem value="claim">Token Claims</SelectItem>
<SelectItem value="operation">NFT Operations</SelectItem>
<SelectItem value="purchase">User Purchases</SelectItem>
<SelectItem value="refund">User Refunds</SelectItem>
<SelectItem value="deposit">Asset Deposits</SelectItem>
</SelectContent>
</Select>
</div>
<div className="text-sm text-gray-600">
{table.getFilteredRowModel().rows.length} activities found
</div>
</div>
</div>
{/* Activities Table */}
<div className="bg-white border border-gray-200">
{isLoading ? (
<div className="p-6">
<div className="space-y-4">
{[...Array(5)].map((_, i) => (
<div key={i} className="flex items-center space-x-4 p-4 animate-pulse">
<div className="w-3 h-3 bg-gray-300 rounded-full" />
<div className="flex-1 space-y-2">
<div className="h-4 bg-gray-300 w-1/4" />
<div className="h-3 bg-gray-200 w-1/2" />
</div>
</div>
))}
</div>
</div>
) : table.getFilteredRowModel().rows.length > 0 ? (
<>
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead
key={header.id}
className={header.column.getCanSort() ? 'cursor-pointer select-none hover:bg-muted/50' : ''}
onClick={header.column.getToggleSortingHandler()}
>
<div className="flex items-center space-x-2">
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
{header.column.getCanSort() && (
<div className="ml-2">
{header.column.getIsSorted() === 'asc' && <ChevronUp className="h-4 w-4" />}
{header.column.getIsSorted() === 'desc' && <ChevronDown className="h-4 w-4" />}
{!header.column.getIsSorted() && <ChevronsUpDown className="h-4 w-4 opacity-50" />}
</div>
)}
</div>
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
{/* Pagination */}
<div className="flex items-center justify-between p-6 border-t border-gray-200">
<div className="text-sm text-gray-600">
Showing {table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1} to{' '}
{Math.min(
(table.getState().pagination.pageIndex + 1) * table.getState().pagination.pageSize,
table.getFilteredRowModel().rows.length
)}{' '}
of {table.getFilteredRowModel().rows.length} results
</div>
<div className="flex space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
</>
) : (
<div className="text-center py-12">
<Activity className="mx-auto h-16 w-16 text-gray-300 mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No activities found</h3>
<p className="text-gray-600">
{table.getState().globalFilter ? 'Try adjusting your search criteria' : 'No activities recorded yet'}
</p>
</div>
)}
</div>
</div>
)
}Project Metrics Dashboard
import {
useProjectDetailData,
useProjectMetrics,
useProjectInvestorData,
useCachedIFIFData,
useCachedFactoryData
} from '@/lib/progressive-ifif-hooks'
import { MetricGrid, BarChartComponent, DonutChartComponent } from '@/components/shared'
// Main Projects Listing Page - Overview Metrics
function IFIFProjectsMetrics() {
const { projects, isProjectsLoading } = useCachedIFIFData()
const { projectLookup } = useCachedFactoryData()
// Calculate aggregate metrics across all projects
const metrics = useMemo(() => {
if (!projects) return {
totalProjects: 0,
activeProjects: 0,
completedProjects: 0,
totalFunding: '0',
successRate: 0,
averageFunding: '0'
}
const totalProjects = projects.length
const activeProjects = projects.filter(p => p.stage === 2 || p.stage === 3).length // PRIVATE_SALE=2, PUBLIC_SALE=3
const completedProjects = projects.filter(p => p.stage === 4).length // SALE_SUCCESSED=4
const totalFunding = projects.reduce((sum, p) => sum + Number(p.totalPurchase || 0), 0)
const successRate = totalProjects > 0 ? (completedProjects / totalProjects) * 100 : 0
const averageFunding = totalProjects > 0 ? totalFunding / totalProjects : 0
return {
totalProjects,
activeProjects,
completedProjects,
totalFunding: Number(formatEther(BigInt(totalFunding))).toFixed(2),
successRate: Math.round(successRate),
averageFunding: Number(formatEther(BigInt(Math.round(averageFunding)))).toFixed(2)
}
}, [projects])
// Chart data for project stage distribution
const stageDistribution = useMemo(() => {
if (!projects) return []
const stageNumbers = [0, 1, 2, 3, 4, 5, 6] // NONE, INIT, PRIVATE_SALE, PUBLIC_SALE, SALE_SUCCESSED, SALE_FAILED, CLAIM
return stageNumbers.map(stageNum => ({
name: PROJECT_STAGES[stageNum]?.name || `Stage ${stageNum}`,
value: projects.filter(p => p.stage === stageNum).length,
color: PROJECT_STAGES[stageNum]?.color || '#64748b'
})).filter(item => item.value > 0)
}, [projects])
// Funding progress distribution
const fundingProgress = useMemo(() => {
if (!projects) return []
const ranges = [
{ name: '0-25%', min: 0, max: 0.25 },
{ name: '25-50%', min: 0.25, max: 0.5 },
{ name: '50-75%', min: 0.5, max: 0.75 },
{ name: '75-100%', min: 0.75, max: 1 },
{ name: '100%+', min: 1, max: Infinity }
]
return ranges.map(range => ({
name: range.name,
value: projects.filter(p => {
const ratio = p.totalPurchase && p.fundingTarget
? Number(p.totalPurchase) / Number(p.fundingTarget)
: 0
return ratio >= range.min && ratio < range.max
}).length
})).filter(item => item.value > 0)
}, [projects])
return (
<div className="space-y-6">
{/* Overview Metrics Grid */}
<MetricGrid
metrics={[
{
key: 'totalProjects',
value: metrics.totalProjects,
subtitle: 'Total Projects'
},
{
key: 'activeProjects',
value: metrics.activeProjects,
subtitle: 'Active Projects'
},
{
key: 'completedProjects',
value: metrics.completedProjects,
subtitle: 'Completed Projects'
},
{
key: 'totalFunding',
value: `${metrics.totalFunding}`,
subtitle: 'Total Funding'
}
]}
isLoading={isProjectsLoading}
/>
{/* Charts Section */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-white p-6 border border-slate-200">
<h3 className="text-lg font-semibold mb-4">Project Stage Distribution</h3>
<DonutChartComponent
data={stageDistribution}
category="value"
dataKey="name"
showLabel={true}
valueFormatter={(value) => `${value} project${value !== 1 ? 's' : ''}`}
/>
</div>
<div className="bg-white p-6 border border-slate-200">
<h3 className="text-lg font-semibold mb-4">Funding Progress Distribution</h3>
<BarChartComponent
data={fundingProgress}
dataKey="name"
categories={['value']}
/>
</div>
</div>
</div>
)
}
// Individual Project Detail Page - Comprehensive Dashboard
function ProjectDetailDashboard({ projectId }: { projectId: number }) {
// Get comprehensive project details and metrics
const { project, overview, isLoading, error } = useProjectDetailData(projectId)
const metrics = useProjectMetrics(projectId)
const { projectLookup } = useCachedFactoryData()
if (isLoading) {
return (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-6">
<div className="bg-white border border-slate-200 space-y-4">
<div className="p-6">
<div className="space-y-4">
<div className="h-6 w-48 bg-muted rounded animate-pulse" />
<div className="h-4 w-full bg-muted rounded animate-pulse" />
<div className="h-4 w-3/4 bg-muted rounded animate-pulse" />
</div>
</div>
</div>
</div>
</div>
)
}
if (error || !project || !overview) {
return (
<Alert variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertTitle>Failed to Load Project Data</AlertTitle>
<AlertDescription>
Project with ID {projectId} could not be loaded.
</AlertDescription>
</Alert>
)
}
const projectInfo = projectLookup.get(overview.projectId.toString())
return (
<div className="space-y-6">
{/* Header with project identification */}
<div className="space-y-2">
<h1 className="text-4xl font-bold tracking-tight text-slate-900 flex items-center gap-3">
<TrendingUp className="h-8 w-8" />
#{overview.projectId}{projectInfo?.name ? ` - ${projectInfo.name}` : ''}
</h1>
<div className="flex items-center gap-4">
<StageBadge stage={overview.stage} />
<ExplorerLink
hash={overview.projectAddress}
type="address"
variant="button"
className="flex items-center gap-2"
>
<ExternalLink className="h-4 w-4" />
View on Explorer
</ExplorerLink>
</div>
</div>
{/* Key Metrics Grid */}
<MetricGrid
metrics={[
{
key: 'users' as const,
value: overview.investorCount,
subtitle: 'Total investors'
},
{
key: 'totalFunding' as const,
value: `${overview.fundingProgress.toFixed(1)}%`,
subtitle: 'Funding progress'
},
{
key: 'activities' as const,
value: overview.activeNFTCount,
subtitle: 'Active NFTs'
},
{
key: 'last24h' as const,
value: overview.totalTransactions,
subtitle: 'Total transactions'
}
]}
isLoading={isLoading}
columns={4}
/>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main Project Status Card */}
<div className="lg:col-span-2">
<div className="bg-white border border-slate-200 space-y-6 p-6">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold">Project Status</h2>
<StageBadge stage={overview.stage} />
</div>
{/* Funding Progress */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="font-semibold flex items-center gap-2">
<DollarSign className="h-4 w-4" />
Funding Progress
</h3>
<span className="text-sm text-muted-foreground">
{overview.fundingProgress.toFixed(1)}% complete
</span>
</div>
<Progress value={overview.fundingProgress} className="h-3" />
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">
Raised: {Number(formatEther(overview.totalPurchase)).toFixed(2)} ETH
</span>
<span className="text-muted-foreground">
Target: {Number(formatEther(overview.fundingTarget)).toFixed(2)} ETH
</span>
</div>
{overview.isFullyFunded && (
<Badge variant="outline" className="w-full justify-center bg-green-50 text-green-700 border-green-200">
<Target className="h-3 w-3 mr-1" />
Funding Target Reached
</Badge>
)}
</div>
{/* Project Details Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-4 border-t">
<div className="space-y-2">
<label className="text-sm font-medium text-muted-foreground">Created</label>
<div className="flex items-center gap-2">
<Calendar className="h-3 w-3 text-muted-foreground" />
<span className="text-sm">
{format(new Date(Number(overview.createdAt) * 1000), 'MMM dd, yyyy')}
</span>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-muted-foreground">Last Updated</label>
<div className="flex items-center gap-2">
<Clock className="h-3 w-3 text-muted-foreground" />
<span className="text-sm">
{format(new Date(Number(overview.lastUpdated) * 1000), 'MMM dd, yyyy')}
</span>
</div>
</div>
</div>
</div>
</div>
{/* Sidebar with Quick Stats and Status */}
<div className="space-y-6">
{/* Quick Stats */}
<div className="bg-white border border-slate-200 space-y-4">
<div className="p-6 pb-0">
<h2 className="text-lg font-semibold">Quick Stats</h2>
</div>
<div className="px-6 pb-6 space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="text-center space-y-1">
<div className="flex items-center justify-center">
<Users className="h-4 w-4 text-blue-500 mr-1" />
<span className="text-2xl font-bold">{overview.investorCount}</span>
</div>
<p className="text-xs text-muted-foreground">Investors</p>
</div>
<div className="text-center space-y-1">
<div className="flex items-center justify-center">
<Coins className="h-4 w-4 text-purple-500 mr-1" />
<span className="text-2xl font-bold">{overview.activeNFTCount}</span>
</div>
<p className="text-xs text-muted-foreground">Active NFTs</p>
</div>
<div className="text-center space-y-1">
<div className="flex items-center justify-center">
<Zap className="h-4 w-4 text-orange-500 mr-1" />
<span className="text-2xl font-bold">{overview.totalTransactions}</span>
</div>
<p className="text-xs text-muted-foreground">Transactions</p>
</div>
<div className="text-center space-y-1">
<div className="flex items-center justify-center">
<BarChart3 className="h-4 w-4 text-green-500 mr-1" />
<span className="text-2xl font-bold">{overview.recentStageChanges}</span>
</div>
<p className="text-xs text-muted-foreground">Stage Changes</p>
</div>
</div>
</div>
</div>
{/* Status Indicators */}
<div className="bg-white border border-slate-200 space-y-4">
<div className="p-6 pb-0">
<h2 className="text-lg font-semibold">Status Indicators</h2>
</div>
<div className="px-6 pb-6 space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm">Funding Status</span>
<Badge variant={overview.isFullyFunded ? "default" : "secondary"}>
{overview.isFullyFunded ? "Complete" : "In Progress"}
</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-sm">Project Activity</span>
<Badge variant={overview.isActive ? "default" : "secondary"}>
{overview.isActive ? "Active" : "Inactive"}
</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-sm">Token Deployment</span>
<Badge variant={overview.hasTokenDeployment ? "default" : "secondary"}>
{overview.hasTokenDeployment ? "Deployed" : "Pending"}
</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-sm">Recent Activity</span>
<Badge variant={overview.hasRecentActivity ? "default" : "secondary"}>
{overview.hasRecentActivity ? "Active" : "Quiet"}
</Badge>
</div>
</div>
</div>
</div>
</div>
</div>
)
}
// Project Investor Dashboard - Investment-focused Metrics
function ProjectInvestorMetrics({ projectId }: { projectId: string }) {
const { project } = useProjectDetailData(parseInt(projectId) || 0)
const {
investors,
purchases,
refunds,
claims,
isLoading
} = useProjectInvestorData(project?.id || '')
// Calculate investment-specific metrics
const investorMetrics = useMemo(() => {
const totalInvestors = investors.length
const totalInvested = purchases.reduce((sum, p) => sum + BigInt(p.amount), BigInt(0))
const totalRefunded = refunds.reduce((sum, r) => sum + BigInt(r.amount), BigInt(0))
const totalClaimed = claims.reduce((sum, c) => sum + BigInt(c.amount), BigInt(0))
const netInvestment = totalInvested - totalRefunded
return [
{
key: 'users' as const,
value: totalInvestors,
subtitle: 'Total investors'
},
{
key: 'totalFunding' as const,
value: parseFloat(formatEther(totalInvested)).toFixed(2),
subtitle: 'Total invested'
},
{
key: 'activeProjects' as const,
value: parseFloat(formatEther(netInvestment)).toFixed(2),
subtitle: 'Net investment'
},
{
key: 'activities' as const,
value: parseFloat(formatEther(totalClaimed)).toFixed(2),
subtitle: 'Total claimed'
}
]
}, [investors, purchases, refunds, claims])
return (
<div className="space-y-6">
{/* Investment Metrics */}
<MetricGrid
metrics={investorMetrics}
isLoading={isLoading}
columns={4}
/>
{/* Investment Status Distribution */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-white p-6 border border-slate-200">
<h3 className="text-lg font-semibold mb-4">Investment Status Distribution</h3>
<DonutChartComponent
data={[
{ name: 'Active', value: investors.filter(i => i.investmentStatus === 'active').length },
{ name: 'Refunded', value: investors.filter(i => i.investmentStatus === 'refunded').length },
{ name: 'Claimed', value: investors.filter(i => i.investmentStatus === 'claimed').length }
].filter(item => item.value > 0)}
category="value"
dataKey="name"
showLabel={true}
valueFormatter={(value) => `${value} investor${value !== 1 ? 's' : ''}`}
/>
</div>
<div className="bg-white p-6 border border-slate-200">
<h3 className="text-lg font-semibold mb-4">Transaction Timeline</h3>
<BarChartComponent
data={[
{ name: 'Purchases', value: purchases.length },
{ name: 'Refunds', value: refunds.length },
{ name: 'Claims', value: claims.length }
]}
dataKey="name"
categories={['value']}
/>
</div>
</div>
</div>
)
}User Interface Patterns
Project Management Modals
The IFIF system includes comprehensive modal components for project management operations:
import {
DeployProjectModal,
UpdateDeploymentDetailsModal,
UpdateProjectConfigModal,
StartPrivateSaleModal,
StartPublicSaleModal,
EndSalesModal,
PublicEndSalesModal
} from '@/components/ifif-project-management-modals'
import {
useSignatureHelper,
FloatingSignatureHelper
} from '@/lib/signature-store'
function ProjectManagementInterface({
projectAddress,
projectId,
projectName,
currentConfig
}: {
projectAddress?: Address
projectId?: number
projectName?: string
currentConfig?: {
projectOwner: Address
distributor: Address
distributorFeePercent: number
projectOwnerFeePercent: number
platformFeePercent: number
pairPrice: bigint
}
}) {
const [activeModal, setActiveModal] = useState<string | null>(null)
// Signature helper for pre-signed configurations
const { openModal: openSignatureModal } = useSignatureHelper()
return (
<div className="space-y-6">
{/* Action Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4">
<Button
onClick={() => setActiveModal('deploy')}
className="h-24 flex flex-col items-center justify-center"
>
<FileText className="h-6 w-6 mb-2" />
Deploy Project
</Button>
<Button
onClick={() => setActiveModal('updateDetails')}
className="h-24 flex flex-col items-center justify-center"
variant="outline"
>
<Edit3 className="h-6 w-6 mb-2" />
Update Details
</Button>
<Button
onClick={() => setActiveModal('configure')}
className="h-24 flex flex-col items-center justify-center"
variant="outline"
>
<Settings className="h-6 w-6 mb-2" />
Update Config
</Button>
<Button
onClick={openSignatureModal}
className="h-24 flex flex-col items-center justify-center"
variant="outline"
>
<PenTool className="h-6 w-6 mb-2" />
Signature Helper
</Button>
</div>
{/* Sale Management Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Button
onClick={() => setActiveModal('privateSale')}
className="h-20 flex flex-col items-center justify-center"
variant="outline"
>
<Users className="h-5 w-5 mb-1" />
Start Private Sale
</Button>
<Button
onClick={() => setActiveModal('publicSale')}
className="h-20 flex flex-col items-center justify-center"
variant="outline"
>
<Globe className="h-5 w-5 mb-1" />
Start Public Sale
</Button>
<Button
onClick={() => setActiveModal('endSales')}
className="h-20 flex flex-col items-center justify-center"
variant="outline"
>
<Square className="h-5 w-5 mb-1" />
End Sales
</Button>
<Button
onClick={() => setActiveModal('publicEnd')}
className="h-20 flex flex-col items-center justify-center"
variant="outline"
>
<Clock className="h-5 w-5 mb-1" />
Emergency End
</Button>
</div>
{/* Deploy Project Modal */}
<DeployProjectModal
isOpen={activeModal === 'deploy'}
onClose={() => setActiveModal(null)}
onSuccess={() => {
console.log('Project deployed successfully')
setActiveModal(null)
}}
/>
{/* Update Deployment Details Modal */}
<UpdateDeploymentDetailsModal
isOpen={activeModal === 'updateDetails'}
onClose={() => setActiveModal(null)}
projectId={projectId}
currentName={projectName}
currentSymbol="" // Would need to be provided from props
onSuccess={() => {
console.log('Project details updated')
setActiveModal(null)
}}
/>
{/* Update Project Config Modal */}
<UpdateProjectConfigModal
isOpen={activeModal === 'configure'}
onClose={() => setActiveModal(null)}
projectAddress={projectAddress}
projectName={projectName}
currentConfig={currentConfig}
onSuccess={() => {
console.log('Project config updated')
setActiveModal(null)
}}
/>
{/* Start Private Sale Modal */}
<StartPrivateSaleModal
isOpen={activeModal === 'privateSale'}
onClose={() => setActiveModal(null)}
projectAddress={projectAddress}
projectName={projectName}
privateSaleTime={7 * 24 * 60 * 60} // 7 days in seconds
onSuccess={() => {
console.log('Private sale started')
setActiveModal(null)
}}
/>
{/* Start Public Sale Modal */}
<StartPublicSaleModal
isOpen={activeModal === 'publicSale'}
onClose={() => setActiveModal(null)}
projectAddress={projectAddress}
projectName={projectName}
publicSaleTime={14 * 24 * 60 * 60} // 14 days in seconds
onSuccess={() => {
console.log('Public sale started')
setActiveModal(null)
}}
/>
{/* End Sales Modal */}
<EndSalesModal
isOpen={activeModal === 'endSales'}
onClose={() => setActiveModal(null)}
projectAddress={projectAddress}
projectName={projectName}
onSuccess={() => {
console.log('Sales ended')
setActiveModal(null)
}}
/>
{/* Public End Sales Modal */}
<PublicEndSalesModal
isOpen={activeModal === 'publicEnd'}
onClose={() => setActiveModal(null)}
projectAddress={projectAddress}
projectName={projectName}
canPublicEnd={true} // Would need logic to determine this
onSuccess={() => {
console.log('Sales ended via emergency mechanism')
setActiveModal(null)
}}
/>
{/* Floating Signature Helper */}
<FloatingSignatureHelper />
</div>
)
}Client Investment Interface
import {
PurchaseModal,
RefundModal,
ClaimModal,
ClaimNFTModal,
DepositModal
} from '@/components/ifif-project-client-modals'
import { useProjectDetailData } from '@/lib/progressive-ifif-hooks'
import { useUserProfileData } from '@/lib/progressive-user-hooks'
import { useCachedIFIFData } from '@/lib/progressive-ifif-hooks'
import { useAccount } from 'wagmi'
function ClientInvestmentInterface({
projectId,
projectAddress
}: {
projectId: number
projectAddress?: Address
}) {
const [modals, setModals] = useState({
purchase: false,
refund: false,
claim: false,
claimNFT: false,
deposit: false
})
// Get wallet connection status
const { isConnected: walletConnected, address } = useAccount()
// Get project details and overview data
const { project, overview, isLoading } = useProjectDetailData(projectId)
// Get user profile data for investment tracking
const { profile: userProfile, investments: userInvestments } = useUserProfileData(address || '')
// Get cached IFIF data for purchase/refund/claim tracking
const { purchases, refunds, claims } = useCachedIFIFData()
// Calculate user-specific investment data for this project
const userData = useMemo(() => {
if (!address || !overview?.projectAddress) {
return {
hasInvestment: false,
canRefund: false,
canClaim: false,
canClaimNFT: false,
netInvestment: 0,
userInvestment: null
}
}
// Find user investment for this specific project
const projectInvestment = userInvestments.find(
inv => inv.projectAddress.toLowerCase() === overview.projectAddress.toLowerCase()
)
if (!projectInvestment) {
return {
hasInvestment: false,
canRefund: false,
canClaim: false,
canClaimNFT: false,
netInvestment: 0,
userInvestment: null
}
}
// Calculate net investment (total invested - refunded)
const totalInvested = Number(formatEther(BigInt(projectInvestment.totalInvested)))
const totalRefunded = Number(formatEther(BigInt(projectInvestment.totalRefunded)))
const netInvestment = totalInvested - totalRefunded
// Determine what actions are available
const hasInvestment = netInvestment > 0
const canRefund = hasInvestment && overview.stage === 5 // SALE_FAILED
const canClaim = hasInvestment && overview.stage === 6 && !projectInvestment.tokenAllocation // CLAIM stage
const canClaimNFT = hasInvestment && overview.stage === 4 // SALE_SUCCESSED
return {
hasInvestment,
canRefund,
canClaim,
canClaimNFT,
netInvestment,
userInvestment: projectInvestment
}
}, [address, overview, userInvestments])
const openModal = (modalName: keyof typeof modals) => {
setModals(prev => ({ ...prev, [modalName]: true }))
}
const closeModal = (modalName: keyof typeof modals) => {
setModals(prev => ({ ...prev, [modalName]: false }))
}
if (isLoading) {
return (
<div className="bg-white border border-slate-200 space-y-4">
<div className="p-6 pb-0">
<h2 className="text-lg font-semibold">Client Actions</h2>
</div>
<div className="px-6 pb-6 space-y-3">
{[...Array(5)].map((_, i) => (
<div key={i} className="h-12 bg-slate-200 rounded animate-pulse" />
))}
</div>
</div>
)
}
return (
<div className="bg-white border border-slate-200 space-y-4">
<div className="p-6 pb-0">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold flex items-center gap-2">
<Wallet className="h-5 w-5" />
Client Actions
</h2>
{walletConnected && (
<Badge variant="default" className="bg-blue-100 text-blue-800 border-blue-200">
Connected
</Badge>
)}
</div>
{/* User Investment Summary */}
{userData.hasInvestment && (
<div className="mt-4 p-3 bg-slate-50 rounded-lg">
<div className="flex items-center justify-between">
<span className="text-sm text-slate-600">Your Investment:</span>
<span className="font-medium">
{userData.netInvestment.toFixed(4)} ETH
</span>
</div>
</div>
)}
</div>
<div className="px-6 pb-6 space-y-3">
{/* Purchase Action */}
<Button
className="w-full justify-start"
variant="outline"
disabled={!walletConnected || !overview || (overview.stage !== 2 && overview.stage !== 3)}
onClick={() => openModal('purchase')}
>
<ShoppingCart className="h-4 w-4 mr-2" />
Purchase Tokens
{overview?.stage === 2 && (
<Badge variant="outline" className="ml-auto bg-purple-50 text-purple-700 border-purple-200">
Private Sale
</Badge>
)}
{overview?.stage === 3 && (
<Badge variant="outline" className="ml-auto bg-blue-50 text-blue-700 border-blue-200">
Public Sale
</Badge>
)}
</Button>
{/* Refund Action */}
<Button
className="w-full justify-start"
variant="outline"
disabled={!walletConnected || !userData.canRefund}
onClick={() => openModal('refund')}
>
<RefreshCw className="h-4 w-4 mr-2" />
Request Refund
{userData.canRefund && (
<Badge variant="outline" className="ml-auto bg-red-50 text-red-700 border-red-200">
Available
</Badge>
)}
</Button>
{/* Claim NFT Action */}
<Button
className="w-full justify-start"
variant="outline"
disabled={!walletConnected || !userData.canClaimNFT}
onClick={() => openModal('claimNFT')}
>
<Coins className="h-4 w-4 mr-2" />
Claim Investment NFT
{userData.canClaimNFT && (
<Badge variant="outline" className="ml-auto bg-green-50 text-green-700 border-green-200">
Available
</Badge>
)}
</Button>
{/* Direct Claim Action */}
<Button
className="w-full justify-start"
variant="outline"
disabled={!walletConnected || !userData.canClaim}
onClick={() => openModal('claim')}
>
<Gift className="h-4 w-4 mr-2" />
Claim Tokens (Direct)
{userData.canClaim && (
<Badge variant="outline" className="ml-auto bg-green-50 text-green-700 border-green-200">
Available
</Badge>
)}
</Button>
{/* Deposit Action for Distributors */}
<Button
className="w-full justify-start"
variant="outline"
disabled={!walletConnected || !overview || overview.stage !== 4}
onClick={() => openModal('deposit')}
>
<TrendingDown className="h-4 w-4 mr-2" />
Deposit Tokens (Distributor)
</Button>
</div>
{/* Purchase Modal */}
<PurchaseModal
isOpen={modals.purchase}
onClose={() => closeModal('purchase')}
projectAddress={overview?.projectAddress}
projectId={projectId}
fundTokenAddress="0x..." // Would be provided from project config
merkleProof={[]} // Would be calculated from whitelist
isVIP={false} // Would be determined from user status
onSuccess={(txHash) => {
console.log('Purchase successful:', txHash)
closeModal('purchase')
}}
onApprovalSuccess={(txHash) => {
console.log('Token approval successful:', txHash)
}}
/>
{/* Refund Modal */}
<RefundModal
isOpen={modals.refund}
onClose={() => closeModal('refund')}
projectAddress={overview?.projectAddress}
projectId={projectId}
fundTokenAddress="0x..." // Would be provided from project config
onSuccess={(txHash) => {
console.log('Refund processed:', txHash)
closeModal('refund')
}}
/>
{/* Claim Modal */}
<ClaimModal
isOpen={modals.claim}
onClose={() => closeModal('claim')}
projectAddress={overview?.projectAddress}
fundTokenAddress="0x..." // Would be provided from project config
onSuccess={(txHash) => {
console.log('Tokens claimed:', txHash)
closeModal('claim')
}}
/>
{/* Claim NFT Modal */}
<ClaimNFTModal
isOpen={modals.claimNFT}
onClose={() => closeModal('claimNFT')}
projectAddress={overview?.projectAddress}
onSuccess={(txHash) => {
console.log('NFT claimed:', txHash)
closeModal('claimNFT')
}}
/>
{/* Deposit Modal */}
<DepositModal
isOpen={modals.deposit}
onClose={() => closeModal('deposit')}
projectAddress={overview?.projectAddress}
fundTokenAddress="0x..." // Would be provided from project config
onSuccess={(txHash) => {
console.log('Deposit successful:', txHash)
closeModal('deposit')
}}
onApprovalSuccess={(txHash) => {
console.log('Token approval successful:', txHash)
}}
/>
</div>
)
}NFT Operations Interface
import {
SplitNFTModal,
MergeNFTModal,
ConvertNFTModal
} from '@/components/ifif-nft-action-modals'
import { useProjectNFTData, useProjectDetailData } from '@/lib/progressive-ifif-hooks'
import { useAccount } from 'wagmi'
import { useSplitNFT, useMergeNFT, useConvertNFT } from '@/lib/ifif-nft-management-hooks'
function NFTOperationsInterface({
projectId,
selectedNFT,
userAddress
}: {
projectId: string
selectedNFT?: any // NFT allocation data
userAddress?: Address
}) {
const [modals, setModals] = useState({
split: false,
merge: false,
convert: false
})
// Get wallet connection status
const { address: connectedWallet } = useAccount()
// Get project details for stage checking
const { project } = useProjectDetailData(parseInt(projectId) || 0)
// Get project NFT data for available NFTs
const { allocations, operations, isLoading } = useProjectNFTData(project?.id || '')
// Get NFT operation hooks for transaction management
const { splitNFT, isLoading: isSplitting } = useSplitNFT()
const { mergeNFT, isLoading: isMerging } = useMergeNFT()
const { convertNFT, isLoading: isConverting } = useConvertNFT()
// Calculate operation availability based on NFT and project state
const operationStates = useMemo(() => {
if (!selectedNFT || !connectedWallet || !project?.overview) {
return {
canSplit: false,
canMerge: false,
canConvert: false,
isOwner: false
}
}
const isOwner = connectedWallet.toLowerCase() === selectedNFT.owner?.toLowerCase()
const isStageValid = project.overview.stage === 4 // SALE_SUCCESSED
const isNFTActive = selectedNFT.isActive
// Get user's NFTs for merge operations
const userNFTs = allocations.filter(nft =>
nft.owner?.toLowerCase() === connectedWallet.toLowerCase() &&
nft.isActive &&
nft.id !== selectedNFT.id
)
return {
canSplit: isOwner && isNFTActive && isStageValid,
canMerge: isOwner && isStageValid && userNFTs.length > 0,
canConvert: isOwner && isNFTActive && isStageValid,
isOwner,
availableForMerge: userNFTs
}
}, [selectedNFT, connectedWallet, project?.overview, allocations])
const openModal = (modalName: keyof typeof modals) => {
setModals(prev => ({ ...prev, [modalName]: true }))
}
const closeModal = (modalName: keyof typeof modals) => {
setModals(prev => ({ ...prev, [modalName]: false }))
}
if (!selectedNFT) {
return (
<div className="bg-white border border-slate-200 p-6 space-y-6">
<div>
<h3 className="text-lg font-semibold text-slate-900 mb-2">NFT Operations</h3>
<p className="text-sm text-slate-600">
Select an NFT to perform operations
</p>
</div>
</div>
)
}
return (
<div className="bg-white border border-slate-200 space-y-4">
<div className="p-6 pb-0">
<h3 className="text-lg font-semibold text-slate-900 mb-2">NFT Operations</h3>
<div className="space-y-1">
<p className="text-sm text-slate-600">
Token #{selectedNFT.tokenId}
</p>
<p className="text-sm text-slate-600">
Weight: {formatEther(selectedNFT.weight || 0)} units
</p>
<div className="flex items-center gap-2">
<Badge variant={selectedNFT.isActive ? "default" : "outline"}>
{selectedNFT.isActive ? "Active" : "Inactive"}
</Badge>
{operationStates.isOwner && (
<Badge variant="outline" className="bg-blue-50 text-blue-700 border-blue-200">
Owner
</Badge>
)}
</div>
</div>
</div>
<div className="px-6 pb-6 space-y-3">
{/* Split NFT Action */}
<Button
className="w-full justify-start"
variant="outline"
disabled={!operationStates.canSplit || isSplitting}
onClick={() => openModal('split')}
>
<Scissors className="h-4 w-4 mr-2" />
Split NFT
{operationStates.canSplit && (
<Badge variant="outline" className="ml-auto bg-green-50 text-green-700 border-green-200">
Available
</Badge>
)}
</Button>
{/* Merge NFT Action */}
<Button
className="w-full justify-start"
variant="outline"
disabled={!operationStates.canMerge || isMerging}
onClick={() => openModal('merge')}
>
<MergeIcon className="h-4 w-4 mr-2" />
Merge NFTs
{operationStates.canMerge && (
<Badge variant="outline" className="ml-auto bg-green-50 text-green-700 border-green-200">
{operationStates.availableForMerge?.length} available
</Badge>
)}
</Button>
{/* Convert NFT Action */}
<Button
className="w-full justify-start"
variant="outline"
disabled={!operationStates.canConvert || isConverting}
onClick={() => openModal('convert')}
>
<ArrowRightLeft className="h-4 w-4 mr-2" />
Convert to Tokens
{operationStates.canConvert && (
<Badge variant="outline" className="ml-auto bg-green-50 text-green-700 border-green-200">
Available
</Badge>
)}
</Button>
{/* Operation Status Info */}
{!operationStates.isOwner && connectedWallet && (
<Alert className="mt-4">
<Info className="h-4 w-4" />
<AlertDescription>
You can only perform operations on NFTs you own.
</AlertDescription>
</Alert>
)}
{!connectedWallet && (
<Alert className="mt-4">
<Wallet className="h-4 w-4" />
<AlertDescription>
Connect your wallet to perform NFT operations.
</AlertDescription>
</Alert>
)}
</div>
{/* Split NFT Modal */}
<SplitNFTModal
isOpen={modals.split}
onClose={() => closeModal('split')}
nftId={selectedNFT.id}
projectAddress={project?.projectAddress}
currentWeight={selectedNFT.weight}
currentStage={project?.overview?.stage}
nftOwner={selectedNFT.owner}
onSuccess={() => {
console.log('NFT split successful')
closeModal('split')
}}
/>
{/* Merge NFT Modal */}
<MergeNFTModal
isOpen={modals.merge}
onClose={() => closeModal('merge')}
availableNFTs={operationStates.availableForMerge?.map(nft => ({
id: nft.id,
tokenId: nft.tokenId,
weight: nft.weight,
isActive: nft.isActive
})) || []}
projectAddress={project?.projectAddress}
currentStage={project?.overview?.stage}
nftOwner={selectedNFT.owner}
onSuccess={() => {
console.log('NFTs merged successfully')
closeModal('merge')
}}
/>
{/* Convert NFT Modal */}
<ConvertNFTModal
isOpen={modals.convert}
onClose={() => closeModal('convert')}
nftId={selectedNFT.id}
projectAddress={project?.projectAddress}
nftWeight={selectedNFT.weight}
currentStage={project?.overview?.stage}
nftOwner={selectedNFT.owner}
onSuccess={() => {
console.log('NFT converted successfully')
closeModal('convert')
}}
/>
</div>
)
}
// Example usage in project NFT page context
function ProjectNFTPageIntegration({ projectId }: { projectId: string }) {
const [selectedNFT, setSelectedNFT] = useState<any>(null)
// Get project NFT data
const { allocations, operations, isLoading } = useProjectNFTData(projectId)
// NFT selection handler for table row clicks
const handleNFTSelection = (nft: any) => {
setSelectedNFT(nft)
}
return (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* NFT List/Table */}
<div className="lg:col-span-2">
{/* NFT allocation table would go here */}
<div className="bg-white border border-slate-200">
{/* Table implementation with row selection */}
{allocations.map(nft => (
<div
key={nft.id}
onClick={() => handleNFTSelection(nft)}
className="p-4 border-b cursor-pointer hover:bg-slate-50"
>
NFT #{nft.tokenId} - Weight: {formatEther(nft.weight)}
</div>
))}
</div>
</div>
{/* NFT Operations Panel */}
<div>
<NFTOperationsInterface
projectId={projectId}
selectedNFT={selectedNFT}
/>
</div>
</div>
)
}Error Handling
Comprehensive Error Management
import { useState, useCallback } from 'react'
import { useWriteContract } from 'wagmi'
import { Address, parseUnits } from 'viem'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { AlertTriangle } from 'lucide-react'
import { TransactionInfo } from '@/components/ui/transaction-info'
import { useSettingsStore } from '@/lib/settings-store'
import { useTransactionQueue } from '@/lib/transaction-queue-store'
// Real error state interface used across IFIF hooks
interface ProjectOperationState {
isLoading: boolean
isSuccess: boolean
isError: boolean
error: string | null
txHash: string | null
}
function IFIFProjectManagement({ projectAddress }: { projectAddress: Address }) {
const [state, setState] = useState<ProjectOperationState>({
isLoading: false,
isSuccess: false,
isError: false,
error: null,
txHash: null
})
const { writeContractAsync } = useWriteContract()
const { config } = useSettingsStore()
const { addTransaction, updateTransaction } = useTransactionQueue()
const handlePurchase = useCallback(async (amount: string) => {
let transactionId: string | undefined
try {
setState({
isLoading: true,
isSuccess: false,
isError: false,
error: null,
txHash: null
})
// Validate contract configuration (real pattern)
if (!config?.contracts?.["IFIF"]) {
throw new Error('IFIF contract not configured. Please check your connection settings.')
}
const ififContract = config.contracts["IFIF"]
if (!ififContract.abi) {
throw new Error('IFIF contract ABI not available.')
}
// Execute transaction with error handling
const hash = await writeContractAsync({
address: projectAddress,
abi: ififContract.abi,
functionName: 'purchase',
args: [parseUnits(amount, 18), '0x123...', []]
})
// Add to transaction queue (real pattern)
transactionId = addTransaction({
hash,
type: 'Purchase Investment',
description: `Purchase ${amount} tokens in project`,
status: 'pending'
})
setState({
isLoading: false,
isSuccess: true,
isError: false,
error: null,
txHash: hash
})
return hash
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Purchase failed'
// Update transaction with error if it was added
if (transactionId) {
updateTransaction(transactionId, {
status: 'failed',
error: errorMessage
})
}
setState({
isLoading: false,
isSuccess: false,
isError: true,
error: errorMessage,
txHash: null
})
throw error
}
}, [writeContractAsync, config, addTransaction, updateTransaction, projectAddress])
const reset = useCallback(() => {
setState({
isLoading: false,
isSuccess: false,
isError: false,
error: null,
txHash: null
})
}, [])
return (
<div className="space-y-6">
{/* Contract Configuration Error */}
{!config?.contracts?.["IFIF"] && (
<Alert className="border-yellow-200 bg-yellow-50">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
<strong>Configuration Required:</strong> IFIF contract not configured.
Please check your connection settings.
</AlertDescription>
</Alert>
)}
{/* Real TransactionInfo component for comprehensive status display */}
<TransactionInfo
isLoading={state.isLoading}
isSuccess={state.isSuccess}
isError={state.isError}
error={state.error}
txHash={state.txHash}
loadingText="Processing investment..."
successText="Investment submitted successfully!"
/>
{/* Operation buttons with real state management */}
<div className="flex gap-3">
<Button
onClick={() => handlePurchase('100')}
disabled={state.isLoading || !config?.contracts?.["IFIF"]}
>
{state.isLoading ? 'Processing...' : 'Invest 100 USDC'}
</Button>
{(state.isError || state.isSuccess) && (
<Button
variant="outline"
onClick={reset}
>
Reset
</Button>
)}
</div>
</div>
)
}Real Error Categorization and Recovery
import { useState, useCallback } from 'react'
import { usePurchase, useUpdateProjectConfig } from '@/lib/ifif-project-management-hooks'
import { useAccount } from 'wagmi'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { AlertTriangle } from 'lucide-react'
import { TransactionInfo } from '@/components/ui/transaction-info'
// Real error categorization based on actual hook patterns
function ProjectOperationManager({ projectAddress }: { projectAddress: Address }) {
const { address: userAddress } = useAccount()
// Real hook with comprehensive error handling
const {
purchase,
isLoading: isPurchasing,
isSuccess: purchaseSuccess,
isError: purchaseError,
error: purchaseErrorMessage,
txHash: purchaseTxHash,
reset: resetPurchase
} = usePurchase()
const {
updateProjectConfig,
isLoading: isUpdating,
isSuccess: updateSuccess,
isError: updateError,
error: updateErrorMessage,
txHash: updateTxHash,
reset: resetUpdate
} = useUpdateProjectConfig()
// Real error categorization patterns from actual hooks
const [approvalError, setApprovalError] = useState<string | null>(null)
const [configError, setConfigError] = useState<string | null>(null)
const [walletError, setWalletError] = useState<string | null>(null)
const handlePurchase = useCallback(async (amount: bigint) => {
try {
// Clear previous errors
setApprovalError(null)
setConfigError(null)
setWalletError(null)
await purchase({
projectAddress,
amount,
fundTokenAddress: '0x123...' as Address,
merkleProof: []
})
} catch (error) {
// Real error categorization pattern from actual hooks
const errorMessage = error instanceof Error ? error.message : 'Transaction failed'
// Contract configuration errors (from actual hooks)
if (errorMessage.includes('contract not configured') ||
errorMessage.includes('connection settings')) {
setConfigError(errorMessage)
}
// Wallet connection errors (from actual hooks)
else if (errorMessage.includes('connect your wallet') ||
errorMessage.includes('Please connect your wallet')) {
setWalletError(errorMessage)
}
// Approval/allowance errors (from NFT modals pattern)
else if (errorMessage.toLowerCase().includes('approve') ||
errorMessage.toLowerCase().includes('allowance')) {
setApprovalError(errorMessage)
} else {
console.error('Failed to purchase tokens:', error)
}
}
}, [purchase, projectAddress])
const handleConfigUpdate = useCallback(async (nextConfig: any, signatures: [string, string, string]) => {
try {
setApprovalError(null)
setConfigError(null)
setWalletError(null)
await updateProjectConfig({
projectAddress,
nextConfig,
signatures
})
} catch (error) {
// Same error categorization patterns
const errorMessage = error instanceof Error ? error.message : 'Configuration update failed'
if (errorMessage.includes('contract not configured')) {
setConfigError(errorMessage)
} else if (errorMessage.includes('connect your wallet')) {
setWalletError(errorMessage)
} else {
console.error('Failed to update project config:', error)
}
}
}, [updateProjectConfig, projectAddress])
return (
<div className="space-y-6">
{/* Contract Configuration Errors */}
{configError && (
<Alert className="border-yellow-200 bg-yellow-50">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
<strong>Configuration Error:</strong> {configError}
<div className="mt-2">
<Button size="sm" variant="outline" onClick={() => setConfigError(null)}>
Dismiss
</Button>
</div>
</AlertDescription>
</Alert>
)}
{/* Wallet Connection Errors */}
{walletError && (
<Alert className="border-orange-200 bg-orange-50">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
<strong>Wallet Required:</strong> {walletError}
<div className="mt-2">
<Button size="sm" variant="outline" onClick={() => setWalletError(null)}>
Dismiss
</Button>
</div>
</AlertDescription>
</Alert>
)}
{/* Approval-specific Error Handling */}
{approvalError && (
<Alert className="border-red-200 bg-red-50">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
<strong>Approval Failed:</strong> {approvalError}
<div className="mt-2">
<Button size="sm" variant="outline" onClick={() => setApprovalError(null)}>
Retry Approval
</Button>
</div>
</AlertDescription>
</Alert>
)}
{/* Purchase Transaction Status */}
<div className="space-y-2">
<h4 className="font-medium">Purchase Tokens</h4>
<TransactionInfo
isLoading={isPurchasing}
isSuccess={purchaseSuccess}
isError={purchaseError && !approvalError && !configError && !walletError}
error={purchaseErrorMessage}
txHash={purchaseTxHash}
loadingText="Processing token purchase..."
successText="Tokens purchased successfully!"
/>
<div className="flex gap-2">
<Button
onClick={() => handlePurchase(parseUnits('100', 18))}
disabled={isPurchasing || !userAddress}
>
Purchase 100 Tokens
</Button>
{(purchaseError || purchaseSuccess) && (
<Button variant="outline" onClick={resetPurchase}>
Reset
</Button>
)}
</div>
</div>
{/* Configuration Update Status */}
<div className="space-y-2">
<h4 className="font-medium">Update Configuration</h4>
<TransactionInfo
isLoading={isUpdating}
isSuccess={updateSuccess}
isError={updateError && !configError && !walletError}
error={updateErrorMessage}
txHash={updateTxHash}
loadingText="Updating project configuration..."
successText="Configuration updated successfully!"
/>
<div className="flex gap-2">
<Button
onClick={() => handleConfigUpdate(
{
projectOwner: '0x...' as Address,
distributor: '0x...' as Address,
distributorFeePercent: 500,
projectOwnerFeePercent: 300,
platformFeePercent: 200,
pairPrice: parseUnits('1', 18)
},
['0x...', '0x...', '0x...']
)}
disabled={isUpdating || !userAddress}
>
Update Configuration
</Button>
{(updateError || updateSuccess) && (
<Button variant="outline" onClick={resetUpdate}>
Reset
</Button>
)}
</div>
</div>
</div>
)
}Stage-Based Error Prevention
import { useProjectDetailData } from '@/lib/progressive-ifif-hooks'
import { useAccount } from 'wagmi'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { AlertTriangle, Info } from 'lucide-react'
import { PROJECT_STAGES } from '@/config/constants'
// Real stage validation patterns from actual project pages
function StageAwareOperations({ projectId }: { projectId: string }) {
const { address: walletConnected } = useAccount()
const { project, overview, isLoading } = useProjectDetailData(parseInt(projectId) || 0)
// Real stage validation logic from project pages
const stageValidations = useMemo(() => {
if (!overview) return null
const currentStage = overview.stage
const stageInfo = PROJECT_STAGES[currentStage] || {
name: 'Unknown',
color: 'bg-gray-500',
description: 'Unknown stage'
}
return {
// From actual project/[id]/page.tsx validation patterns
canPurchase: currentStage === 2 || currentStage === 3, // PRIVATE_SALE or PUBLIC_SALE
canRefund: currentStage === 5, // SALE_FAILED
canClaimNFT: currentStage === 4, // SALE_SUCCESSED
canClaimTokens: currentStage === 6, // CLAIM
canDeposit: currentStage === 4, // SALE_SUCCESSED (for distributors)
// From NFT action modals validation patterns
canSplitNFT: currentStage === 4, // SALE_SUCCESSED
canMergeNFT: currentStage === 4, // SALE_SUCCESSED
canConvertNFT: currentStage === 4, // SALE_SUCCESSED
// Management operations (project owner only)
canUpdateConfig: currentStage === 1, // INITIALIZED
canStartPrivate: currentStage === 1, // INITIALIZED
canStartPublic: currentStage === 2, // PRIVATE_SALE
canEndSales: currentStage === 2 || currentStage === 3, // PRIVATE_SALE or PUBLIC_SALE
canPublicEnd: currentStage === 3, // PUBLIC_SALE
currentStage,
stageInfo
}
}, [overview])
if (isLoading) {
return <div>Loading project stage validation...</div>
}
if (!stageValidations) {
return (
<Alert className="border-red-200 bg-red-50">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
<strong>Project Not Found:</strong> Unable to load project stage information.
</AlertDescription>
</Alert>
)
}
const {
canPurchase, canRefund, canClaimNFT, canClaimTokens, canDeposit,
canSplitNFT, canMergeNFT, canConvertNFT,
canUpdateConfig, canStartPrivate, canStartPublic, canEndSales, canPublicEnd,
currentStage, stageInfo
} = stageValidations
return (
<div className="space-y-6">
{/* Current Stage Information */}
<div className="bg-slate-50 p-4 rounded-lg">
<h4 className="font-medium text-slate-900 mb-2">Current Project Stage</h4>
<div className="flex items-center gap-2">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${stageInfo.color.replace('bg-', 'bg-').replace('-500', '-100')} text-${stageInfo.color.split('-')[1]}-800`}>
Stage {currentStage}: {stageInfo.name}
</span>
<span className="text-sm text-slate-600">{stageInfo.description}</span>
</div>
</div>
{/* Client Investment Operations */}
<div className="space-y-4">
<h4 className="font-medium text-slate-900">Client Investment Operations</h4>
{/* Purchase validation */}
{!canPurchase && (
<Alert className="border-orange-200 bg-orange-50">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
<strong>Purchase Not Available:</strong> Token purchase is only available during
Private Sale (stage 2) or Public Sale (stage 3).
Current stage: {currentStage} ({stageInfo.name})
</AlertDescription>
</Alert>
)}
{/* Refund validation */}
{!canRefund && currentStage > 3 && (
<Alert className="border-orange-200 bg-orange-50">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
<strong>Refund Not Available:</strong> Refunds are only available when
the sale has failed (stage 5). Current stage: {currentStage} ({stageInfo.name})
</AlertDescription>
</Alert>
)}
{/* NFT Claim validation */}
{!canClaimNFT && currentStage > 2 && (
<Alert className="border-orange-200 bg-orange-50">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
<strong>NFT Claim Not Available:</strong> NFT claiming is only available after
successful sale completion (stage 4). Current stage: {currentStage} ({stageInfo.name})
</AlertDescription>
</Alert>
)}
{/* Token Claim validation */}
{!canClaimTokens && currentStage !== 6 && (
<Alert className="border-orange-200 bg-orange-50">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
<strong>Token Claim Not Available:</strong> Direct token claiming is only available
during the claim phase (stage 6). Current stage: {currentStage} ({stageInfo.name})
</AlertDescription>
</Alert>
)}
{/* Operation buttons with real stage validation */}
<div className="flex flex-wrap gap-2">
<Button
disabled={!canPurchase || !walletConnected}
variant={canPurchase ? "default" : "outline"}
>
Purchase Tokens
</Button>
<Button
disabled={!canRefund || !walletConnected}
variant={canRefund ? "default" : "outline"}
>
Request Refund
</Button>
<Button
disabled={!canClaimNFT || !walletConnected}
variant={canClaimNFT ? "default" : "outline"}
>
Claim NFT
</Button>
<Button
disabled={!canClaimTokens || !walletConnected}
variant={canClaimTokens ? "default" : "outline"}
>
Claim Tokens
</Button>
</div>
</div>
{/* NFT Operations */}
<div className="space-y-4">
<h4 className="font-medium text-slate-900">NFT Operations</h4>
{/* NFT operations validation (from actual NFT modals) */}
{!canSplitNFT && (
<Alert className="border-orange-200 bg-orange-50">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
<strong>NFT Split Not Available:</strong> NFT splitting is only available during
SALE_SUCCESSED stage (stage 4). Current stage: {currentStage} ({stageInfo.name})
</AlertDescription>
</Alert>
)}
{!canMergeNFT && (
<Alert className="border-orange-200 bg-orange-50">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
<strong>NFT Merge Not Available:</strong> NFT merging is only available during
SALE_SUCCESSED stage (stage 4). Current stage: {currentStage} ({stageInfo.name})
</AlertDescription>
</Alert>
)}
{!canConvertNFT && (
<Alert className="border-orange-200 bg-orange-50">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
<strong>NFT Convert Not Available:</strong> NFT conversion is only available during
SALE_SUCCESSED stage (stage 4). Current stage: {currentStage} ({stageInfo.name})
</AlertDescription>
</Alert>
)}
<div className="flex flex-wrap gap-2">
<Button
disabled={!canSplitNFT || !walletConnected}
variant={canSplitNFT ? "default" : "outline"}
>
Split NFT
</Button>
<Button
disabled={!canMergeNFT || !walletConnected}
variant={canMergeNFT ? "default" : "outline"}
>
Merge NFTs
</Button>
<Button
disabled={!canConvertNFT || !walletConnected}
variant={canConvertNFT ? "default" : "outline"}
>
Convert to Tokens
</Button>
</div>
</div>
{/* Management Operations (Project Owner Only) */}
<div className="space-y-4">
<h4 className="font-medium text-slate-900">Management Operations</h4>
<Alert className="border-blue-200 bg-blue-50">
<Info className="h-4 w-4" />
<AlertDescription>
<strong>Management Access:</strong> These operations are only available to the project owner
and are subject to stage restrictions based on the project lifecycle.
</AlertDescription>
</Alert>
<div className="flex flex-wrap gap-2">
<Button
disabled={!canUpdateConfig}
variant={canUpdateConfig ? "default" : "outline"}
size="sm"
>
Update Config {!canUpdateConfig && '(Stage 1 only)'}
</Button>
<Button
disabled={!canStartPrivate}
variant={canStartPrivate ? "default" : "outline"}
size="sm"
>
Start Private Sale {!canStartPrivate && '(Stage 1 only)'}
</Button>
<Button
disabled={!canStartPublic}
variant={canStartPublic ? "default" : "outline"}
size="sm"
>
Start Public Sale {!canStartPublic && '(Stage 2 only)'}
</Button>
<Button
disabled={!canEndSales}
variant={canEndSales ? "default" : "outline"}
size="sm"
>
End Sales {!canEndSales && '(Stage 2-3 only)'}
</Button>
</div>
</div>
{/* Stage Progression Information */}
<div className="bg-blue-50 p-4 rounded-lg">
<h4 className="font-medium text-blue-900 mb-2">Stage Progression Guide</h4>
<div className="text-sm text-blue-800 space-y-1">
<p><strong>Stage 0-1:</strong> Project setup and configuration</p>
<p><strong>Stage 2:</strong> Private Sale - Limited participant purchase phase</p>
<p><strong>Stage 3:</strong> Public Sale - Open purchase phase</p>
<p><strong>Stage 4:</strong> Sale Success - NFT operations and claiming available</p>
<p><strong>Stage 5:</strong> Sale Failed - Refund phase</p>
<p><strong>Stage 6:</strong> Claim Phase - Final token distribution</p>
</div>
</div>
</div>
)
}Constants
IFIF Configuration
// Real configuration values from the IFIF system based on actual implementation
// examples/config/constants.ts
export const PROJECT_STAGES: Record<number, { name: string; color: string; description: string }> = {
0: {
name: 'None',
color: 'bg-gray-500',
description: 'Project not initialized'
},
1: {
name: 'Initialized',
color: 'bg-blue-500',
description: 'Project initialized but not started'
},
2: {
name: 'Private Sale',
color: 'bg-yellow-500',
description: 'Private sale is active'
},
3: {
name: 'Public Sale',
color: 'bg-orange-500',
description: 'Public sale is active'
},
4: {
name: 'Sale Success',
color: 'bg-green-500',
description: 'Sale completed successfully'
},
5: {
name: 'Sale Failed',
color: 'bg-red-500',
description: 'Sale failed to reach target'
},
6: {
name: 'Claim',
color: 'bg-purple-500',
description: 'Claim phase is active'
}
}
export const ROLE_NAMES: Record<string, { name: string; color: string; description: string }> = {
'0x0000000000000000000000000000000000000000000000000000000000000000': {
name: 'Default Admin',
color: 'bg-red-500',
description: 'Super administrator with all permissions'
},
'0xa49807205ce4d355092ef5a8a18f56e8913cf4a201fbe287825b095693c21775': {
name: 'Admin',
color: 'bg-blue-500',
description: 'Administrative permissions'
},
'0x241ecf16d79d0f8dbfb92cbc07fe17840425976cf0667f022fe9877caa831b08': {
name: 'Manager',
color: 'bg-green-500',
description: 'Management and operational permissions'
},
'0xfbd454f36a7e1a388bd6fc3ab10d434aa4578f811acbbcf33afb1c697486313c': {
name: 'Distributor',
color: 'bg-purple-500',
description: 'Distribution and allocation permissions'
}
}
export const NFT_OPERATION_TYPES: Record<number, { name: string; color: string; description: string }> = {
0: {
name: 'Claim',
color: 'bg-green-500',
description: 'NFT claim operation'
},
1: {
name: 'Split',
color: 'bg-blue-500',
description: 'NFT split operation'
},
2: {
name: 'Merge',
color: 'bg-purple-500',
description: 'NFT merge operation'
},
3: {
name: 'Convert',
color: 'bg-orange-500',
description: 'NFT conversion to tokens'
}
}
// Progressive loading configuration from actual implementation
// examples/lib/progressive-ifif-hooks.ts
export const IFIF_PROGRESSIVE_CONFIG = {
projects: { batchSize: 25, staleTime: 30000 },
tokenDeployments: { batchSize: 30, staleTime: 60000 },
tokenClaims: { batchSize: 100, staleTime: 30000 },
nftOperations: { batchSize: 50, staleTime: 30000 },
factoryDeployments: { batchSize: 20, staleTime: 60000 },
userInvestments: { batchSize: 25, staleTime: 30000 },
userPurchases: { batchSize: 50, staleTime: 30000 },
userRefunds: { batchSize: 50, staleTime: 30000 },
userClaims: { batchSize: 50, staleTime: 30000 },
userNFTs: { batchSize: 30, staleTime: 30000 }
} as const
// Analytics configuration from actual implementation
// examples/config/constants.ts
export const ANALYTICS_PROGRESSIVE_CONFIG = {
dailyMetrics: { batchSize: 30, staleTime: 120000 },
summaryAnalytics: { batchSize: 10, staleTime: 300000 },
trendsAnalysis: { batchSize: 50, staleTime: 180000 },
performance: { batchSize: 25, staleTime: 240000 }
} as const
// Investment status types from actual implementation
// examples/lib/types.ts
type InvestmentStatus = 'active' | 'refunded' | 'claimed'
type TransactionType = 'purchase' | 'refund' | 'claim'Security and Performance
Security Best Practices
The IFIF application implements comprehensive security patterns across all page types. Here are the actual security implementations from the codebase:
Connection and Authentication Patterns
// Real connection checking pattern used across all pages
// examples/app/ifif-projects/page.tsx, examples/app/user/[address]/page.tsx
import { useSettingsStore } from '@/lib/settings-store'
export default function SecurePage() {
const { isConnected: _isConnected } = useSettingsStore()
const [isClient, setIsClient] = useState(false)
useEffect(() => {
setIsClient(true)
}, [])
// Security: Prevent hydration mismatch and ensure client-side rendering
const isConnected = useMemo(() => isClient ? _isConnected : false, [isClient, _isConnected])
// Security: Block access to sensitive data without connection
if (!isConnected) {
return (
<div className="flex-1 space-y-8 p-8">
<div className="text-center py-20">
<div className="bg-gradient-to-br from-blue-50 to-purple-50 p-8 max-w-md mx-auto">
<h1 className="text-3xl font-bold text-slate-900 mb-3">Connection Required</h1>
<p className="text-slate-600 mb-6 leading-relaxed">
Connect to Ponder indexer to view data
</p>
<Badge variant="outline" className="text-yellow-700 border-yellow-300">
Not Connected
</Badge>
</div>
</div>
</div>
)
}
// Render secure content only after connection validation
return <SecureContent />
}Project Owner Access Control
// Real project ownership validation from examples/app/project/[id]/page.tsx
import { useAccount } from 'wagmi'
import { useProjectDetailData } from '@/lib/progressive-ifif-hooks'
import { useUserProfileData } from '@/lib/progressive-user-hooks'
export default function ProjectDetailPage() {
const { isConnected: walletConnected, address } = useAccount()
const { project, overview, isLoading, error } = useProjectDetailData(projectId)
// Security: Verify project ownership for management actions
const isProjectOwner = useMemo(() => {
if (!walletConnected || !address || !overview?.owner) {
return false
}
return address.toLowerCase() === overview.owner.toLowerCase()
}, [walletConnected, address, overview?.owner])
// Security: User investment calculation with address validation
const { profile: userProfile, investments: userInvestments } = useUserProfileData(address || '')
const userData = useMemo(() => {
if (!address || !overview?.projectAddress) {
return {
userInvestment: 0,
userTokens: 0,
userPurchase: 0
}
}
// Find user investment for this specific project using both sources
const projectInvestment = userInvestments.find(
inv => inv.projectAddress.toLowerCase() === overview.projectAddress.toLowerCase()
)
return calculateUserData(projectInvestment, address, overview)
}, [address, overview?.projectAddress, userInvestments])
return (
<div>
{/* Project Management Actions */}
<div className="bg-white border border-slate-200 space-y-4">
<div className="p-6 pb-0">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold flex items-center gap-2">
<Settings className="h-5 w-5" />
Project Management
</h2>
{isProjectOwner && (
<Badge variant="default" className="bg-green-100 text-green-800 border-green-200">
Owner Access
</Badge>
)}
</div>
</div>
{/* Management actions only for verified owners with stage validation */}
<div className="px-6 pb-6 space-y-3">
<Button
className="w-full justify-start"
variant="outline"
disabled={!isProjectOwner || !overview || overview.stage !== 1}
onClick={() => openModal('updateDetails')}
>
<Edit3 className="h-4 w-4 mr-2" />
Update Project Details
</Button>
<Button
className="w-full justify-start"
variant="outline"
disabled={!isProjectOwner || !overview || overview.stage === 6}
onClick={() => openModal('updateConfig')}
>
<Settings className="h-4 w-4 mr-2" />
Update Configuration
</Button>
<Button
className="w-full justify-start"
variant="outline"
disabled={!isProjectOwner || !overview || overview.stage !== 1}
onClick={() => openModal('startPrivateSale')}
>
<Users className="h-4 w-4 mr-2" />
Start Private Sale
</Button>
<Button
className="w-full justify-start"
variant="outline"
disabled={!isProjectOwner || !overview || overview.stage !== 2}
onClick={() => openModal('startPublicSale')}
>
<Globe className="h-4 w-4 mr-2" />
Start Public Sale
</Button>
<Button
className="w-full justify-start"
variant="outline"
disabled={!isProjectOwner || !overview || (overview.stage !== 2 && overview.stage !== 3)}
onClick={() => openModal('endSales')}
>
<Square className="h-4 w-4 mr-2" />
End Sales
</Button>
</div>
</div>
</div>
)
}Address Validation and Input Security
// Real address validation patterns from components
import { isAddress } from 'viem'
import { useForm } from '@tanstack/react-form'
import { FieldInfo } from '@/components/ui/field-info'
function SecureAddressForm() {
const form = useForm({
defaultValues: {
projectOwner: '' as Address,
distributor: '' as Address,
fundToken: '' as Address
},
onSubmit: async ({ value }) => {
// All addresses validated before submission
await processSecureForm(value)
}
})
return (
<div className="space-y-4">
{/* Comprehensive validation pattern */}
<form.Field
name="projectOwner"
validators={{
onChange: ({ value }) =>
!value || value.trim().length === 0
? 'Project owner address is required'
: !isAddress(value as Address)
? 'Invalid address format'
: undefined,
}}
>
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Project Owner Address</Label>
<Input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
placeholder="0x..."
className={field.state.meta.isTouched && !field.state.meta.isValid ? 'border-red-300' : ''}
/>
<FieldInfo field={field} />
</div>
)}
</form.Field>
{/* Simplified validation pattern for optional fields */}
<form.Field
name="distributor"
validators={{
onChange: ({ value }) =>
value && !isAddress(value) ? 'Invalid address' : undefined,
}}
>
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Distributor Address (Optional)</Label>
<Input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
placeholder="0x..."
/>
<FieldInfo field={field} />
</div>
)}
</form.Field>
{/* Advanced validation with sanitization */}
<form.Field
name="fundToken"
validators={{
onChange: ({ value }) => {
if (!value || value.trim() === '') {
return 'Fund token address is required'
}
if (!isAddress(value.trim() as Address)) {
return 'Please enter a valid Ethereum address'
}
return undefined
},
}}
>
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Fund Token Address</Label>
<Input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
placeholder="0x1234...abcd"
required
className={field.state.meta.errors.length > 0 ? 'border-red-500' : ''}
/>
<FieldInfo field={field} />
<p className="text-xs text-slate-500">
Enter the token contract address for project funding
</p>
</div>
)}
</form.Field>
</div>
)
}Multi-Layered Access Control
The IFIF application implements different security patterns depending on context:
// 1. Connection-Based Security (All Pages)
import { useAccount } from 'wagmi'
import { useSettingsStore } from '@/lib/settings-store'
export default function SecurePage() {
const { isConnected: _isConnected } = useSettingsStore()
const [isClient, setIsClient] = useState(false)
useEffect(() => {
setIsClient(true)
}, [])
const isConnected = useMemo(() => isClient ? _isConnected : false, [isClient, _isConnected])
if (!isConnected) {
return (
<div className="flex-1 space-y-8 p-8">
<div className="text-center py-20">
<div className="bg-gradient-to-br from-blue-50 to-purple-50 p-8 max-w-md mx-auto">
<FolderIcon className="mx-auto h-16 w-16 text-slate-400 mb-6" />
<h1 className="text-3xl font-bold text-slate-900 mb-3">Page Title</h1>
<p className="text-slate-600 mb-6 leading-relaxed">
Connect to Ponder indexer to view data
</p>
<Badge variant="outline" className="text-yellow-700 border-yellow-300">
Not Connected
</Badge>
</div>
</div>
</div>
)
}
// Page content...
}
// 2. Ownership-Based Security (Project Management)
function ProjectManagementActions() {
const { isConnected: walletConnected, address } = useAccount()
const { project, overview } = useProjectDetailData(projectId)
// Security: Verify project ownership for management actions
const isProjectOwner = useMemo(() => {
if (!walletConnected || !address || !overview?.owner) {
return false
}
return address.toLowerCase() === overview.owner.toLowerCase()
}, [walletConnected, address, overview?.owner])
return (
<div className="bg-white border border-slate-200 space-y-4">
<div className="p-6 pb-0">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold flex items-center gap-2">
<Settings className="h-5 w-5" />
Project Management
</h2>
{isProjectOwner && (
<Badge variant="default" className="bg-green-100 text-green-800 border-green-200">
Owner Access
</Badge>
)}
</div>
</div>
<div className="px-6 pb-6 space-y-3">
<Button
className="w-full justify-start"
variant="outline"
disabled={!isProjectOwner || !overview || overview.stage !== 1}
onClick={() => openModal('updateDetails')}
>
<Edit3 className="h-4 w-4 mr-2" />
Update Project Details
</Button>
<Button
className="w-full justify-start"
variant="outline"
disabled={!isProjectOwner || !overview || overview.stage === 6}
onClick={() => openModal('updateConfig')}
>
<Settings className="h-4 w-4 mr-2" />
Update Configuration
</Button>
</div>
</div>
)
}
// 3. Role-Based Security (Component Actions)
import { useHasRole } from '@/lib/progressive-role-hooks'
function DistributorDepositAction() {
const { address: connectedAddress, isConnected } = useAccount()
// DISTRIBUTOR_ROLE constant
const DISTRIBUTOR_ROLE = '0xfbd454f36a7e1a388bd6fc3ab10d434aa4578f811acbbcf33afb1c697486313c'
const {
hasRole: hasDistributorRole,
isLoading: isCheckingRole,
error: roleCheckError
} = useHasRole(DISTRIBUTOR_ROLE, connectedAddress || null)
const canDeposit = useMemo(() => {
return isConnected && hasDistributorRole && !roleCheckError
}, [isConnected, hasDistributorRole, roleCheckError])
return (
<div className="space-y-4">
{/* Role Status Display */}
{isCheckingRole ? (
<Alert className="border-blue-200 bg-blue-50">
<Shield className="h-4 w-4" />
<AlertDescription>
<strong>Checking Authorization:</strong> Verifying distributor role permissions...
</AlertDescription>
</Alert>
) : roleCheckError ? (
<Alert className="border-red-200 bg-red-50">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
<strong>Role Check Error:</strong> Unable to verify role permissions. {roleCheckError}
</AlertDescription>
</Alert>
) : !hasDistributorRole ? (
<Alert className="border-red-200 bg-red-50">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
<strong>Insufficient Permissions:</strong> You need the DISTRIBUTOR role to make deposits.
Only authorized distributors can deposit project tokens.
</AlertDescription>
</Alert>
) : (
<Alert className="border-green-200 bg-green-50">
<Shield className="h-4 w-4" />
<AlertDescription>
<strong>✅ Authorized Distributor:</strong> You have the required DISTRIBUTOR role and can make deposits.
</AlertDescription>
</Alert>
)}
{/* Action Button */}
<Button
disabled={!canDeposit}
onClick={performDepositAction}
className="w-full"
>
{hasDistributorRole ? 'Deposit Tokens' : 'Access Denied'}
</Button>
</div>
)
}Parameter and Route Security
Dynamic Route Parameter Patterns// Pattern 1: Address parameter (from examples/app/user/[address]/page.tsx)
import { useParams } from 'next/navigation'
import { isAddress } from 'viem'
import { useSettingsStore } from '@/lib/settings-store'
export default function UserProfilePage() {
const params = useParams()
const userAddress = params.address as string
const { isConnected: _isConnected } = useSettingsStore()
const [isClient, setIsClient] = useState(false)
// Security: Client-side hydration protection pattern
useEffect(() => {
setIsClient(true)
}, [])
const isConnected = useMemo(() => isClient ? _isConnected : false, [isClient, _isConnected])
// Security: Optional parameter validation (recommended for production)
const isValidAddress = useMemo(() => {
return userAddress && isAddress(userAddress)
}, [userAddress])
// Security: Use route parameter directly in hooks with progressive loading
const {
profile,
investments,
activities,
nfts,
purchases,
refunds,
claims,
isLoading
} = useUserProfileData(userAddress)
// Security: Connection validation before showing user data
if (!isConnected) {
return (
<div className="flex-1 space-y-8 p-8">
<div className="text-center py-20">
<div className="bg-gradient-to-br from-blue-50 to-purple-50 p-8 max-w-md mx-auto">
<User className="mx-auto h-16 w-16 text-slate-400 mb-6" />
<h1 className="text-3xl font-bold text-slate-900 mb-3">User Profile</h1>
<p className="text-slate-600 mb-6 leading-relaxed">
Connect to Ponder indexer to view user data
</p>
<Badge variant="outline" className="text-yellow-700 border-yellow-300">
Not Connected
</Badge>
</div>
</div>
</div>
)
}
// Security: Parameter validation error state (optional)
if (!isValidAddress) {
return (
<div className="flex-1 space-y-8 p-8">
<div className="text-center py-20">
<h1 className="text-2xl font-bold text-slate-900 mb-3">Invalid Address</h1>
<p className="text-slate-600">Please provide a valid Ethereum address</p>
</div>
</div>
)
}
return (
<div className="flex-1 space-y-8 p-8">
<p className="text-lg text-slate-600 mt-1">
Investment activity and portfolio for <span className="font-mono">{shortenAddress(userAddress)}</span>
</p>
{/* User profile content */}
</div>
)
}// Pattern 2: Numeric parameter (from examples/app/project/[id]/page.tsx)
export default function ProjectDetailPage() {
const params = useParams()
const { isConnected: _isConnected } = useSettingsStore()
const [isClient, setIsClient] = useState(false)
// Security: Numeric parameter conversion with validation
const projectId = useMemo(() => {
const id = Number(params.id)
return !isNaN(id) && id > 0 ? id : null
}, [params.id])
// Security: Client-side hydration protection
useEffect(() => {
setIsClient(true)
}, [])
const isConnected = useMemo(() => isClient ? _isConnected : false, [isClient, _isConnected])
// Security: Use validated parameter in data hooks
const { project, overview, isLoading, error } = useProjectDetailData(projectId || 0)
// Security: Connection validation
if (!isConnected) {
return (
<div className="flex-1 space-y-8 p-8">
<div className="text-center py-20">
<div className="bg-gradient-to-br from-blue-50 to-purple-50 p-8 max-w-md mx-auto">
<TrendingUp className="mx-auto h-16 w-16 text-slate-400 mb-6" />
<h1 className="text-3xl font-bold text-slate-900 mb-3">Project Details</h1>
<p className="text-slate-600 mb-6 leading-relaxed">
Connect to Ponder indexer to view project information
</p>
<Badge variant="outline" className="text-yellow-700 border-yellow-300">
Not Connected
</Badge>
</div>
</div>
</div>
)
}
// Security: Parameter validation error state
if (!projectId) {
return (
<div className="flex-1 space-y-8 p-8">
<div className="text-center py-20">
<h1 className="text-2xl font-bold text-slate-900 mb-3">Invalid Project ID</h1>
<p className="text-slate-600">Please provide a valid numeric project ID</p>
</div>
</div>
)
}
// Security: Data validation before rendering
if (error || !project || !overview) {
return (
<div className="flex-1 space-y-8 p-8">
<div className="text-center py-20">
<h1 className="text-2xl font-bold text-slate-900 mb-3">Project Not Found</h1>
<p className="text-slate-600">
The project you're looking for doesn't exist or hasn't been indexed yet.
</p>
</div>
</div>
)
}
return (
<div className="flex-1 space-y-8 p-8">
<h1 className="text-4xl font-bold tracking-tight text-slate-900">
#{overview.projectId}
</h1>
{/* Project content */}
</div>
)
}// Pattern 3: Static routes (from examples/app/ifif-projects/page.tsx)
export default function IFIFProjectsPage() {
const router = useRouter()
const [isClient, setIsClient] = useState(false)
const { isConnected: _isConnected } = useSettingsStore()
// Security: Client-side hydration protection (still required)
useEffect(() => {
setIsClient(true)
}, [])
const isConnected = useMemo(() => isClient ? _isConnected : false, [isClient, _isConnected])
// Security: Connection validation for data access
if (!isConnected) {
return (
<div className="flex-1 space-y-8 p-8">
<div className="text-center py-20">
<div className="bg-gradient-to-br from-blue-50 to-purple-50 p-8 max-w-md mx-auto">
<Folder className="mx-auto h-16 w-16 text-slate-400 mb-6" />
<h1 className="text-3xl font-bold text-slate-900 mb-3">IFIF Projects</h1>
<p className="text-slate-600 mb-6 leading-relaxed">
Connect to Ponder indexer to view project data
</p>
<Badge variant="outline" className="text-yellow-700 border-yellow-300">
Not Connected
</Badge>
</div>
</div>
</div>
)
}
// Navigation with parameter passing
const handleProjectSelect = useCallback((projectId: string) => {
router.push(`/project/${projectId}`)
}, [router])
return (
<div className="flex-1 space-y-8 p-8">
{/* Static route content */}
</div>
)
}- Parameter Validation: Always validate route parameters before use
- Type Conversion: Use proper type conversion with validation for numeric parameters
- Hydration Protection: Implement client-side hydration protection for all routes
- Connection Validation: Verify data source connection before displaying content
- Error Boundaries: Provide meaningful error states for invalid parameters
- Progressive Loading: Use validated parameters safely in data fetching hooks
Performance Optimization
Progressive Data Loading Architecture// Pattern 1: Progressive Loading Hook (from examples/lib/progressive-ifif-hooks.ts)
import { useDataCacheStore } from './data-cache-store'
import { usePonderQuery } from '@ponder/react'
export const useProgressiveIFIFProjectsLoader = () => {
const [currentBatch, setCurrentBatch] = useState(0)
const [isLoadingMore, setIsLoadingMore] = useState(false)
const [allProjects, setAllProjects] = useState<IFIFProject[]>([])
const updateProgress = useDataCacheStore((state) => state.updateProgress)
const setProjectsData = useDataCacheStore((state) => state.setIFIFProjectsData)
const batchSize = IFIF_PROGRESSIVE_CONFIG.projects.batchSize
// Get total count for progress calculation
const { data: totalCountData } = usePonderQuery({
queryFn: (db) => db.select({ count: count() }).from(project),
})
// Load data in batches
const { data: batchData, isLoading } = usePonderQuery({
queryFn: (db) =>
db.select().from(project)
.orderBy(desc(project.createdAt))
.limit(batchSize)
.offset(currentBatch * batchSize),
})
// Progressive loading effect
useEffect(() => {
if (batchData && batchData.length > 0 && !isLoading) {
setAllProjects(prev => {
const combined = [...prev, ...batchData]
const unique = combined.filter((item, index, self) =>
index === self.findIndex(t => t.id === item.id)
)
return unique
})
setIsLoadingMore(false)
}
}, [batchData, isLoading])
// Cache management effect
useEffect(() => {
const totalCount = totalCountData?.[0]?.count || 0
if (allProjects.length > 0) {
updateProgress('ififProjects', {
current: allProjects.length,
total: totalCount,
percentage: totalCount ? Math.round((allProjects.length / totalCount) * 100) : 0,
isComplete: allProjects.length >= totalCount,
isLoading: isLoading || isLoadingMore
})
setProjectsData(allProjects)
}
}, [allProjects, totalCountData, isLoading, isLoadingMore, updateProgress, setProjectsData])
return {
projects: allProjects,
isLoading: isLoading || isLoadingMore,
progress: allProjects.length,
total: totalCountData?.[0]?.count || 0
}
}// Pattern 2: Data Cache Store (from examples/lib/data-cache-store.ts)
import { create } from 'zustand'
import { subscribeWithSelector } from 'zustand/middleware'
interface DataCacheState {
// Cached data
ififProjects: IFIFProject[]
operations: NFTOperation[]
purchases: UserPurchase[]
// Progress tracking
ififProjectsProgress: ProgressState
operationsProgress: ProgressState
// Actions
setIFIFProjectsData: (data: IFIFProject[]) => void
updateProgress: (type: string, progress: Partial<ProgressState>) => void
getTotalProgress: () => number
isAnyLoading: () => boolean
}
export const useDataCacheStore = create<DataCacheState>()(
subscribeWithSelector((set, get) => ({
// Initial state
ififProjects: [],
operations: [],
purchases: [],
ififProjectsProgress: { current: 0, total: 0, percentage: 0, isComplete: false, isLoading: false },
operationsProgress: { current: 0, total: 0, percentage: 0, isComplete: false, isLoading: false },
// Update cached data
setIFIFProjectsData: (data) => set({ ififProjects: data }),
// Update progress tracking
updateProgress: (type, progress) => set((state) => ({
[`${type}Progress`]: { ...state[`${type}Progress` as keyof DataCacheState], ...progress }
})),
// Calculate overall progress across all data types
getTotalProgress: () => {
const state = get()
const allProgress = [
state.ififProjectsProgress.percentage,
state.operationsProgress.percentage,
// Add other progress states...
]
return Math.round(allProgress.reduce((sum, p) => sum + p, 0) / allProgress.length)
},
// Check if any data loading is in progress
isAnyLoading: () => {
const state = get()
return state.ififProjectsProgress.isLoading ||
state.operationsProgress.isLoading
}
}))
)// Pattern 3: Project Detail Performance (from examples/app/project/[id]/page.tsx)
export default function ProjectDetailPage() {
const params = useParams()
const [isClient, setIsClient] = useState(false)
// Performance: Memoized parameter validation
const projectId = useMemo(() => {
const id = Number(params.id)
return !isNaN(id) && id > 0 ? id : null
}, [params.id])
// Performance: Progressive data loading
const { project, overview, isLoading, error } = useProjectDetailData(projectId || 0)
const { projects: ififProjects } = useCachedIFIFData()
// Performance: Memoized project owner check
const isProjectOwner = useMemo(() => {
if (!walletConnected || !address || !overview?.owner) {
return false
}
return address.toLowerCase() === overview.owner.toLowerCase()
}, [walletConnected, address, overview?.owner])
// Performance: Memoized activity data aggregation
const activitiesData = useMemo(() => {
let activities: ActivityData[] = []
// Filter and combine data from cached collections
if (deployments) activities.push(...deployments.filter(d => Number(d.projectId) === projectId).map(d => ({ ...d, type: 'deployment' } as ActivityData)))
if (claims) activities.push(...claims.filter(c => c.projectAddress === overview?.projectAddress).map(c => ({ ...c, type: 'claim' } as ActivityData)))
if (operations) activities.push(...operations.filter(op => op.projectAddress === overview?.projectAddress).map(op => ({ ...op, type: 'operation' } as ActivityData)))
// Performance: Sort once with optimized comparison
return activities.sort((a, b) => {
const timestampA = typeof a.timestamp === 'string' ? parseFloat(a.timestamp) : Number(a.timestamp)
const timestampB = typeof b.timestamp === 'string' ? parseFloat(b.timestamp) : Number(b.timestamp)
return timestampB - timestampA
})
}, [deployments, claims, operations, purchases, refunds, deposits, projectId, overview?.projectAddress])
// Performance: Memoized user data calculations
const userData = useMemo(() => {
if (!address || !overview?.projectAddress) {
return { hasInvestment: false, investmentAmount: 0n, canRefund: false, canClaim: false }
}
const userInvestment = userInvestments?.find(inv =>
inv.projectAddress.toLowerCase() === overview.projectAddress.toLowerCase()
)
return {
hasInvestment: !!userInvestment,
investmentAmount: BigInt(userInvestment?.totalInvested || '0'),
canRefund: overview.stage === 2 || overview.stage === 3, // During sales
canClaim: overview.stage === 6, // After completion
}
}, [address, overview?.projectAddress, overview?.stage, userInvestments])
return (
<div className="flex-1 space-y-8 p-8">
{/* Optimized content rendering */}
</div>
)
}// Pattern 4: Investor Page Optimizations (from examples/app/project/[id]/investors/page.tsx)
export default function ProjectInvestorsPage() {
const params = useParams()
const projectId = params.id as string
const { project } = useProjectDetailData(parseInt(projectId) || 0)
const { investors, purchases, refunds, claims, isLoading } = useProjectInvestorData(project?.id || '')
// Performance: Memoized metric calculations with BigInt optimization
const totalInvestors = useMemo(() => investors.length, [investors.length])
const totalInvested = useMemo(() =>
purchases.reduce((sum: bigint, p: UserPurchase) => sum + BigInt(p.amount), BigInt(0)),
[purchases]
)
const totalRefunded = useMemo(() =>
refunds.reduce((sum: bigint, r: UserRefund) => sum + BigInt(r.amount), BigInt(0)),
[refunds]
)
const totalClaimed = useMemo(() =>
claims.reduce((sum: bigint, c: TokenClaim) => sum + BigInt(c.amount), BigInt(0)),
[claims]
)
const netInvestment = useMemo(() =>
totalInvested - totalRefunded,
[totalInvested, totalRefunded]
)
// Performance: Memoized metrics array for MetricGrid
const investorMetrics = useMemo(() => [
{
key: 'users' as const,
value: totalInvestors,
subtitle: 'Total investors'
},
{
key: 'totalFunding' as const,
value: parseFloat(formatEther(totalInvested)).toFixed(2),
subtitle: 'Total invested'
},
{
key: 'activeProjects' as const,
value: parseFloat(formatEther(netInvestment)).toFixed(2),
subtitle: 'Net investment'
},
{
key: 'last24h' as const,
value: purchases.length + refunds.length + claims.length,
subtitle: 'Total transactions'
}
], [totalInvestors, totalInvested, netInvestment, purchases.length, refunds.length, claims.length])
return (
<div className="flex-1 space-y-8 p-8">
<MetricGrid metrics={investorMetrics} />
{/* Performance-optimized table rendering */}
</div>
)
}// Pattern 5: NFT Page Performance (from examples/app/project/[id]/nft/page.tsx)
export default function ProjectNFTPage() {
const params = useParams()
const projectId = params.id as string
const { project } = useProjectDetailData(parseInt(projectId) || 0)
const { allocations, operations, isLoading } = useProjectNFTData(project?.id || '')
// Performance: Complex metrics calculation with single memoization
const { totalNFTs, activeNFTs, totalOperations, uniqueHolders } = useMemo(() => {
const totalNFTs = allocations?.length || 0
const activeNFTs = allocations?.filter((nft: any) => nft.isActive).length || 0
const totalOperations = operations?.length || 0
const uniqueHolders = new Set(allocations?.map((nft: any) => nft.owner)).size || 0
return {
totalNFTs,
activeNFTs,
totalOperations,
uniqueHolders
}
}, [allocations, operations])
// Performance: Memoized metrics for UI components
const nftMetrics = useMemo(() => [
{
key: 'totalActivities' as const,
value: totalNFTs,
subtitle: 'Total NFTs minted'
},
{
key: 'activities' as const,
value: activeNFTs,
subtitle: 'Currently active'
},
{
key: 'users' as const,
value: uniqueHolders,
subtitle: 'Unique holders'
},
{
key: 'last24h' as const,
value: totalOperations,
subtitle: 'Total operations'
}
], [totalNFTs, activeNFTs, uniqueHolders, totalOperations])
return (
<div className="flex-1 space-y-8 p-8">
<MetricGrid metrics={nftMetrics} />
{/* Performance-optimized NFT rendering */}
</div>
)
}// Pattern 6: Performance Monitoring (from examples/lib/progressive-analytics-hooks.ts)
export const usePerformanceMonitoring = () => {
const { getTotalProgress, isAnyLoading } = useDataCacheStore()
// Performance: Track loading states across all data types
const overallProgress = useMemo(() => ({
percentage: getTotalProgress(),
isLoading: isAnyLoading(),
isInitialized: getTotalProgress() > 0
}), [getTotalProgress, isAnyLoading])
// Performance: Monitor component render cycles
const renderMetrics = useRef({
renderCount: 0,
lastRender: Date.now()
})
useEffect(() => {
renderMetrics.current.renderCount++
renderMetrics.current.lastRender = Date.now()
})
return {
overallProgress,
renderMetrics: renderMetrics.current
}
}- Progressive Loading: Load data in chunks to prevent UI blocking
- Intelligent Caching: Use Zustand store for cross-component data sharing
- Memoization Strategy: Optimize expensive calculations with useMemo
- Batched Updates: Minimize re-renders with batched state updates
- BigInt Optimization: Efficient handling of large numbers in calculations
- Component-Level Optimization: Isolate performance-critical calculations
- Progress Tracking: Real-time monitoring of data loading progress
The IFIF Project Management System demonstrates a complete Web3 application with enterprise-grade features, providing developers with practical examples of implementing complex DeFi functionality while maintaining security and performance standards.