From e27013adaf074ebf42860c02c5792423902942c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iva=CC=81n=20Raskovsky?= Date: Thu, 26 Feb 2026 15:01:15 +0000 Subject: [PATCH 1/2] Move featured content from hardcoded frontend to database via API - Add data migration seeding hero banner and 3 featured builds with Cloudinary image URLs - Remove hardcoded fallback data from HeroBanner and FeaturedBuilds components - Components now fetch exclusively from /api/v1/featured/ endpoint --- .../migrations/0037_seed_featured_content.py | 94 +++++++++++++++++++ .../components/portal/FeaturedBuilds.svelte | 67 +------------ .../src/components/portal/HeroBanner.svelte | 25 ++--- 3 files changed, 105 insertions(+), 81 deletions(-) create mode 100644 backend/contributions/migrations/0037_seed_featured_content.py diff --git a/backend/contributions/migrations/0037_seed_featured_content.py b/backend/contributions/migrations/0037_seed_featured_content.py new file mode 100644 index 0000000..922ace6 --- /dev/null +++ b/backend/contributions/migrations/0037_seed_featured_content.py @@ -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), + ] diff --git a/frontend/src/components/portal/FeaturedBuilds.svelte b/frontend/src/components/portal/FeaturedBuilds.svelte index e554654..e59d80b 100644 --- a/frontend/src/components/portal/FeaturedBuilds.svelte +++ b/frontend/src/components/portal/FeaturedBuilds.svelte @@ -4,82 +4,22 @@ let builds = $state([]); let loading = $state(true); - let error = $state(null); - - // Static fallback builds matching the Figma design - const fallbackBuilds = [ - { - id: 1, - title: 'Argue.fun', - subtitle: 'by cognocracy', - user_name: 'cognocracy', - hero_image_url: '/assets/featured-builds/argue-fun-bg.jpg', - user_profile_image_url: '/assets/featured-builds/cognocracy-avatar.png', - url: '#', - link: '#', - }, - { - id: 2, - title: 'Internet Court', - subtitle: 'by raskovsky', - user_name: 'raskovsky', - hero_image_url: '/assets/featured-builds/internet-court-bg.jpg', - user_profile_image_url: '/assets/featured-builds/raskovsky-avatar.png', - url: '#', - link: '#', - }, - { - id: 3, - title: 'Rally', - subtitle: 'by GenLayer', - user_name: 'GenLayer', - hero_image_url: '/assets/featured-builds/rally-bg.jpg', - user_profile_image_url: '/assets/featured-builds/genlayer-avatar.png', - url: '#', - link: '#', - }, - ]; - - // Map of fallback images keyed by build title, used when API returns empty image URLs - const fallbackImages = {}; - for (const fb of fallbackBuilds) { - fallbackImages[fb.title] = { - hero_image_url: fb.hero_image_url, - user_profile_image_url: fb.user_profile_image_url, - }; - } - - /** - * Merge API data with local fallback images. - * If the API returns empty hero_image_url or user_profile_image_url, - * use the matching fallback asset by title. - */ - function mergeWithFallbackImages(apiBuild) { - const fb = fallbackImages[apiBuild.title] || {}; - return { - ...apiBuild, - hero_image_url: apiBuild.hero_image_url || fb.hero_image_url || '', - user_profile_image_url: apiBuild.user_profile_image_url || fb.user_profile_image_url || '', - }; - } onMount(async () => { try { const response = await featuredAPI.getBuilds(); if (response.data && response.data.length > 0) { - builds = response.data.map(mergeWithFallbackImages); - } else { - builds = fallbackBuilds; + builds = response.data; } } catch (err) { - error = err.message; - builds = fallbackBuilds; + // API failed, builds stays empty } finally { loading = false; } }); +{#if loading || builds.length > 0}
@@ -146,3 +86,4 @@
{/if}
+{/if} diff --git a/frontend/src/components/portal/HeroBanner.svelte b/frontend/src/components/portal/HeroBanner.svelte index a3c5bdb..cd0ca8e 100644 --- a/frontend/src/components/portal/HeroBanner.svelte +++ b/frontend/src/components/portal/HeroBanner.svelte @@ -4,16 +4,6 @@ let hero = $state(null); let loading = $state(true); - let error = $state(null); - - // Static fallback values matching the original design - const fallback = { - title: 'Argue.fun Launch', - subtitle: 'cognocracy', - description: 'Deploy intelligent contracts, run validators, and earn GenLayer Points on the latest testnet.', - hero_image_url: null, - link: '#', - }; onMount(async () => { try { @@ -22,15 +12,14 @@ hero = response.data[0]; } } catch (err) { - error = err.message; + // API failed, hero stays null } finally { loading = false; } }); - let displayData = $derived(hero || fallback); - let bgImage = $derived(displayData.hero_image_url || '/assets/hero-bg.png'); - let projectLink = $derived(displayData.link || displayData.url || '#'); + let bgImage = $derived(hero?.hero_image_url || '/assets/hero-bg.png'); + let projectLink = $derived(hero?.link || hero?.url || '#'); {#if loading} @@ -45,7 +34,7 @@
-{:else} +{:else if hero}
@@ -57,14 +46,14 @@
- By {displayData.subtitle || displayData.user_name || 'Unknown'} + By {hero.subtitle || hero.user_name || 'Unknown'} Verified

- {displayData.title} + {hero.title}

- {displayData.description} + {hero.description}

From 7f175e97028ac0bf633e07f5d60f1d33dcb84471 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iva=CC=81n=20Raskovsky?= Date: Fri, 27 Feb 2026 15:21:22 +0000 Subject: [PATCH 2/2] Dashboard redesign with generic UI components and stat card deltas - Create reusable UI components: SectionHeader, StatCardRow, RankedList, UserCardScroller, HighlightCards, CTASection, ChartPlaceholder - Rewrite Dashboard.svelte with category-aware builder/validator layouts - Add monthly delta stats to leaderboard API (new_builders_count, new_validators_count, new_contributions_count, new_points_count) - Fix CategoryIcon light mode with colored CSS-masked icons - Fix stat card overflow clipping delta carets - Fix CTA buttons to use fully rounded pill shape - Fix gradient icon SVGs (renamed from .png, fixed preserveAspectRatio) - Add category-aware navbar button colors --- backend/leaderboard/views.py | 50 +- frontend/CLAUDE.md | 20 + .../icons/gradient-icon-contributions.svg | 49 ++ .../assets/icons/gradient-icon-points.svg | 47 ++ frontend/src/components/Navbar.svelte | 15 +- .../src/components/portal/CategoryIcon.svelte | 39 +- frontend/src/components/ui/CTASection.svelte | 55 ++ .../src/components/ui/ChartPlaceholder.svelte | 13 + .../src/components/ui/HighlightCards.svelte | 109 +++ .../components/ui/MemberCardScroller.svelte | 110 ++++ frontend/src/components/ui/RankedList.svelte | 166 +++++ .../src/components/ui/SectionHeader.svelte | 26 + frontend/src/components/ui/StatCardRow.svelte | 65 ++ .../src/components/ui/UserCardScroller.svelte | 106 +++ frontend/src/routes/Dashboard.svelte | 620 ++++++++---------- frontend/src/stores/category.js | 4 +- 16 files changed, 1150 insertions(+), 344 deletions(-) create mode 100644 frontend/public/assets/icons/gradient-icon-contributions.svg create mode 100644 frontend/public/assets/icons/gradient-icon-points.svg create mode 100644 frontend/src/components/ui/CTASection.svelte create mode 100644 frontend/src/components/ui/ChartPlaceholder.svelte create mode 100644 frontend/src/components/ui/HighlightCards.svelte create mode 100644 frontend/src/components/ui/MemberCardScroller.svelte create mode 100644 frontend/src/components/ui/RankedList.svelte create mode 100644 frontend/src/components/ui/SectionHeader.svelte create mode 100644 frontend/src/components/ui/StatCardRow.svelte create mode 100644 frontend/src/components/ui/UserCardScroller.svelte diff --git a/backend/leaderboard/views.py b/backend/leaderboard/views.py index 81bd114..98d3b93 100644 --- a/backend/leaderboard/views.py +++ b/backend/leaderboard/views.py @@ -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( @@ -194,13 +197,37 @@ 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( @@ -208,14 +235,29 @@ def stats(self, request): 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 @@ -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): diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md index 1b47742..2e1e084 100644 --- a/frontend/CLAUDE.md +++ b/frontend/CLAUDE.md @@ -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 diff --git a/frontend/public/assets/icons/gradient-icon-contributions.svg b/frontend/public/assets/icons/gradient-icon-contributions.svg new file mode 100644 index 0000000..13076f6 --- /dev/null +++ b/frontend/public/assets/icons/gradient-icon-contributions.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/public/assets/icons/gradient-icon-points.svg b/frontend/public/assets/icons/gradient-icon-points.svg new file mode 100644 index 0000000..fb24ded --- /dev/null +++ b/frontend/public/assets/icons/gradient-icon-points.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/components/Navbar.svelte b/frontend/src/components/Navbar.svelte index d072c14..634fa56 100644 --- a/frontend/src/components/Navbar.svelte +++ b/frontend/src/components/Navbar.svelte @@ -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; @@ -49,8 +60,8 @@ + {#if secondaryLinkText && secondaryLinkPath} + + {/if} +
+
diff --git a/frontend/src/components/ui/ChartPlaceholder.svelte b/frontend/src/components/ui/ChartPlaceholder.svelte new file mode 100644 index 0000000..6c3e7e9 --- /dev/null +++ b/frontend/src/components/ui/ChartPlaceholder.svelte @@ -0,0 +1,13 @@ + + +
+
+ + + +

{title}

+

{subtitle}

+
+
diff --git a/frontend/src/components/ui/HighlightCards.svelte b/frontend/src/components/ui/HighlightCards.svelte new file mode 100644 index 0000000..6ca6f2c --- /dev/null +++ b/frontend/src/components/ui/HighlightCards.svelte @@ -0,0 +1,109 @@ + + +{#if loading} +
+ {#each [1, 2, 3] as _} +
+ {/each} +
+{:else if highlights.length === 0} +
+
+ No highlights yet +
+
+{:else} +
+ {#each highlights as highlight} + {@const cat = highlight.contribution_type_category || category} + {@const colors = getColors(cat)} +
+ +
+
+ + + {highlight.user_name || `${highlight.user_address?.slice(0, 6)}...`} + +
+ + {formatPoints(highlight.contribution_points)} pts + +
+ + +
+

{highlight.title}

+

+ {highlight.description} +

+
+ + +
+ + {highlight.contribution_type_name || cat} + + + {formatDate(highlight.contribution_date)} + +
+
+ {/each} +
+{/if} diff --git a/frontend/src/components/ui/MemberCardScroller.svelte b/frontend/src/components/ui/MemberCardScroller.svelte new file mode 100644 index 0000000..191b7de --- /dev/null +++ b/frontend/src/components/ui/MemberCardScroller.svelte @@ -0,0 +1,110 @@ + + +{#if loading} +
+ {#each [1, 2, 3, 4, 5, 6, 7, 8] as _} +
+
+
+
+
+
+
+
+
+ {/each} +
+{:else if members.length === 0} +
No members yet
+{:else} +
+ {#each members as member} + + {/each} +
+{/if} diff --git a/frontend/src/components/ui/RankedList.svelte b/frontend/src/components/ui/RankedList.svelte new file mode 100644 index 0000000..6bd7697 --- /dev/null +++ b/frontend/src/components/ui/RankedList.svelte @@ -0,0 +1,166 @@ + + +
+ {#if loading} +
+
+
+ {#each [1, 2, 3, 4, 5] as _} +
+
+
+ {/each} +
+
+ {#each [1, 2, 3, 4, 5] as _} +
+
+
+
+ {/each} +
+
+
+ {#each [1, 2, 3, 4, 5] as _} +
+
+
+ {/each} +
+
+ {:else if entries.length === 0} +
No data
+ {:else} +
+ +
+ +
+ {#each entries as _, i} +
+ {#if i === 0} + + + + + + + + + + 1 + + {:else if i === 1} + + + + + + + + + + 2 + + {:else if i === 2} + + + + + + + + + + 3 + + {:else} + {i + 1} + {/if} +
+ {/each} +
+ +
+ {#each entries as entry} + + {/each} +
+
+ + +
+ {#each entries as entry} +
+ {#if showDelta} + + + + {/if} + {formatPoints(entry.total_points ?? entry.points)} + GP +
+ {/each} +
+
+ {/if} +
diff --git a/frontend/src/components/ui/SectionHeader.svelte b/frontend/src/components/ui/SectionHeader.svelte new file mode 100644 index 0000000..51f50a9 --- /dev/null +++ b/frontend/src/components/ui/SectionHeader.svelte @@ -0,0 +1,26 @@ + + +
+
+

{title}

+ {#if subtitle} +

{subtitle}

+ {/if} +
+ {#if showLink && linkPath} + + {/if} +
diff --git a/frontend/src/components/ui/StatCardRow.svelte b/frontend/src/components/ui/StatCardRow.svelte new file mode 100644 index 0000000..f663e09 --- /dev/null +++ b/frontend/src/components/ui/StatCardRow.svelte @@ -0,0 +1,65 @@ + + +{#if loading} +
+ {#each Array(columns) as _} +
+
+
+
+
+
+
+ {/each} +
+{:else} +
+ {#each stats as stat} +
+
+
+ {#if stat.iconSrc} + + {:else} + + {/if} +
+
+

{formatNumber(stat.value)}

+

{stat.label}

+
+
+ {#if stat.delta} +
+ + {formatDelta(stat.delta)} +
+ {/if} +
+ {/each} +
+{/if} diff --git a/frontend/src/components/ui/UserCardScroller.svelte b/frontend/src/components/ui/UserCardScroller.svelte new file mode 100644 index 0000000..a8e178d --- /dev/null +++ b/frontend/src/components/ui/UserCardScroller.svelte @@ -0,0 +1,106 @@ + + +{#if loading} +
+ {#each [1, 2, 3, 4, 5, 6] as _} +
+
+
+
+
+
+
+
+
+ {/each} +
+{:else if entries.length === 0} +
No contributors yet
+{:else} +
+ {#each entries as entry} + + {/each} +
+{/if} diff --git a/frontend/src/routes/Dashboard.svelte b/frontend/src/routes/Dashboard.svelte index df99809..84bc1e3 100644 --- a/frontend/src/routes/Dashboard.svelte +++ b/frontend/src/routes/Dashboard.svelte @@ -1,369 +1,329 @@ - -
-

- {$currentCategory === 'builder' ? 'Builders' : - $currentCategory === 'validator' ? 'Validators' : - $currentCategory === 'steward' ? 'Stewards' : 'Dashboard'} -

- - - {#if statsError} -
-
-
- -
-
-

- Having trouble connecting to the API. Some data might not display correctly. -

-
-
-
- {/if} +
+ + - -
- - - +
+ +
- - -
- -
-
-
-
- - - -
-

- Top {$currentCategory === 'builder' ? 'Builders' : - $currentCategory === 'validator' ? 'Validators' : - $currentCategory === 'steward' ? 'Stewards' : 'Participants'} -

-
- -
- +
+
+ +
- - -
-
-
-
- - - -
-

Highlighted Contributions

-
- -
- + +
- - -
- -
-
-
-
- - - -
-

- Newest {$currentCategory === 'builder' ? 'Builders' : - $currentCategory === 'validator' ? 'Validators' : - $currentCategory === 'steward' ? 'Stewards' : 'Participants'} -

-
- -
- - {#if newestValidatorsLoading} -
-
-
- {:else if newestValidatorsError} -
-

{newestValidatorsError}

-
- {:else if newestValidators.length === 0} -
-

No new validators yet.

+ + +
+ + +
+ + + {#if isBuilder} + + {/if} + + + {#if isBuilder} +
+ + +
+ {/if} + + +
+ +
+ + +
+ + +
+ + + {#if isValidator} +
+
+ +
- {:else} -
- {#each newestValidators as validator} -
-
- -
+
+ +
+ {#if recentLoading} +
+ {#each [1, 2, 3, 4, 5] as _} +
+
+
+
+
+
+
+
+ {/each} +
+ {:else if recentContributions.length === 0} +
No recent contributions
+ {:else} +
+ {#each recentContributions as contrib} -
- {formatDate(validator.first_uptime_date || validator.created_at)} -
-
+ {/each}
- -
- {/each} -
- {/if} -
- - -
-
-
-
- - - + {/if}
-

Recent Contributions

-
- + {#if isBuilder} + -
-
+ {:else} + + {/if}
+
diff --git a/frontend/src/stores/category.js b/frontend/src/stores/category.js index 68f6cf6..66b3b60 100644 --- a/frontend/src/stores/category.js +++ b/frontend/src/stores/category.js @@ -52,7 +52,7 @@ export const categoryTheme = derived(currentCategory, $category => { }, builder: { // Orange/sunset theme - bg: 'bg-orange-50', + bg: 'bg-white', bgSecondary: 'bg-orange-100', primary: 'bg-orange-500', primaryHover: 'hover:bg-orange-600', @@ -67,7 +67,7 @@ export const categoryTheme = derived(currentCategory, $category => { }, validator: { // Blue/technical theme - bg: 'bg-sky-50', + bg: 'bg-white', bgSecondary: 'bg-sky-100', primary: 'bg-sky-500', primaryHover: 'hover:bg-sky-600',