Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions backend/contributions/migrations/0037_seed_featured_content.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
from django.db import migrations


def seed_featured_content(apps, schema_editor):
User = apps.get_model('users', 'User')
FeaturedContent = apps.get_model('contributions', 'FeaturedContent')

albert = User.objects.get(email='albert@genlayer.foundation') # cognocracy
ivan = User.objects.get(email='ivan@genlayer.foundation') # raskovsky

FeaturedContent.objects.create(
content_type='hero',
title='Argue.fun Launch',
description='Deploy intelligent contracts, run validators, and earn GenLayer Points on the latest testnet.',
subtitle='cognocracy',
user=albert,
hero_image_url='https://res.cloudinary.com/dfqmoeawa/image/upload/v1772117991/tally/featured_1_hero_1772117989.png',
hero_image_public_id='tally/featured_1_hero_1772117989',
url='',
is_active=True,
order=0,
)

FeaturedContent.objects.create(
content_type='build',
title='Argue.fun',
description='',
subtitle='',
user=albert,
hero_image_url='https://res.cloudinary.com/dfqmoeawa/image/upload/v1772117992/tally/featured_2_hero_1772117992.png',
hero_image_public_id='tally/featured_2_hero_1772117992',
user_profile_image_url='https://res.cloudinary.com/dfqmoeawa/image/upload/v1772117994/tally/featured_2_avatar_1772117993.png',
user_profile_image_public_id='tally/featured_2_avatar_1772117993',
url='',
is_active=True,
order=0,
)

FeaturedContent.objects.create(
content_type='build',
title='Internet Court',
description='',
subtitle='',
user=ivan,
hero_image_url='https://res.cloudinary.com/dfqmoeawa/image/upload/v1772117994/tally/featured_3_hero_1772117994.png',
hero_image_public_id='tally/featured_3_hero_1772117994',
user_profile_image_url='https://res.cloudinary.com/dfqmoeawa/image/upload/v1772117995/tally/featured_3_avatar_1772117995.png',
user_profile_image_public_id='tally/featured_3_avatar_1772117995',
url='',
is_active=True,
order=1,
)

FeaturedContent.objects.create(
content_type='build',
title='Rally',
description='',
subtitle='',
user=ivan,
hero_image_url='https://res.cloudinary.com/dfqmoeawa/image/upload/v1772117996/tally/featured_4_hero_1772117995.png',
hero_image_public_id='tally/featured_4_hero_1772117995',
user_profile_image_url='https://res.cloudinary.com/dfqmoeawa/image/upload/v1772117996/tally/featured_4_avatar_1772117996.png',
user_profile_image_public_id='tally/featured_4_avatar_1772117996',
url='',
is_active=True,
order=2,
)


def reverse_featured_content(apps, schema_editor):
FeaturedContent = apps.get_model('contributions', 'FeaturedContent')
FeaturedContent.objects.filter(
title='Argue.fun Launch', content_type='hero'
).delete()
FeaturedContent.objects.filter(
title='Argue.fun', content_type='build'
).delete()
FeaturedContent.objects.filter(
title='Internet Court', content_type='build'
).delete()
FeaturedContent.objects.filter(
title='Rally', content_type='build'
).delete()


class Migration(migrations.Migration):

dependencies = [
('contributions', '0036_alert'),
]

operations = [
migrations.RunPython(seed_featured_content, reverse_featured_content),
]
50 changes: 48 additions & 2 deletions backend/leaderboard/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,9 @@ def stats(self, request):

leaderboard_type = request.query_params.get('type')

now = timezone.now()
month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)

if leaderboard_type:
# Category-specific stats
leaderboard_entries = LeaderboardEntry.objects.filter(
Expand All @@ -194,28 +197,67 @@ def stats(self, request):
category = category_map.get(leaderboard_type)

if category:
contribution_count = Contribution.objects.filter(
category_contributions = Contribution.objects.filter(
contribution_type__category__slug=category
).exclude(
contribution_type__slug__in=['builder-welcome', 'validator-waitlist']
)
contribution_count = category_contributions.count()
new_contributions_count = category_contributions.filter(
created_at__gte=month_start
).count()
new_points_count = category_contributions.filter(
created_at__gte=month_start
).aggregate(total=Sum('frozen_global_points'))['total'] or 0
else:
contribution_count = 0
new_contributions_count = 0
new_points_count = 0

# New participants this month for the specific leaderboard type
if leaderboard_type == 'builder':
new_builders_count = LeaderboardEntry.objects.filter(
type='builder', user__created_at__gte=month_start
).count()
new_validators_count = 0
elif leaderboard_type == 'validator':
new_builders_count = 0
new_validators_count = LeaderboardEntry.objects.filter(
type='validator', user__created_at__gte=month_start
).count()
else:
new_builders_count = 0
new_validators_count = 0
else:
# Global stats
participant_count = User.objects.filter(
contributions__isnull=False,
visible=True
).distinct().count()

contribution_count = Contribution.objects.exclude(
all_contributions = Contribution.objects.exclude(
contribution_type__slug__in=['builder-welcome', 'validator-waitlist']
)
contribution_count = all_contributions.count()
new_contributions_count = all_contributions.filter(
created_at__gte=month_start
).count()

total_points = Contribution.objects.aggregate(
total=Sum('frozen_global_points')
)['total'] or 0

new_points_count = Contribution.objects.filter(
created_at__gte=month_start
).aggregate(total=Sum('frozen_global_points'))['total'] or 0

new_builders_count = LeaderboardEntry.objects.filter(
type='builder', user__created_at__gte=month_start
).count()
new_validators_count = LeaderboardEntry.objects.filter(
type='validator', user__created_at__gte=month_start
).count()

# Category-specific counts (always included)
builder_count = LeaderboardEntry.objects.filter(
type='builder', user__visible=True
Expand All @@ -239,6 +281,10 @@ def stats(self, request):
'builder_count': builder_count,
'validator_count': validator_count,
'creator_count': creator_count,
'new_builders_count': new_builders_count,
'new_validators_count': new_validators_count,
'new_contributions_count': new_contributions_count,
'new_points_count': new_points_count,
})

def _get_user_stats(self, user, category=None):
Expand Down
20 changes: 20 additions & 0 deletions frontend/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,26 @@ const routes = {

### Components

#### Generic UI Components (`src/components/ui/`)
Reusable, data-driven display components that accept data via props. Used on Dashboard and can be reused on any page.

- **`SectionHeader.svelte`** - Reusable section header with title, subtitle, and "View all" link
- Props: `title`, `subtitle`, `linkText="View all"`, `linkPath=""`, `showLink=true`, `showArrow=true`
- **`StatCardRow.svelte`** - Row of stat cards with hexagon icons, large numbers, and delta indicators
- Props: `stats=[]` (array of `{ value, label, delta, category }`), `loading=false`, `columns=4`
- **`RankedList.svelte`** - Ranked list with rank + avatar + name + value
- Props: `entries=[]`, `loading=false`, `accentColor="#3eb359"`, `valueLabel="GP"`, `showDelta=true`, `onRowClick=null`
- **`UserCardScroller.svelte`** - Horizontal scroll of user cards with avatar, name, points, category icon
- Props: `entries=[]`, `loading=false`, `onCardClick=null`
- **`MemberCardScroller.svelte`** - Horizontal scroll of member cards with avatar, name, join date, category hex
- Props: `members=[]`, `loading=false`
- **`HighlightCards.svelte`** - Highlighted contribution cards in grid or scroll layout
- Props: `highlights=[]`, `loading=false`, `layout="grid"|"scroll"`, `category="builder"`
- **`CTASection.svelte`** - Configurable CTA footer section
- Props: `title`, `description`, `primaryButtonText`, `primaryButtonPath`, `primaryButtonColor="dark"`, `secondaryLinkText`, `secondaryLinkPath`, `secondaryLinkExternal=false`
- **`ChartPlaceholder.svelte`** - Empty placeholder for future chart content
- Props: `title`, `subtitle`

#### Data Display
- `LeaderboardTable.svelte` - Ranking table
- `ContributionsList.svelte` - List of contributions
Expand Down
49 changes: 49 additions & 0 deletions frontend/public/assets/icons/gradient-icon-contributions.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
47 changes: 47 additions & 0 deletions frontend/public/assets/icons/gradient-icon-points.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 13 additions & 2 deletions frontend/src/components/Navbar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,21 @@
import AuthButton from './AuthButton.svelte';
import SearchBar from './SearchBar.svelte';
import { authState } from '../lib/auth.js';
import { currentCategory } from '../stores/category.js';

let { toggleSidebar, sidebarOpen = false } = $props();

let isMenuOpen = $state(false);

const submitButtonBaseClass = 'h-10 px-4 text-white rounded-[20px] flex items-center gap-2 font-medium text-sm';

let submitButtonStyle = $derived(
$currentCategory === 'builder'
? 'background: linear-gradient(168deg, #f8b93d 15%, #ee8d24 50%, #db6917 85%);'
: $currentCategory === 'validator'
? 'background: linear-gradient(168deg, #6fa3f8 15%, #4f76f6 50%, #3b5dd6 85%);'
: 'background: linear-gradient(to right, #be8ff5, #ac6df3);'
);

function toggleMenu() {
isMenuOpen = !isMenuOpen;
Expand Down Expand Up @@ -49,8 +60,8 @@
<SearchBar />
<button
onclick={handleSubmitContribution}
class="h-10 px-4 bg-gradient-to-r from-[#be8ff5] to-[#ac6df3] text-white rounded-[20px] flex items-center gap-2 font-medium text-sm"
style="letter-spacing: 0.28px;"
class={submitButtonBaseClass}
style={submitButtonStyle}
>
<span>Submit a contribution</span>
<img src="/assets/icons/add-line.svg" alt="" class="w-4 h-4">
Expand Down
Loading