diff --git a/backend/leaderboard/views.py b/backend/leaderboard/views.py index f5b9e16f..61b62b04 100644 --- a/backend/leaderboard/views.py +++ b/backend/leaderboard/views.py @@ -204,7 +204,8 @@ def monthly(self, request): from users.serializers import LightUserSerializer leaderboard_type = request.query_params.get('type', 'validator') - if leaderboard_type not in LEADERBOARD_CONFIG: + monthly_types = set(LEADERBOARD_CONFIG.keys()) | {'community'} + if leaderboard_type not in monthly_types: return Response( {'detail': f'Unknown leaderboard type: {leaderboard_type}'}, status=status.HTTP_400_BAD_REQUEST, diff --git a/frontend/src/components/portal/PortalContributionCard.svelte b/frontend/src/components/portal/PortalContributionCard.svelte index 73b3b711..df944e51 100644 --- a/frontend/src/components/portal/PortalContributionCard.svelte +++ b/frontend/src/components/portal/PortalContributionCard.svelte @@ -2,7 +2,7 @@ import { push } from 'svelte-spa-router'; import { format } from 'date-fns'; - let { contribution, category = null, height = 180 } = $props(); + let { contribution, category = null, height = 180, pathPrefix = '/contribution' } = $props(); function getCategoryColors(cat) { const map = { @@ -67,7 +67,7 @@ function handleCardClick(event) { if (event.target.closest('button') || event.target.closest('a')) return; - if (realId) push(`/contribution/${realId}`); + if (realId) push(`${pathPrefix}/${realId}`); } function handleKeydown(event) { diff --git a/frontend/src/routes/Dashboard.svelte b/frontend/src/routes/Dashboard.svelte index 2cde3c73..c81b73a4 100644 --- a/frontend/src/routes/Dashboard.svelte +++ b/frontend/src/routes/Dashboard.svelte @@ -10,6 +10,7 @@ import RankedList from '../components/ui/RankedList.svelte'; import UserCardScroller from '../components/ui/UserCardScroller.svelte'; import PortalHighlights from '../components/portal/PortalHighlights.svelte'; + import PortalContributionCard from '../components/portal/PortalContributionCard.svelte'; import CTASection from '../components/ui/CTASection.svelte'; import Podium from '../components/ui/Podium.svelte'; @@ -31,6 +32,9 @@ let waitlistLoading = $state(true); let trendingLoading = $state(true); let recentLoading = $state(true); + let recentSlider = $state(null); + let canRecentLeft = $state(false); + let canRecentRight = $state(false); let category = $derived($currentCategory); let isBuilder = $derived(category === 'builder'); @@ -42,11 +46,21 @@ isBuilder ? "Builder's Live Dashboard" : isCommunity ? "Community Live Dashboard" : "Validator's Live Dashboard" ); let leaderboardTitle = $derived(isCommunity ? 'Top Community Contributors' : 'Top Contributors'); - let leaderboardSubtitle = $derived(isCommunity ? 'Highest community contribution points' : 'This month curated builds'); + let leaderboardSubtitle = $derived( + isCommunity + ? 'This month community contributions' + : isValidator + ? 'All-time validator contributors' + : 'This month curated builds' + ); let leaderboardPath = $derived(isBuilder ? '/builders/leaderboard' : isCommunity ? '/community/leaderboard' : '/validators/leaderboard'); - let podiumTitle = $derived(isCommunity ? 'Community Podium' : "This month's Podium"); + let podiumTitle = $derived(isValidator ? 'All-time Podium' : "This month's Podium"); let podiumSubtitle = $derived( - isCommunity ? "Who's contributing most to the community?" : "Who's contributing more to GenLayer this month?" + isCommunity + ? "Who's contributing most to the community this month?" + : isValidator + ? "Who's contributed most to GenLayer?" + : "Who's contributing more to GenLayer this month?" ); let newestTitle = $derived(isBuilder ? 'Newest Builders' : isCommunity ? 'Newest Community Contributors' : 'Newest Validators'); let newestPath = $derived(isBuilder ? '/builders/leaderboard' : isCommunity ? '/community/all-contributions' : '/validators/participants'); @@ -78,8 +92,8 @@ if (cat === 'community') { return [ { value: data.community_member_count ?? data.creator_count ?? data.participant_count, label: 'Community Members', delta: data.new_community_members_count || '', category: 'community' }, - { value: data.total_points, label: 'Community points earned', delta: data.new_points_count || '', iconSrc: '/assets/icons/gradient-icon-points.svg' }, - { value: data.contribution_count, label: 'Community Contributions', delta: data.new_contributions_count || '', iconSrc: '/assets/icons/gradient-icon-contributions.svg' }, + { value: data.total_points, label: 'Community points earned', delta: data.new_points_count || '', category: 'genlayer', hexCategory: 'community' }, + { value: data.contribution_count, label: 'Community Contributions', delta: data.new_contributions_count || '', category: 'community' }, ]; } // validator @@ -101,11 +115,11 @@ statsLoading = false; }).catch(() => { statsLoading = false; }), - // Top contributors. Community uses actual community contribution points, - // not referral points. - (cat === 'community' - ? leaderboardAPI.getLeaderboard({ type: 'community', limit: 5 }) - : leaderboardAPI.getMonthlyLeaderboardByType(cat, 5) + // Top contributors. Validator dashboard is intentionally all-time; + // builder and community dashboards use current-month contribution totals. + (cat === 'validator' + ? leaderboardAPI.getLeaderboard({ type: 'validator', order: 'asc', limit: 5 }) + : leaderboardAPI.getMonthlyLeaderboardByType(cat, 5) ).then(res => { leaderboardEntries = Array.isArray(res.data) ? res.data : (res.data?.results ?? []); leaderboardLoading = false; @@ -190,6 +204,27 @@ if (isBuilder) return `/builders/contribution/${contrib.id}`; return `/badge/${contrib.id}`; } + + function updateRecentArrows() { + if (!recentSlider) return; + const { scrollLeft, scrollWidth, clientWidth } = recentSlider; + canRecentLeft = scrollLeft > 4; + canRecentRight = scrollLeft + clientWidth < scrollWidth - 4; + } + + function scrollRecent(direction) { + if (!recentSlider) return; + recentSlider.scrollBy({ + left: direction * Math.round(recentSlider.clientWidth * 0.8), + behavior: 'smooth', + }); + } + + $effect(() => { + if (!recentSlider) return; + void recentContributions.length; + requestAnimationFrame(updateRecentArrows); + });