diff --git a/public/avatar.png b/public/avatar.png new file mode 100644 index 0000000..ecc0704 Binary files /dev/null and b/public/avatar.png differ diff --git a/public/avatar1.png b/public/avatar1.png new file mode 100644 index 0000000..ccb205e Binary files /dev/null and b/public/avatar1.png differ diff --git a/public/cuida_copy-outline.png b/public/cuida_copy-outline.png new file mode 100644 index 0000000..ea493c4 Binary files /dev/null and b/public/cuida_copy-outline.png differ diff --git a/src/app/globals.css b/src/app/globals.css index 37d72f8..5b60c55 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -24,3 +24,14 @@ body { color: var(--foreground); font-family: Arial, Helvetica, sans-serif; } + +/* Hide scrollbar for Chrome, Safari and Opera */ +.scrollbar-hide::-webkit-scrollbar { + display: none; +} + +/* Hide scrollbar for IE, Edge and Firefox */ +.scrollbar-hide { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ +} diff --git a/src/app/leaderboard/loading.tsx b/src/app/leaderboard/loading.tsx new file mode 100644 index 0000000..00a7ba2 --- /dev/null +++ b/src/app/leaderboard/loading.tsx @@ -0,0 +1,212 @@ +'use client' + +import React, { useState } from 'react' +import { Trophy, Award, Star } from 'lucide-react' + +const Leaderboard = () => { + const [timeframe, setTimeframe] = useState<'weekly' | 'monthly' | 'allTime'>( + 'monthly' + ) + + const leaderboardData = { + organizations: [ + { + rank: 1, + name: 'EcoThailand', + impact: 2500, + projects: 15, + rebaz: 45000, + }, + { + rank: 2, + name: 'Clean Phangan', + impact: 2100, + projects: 12, + rebaz: 38000, + }, + { + rank: 3, + name: 'Forest Guardian', + impact: 1800, + projects: 10, + rebaz: 32000, + }, + { + rank: 4, + name: 'Ocean Care', + impact: 1500, + projects: 8, + rebaz: 28000, + }, + { + rank: 5, + name: 'Green Future', + impact: 1200, + projects: 6, + rebaz: 25000, + }, + ], + individuals: [ + { + rank: 1, + name: 'Sarah L.', + impact: 850, + contributions: 25, + rebaz: 15000, + }, + { + rank: 2, + name: 'Michael R.', + impact: 720, + contributions: 20, + rebaz: 12000, + }, + { + rank: 3, + name: 'Emma T.', + impact: 650, + contributions: 18, + rebaz: 10000, + }, + { + rank: 4, + name: 'James K.', + impact: 580, + contributions: 15, + rebaz: 8500, + }, + { + rank: 5, + name: 'Lisa M.', + impact: 520, + contributions: 12, + rebaz: 7000, + }, + ], + } + + const getRankIcon = (rank: number) => { + switch (rank) { + case 1: + return + case 2: + return + case 3: + return + default: + return {rank} + } + } + + return ( +
+
+

+ IMPACT LEADERBOARD +

+ + {/* Timeframe Selection */} +
+ {[ + { value: 'weekly', label: 'This Week' }, + { value: 'monthly', label: 'This Month' }, + { value: 'allTime', label: 'All Time' }, + ].map(({ value, label }) => ( + + ))} +
+ +
+ {/* Organizations Leaderboard */} +
+

+ Top Organizations +

+
+
+
Organization
+
Impact
+
Projects
+
REBAZ
+
+ {leaderboardData.organizations.map((org) => ( +
+
+
+ {getRankIcon(org.rank)} +
+ {org.name} +
+
{org.impact}
+
{org.projects}
+
+ {org.rebaz.toLocaleString()} +
+
+ ))} +
+
+ + {/* Individual Contributors Leaderboard */} +
+

+ Top Contributors +

+
+
+
Contributor
+
Impact
+
Actions
+
REBAZ
+
+ {leaderboardData.individuals.map((individual) => ( +
+
+
+ {getRankIcon(individual.rank)} +
+ {individual.name} +
+
+ {individual.impact} +
+
+ {individual.contributions} +
+
+ {individual.rebaz.toLocaleString()} +
+
+ ))} +
+
+
+
+
+ ) +} + +// Original component: Leaderboard; +// Auto-generated App Router page wrapper +// Original file: Leaderboard.tsx +// Generated: 2025-05-30T01:49:22.850Z + +export default function Page() { + return +} diff --git a/src/app/leaderboard/page.tsx b/src/app/leaderboard/page.tsx index 00a7ba2..2532033 100644 --- a/src/app/leaderboard/page.tsx +++ b/src/app/leaderboard/page.tsx @@ -1,212 +1,5 @@ -'use client' +import { LeaderboardMain } from '@/components/leaderboard/LeaderboardMain' -import React, { useState } from 'react' -import { Trophy, Award, Star } from 'lucide-react' - -const Leaderboard = () => { - const [timeframe, setTimeframe] = useState<'weekly' | 'monthly' | 'allTime'>( - 'monthly' - ) - - const leaderboardData = { - organizations: [ - { - rank: 1, - name: 'EcoThailand', - impact: 2500, - projects: 15, - rebaz: 45000, - }, - { - rank: 2, - name: 'Clean Phangan', - impact: 2100, - projects: 12, - rebaz: 38000, - }, - { - rank: 3, - name: 'Forest Guardian', - impact: 1800, - projects: 10, - rebaz: 32000, - }, - { - rank: 4, - name: 'Ocean Care', - impact: 1500, - projects: 8, - rebaz: 28000, - }, - { - rank: 5, - name: 'Green Future', - impact: 1200, - projects: 6, - rebaz: 25000, - }, - ], - individuals: [ - { - rank: 1, - name: 'Sarah L.', - impact: 850, - contributions: 25, - rebaz: 15000, - }, - { - rank: 2, - name: 'Michael R.', - impact: 720, - contributions: 20, - rebaz: 12000, - }, - { - rank: 3, - name: 'Emma T.', - impact: 650, - contributions: 18, - rebaz: 10000, - }, - { - rank: 4, - name: 'James K.', - impact: 580, - contributions: 15, - rebaz: 8500, - }, - { - rank: 5, - name: 'Lisa M.', - impact: 520, - contributions: 12, - rebaz: 7000, - }, - ], - } - - const getRankIcon = (rank: number) => { - switch (rank) { - case 1: - return - case 2: - return - case 3: - return - default: - return {rank} - } - } - - return ( -
-
-

- IMPACT LEADERBOARD -

- - {/* Timeframe Selection */} -
- {[ - { value: 'weekly', label: 'This Week' }, - { value: 'monthly', label: 'This Month' }, - { value: 'allTime', label: 'All Time' }, - ].map(({ value, label }) => ( - - ))} -
- -
- {/* Organizations Leaderboard */} -
-

- Top Organizations -

-
-
-
Organization
-
Impact
-
Projects
-
REBAZ
-
- {leaderboardData.organizations.map((org) => ( -
-
-
- {getRankIcon(org.rank)} -
- {org.name} -
-
{org.impact}
-
{org.projects}
-
- {org.rebaz.toLocaleString()} -
-
- ))} -
-
- - {/* Individual Contributors Leaderboard */} -
-

- Top Contributors -

-
-
-
Contributor
-
Impact
-
Actions
-
REBAZ
-
- {leaderboardData.individuals.map((individual) => ( -
-
-
- {getRankIcon(individual.rank)} -
- {individual.name} -
-
- {individual.impact} -
-
- {individual.contributions} -
-
- {individual.rebaz.toLocaleString()} -
-
- ))} -
-
-
-
-
- ) -} - -// Original component: Leaderboard; -// Auto-generated App Router page wrapper -// Original file: Leaderboard.tsx -// Generated: 2025-05-30T01:49:22.850Z - -export default function Page() { - return +export default function Leaderboard() { + return } diff --git a/src/components/leaderboard/LeaderboardConfig.ts b/src/components/leaderboard/LeaderboardConfig.ts new file mode 100644 index 0000000..defb1e8 --- /dev/null +++ b/src/components/leaderboard/LeaderboardConfig.ts @@ -0,0 +1,79 @@ +import { + buyerLeaderboardData, + sellerLeaderboardData, +} from '@/mocks/leaderboardData' + +export type LeaderboardType = 'buyer' | 'seller' + +export interface LeaderboardColumn { + key: string + label: string + className?: string + format?: (value: string | number) => string +} + +export interface LeaderboardData { + rank: number + user: string + [key: string]: string | number | boolean +} + +export const LEADERBOARD_CONFIGS = { + buyer: { + title: 'Buyer Leaderboard', + columns: [ + { key: 'rank', label: 'Rank', className: 'text-center' }, + { key: 'user', label: 'User', className: 'text-center' }, + { + key: 'ip_owned', + label: 'Number of IP owned', + className: 'text-center', + }, + { + key: 'impact_score', + label: 'Total Impact Score', + className: 'text-center', + }, + { + key: 'rebaz_spent', + label: '$REBAZ spent', + className: 'text-center', + format: (value) => `$${value}`, + }, + { + key: 'staked', + label: 'Staked $REBAR', + className: '', + format: (value) => `$${value}`, + }, + ] as LeaderboardColumn[], + data: buyerLeaderboardData, + }, + seller: { + title: 'Seller Leaderboard', + columns: [ + { key: 'rank', label: 'Rank', className: 'text-center' }, + { key: 'user', label: 'User', className: 'text-center' }, + { key: 'ip_created', label: 'IP Created', className: 'text-center' }, + { + key: 'total_sales', + label: 'Total Impact Generated', + className: 'text-center', + }, + { + key: 'rebaz_earned', + label: '$REBAZ earned', + className: 'text-center', + format: (value) => `$${value}`, + }, + { key: 'ip_sold', label: 'Number of IP Sold', className: 'text-center' }, + { + key: 'staked', + label: 'Staked $REBAR', + className: '', + format: (value) => `$${value}`, + }, + ] as LeaderboardColumn[], + data: sellerLeaderboardData, + }, +} diff --git a/src/components/leaderboard/LeaderboardMain.tsx b/src/components/leaderboard/LeaderboardMain.tsx new file mode 100644 index 0000000..79be4b2 --- /dev/null +++ b/src/components/leaderboard/LeaderboardMain.tsx @@ -0,0 +1,99 @@ +'use client' + +import { useEffect, useState } from 'react' +import { userService, UserProfile } from '@/lib/userService' +import { useWallet } from '@/context/WalletContext' +import { UserProfile as UserProfileComponent } from './UserProfile' +import { LeaderboardTable } from './LeaderboardTable' + +import { + LEADERBOARD_CONFIGS, + LeaderboardData, + LeaderboardType, +} from './LeaderboardConfig' + +interface LeaderboardMainProps { + type?: LeaderboardType +} + +export function LeaderboardMain({ type = 'buyer' }: LeaderboardMainProps) { + const { walletAddress } = useWallet() + const [userProfiles, setUserProfiles] = useState([]) + const [currentUserProfile, setCurrentUserProfile] = + useState(null) + const [loading, setLoading] = useState(true) + + const currentConfig = LEADERBOARD_CONFIGS[type] + + useEffect(() => { + const fetchProfiles = async () => { + try { + // Extract wallet addresses from leaderboard data + const walletAddresses = currentConfig.data.map( + (item: LeaderboardData) => item.user + ) + + // Fetch profiles for all users in leaderboard + const profiles = + await userService.getLeaderboardProfiles(walletAddresses) + console.log('profiles', profiles) + setUserProfiles(profiles) + + // Fetch current user profile if wallet is connected + if (walletAddress) { + const currentProfile = await userService.getUserProfile(walletAddress) + setCurrentUserProfile(currentProfile) + } + } catch (error) { + console.error('Error fetching profiles:', error) + } finally { + setLoading(false) + } + } + + fetchProfiles() + }, [walletAddress, type, currentConfig.data]) + + const getUserProfile = (userAddress: string) => { + return userProfiles.find( + (profile) => profile.wallet_address === userAddress + ) + } + + const getAvatarUrl = (userAddress: string) => { + const profile = getUserProfile(userAddress) + if (profile) { + return userService.getAvatarUrl(profile) + } + return null + } + + if (loading) { + return ( +
+
+
Loading leaderboard...
+
+
+ ) + } + + return ( +
+
+
+ + + +
+
+
+ ) +} diff --git a/src/components/leaderboard/LeaderboardTable.tsx b/src/components/leaderboard/LeaderboardTable.tsx new file mode 100644 index 0000000..b4ade61 --- /dev/null +++ b/src/components/leaderboard/LeaderboardTable.tsx @@ -0,0 +1,81 @@ +import { Card, CardContent } from '@/components/ui/card' +import { UserAvatar } from './UserAvatar' +import { LeaderboardColumn, LeaderboardData } from './LeaderboardConfig' + +interface LeaderboardTableProps { + columns: LeaderboardColumn[] + data: LeaderboardData[] + getAvatarUrl: (userAddress: string) => string | null +} + +export function LeaderboardTable({ + columns, + data, + getAvatarUrl, +}: LeaderboardTableProps) { + return ( +
+
+
+ + + {/* Header */} +
+
+ {columns.map((column) => ( +
+ {column.label} +
+ ))} +
+
+ {/* Leaderboard Rows */} +
+ {data.map((item) => { + const avatarUrl = getAvatarUrl(item.user) + return ( +
+ {/* Rank */} +
+ {item.rank} +
+ {/* User */} +
+ + + {item.user} + +
+ {columns.slice(2).map((column) => ( +
+ {column.format + ? column.format(item[column.key] as string | number) + : (item[column.key] as string | number)} +
+ ))} +
+ ) + })} +
+
+
+
+
+
+ ) +} diff --git a/src/components/leaderboard/LeaderboardToggle.tsx b/src/components/leaderboard/LeaderboardToggle.tsx new file mode 100644 index 0000000..8a9b435 --- /dev/null +++ b/src/components/leaderboard/LeaderboardToggle.tsx @@ -0,0 +1,38 @@ +import { LeaderboardType } from './LeaderboardConfig' + +interface LeaderboardToggleProps { + currentType: LeaderboardType + onTypeChange: (type: LeaderboardType) => void +} + +export function LeaderboardToggle({ + currentType, + onTypeChange, +}: LeaderboardToggleProps) { + return ( +
+
+ + +
+
+ ) +} diff --git a/src/components/leaderboard/UserAvatar.tsx b/src/components/leaderboard/UserAvatar.tsx new file mode 100644 index 0000000..90d58f8 --- /dev/null +++ b/src/components/leaderboard/UserAvatar.tsx @@ -0,0 +1,49 @@ +import Image from 'next/image' + +interface UserAvatarProps { + avatarUrl?: string | null + alt?: string + size?: 'sm' | 'md' | 'lg' + className?: string +} + +const sizeClasses = { + sm: 'h-8 w-8', + md: 'h-[40px] w-[40px]', + lg: 'h-[120px] w-[120px]', +} + +export function UserAvatar({ + avatarUrl, + alt = 'User Avatar', + size = 'md', + className = '', +}: UserAvatarProps) { + const baseClasses = `${sizeClasses[size]} overflow-hidden rounded-xl ${className}` + + if (avatarUrl) { + return ( +
+ {alt} +
+ ) + } + + return ( +
+ {alt} +
+ ) +} diff --git a/src/components/leaderboard/UserProfile.tsx b/src/components/leaderboard/UserProfile.tsx new file mode 100644 index 0000000..f4e2145 --- /dev/null +++ b/src/components/leaderboard/UserProfile.tsx @@ -0,0 +1,110 @@ +import { Card, CardContent } from '@/components/ui/card' +import Image from 'next/image' +import { UserAvatar } from './UserAvatar' +import { userService, UserProfile as UserProfileType } from '@/lib/userService' + +interface UserProfileProps { + currentUserProfile: UserProfileType | null + walletAddress: string | null + portfolioData?: { + position: number + ipCollection: number + ipSold: number + totalFunding: number + referrals: number + } +} + +export function UserProfile({ + currentUserProfile, + walletAddress, + portfolioData = { + position: 7, + ipCollection: 32, + ipSold: 19, + totalFunding: 300, + referrals: 29, + }, +}: UserProfileProps) { + const avatarUrl = currentUserProfile + ? userService.getAvatarUrl(currentUserProfile) + : null + + return ( +
+ + +

My Profile

+ + {/* Profile Avatar */} +
+
+ +
+
+ + {/* Username */} +
+
+ + {walletAddress || '0xdafea492d9c673...'} + + Copy { + if (walletAddress) { + navigator.clipboard.writeText(walletAddress) + } + }} + /> +
+
+ + {/* Portfolio Overview */} +
+

Portfolio overview

+
+
+ + LEADERBOARD POSITION + + {portfolioData.position} +
+
+ IP COLLECTION + + {portfolioData.ipCollection} + +
+
+ IP SOLD + {portfolioData.ipSold} +
+
+ TOTAL QUADRATIC FUNDING + + ${portfolioData.totalFunding} + +
+
+ REFERRALS + + {portfolioData.referrals} + +
+
+
+
+
+
+ ) +} diff --git a/src/components/leaderboard/index.ts b/src/components/leaderboard/index.ts new file mode 100644 index 0000000..82beed0 --- /dev/null +++ b/src/components/leaderboard/index.ts @@ -0,0 +1,4 @@ +export { LeaderboardMain } from './LeaderboardMain' +export { UserProfile } from './UserProfile' +export { LeaderboardTable } from './LeaderboardTable' +export { UserAvatar } from './UserAvatar' diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..884c30c --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,31 @@ +import * as React from 'react' + +interface ButtonProps extends React.ButtonHTMLAttributes { + size?: 'sm' | 'md' | 'lg' + variant?: 'default' | 'ghost' +} + +export function Button({ + className, + size = 'md', + variant = 'default', + ...props +}: ButtonProps) { + const sizeClasses = { + sm: 'h-8 px-3 text-sm', + md: 'h-10 px-4', + lg: 'h-12 px-6 text-lg', + } + + const variantClasses = { + default: 'bg-primary text-primary-foreground hover:bg-primary/90', + ghost: 'hover:bg-accent hover:text-accent-foreground', + } + + return ( +