diff --git a/app/[hackathon]/InteractionClient.tsx b/app/[hackathon]/InteractionClient.tsx index b821473..e0c8a1a 100644 --- a/app/[hackathon]/InteractionClient.tsx +++ b/app/[hackathon]/InteractionClient.tsx @@ -35,7 +35,9 @@ import { RefreshCw, Eye, History, - Settings + Settings, + Code, + FileText } from "lucide-react" import { useChainId, useAccount, useWriteContract, useWaitForTransactionReceipt, useReadContract } from "wagmi" import { toast } from "sonner" @@ -97,6 +99,10 @@ export default function InteractionClient() { const [prizeRecipient, setPrizeRecipient] = useState("") const [isEditing, setIsEditing] = useState(false) + // Modal state for external links + const [modalOpen, setModalOpen] = useState(false) + const [modalLink, setModalLink] = useState({ url: '', type: '' }) + const searchParams = useSearchParams() const hackAddr = searchParams.get('hackAddr') const urlChainId = searchParams.get('chainId') @@ -142,9 +148,11 @@ export default function InteractionClient() { // Format token amounts for display - convert from base units to human-readable const formatTokenAmount = (amount: bigint, token: string): string => { if (token === '0x0000000000000000000000000000000000000000') { - // ETH - convert from wei to ether for display - const result = Math.floor(Number(formatEther(amount))).toString() - return result + // ETH - convert from wei to ether for display with up to 4 decimal places + const etherValue = Number(formatEther(amount)) + const result = etherValue.toFixed(4) + // Remove trailing zeros and decimal point if not needed + return parseFloat(result).toString() } else { // ERC20 - convert using token decimals for display const decimals = tokenDecimals[token] ?? 18 @@ -212,6 +220,7 @@ export default function InteractionClient() { let localApprovedTokens: string[] = [] let localTokenTotals: Record = {} let localTokenSymbols: Record = {} + let localSponsors: Sponsor[] = [] try { const tokens = await publicClient.readContract({ address: contractAddress, abi: HACKHUB_ABI, functionName: 'getApprovedTokensList' }) as string[] console.log('Approved tokens from contract:', tokens) @@ -347,8 +356,9 @@ export default function InteractionClient() { } } - console.log('Setting sponsors:', sponsorsData.length, sponsorsData) + console.log('📊 Setting sponsors:', sponsorsData.length, sponsorsData) setSponsors(sponsorsData) + localSponsors = sponsorsData } catch (e) { console.warn('Failed to fetch sponsors', e) setSponsors([]) @@ -363,6 +373,7 @@ export default function InteractionClient() { localApprovedTokens = [] localTokenTotals = {} localTokenSymbols = {} + localSponsors = [] } // Fetch judges @@ -486,9 +497,22 @@ export default function InteractionClient() { setHackathonData(hackathon) setLastSynced(new Date()) + console.log('📊 Saving hackathon data with judges:', judges.length, judges) + console.log('📊 Saving sponsors data:', localSponsors.length, localSponsors) try { - await hackathonDB.setHackathonDetails(contractAddress, chainId, hackathon) - } catch {} + // Save extended hackathon details including all interaction data + await hackathonDB.setExtendedHackathonDetails(contractAddress, chainId, { + hackathonData: hackathon, + approvedTokens: localApprovedTokens, + tokenMinAmounts: tokenMinAmounts, + tokenSymbols: localTokenSymbols, + tokenTotals: localTokenTotals, + tokenDecimals: tokenDecimals, + sponsors: localSponsors + }) + } catch (error) { + console.warn('Failed to save extended hackathon details to cache:', error) + } } catch (err) { console.error('Error fetching hackathon data:', err) setError('Failed to load hackathon data from blockchain') @@ -558,6 +582,12 @@ export default function InteractionClient() { } } + // Handle opening external links with modal + const handleOpenLink = (url: string, type: string) => { + setModalLink({ url, type }) + setModalOpen(true) + } + // Handle edit project const handleEditProject = () => { const userProject = hackathonData?.projects.find(p => @@ -584,13 +614,21 @@ export default function InteractionClient() { let shouldFetchFromBlockchain = false try { - const cached = await hackathonDB.getHackathonDetails(contractAddress, chainId) + const cached = await hackathonDB.getExtendedHackathonDetails(contractAddress, chainId) if (cached) { - setHackathonData(cached) + console.log('📊 Loading hackathon data from cache with judges:', cached.hackathonData.judges?.length, cached.hackathonData.judges) + // Restore all state from cache (types are already converted by IndexedDB) + setHackathonData(cached.hackathonData) + setApprovedTokens(cached.approvedTokens) + setTokenMinAmounts(cached.tokenMinAmounts) + setTokenSymbols(cached.tokenSymbols) + setTokenTotals(cached.tokenTotals) + setTokenDecimals(cached.tokenDecimals) + console.log('📊 Loading sponsors from cache:', cached.sponsors?.length, cached.sponsors) + setSponsors(cached.sponsors) + // Set last synced from cache timestamp - if ((cached as any).timestamp) { - setLastSynced(new Date((cached as any).timestamp)) - } + setLastSynced(new Date(cached.timestamp)) setLoading(false) // Successfully loaded from cache, no need to fetch from blockchain return @@ -987,36 +1025,32 @@ export default function InteractionClient() { + + {/* Right side - Sync Button */} +
+ + + {lastSynced && ( +
+
Last synced:
+
{lastSynced.toLocaleString()}
+
+ )} +
{/* Bottom Decorative Element */}
- {/* Sync Controls */} -
-
- - - {lastSynced && ( -
- Last synced: {lastSynced.toLocaleString()} -
- )} -
- -
- Data refreshes automatically and is cached locally -
-
-
{/* Main Content */}
@@ -1111,12 +1145,16 @@ export default function InteractionClient() { {approvedTokens.map((t) => (
-
{tokenSymbols[t] || short(t)}
-
{short(t)}
+
{t === '0x0000000000000000000000000000000000000000' ? 'Native ETH' : (tokenSymbols[t] || short(t))}
+
{t === '0x0000000000000000000000000000000000000000' ? 'ETH' : short(t)}
Total: {formatTokenAmount(tokenTotals[t] ?? BigInt(0), t)}
Min deposit: {(() => { + // Hardcode ETH minimum to 1 Wei + if (t === '0x0000000000000000000000000000000000000000') { + return '1 Wei' // 1 Wei = 0.000000000000000001 ETH, but showing practical minimum + } const minAmount = tokenMinAmounts[t] if (minAmount !== undefined && minAmount > BigInt(0)) { return formatTokenAmount(minAmount, t) @@ -1183,6 +1221,8 @@ export default function InteractionClient() { )} + + {/* Judges Section */} @@ -1258,9 +1298,9 @@ export default function InteractionClient() { @@ -1282,12 +1322,14 @@ export default function InteractionClient() { - {depositToken && tokenMinAmounts[depositToken] !== undefined && ( + {depositToken && (

Min amount to be listed: { - tokenMinAmounts[depositToken] !== undefined && tokenMinAmounts[depositToken] > BigInt(0) - ? formatTokenAmount(tokenMinAmounts[depositToken], depositToken) - : 'No minimum required' + depositToken === '0x0000000000000000000000000000000000000000' + ? '0.0001 ETH' // Practical minimum for ETH + : tokenMinAmounts[depositToken] !== undefined && tokenMinAmounts[depositToken] > BigInt(0) + ? formatTokenAmount(tokenMinAmounts[depositToken], depositToken) + : 'No minimum required' }

)} @@ -1359,6 +1401,44 @@ export default function InteractionClient() {
)} + {/* View Projects */} + {hackathonData.projects.length > 0 && ( + + +
+ +

View Submitted Projects

+

+ {hackathonData.projects.length} projects submitted to this hackathon +

+
+
+ Total Votes Cast: + {hackathonData.totalTokens} +
+
+ Prizes Claimed: + + {hackathonData.projects.filter(p => p.prizeClaimed).length} + +
+
+ + + + +
+
+
+ )} + {/* Project Submission */} {status === 'accepting-submissions' && ( @@ -1491,6 +1571,47 @@ export default function InteractionClient() { )}
+ + {/* External Link Warning Modal */} + + + + + + External Link Warning + + + You are about to visit an external website. Please verify the URL before proceeding. + + +
+
+ +
+

{modalLink.url}

+
+
+
+ + + + +
+
) } diff --git a/app/manage/ManageClient.tsx b/app/manage/ManageClient.tsx index ce16863..3239e5a 100644 --- a/app/manage/ManageClient.tsx +++ b/app/manage/ManageClient.tsx @@ -341,12 +341,15 @@ export default function ManageHackathonPage() { // Token address pretty const short = (addr: string) => `${addr.slice(0,6)}...${addr.slice(-4)}` - // Format token amounts to whole numbers considering decimals + // Format token amounts for display - convert from base units to human-readable const formatTokenAmount = (amount: string, token: string, decimals?: number): string => { const amountBigInt = BigInt(amount) if (token === '0x0000000000000000000000000000000000000000') { - // ETH - convert from wei to ether and show as whole number - return Math.floor(Number(formatEther(amountBigInt))).toString() + // ETH - convert from wei to ether for display with up to 4 decimal places + const etherValue = Number(formatEther(amountBigInt)) + const result = etherValue.toFixed(4) + // Remove trailing zeros and decimal point if not needed + return parseFloat(result).toString() } else { // ERC20 - convert using token decimals const tokenDecimals = decimals ?? 18 @@ -510,67 +513,60 @@ export default function ManageHackathonPage() { ) : (
{judges.map((judge) => ( -
-
+
+
+ {/* Left: judge details */}

{judge.name}

{judge.address.slice(0, 10)}...{judge.address.slice(-6)}

+

Current tokens: {judge.tokensAllocated}

- - {judge.tokensAllocated} tokens - -
- - {/* Token Adjustment Section */} - {!hackathonInfo?.concluded && ( -
- -
- setTokenAdjustments(prev => ({ - ...prev, - [judge.address]: e.target.value - }))} - className="w-48 bg-white border-gray-300 text-black" - /> - + className="bg-[#8B6914] text-white hover:bg-[#A0471D]" + > + {adjustingTokens[judge.address] ? ( + <> + + Updating... + + ) : ( + 'Update Tokens' + )} + +
+

Set to 0 to remove voting power, increase to allocate more tokens

-

- Current tokens: {judge.tokensAllocated} | - Set to 0 to remove voting power, increase to allocate more tokens -

-
- )} + )} +
))}
@@ -643,12 +639,14 @@ export default function ManageHackathonPage() { approvedTokens.map((t) => (
-

{t.symbol || short(t.token)}

+

{t.token === '0x0000000000000000000000000000000000000000' ? 'Native ETH' : (t.symbol || short(t.token))}

Min: { - BigInt(t.minAmount) > BigInt(0) - ? formatTokenAmount(t.minAmount, t.token, t.decimals) - : 'No minimum' + t.token === '0x0000000000000000000000000000000000000000' + ? '1 Wei' // Hardcode ETH minimum to 1 Wei + : BigInt(t.minAmount) > BigInt(0) + ? formatTokenAmount(t.minAmount, t.token, t.decimals) + : 'No minimum' }

diff --git a/app/projects/ProjectsClient.tsx b/app/projects/ProjectsClient.tsx new file mode 100644 index 0000000..4dea10e --- /dev/null +++ b/app/projects/ProjectsClient.tsx @@ -0,0 +1,727 @@ +"use client" + +import { useState, useEffect } from "react" +import { useSearchParams, useRouter } from "next/navigation" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Avatar, AvatarFallback } from "@/components/ui/avatar" +import { Alert, AlertDescription } from "@/components/ui/alert" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" + +// Helper function to get the correct image path for GitHub Pages +const getImagePath = (path: string) => { + const basePath = process.env.NODE_ENV === 'production' ? '/HackHub-WebUI' : ''; + return `${basePath}${path}`; +}; + +import { HackathonData, getHackathonStatus, getDaysRemaining, Judge, Project } from "@/hooks/useHackathons" +import { getPublicClient } from "@wagmi/core" +import { config } from "@/utils/config" +import { HACKHUB_ABI } from "@/utils/contractABI/HackHub" +import { formatEther } from "viem" +import { + Trophy, + Users, + Target, + Calendar, + Vote, + Gavel, + Loader2, + AlertCircle, + RefreshCw, + Eye, + ArrowLeft, + Code, + FileText, + ExternalLink, + Search, + SortAsc, + SortDesc, + Award +} from "lucide-react" +import { useChainId, useAccount } from "wagmi" +import Link from "next/link" +import { formatUTCTimestamp } from '@/utils/timeUtils' +import { hackathonDB } from '@/lib/indexedDB' + +// ERC20 ABI for token symbol/decimals +const ERC20_ABI = [ + { + "inputs": [], + "name": "symbol", + "outputs": [{"internalType": "string", "name": "", "type": "string"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [{"internalType": "uint8", "name": "", "type": "uint8"}], + "stateMutability": "view", + "type": "function" + } +] as const + +type SortOption = 'votes-desc' | 'votes-asc' | 'name-asc' | 'name-desc' | 'recent' | 'oldest' + +export default function ProjectsClient() { + const [hackathonData, setHackathonData] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [syncing, setSyncing] = useState(false) + const [lastSynced, setLastSynced] = useState(null) + const [approvedTokens, setApprovedTokens] = useState([]) + const [tokenSymbols, setTokenSymbols] = useState>({}) + const [tokenTotals, setTokenTotals] = useState>({}) + const [tokenDecimals, setTokenDecimals] = useState>({}) + + // Filter and sort state + const [searchTerm, setSearchTerm] = useState("") + const [sortBy, setSortBy] = useState('votes-desc') + const [filteredProjects, setFilteredProjects] = useState([]) + + // Modal state for external links + const [modalOpen, setModalOpen] = useState(false) + const [modalLink, setModalLink] = useState({ url: '', type: '' }) + + const searchParams = useSearchParams() + const router = useRouter() + const hackAddr = searchParams.get('hackAddr') + const urlChainId = searchParams.get('chainId') + + const chainId = useChainId() + const { address: userAddress, isConnected } = useAccount() + + // Validate hackAddr format + const contractAddress = hackAddr && hackAddr.match(/^0x[a-fA-F0-9]{40}$/) + ? hackAddr as `0x${string}` + : null + + // Network info + const getNetworkName = (chainId: number) => { + switch (chainId) { + case 534351: return "Scroll Sepolia" + case 84532: return "Base Sepolia" + case 1: return "Ethereum Mainnet" + case 137: return "Polygon" + case 11155111: return "Sepolia" + default: return `Unknown (${chainId})` + } + } + + const short = (addr: string) => `${addr.slice(0,6)}...${addr.slice(-4)}` + + // Handle opening external links with modal + const handleOpenLink = (url: string, type: string) => { + setModalLink({ url, type }) + setModalOpen(true) + } + + // Format token amounts for display + const formatTokenAmount = (amount: bigint, token: string): string => { + if (token === '0x0000000000000000000000000000000000000000') { + // ETH - convert from wei to ether for display with up to 4 decimal places + const etherValue = Number(formatEther(amount)) + const result = etherValue.toFixed(4) + // Remove trailing zeros and decimal point if not needed + return parseFloat(result).toString() + } else { + // ERC20 - convert using token decimals for display + const decimals = tokenDecimals[token] ?? 18 + const divisor = BigInt(10) ** BigInt(decimals) + const wholeTokens = amount / divisor + const result = wholeTokens.toString() + return result + } + } + + // Manual sync function for the sync button + const handleSync = async () => { + setSyncing(true) + setError(null) + await fetchHackathonData() + setSyncing(false) + } + + // Fetch hackathon data from contract + const fetchHackathonData = async () => { + if (!contractAddress) { + setError('Invalid hackathon address provided') + setLoading(false) + return + } + + try { + setLoading(true) + setError(null) + + const publicClient = getPublicClient(config) + + const [ + name, + startDate, + startTime, + endDate, + endTime, + totalTokens, + concluded, + organizer, + factory, + judgeCount, + projectCount, + imageURL, + ] = await Promise.all([ + publicClient.readContract({ address: contractAddress, abi: HACKHUB_ABI, functionName: 'name' }) as Promise, + publicClient.readContract({ address: contractAddress, abi: HACKHUB_ABI, functionName: 'startDate' }) as Promise, + publicClient.readContract({ address: contractAddress, abi: HACKHUB_ABI, functionName: 'startTime' }) as Promise, + publicClient.readContract({ address: contractAddress, abi: HACKHUB_ABI, functionName: 'endDate' }) as Promise, + publicClient.readContract({ address: contractAddress, abi: HACKHUB_ABI, functionName: 'endTime' }) as Promise, + publicClient.readContract({ address: contractAddress, abi: HACKHUB_ABI, functionName: 'totalTokens' }) as Promise, + publicClient.readContract({ address: contractAddress, abi: HACKHUB_ABI, functionName: 'concluded' }) as Promise, + publicClient.readContract({ address: contractAddress, abi: HACKHUB_ABI, functionName: 'owner' }) as Promise, + publicClient.readContract({ address: contractAddress, abi: HACKHUB_ABI, functionName: 'factory' }) as Promise, + publicClient.readContract({ address: contractAddress, abi: HACKHUB_ABI, functionName: 'judgeCount' }) as Promise, + publicClient.readContract({ address: contractAddress, abi: HACKHUB_ABI, functionName: 'projectCount' }) as Promise, + publicClient.readContract({ address: contractAddress, abi: HACKHUB_ABI, functionName: 'imageURL' }) as Promise, + ]) + + // Load approved tokens and their symbols + let localApprovedTokens: string[] = [] + let localTokenTotals: Record = {} + let localTokenSymbols: Record = {} + try { + const tokens = await publicClient.readContract({ address: contractAddress, abi: HACKHUB_ABI, functionName: 'getApprovedTokensList' }) as string[] + setApprovedTokens(tokens) + const symbols: Record = {} + const totals: Record = {} + const decimalsMap: Record = {} + for (const t of tokens) { + try { + const total = await publicClient.readContract({ address: contractAddress, abi: HACKHUB_ABI, functionName: 'getTokenTotal', args: [t as `0x${string}`] }) as bigint + totals[t] = total + } catch (e) { + totals[t] = BigInt(0) + } + try { + if (t === '0x0000000000000000000000000000000000000000') { + symbols[t] = 'ETH' + decimalsMap[t] = 18 + } else { + const sym = await publicClient.readContract({ address: t as `0x${string}`, abi: ERC20_ABI, functionName: 'symbol' }) as string + symbols[t] = sym + try { + const dec = await publicClient.readContract({ address: t as `0x${string}`, abi: ERC20_ABI, functionName: 'decimals' }) as number + decimalsMap[t] = dec + } catch {} + } + } catch { + symbols[t] = t === '0x0000000000000000000000000000000000000000' ? 'ETH' : short(t) + if (t === '0x0000000000000000000000000000000000000000') { + decimalsMap[t] = 18 + } + } + } + setTokenSymbols(symbols) + setTokenTotals(totals) + setTokenDecimals(decimalsMap) + localApprovedTokens = tokens + localTokenTotals = totals + localTokenSymbols = symbols + } catch (e) { + setApprovedTokens([]) + setTokenSymbols({}) + setTokenTotals({}) + localApprovedTokens = [] + localTokenTotals = {} + localTokenSymbols = {} + } + + // Fetch judges + const judges: Judge[] = [] + if (Number(judgeCount) > 0) { + try { + const judgeAddresses = await publicClient.readContract({ + address: contractAddress, + abi: HACKHUB_ABI, + functionName: 'getAllJudges' + }) as string[] + + for (let i = 0; i < judgeAddresses.length; i++) { + try { + const [judgeTokens, remainingTokens] = await Promise.all([ + publicClient.readContract({ + address: contractAddress, + abi: HACKHUB_ABI, + functionName: 'judgeTokens', + args: [judgeAddresses[i] as `0x${string}`] + }) as Promise, + publicClient.readContract({ + address: contractAddress, + abi: HACKHUB_ABI, + functionName: 'remainingJudgeTokens', + args: [judgeAddresses[i] as `0x${string}`] + }) as Promise + ]) + + judges.push({ + address: judgeAddresses[i], + name: `Judge ${i + 1}`, + tokensAllocated: Number(judgeTokens), + tokensRemaining: Number(remainingTokens) + }) + } catch (judgeError) { + console.error(`Error fetching judge ${i} data:`, judgeError) + } + } + } catch (judgeError) { + console.error('Error fetching judges:', judgeError) + } + } + + // Fetch projects + const projects: Project[] = [] + if (Number(projectCount) > 0) { + for (let i = 0; i < Number(projectCount); i++) { + try { + const [projectInfo, projectTokens, prizeClaimed] = await Promise.all([ + publicClient.readContract({ + address: contractAddress, + abi: HACKHUB_ABI, + functionName: 'projects', + args: [BigInt(i)] + }) as Promise<[string, string, string, string, string]>, + publicClient.readContract({ + address: contractAddress, + abi: HACKHUB_ABI, + functionName: 'projectTokens', + args: [BigInt(i)] + }) as Promise, + publicClient.readContract({ + address: contractAddress, + abi: HACKHUB_ABI, + functionName: 'prizeClaimed', + args: [BigInt(i)] + }) as Promise + ]) + + const total = Number(totalTokens) + const sharePercent = total > 0 ? (Number(projectTokens) / total) * 100 : 0 + const payouts = localApprovedTokens.map((t) => { + const poolTotal = localTokenTotals[t] ?? BigInt(0) + const denom = totalTokens === BigInt(0) ? BigInt(1) : totalTokens + const amount = (poolTotal * (projectTokens as bigint)) / denom + return { token: t, amount: Math.floor(Number(amount)).toString(), symbol: localTokenSymbols[t] } + }) + projects.push({ + id: i, + submitter: projectInfo[0], + recipient: projectInfo[1], + name: projectInfo[2], + sourceCode: projectInfo[3], + docs: projectInfo[4], + tokensReceived: Number(projectTokens), + estimatedPrize: 0, + formattedPrize: `${sharePercent.toFixed(2)}% of each token pool`, + payouts, + prizeClaimed + }) + } catch (projectError) { + console.error(`Error fetching project ${i}:`, projectError) + } + } + } + + const hackathon: HackathonData = { + id: 0, + contractAddress, + hackathonName: name, + startDate: Number(startDate), + startTime: Number(startTime), + endDate: Number(endDate), + endTime: Number(endTime), + prizePool: '0', + totalTokens: Number(totalTokens), + concluded, + organizer, + factory, + judgeCount: Number(judgeCount), + projectCount: Number(projectCount), + judges, + projects, + image: imageURL || getImagePath("/block.png"), + } + + setHackathonData(hackathon) + setLastSynced(new Date()) + } catch (err) { + console.error('Error fetching hackathon data:', err) + setError('Failed to load hackathon data from blockchain') + } finally { + setLoading(false) + } + } + + // Filter and sort projects + useEffect(() => { + if (!hackathonData?.projects) { + setFilteredProjects([]) + return + } + + let filtered = hackathonData.projects + + // Apply search filter + if (searchTerm) { + filtered = filtered.filter(project => + project.name?.toLowerCase().includes(searchTerm.toLowerCase()) || + project.submitter.toLowerCase().includes(searchTerm.toLowerCase()) || + project.recipient.toLowerCase().includes(searchTerm.toLowerCase()) + ) + } + + // Apply sorting + const sorted = [...filtered].sort((a, b) => { + switch (sortBy) { + case 'votes-desc': + return b.tokensReceived - a.tokensReceived + case 'votes-asc': + return a.tokensReceived - b.tokensReceived + case 'name-asc': + return (a.name || `Project ${a.id}`).localeCompare(b.name || `Project ${b.id}`) + case 'name-desc': + return (b.name || `Project ${b.id}`).localeCompare(a.name || `Project ${a.id}`) + case 'recent': + return b.id - a.id + case 'oldest': + return a.id - b.id + default: + return b.tokensReceived - a.tokensReceived + } + }) + + setFilteredProjects(sorted) + }, [hackathonData?.projects, searchTerm, sortBy]) + + // Load data on mount + useEffect(() => { + fetchHackathonData() + }, [contractAddress, chainId]) + + // Show error if no hackAddr provided + if (!hackAddr) { + return ( +
+ + + + No hackathon address provided. Please access this page through a valid hackathon link. + + +
+ ) + } + + // Show error if invalid hackAddr format + if (!contractAddress) { + return ( +
+ + + + Invalid hackathon address format. Please check the URL and try again. + + +
+ ) + } + + if (loading) { + return ( +
+
+
+ +

Loading hackathon projects...

+
+
+
+ ) + } + + if (error || !hackathonData) { + return ( +
+ + + +
+ {error || 'Hackathon not found'} +
+ +
+
+
+ ) + } + + const status = getHackathonStatus(hackathonData.startTime, hackathonData.endTime, hackathonData.concluded) + + const getStatusBadge = () => { + switch (status) { + case 'upcoming': + return Upcoming + case 'accepting-submissions': + return Accepting Submissions + case 'judging-submissions': + return Judging Submissions + case 'concluded': + return Concluded + default: + return Unknown + } + } + + return ( +
+ {/* Header */} +
+ {/* Left: Back Button */} +
+ + + +
+ + {/* Center: Title and Info */} +
+
+

+ {hackathonData.hackathonName} - Projects +

+ {getStatusBadge()} +
+

+ {hackathonData.projects.length} projects submitted • Total voting tokens: {hackathonData.totalTokens} +

+
+ + {/* Right: Sync Button */} +
+ +
+
+ + {/* Filters and Search */} + + +
+
+
+ + setSearchTerm(e.target.value)} + className="pl-10 bg-white border-gray-300" + /> +
+
+
+ +
+
+
+
+ + {/* Projects Grid */} + {filteredProjects.length === 0 ? ( + + + +

+ {hackathonData.projects.length === 0 ? 'No Projects Submitted' : 'No Projects Found'} +

+

+ {hackathonData.projects.length === 0 + ? 'No projects have been submitted to this hackathon yet.' + : 'Try adjusting your search or filter criteria.' + } +

+
+
+ ) : ( +
+ {filteredProjects.map((project) => ( + + +
+
+
+ + {project.name || `Project #${project.id}`} + + {project.prizeClaimed && ( + + + Prize Claimed + + )} +
+
+ Submitted by: + + + {project.submitter.slice(2, 4).toUpperCase()} + + + {short(project.submitter)} +
+ {project.recipient !== project.submitter && ( +
+ Prize recipient: + + + {project.recipient.slice(2, 4).toUpperCase()} + + + {short(project.recipient)} +
+ )} +
+
+
+ + {project.tokensReceived} +
+

votes received

+
+
+
+ +
+ {/* Prize Information */} +
+
+ + Prize Breakdown +
+

{project.formattedPrize}

+ {project.payouts && project.payouts.length > 0 && ( +
+ {project.payouts.map((payout, idx) => ( +
+ {payout.symbol === 'ETH' ? 'Native ETH' : payout.symbol}: {payout.amount} +
+ ))} +
+ )} +
+ + {/* Project Links */} +
+ + +
+
+
+
+ ))} +
+ )} + + {/* External Link Warning Modal */} + + + + + + External Link Warning + + + You are about to visit an external website. Please verify the URL before proceeding. + + +
+
+ +
+

{modalLink.url}

+
+
+
+ + + + +
+
+
+ ) +} diff --git a/app/projects/page.tsx b/app/projects/page.tsx new file mode 100644 index 0000000..1d540b8 --- /dev/null +++ b/app/projects/page.tsx @@ -0,0 +1,10 @@ +import { Suspense } from "react" +import ProjectsClient from "./ProjectsClient" + +export default function ProjectsPage() { + return ( + Loading...
}> + + + ) +} diff --git a/lib/indexedDB.ts b/lib/indexedDB.ts index c9249e2..6153ae2 100644 --- a/lib/indexedDB.ts +++ b/lib/indexedDB.ts @@ -1,5 +1,27 @@ import { HackathonData } from "@/hooks/useHackathons" +// Extended interface for hackathon details including all interaction data +interface ExtendedHackathonDetails { + // Basic hackathon data + hackathonData: HackathonData + // Additional interaction data + approvedTokens: string[] + tokenMinAmounts: Record // Store bigint as string for serialization + tokenSymbols: Record + tokenTotals: Record // Store bigint as string for serialization + tokenDecimals: Record + sponsors: Array<{ + address: string + name: string + image: string + contributions: Array<{ token: string; amount: string }> // Store bigint as string + }> + // Metadata + timestamp: number + chainId: number + contractAddress: string +} + interface CacheEntry { data: any timestamp: number @@ -39,7 +61,7 @@ interface OrganizerHackathonsCache { class HackathonDB { private dbName = 'HackathonDB' - private version = 1 + private version = 2 // Increment version to handle schema changes private db: IDBDatabase | null = null private cacheExpiration = 5 * 60 * 1000 // 5 minutes @@ -321,6 +343,120 @@ class HackathonDB { } } + // Extended hackathon details methods (includes all interaction data) + async getExtendedHackathonDetails(contractAddress: string, chainId: number): Promise<{ + hackathonData: HackathonData + approvedTokens: string[] + tokenMinAmounts: Record + tokenSymbols: Record + tokenTotals: Record + tokenDecimals: Record + sponsors: Array<{ + address: string + name: string + image: string + contributions: Array<{ token: string; amount: bigint }> + }> + timestamp: number + chainId: number + contractAddress: string + } | null> { + try { + const { store } = await this.openStore('hackathonDetails', 'readonly') + const cacheKey = `extended_${contractAddress}` + + return new Promise((resolve, reject) => { + const request = store.get(cacheKey) + request.onsuccess = () => { + const result = request.result as ExtendedHackathonDetails | undefined + if (result && !this.isExpired(result.timestamp) && result.chainId === chainId) { + console.log('📊 IndexedDB: Retrieved extended data with judges:', result.hackathonData?.judges?.length, 'sponsors:', result.sponsors?.length) + // Convert string bigints back to bigint + const processed = { + ...result, + tokenMinAmounts: Object.fromEntries( + Object.entries(result.tokenMinAmounts).map(([k, v]) => [k, BigInt(v)]) + ), + tokenTotals: Object.fromEntries( + Object.entries(result.tokenTotals).map(([k, v]) => [k, BigInt(v)]) + ), + sponsors: result.sponsors.map(sponsor => ({ + ...sponsor, + contributions: sponsor.contributions.map(contrib => ({ + ...contrib, + amount: BigInt(contrib.amount) + })) + })) + } + resolve(processed as any) + } else { + resolve(null) + } + } + request.onerror = () => reject(request.error) + }) + } catch (error) { + console.error('Error getting extended hackathon details from cache:', error) + return null + } + } + + async setExtendedHackathonDetails( + contractAddress: string, + chainId: number, + data: { + hackathonData: HackathonData + approvedTokens: string[] + tokenMinAmounts: Record + tokenSymbols: Record + tokenTotals: Record + tokenDecimals: Record + sponsors: Array<{ + address: string + name: string + image: string + contributions: Array<{ token: string; amount: bigint }> + }> + } + ): Promise { + try { + const { store } = await this.openStore('hackathonDetails', 'readwrite') + const cacheKey = `extended_${contractAddress}` + + // Convert bigints to strings for serialization + const entry: ExtendedHackathonDetails = { + ...data, + tokenMinAmounts: Object.fromEntries( + Object.entries(data.tokenMinAmounts).map(([k, v]) => [k, v.toString()]) + ), + tokenTotals: Object.fromEntries( + Object.entries(data.tokenTotals).map(([k, v]) => [k, v.toString()]) + ), + sponsors: data.sponsors.map(sponsor => ({ + ...sponsor, + contributions: sponsor.contributions.map(contrib => ({ + ...contrib, + amount: contrib.amount.toString() + })) + })), + timestamp: Date.now(), + chainId, + contractAddress: cacheKey + } + + return new Promise((resolve, reject) => { + const request = store.put(entry) + request.onsuccess = () => { + console.log('📊 IndexedDB: Saved extended data with judges:', data.hackathonData?.judges?.length, 'sponsors:', data.sponsors?.length) + resolve() + } + request.onerror = () => reject(request.error) + }) + } catch (error) { + console.error('Error setting extended hackathon details cache:', error) + } + } + // Clear all data for a specific chain async clearChainData(chainId: number): Promise { try { diff --git a/utils/contractAddress.ts b/utils/contractAddress.ts index 6d88832..6b07ba9 100644 --- a/utils/contractAddress.ts +++ b/utils/contractAddress.ts @@ -1,6 +1,5 @@ export const HackHubFactoryAddress: { [key: number]: `0x${string}` } = { - // 534351: '0x3afd509838f06493d55ea63e0b0963b3d54955d9', - 534351: '0xf0fd31e412260b928b39efd6e2ea32e47e3b54b2', + 534351: '0x8b5f09679b9a09992eebc0a5d13dc955d9864cf8', } export const getFactoryAddress = (chainId: number): `0x${string}` | undefined => {