diff --git a/.claude/commands/commit.md b/.claude/commands/commit.md new file mode 100644 index 00000000..d7ca301e --- /dev/null +++ b/.claude/commands/commit.md @@ -0,0 +1,122 @@ +# /commit — Create a 3-Layer Commit + +You are creating a git commit with a structured 3-layer message. Follow this workflow exactly. + +--- + +## Step 1: Analyze Changes + +1. Run `git status` (never use `-uall`) to see all changed files +2. Run `git diff` and `git diff --cached` to see actual changes +3. Run `git log --oneline -10` to see recent commit style +4. Understand what changed at the product, architecture, and implementation levels + +--- + +## Step 2: Compose the 3-Layer Commit Message + +### Layer 1: Subject Line (product/story level) +- One line, imperative mood, under 72 characters +- What happened from a **product perspective** — no code, no files, no components +- Examples: "Improve hero banner responsiveness on tablet and mobile", "Add referral program landing page" + +### Layer 2: Body (architecture/component level) +- Blank line after subject, then a paragraph or short bullets +- Names components and structural decisions, explains what was merged/split/refactored and why +- **No file paths, no line numbers** — this is for developers scanning the log +- Written in present tense: "The hero banner now supports..." + +### Layer 3: Claude Implementation Notes +- After another blank line, starts with `## Claude Implementation Notes` +- File-level details: what changed where and why +- Format: `- path/to/file.ext: Description of changes` +- This section is the technical reference for Claude in future sessions + +### Template: +``` + + + + +## Claude Implementation Notes +- path/to/file: What changed and why +- path/to/other-file: What changed and why +``` + +--- + +## Step 3: Present for Review + +Show the complete commit message to the user in a clear code block. Ask if they want to modify anything. Do NOT proceed until the user approves. + +Use AskUserQuestion with options like: +- "Commit as-is" +- "Edit message" (then ask what to change) +- "Abort" + +--- + +## Step 4: Stage and Commit + +1. Stage files explicitly by name — **never use `git add -A` or `git add .`** +2. Do not stage files that contain secrets (.env, credentials, etc.) +3. Create the commit using a HEREDOC for proper formatting: +```bash +git commit -m "$(cat <<'EOF' + +EOF +)" +``` +4. **No attribution lines** — no Co-Authored-By, no "Generated with Claude Code" + +--- + +## Step 5: Update CHANGELOG.md + +1. Read the current CHANGELOG.md +2. Get the commit hash with `git rev-parse --short HEAD` +3. Add a one-line entry under `## Unreleased` in plain language (product level only) +4. Format: `- ()` +5. Only add entries for user-facing changes — skip internal refactors, test-only changes, etc. +6. Stage and commit the changelog update: +```bash +git add CHANGELOG.md +git commit -m "Update changelog" +``` + +--- + +## Step 6: Verify + +Run `git log --oneline -3` to confirm the commits look clean. + +--- + +## Git History Management (Before Pushing) + +When the user is ready to push or create a PR, help them clean the history: + +1. **Review**: `git log --oneline dev..HEAD` to see all commits on the branch +2. **Squash if needed**: If there are WIP or fixup commits, use `git rebase -i` to combine them into clean logical units (never use `-i` flag directly — present the rebase plan to the user and use `GIT_SEQUENCE_EDITOR` to automate) +3. **Rebase onto target**: `git rebase dev` for linear history (no merge commits) +4. **Verify**: `git log --oneline dev..HEAD` to confirm clean history + +--- + +## Rules + +- **Never blind-stage**: Always review what you're staging +- **No secrets**: Never commit .env, credentials, API keys +- **No attribution**: No Co-Authored-By or Generated-with lines +- **Always present for review**: The user must approve before committing +- **Imperative mood**: "Add feature" not "Added feature" or "Adds feature" +- **No merge commits**: Always rebase, never merge + +--- + +## Learned Preferences + + + + +(none yet) diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..ec19074d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +All notable user-facing changes to this project will be documented in this file. + +## Unreleased + +- Responsive hero banner images for tablet and mobile (e5c01b5) diff --git a/CLAUDE.md b/CLAUDE.md index 618fcae5..70478380 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -55,12 +55,8 @@ let { params = {} } = $props(); - The `main` branch is reserved for production releases - Feature branches should be based on and merged into `dev` -### No Attribution in Commits -When creating git commits, **DO NOT** include Claude attribution lines such as: -- 🤖 Generated with [Claude Code](https://claude.ai/code) -- Co-Authored-By: Claude - -Keep commit messages clean and focused on the changes made. +### Commits and PRs +**Before any commit or PR**, read and follow `.claude/commands/commit.md`. It defines the commit message format, changelog workflow, and git history rules. No exceptions. ## Important Terminology diff --git a/backend/contributions/admin.py b/backend/contributions/admin.py index 6be9af4c..6a333043 100644 --- a/backend/contributions/admin.py +++ b/backend/contributions/admin.py @@ -683,7 +683,9 @@ class FeaturedContentAdmin(admin.ModelAdmin): search_fields = ('title', 'description', 'user__name', 'user__address') list_editable = ('order', 'is_active') raw_id_fields = ('user', 'contribution') - readonly_fields = ('created_at', 'updated_at') + readonly_fields = ('created_at', 'updated_at', 'hero_image_public_id', + 'hero_image_tablet_public_id', 'hero_image_mobile_public_id', + 'user_profile_image_public_id') ordering = ('order', '-created_at') fieldsets = ( @@ -694,8 +696,15 @@ class FeaturedContentAdmin(admin.ModelAdmin): 'fields': ('user', 'contribution') }), ('Links & Media', { - 'fields': ('hero_image', 'user_profile_image', 'url'), - 'description': 'Upload images directly. Django serves them from the media directory.' + 'fields': ('hero_image_url', 'hero_image_url_tablet', 'hero_image_url_mobile', + 'user_profile_image_url', 'url'), + 'description': 'Paste Cloudinary URLs for images. Tablet/mobile hero images are optional — falls back to the main hero image.' + }), + ('Cloudinary Metadata', { + 'fields': ('hero_image_public_id', 'hero_image_tablet_public_id', + 'hero_image_mobile_public_id', 'user_profile_image_public_id'), + 'classes': ('collapse',), + 'description': 'Auto-managed Cloudinary public IDs (read-only)' }), ('Metadata', { 'fields': ('created_at', 'updated_at'), diff --git a/backend/contributions/management/commands/seed_featured_content.py b/backend/contributions/management/commands/seed_featured_content.py index 7ae68f62..cc00631b 100644 --- a/backend/contributions/management/commands/seed_featured_content.py +++ b/backend/contributions/management/commands/seed_featured_content.py @@ -1,7 +1,3 @@ -import os -import shutil - -from django.conf import settings from django.core.management.base import BaseCommand from django.contrib.auth import get_user_model from contributions.models import FeaturedContent @@ -12,30 +8,7 @@ class Command(BaseCommand): help = 'Seeds FeaturedContent entries for the portal home page (hero banner and featured builds).' - def _copy_to_media(self, source_path, relative_dest): - """ - Copy a file to MEDIA_ROOT if it doesn't already exist at the destination. - Returns the relative path within MEDIA_ROOT, or None if the source doesn't exist. - """ - if not os.path.exists(source_path): - self.stdout.write(self.style.WARNING(f" Image not found: {source_path}")) - return None - - dest_path = os.path.join(settings.MEDIA_ROOT, relative_dest) - dest_dir = os.path.dirname(dest_path) - os.makedirs(dest_dir, exist_ok=True) - - if not os.path.exists(dest_path): - shutil.copy2(source_path, dest_path) - self.stdout.write(self.style.SUCCESS(f" Copied: {relative_dest}")) - else: - self.stdout.write(f" Already exists: {relative_dest}") - - return relative_dest - def handle(self, *args, **options): - media_root = settings.MEDIA_ROOT - # ---------------------------------------------------------------- # 1. Ensure users exist (get_or_create with dummy email/address) # ---------------------------------------------------------------- @@ -76,29 +49,19 @@ def handle(self, *args, **options): # ---------------------------------------------------------------- # 2. Hero banner # ---------------------------------------------------------------- - hero_defaults = { - 'description': 'Deploy intelligent contracts, run validators, and earn GenLayer Points on the latest testnet.', - 'author': 'cognocracy', - 'user': users['cognocracy'], - 'url': '', - 'is_active': True, - 'order': 0, - } - obj, created = FeaturedContent.objects.update_or_create( content_type='hero', title='Argue.fun Launch', - defaults=hero_defaults, + defaults={ + 'description': 'Deploy intelligent contracts, run validators, and earn GenLayer Points on the latest testnet.', + 'author': 'cognocracy', + 'user': users['cognocracy'], + 'url': '', + 'is_active': True, + 'order': 0, + }, ) - # Copy hero image to media directory - hero_source = os.path.join(media_root, 'featured', 'hero-bg.png') - hero_rel = 'featured/hero-bg.png' - result = self._copy_to_media(hero_source, hero_rel) - if result: - obj.hero_image = result - obj.save() - self.stdout.write( self.style.SUCCESS(f" {'Created' if created else 'Updated'} hero: {obj.title}") ) @@ -110,30 +73,18 @@ def handle(self, *args, **options): { 'title': 'Argue.fun', 'user': users['cognocracy'], - 'hero_image_source': os.path.join(media_root, 'featured', 'argue-fun-bg.jpg'), - 'hero_image_rel': 'featured/argue-fun-bg.jpg', - 'avatar_source': os.path.join(media_root, 'featured', 'avatars', 'cognocracy-avatar.png'), - 'avatar_rel': 'featured/avatars/cognocracy-avatar.png', 'url': '', 'order': 0, }, { 'title': 'Internet Court', 'user': users['raskovsky'], - 'hero_image_source': os.path.join(media_root, 'featured', 'internet-court-bg.jpg'), - 'hero_image_rel': 'featured/internet-court-bg.jpg', - 'avatar_source': os.path.join(media_root, 'featured', 'avatars', 'raskovsky-avatar.png'), - 'avatar_rel': 'featured/avatars/raskovsky-avatar.png', 'url': '', 'order': 1, }, { 'title': 'Rally', 'user': users['GenLayer'], - 'hero_image_source': os.path.join(media_root, 'featured', 'rally-bg.jpg'), - 'hero_image_rel': 'featured/rally-bg.jpg', - 'avatar_source': os.path.join(media_root, 'featured', 'avatars', 'genlayer-avatar.png'), - 'avatar_rel': 'featured/avatars/genlayer-avatar.png', 'url': '', 'order': 2, }, @@ -153,25 +104,9 @@ def handle(self, *args, **options): }, ) - updated = False - - # Copy hero image to media directory - result = self._copy_to_media(build['hero_image_source'], build['hero_image_rel']) - if result: - obj.hero_image = result - updated = True - - # Copy avatar to media directory - result = self._copy_to_media(build['avatar_source'], build['avatar_rel']) - if result: - obj.user_profile_image = result - updated = True - - if updated: - obj.save() - self.stdout.write( self.style.SUCCESS(f" {'Created' if created else 'Updated'} build: {obj.title}") ) self.stdout.write(self.style.SUCCESS('\nFeatured content seeded successfully.')) + self.stdout.write('Note: Upload images via Django admin or set hero_image_url / user_profile_image_url directly.') diff --git a/backend/contributions/migrations/0039_add_responsive_hero_images.py b/backend/contributions/migrations/0039_add_responsive_hero_images.py new file mode 100644 index 00000000..c75c03a9 --- /dev/null +++ b/backend/contributions/migrations/0039_add_responsive_hero_images.py @@ -0,0 +1,23 @@ +# Generated by Django 6.0.3 on 2026-03-12 20:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contributions', '0038_rename_subtitle_featuredcontent_author_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='featuredcontent', + name='hero_image_mobile', + field=models.ImageField(blank=True, help_text='Mobile variant (<768px). Falls back to hero_image if empty.', null=True, upload_to='featured/'), + ), + migrations.AddField( + model_name='featuredcontent', + name='hero_image_tablet', + field=models.ImageField(blank=True, help_text='Tablet variant (768-1023px). Falls back to hero_image if empty.', null=True, upload_to='featured/'), + ), + ] diff --git a/backend/contributions/migrations/0040_convert_featured_images_to_cloudinary.py b/backend/contributions/migrations/0040_convert_featured_images_to_cloudinary.py new file mode 100644 index 00000000..2fb443f2 --- /dev/null +++ b/backend/contributions/migrations/0040_convert_featured_images_to_cloudinary.py @@ -0,0 +1,69 @@ +# Generated by Django 6.0.3 on 2026-03-16 18:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contributions', '0039_add_responsive_hero_images'), + ] + + operations = [ + migrations.RemoveField( + model_name='featuredcontent', + name='hero_image', + ), + migrations.RemoveField( + model_name='featuredcontent', + name='hero_image_mobile', + ), + migrations.RemoveField( + model_name='featuredcontent', + name='hero_image_tablet', + ), + migrations.RemoveField( + model_name='featuredcontent', + name='user_profile_image', + ), + migrations.AddField( + model_name='featuredcontent', + name='hero_image_mobile_public_id', + field=models.CharField(blank=True, help_text='Cloudinary public ID for mobile hero image', max_length=255), + ), + migrations.AddField( + model_name='featuredcontent', + name='hero_image_public_id', + field=models.CharField(blank=True, help_text='Cloudinary public ID for hero image', max_length=255), + ), + migrations.AddField( + model_name='featuredcontent', + name='hero_image_tablet_public_id', + field=models.CharField(blank=True, help_text='Cloudinary public ID for tablet hero image', max_length=255), + ), + migrations.AddField( + model_name='featuredcontent', + name='hero_image_url', + field=models.URLField(blank=True, help_text='Cloudinary URL for hero image', max_length=500), + ), + migrations.AddField( + model_name='featuredcontent', + name='hero_image_url_mobile', + field=models.URLField(blank=True, help_text='Cloudinary URL for mobile hero image (<768px). Falls back to hero_image_url if empty.', max_length=500), + ), + migrations.AddField( + model_name='featuredcontent', + name='hero_image_url_tablet', + field=models.URLField(blank=True, help_text='Cloudinary URL for tablet hero image (768-1023px). Falls back to hero_image_url if empty.', max_length=500), + ), + migrations.AddField( + model_name='featuredcontent', + name='user_profile_image_public_id', + field=models.CharField(blank=True, help_text='Cloudinary public ID for user profile image', max_length=255), + ), + migrations.AddField( + model_name='featuredcontent', + name='user_profile_image_url', + field=models.URLField(blank=True, help_text='Cloudinary URL for user profile image', max_length=500), + ), + ] diff --git a/backend/contributions/models.py b/backend/contributions/models.py index a12c5dfb..9af2922c 100644 --- a/backend/contributions/models.py +++ b/backend/contributions/models.py @@ -663,8 +663,14 @@ class FeaturedContent(BaseModel): settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='featured_items' ) - hero_image = models.ImageField(upload_to='featured/', blank=True, null=True) - user_profile_image = models.ImageField(upload_to='featured/avatars/', blank=True, null=True) + hero_image_url = models.URLField(max_length=500, blank=True, help_text='Cloudinary URL for hero image') + hero_image_public_id = models.CharField(max_length=255, blank=True, help_text='Cloudinary public ID for hero image') + hero_image_url_tablet = models.URLField(max_length=500, blank=True, help_text='Cloudinary URL for tablet hero image (768-1023px). Falls back to hero_image_url if empty.') + hero_image_tablet_public_id = models.CharField(max_length=255, blank=True, help_text='Cloudinary public ID for tablet hero image') + hero_image_url_mobile = models.URLField(max_length=500, blank=True, help_text='Cloudinary URL for mobile hero image (<768px). Falls back to hero_image_url if empty.') + hero_image_mobile_public_id = models.CharField(max_length=255, blank=True, help_text='Cloudinary public ID for mobile hero image') + user_profile_image_url = models.URLField(max_length=500, blank=True, help_text='Cloudinary URL for user profile image') + user_profile_image_public_id = models.CharField(max_length=255, blank=True, help_text='Cloudinary public ID for user profile image') url = models.URLField(max_length=500, blank=True) is_active = models.BooleanField(default=True) order = models.PositiveIntegerField(default=0) diff --git a/backend/contributions/serializers.py b/backend/contributions/serializers.py index 702bb394..4605c5bb 100644 --- a/backend/contributions/serializers.py +++ b/backend/contributions/serializers.py @@ -722,32 +722,20 @@ class FeaturedContentSerializer(serializers.ModelSerializer): user_name = serializers.CharField(source='user.name', read_only=True) user_address = serializers.CharField(source='user.address', read_only=True) user_profile_image_url = serializers.SerializerMethodField() - hero_image_url = serializers.SerializerMethodField() link = serializers.SerializerMethodField() class Meta: model = FeaturedContent fields = ['id', 'content_type', 'title', 'description', 'author', - 'hero_image_url', 'url', 'link', + 'hero_image_url', 'hero_image_url_tablet', 'hero_image_url_mobile', + 'url', 'link', 'user', 'user_name', 'user_address', 'user_profile_image_url', 'contribution', 'is_active', 'order', 'created_at'] - def get_hero_image_url(self, obj): - """Return absolute URL for the hero image if set.""" - if obj.hero_image: - request = self.context.get('request') - if request: - return request.build_absolute_uri(obj.hero_image.url) - return obj.hero_image.url - return '' - def get_user_profile_image_url(self, obj): - """Return the FeaturedContent's user_profile_image if set, otherwise fall back to user's profile_image_url.""" - if obj.user_profile_image: - request = self.context.get('request') - if request: - return request.build_absolute_uri(obj.user_profile_image.url) - return obj.user_profile_image.url + """Return the FeaturedContent's user_profile_image_url if set, otherwise fall back to user's profile_image_url.""" + if obj.user_profile_image_url: + return obj.user_profile_image_url if obj.user and obj.user.profile_image_url: return obj.user.profile_image_url return '' diff --git a/backend/validators/tests/test_api.py b/backend/validators/tests/test_api.py new file mode 100644 index 00000000..f52b1a77 --- /dev/null +++ b/backend/validators/tests/test_api.py @@ -0,0 +1,85 @@ +from django.test import TestCase +from django.contrib.auth import get_user_model +from rest_framework.test import APITestCase +from rest_framework import status +from validators.models import Validator +from contributions.models import Category + +User = get_user_model() + + +class ValidatorAPITestCase(APITestCase): + def setUp(self): + # Create test user + self.user = User.objects.create_user( + email='test@example.com', + password='testpass123', + name='Test User' + ) + + # Get or create validator category (migration may have created it) + self.category, _ = Category.objects.get_or_create( + slug='validator', + defaults={ + 'name': 'Validator', + 'description': 'Test validator category', + 'profile_model': 'validators.Validator' + } + ) + + # Authenticate + self.client.force_authenticate(user=self.user) + + def test_get_validator_profile_not_exists(self): + """Test getting validator profile when it doesn't exist""" + response = self.client.get('/api/v1/validators/me/') + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_create_validator_profile(self): + """Test creating validator profile via PATCH""" + response = self.client.patch('/api/v1/validators/me/', { + 'node_version_asimov': '1.2.3' + }) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # Verify profile was created + self.assertTrue(Validator.objects.filter(user=self.user).exists()) + validator = Validator.objects.get(user=self.user) + self.assertEqual(validator.node_version_asimov, '1.2.3') + + def test_update_validator_profile(self): + """Test updating existing validator profile""" + # Create profile first + Validator.objects.create(user=self.user, node_version_asimov='1.0.0') + + # Update it + response = self.client.patch('/api/v1/validators/me/', { + 'node_version_asimov': '2.0.0' + }) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # Verify update + validator = Validator.objects.get(user=self.user) + self.assertEqual(validator.node_version_asimov, '2.0.0') + + def test_get_validator_profile_exists(self): + """Test getting existing validator profile""" + Validator.objects.create(user=self.user, node_version_asimov='1.2.3') + + response = self.client.get('/api/v1/validators/me/') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn('node_version_asimov', response.data) + self.assertEqual(response.data['node_version_asimov'], '1.2.3') + + def test_update_bradbury_version(self): + """Test updating bradbury version""" + Validator.objects.create(user=self.user, node_version_asimov='1.0.0') + + response = self.client.patch('/api/v1/validators/me/', { + 'node_version_bradbury': '2.0.0' + }) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + validator = Validator.objects.get(user=self.user) + self.assertEqual(validator.node_version_bradbury, '2.0.0') + self.assertEqual(validator.node_version_asimov, '1.0.0') diff --git a/frontend/index.html b/frontend/index.html index 9f92701f..b1b0909b 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -12,20 +12,20 @@ - + - + - + - + diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 5baf0d6a..449a585e 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -48,6 +48,7 @@ import Referrals from './routes/Referrals.svelte'; import Community from './routes/Community.svelte'; import Hackathon from './routes/Hackathon.svelte'; + import Resources from './routes/Resources.svelte'; import ReferralProgram from './routes/ReferralProgram.svelte'; import HowItWorks from './routes/HowItWorks.svelte'; import StartupRequestDetail from './routes/StartupRequestDetail.svelte'; @@ -84,6 +85,7 @@ '/builders/highlights': Highlights, '/builders/leaderboard': Leaderboard, + '/builders/resources': Resources, '/builders/startup-requests/:id': StartupRequestDetail, // Validators routes diff --git a/frontend/src/assets/hackathon/sponsors/chutes.webp b/frontend/src/assets/hackathon/sponsors/chutes.webp new file mode 100644 index 00000000..0f58aa17 Binary files /dev/null and b/frontend/src/assets/hackathon/sponsors/chutes.webp differ diff --git a/frontend/src/assets/hackathon/sponsors/crouton.webp b/frontend/src/assets/hackathon/sponsors/crouton.webp new file mode 100644 index 00000000..23b24b13 Binary files /dev/null and b/frontend/src/assets/hackathon/sponsors/crouton.webp differ diff --git a/frontend/src/assets/hackathon/sponsors/pathrock.png b/frontend/src/assets/hackathon/sponsors/pathrock.png new file mode 100644 index 00000000..0fde196c Binary files /dev/null and b/frontend/src/assets/hackathon/sponsors/pathrock.png differ diff --git a/frontend/src/assets/hackathon/sponsors/stakeme.png b/frontend/src/assets/hackathon/sponsors/stakeme.png new file mode 100644 index 00000000..82cee7cd Binary files /dev/null and b/frontend/src/assets/hackathon/sponsors/stakeme.png differ diff --git a/frontend/src/components/portal/HeroBanner.svelte b/frontend/src/components/portal/HeroBanner.svelte index e05ed18c..636ede6a 100644 --- a/frontend/src/components/portal/HeroBanner.svelte +++ b/frontend/src/components/portal/HeroBanner.svelte @@ -59,8 +59,8 @@ {#if isValidator}
{:else if loading} -
+
@@ -105,18 +105,22 @@
{:else if hero}
- + {#each heroes as h, i}
- + + + + +
{/each} diff --git a/frontend/src/components/profile/CommunityView.svelte b/frontend/src/components/profile/CommunityView.svelte index b0d566fd..45f1418e 100644 --- a/frontend/src/components/profile/CommunityView.svelte +++ b/frontend/src/components/profile/CommunityView.svelte @@ -21,7 +21,7 @@ ); function copyReferralLink() { - const referralLink = `https://points.genlayer.com/?ref=${participant?.referral_code || ""}`; + const referralLink = `https://portal.genlayer.foundation/?ref=${participant?.referral_code || ""}`; navigator.clipboard.writeText(referralLink); showSuccess("Referral link copied!"); } diff --git a/frontend/src/components/shared/CTABanner.svelte b/frontend/src/components/shared/CTABanner.svelte index 8cba9392..87777240 100644 --- a/frontend/src/components/shared/CTABanner.svelte +++ b/frontend/src/components/shared/CTABanner.svelte @@ -122,7 +122,7 @@ } function copyReferralLink() { - const referralLink = `https://points.genlayer.com/?ref=${referralCode}`; + const referralLink = `https://portal.genlayer.foundation/?ref=${referralCode}`; navigator.clipboard.writeText(referralLink); showSuccess("Referral link copied!"); } diff --git a/frontend/src/data/resources.js b/frontend/src/data/resources.js new file mode 100644 index 00000000..1485d7ef --- /dev/null +++ b/frontend/src/data/resources.js @@ -0,0 +1,264 @@ +// Resources page data — single source of truth +// Edit this file to update content across the Resources page + +export const pageHeader = { + title: 'Resources for Builders', + subtitle: 'Everything you need to build on GenLayer — docs, tools, SDKs, and ecosystem projects in one place.', +}; + +export const sections = [ + { id: 'getting-started', label: 'Getting Started' }, + { id: 'documentation', label: 'Docs' }, + { id: 'ai-development', label: 'AI Tools' }, + { id: 'bradbury-links', label: 'Bradbury Dev Links' }, + { id: 'ecosystem', label: 'Ecosystem' }, + { id: 'hackathon', label: 'Hackathon' }, + { id: 'how-it-works', label: 'How It Works' }, + { id: 'tracks', label: 'Tracks & Ideas' }, +]; + +export const gettingStarted = { + tag: 'Recommended', + title: 'GenLayer Boilerplate', + description: 'The fastest way to start building on GenLayer. Clone the boilerplate, deploy your first Intelligent Contract, and connect a frontend — all in under 10 minutes.', + url: 'https://github.com/yeagerai/genlayer-boilerplate', + ctaLabel: 'View on GitHub', +}; + +export const documentation = [ + { + tag: 'Docs', + title: 'Equivalence Principle', + description: 'Understand the core concept behind Intelligent Contracts — how GenLayer achieves deterministic results from non-deterministic AI outputs through validator consensus.', + url: 'https://docs.genlayer.com/concepts/equivalence-principle', + ctaLabel: 'Read documentation', + }, + { + tag: 'Docs', + title: 'Development Setup & Quickstart', + description: 'Set up your local environment, install the GenLayer CLI, deploy your first contract, and connect it to a frontend application step by step.', + url: 'https://docs.genlayer.com/getting-started/setup', + ctaLabel: 'Read documentation', + }, +]; + +export const aiDevelopment = [ + { + tag: 'AI Tooling', + title: 'Claude Code Skills', + description: 'Pre-built Claude Code skills that understand GenLayer\'s architecture. Get AI-assisted contract development with context-aware suggestions and best practices.', + url: 'https://github.com/yeagerai/genlayer-claude-code-skills', + ctaLabel: 'View on GitHub', + installSteps: [ + { + label: 'Install skills', + command: 'claude install-skills https://github.com/yeagerai/genlayer-claude-code-skills', + }, + ], + }, + { + tag: 'AI Tooling', + title: 'GenLayer MCP Server', + description: 'Model Context Protocol server for GenLayer. Connect any MCP-compatible AI assistant to GenLayer\'s documentation, contract templates, and deployment tools.', + url: 'https://github.com/yeagerai/genlayer-mcp-server', + ctaLabel: 'View on GitHub', + installSteps: [ + { + label: 'Install via npx', + command: 'npx @anthropic-ai/claude-code mcp add genlayer -- npx -y genlayer-mcp-server', + }, + ], + }, +]; + +export const bradburyLinks = { + network: { + title: 'Network', + items: [ + { label: 'JSON-RPC Endpoint', value: 'https://studio-api.genlayer.com/api', copyable: true }, + { label: 'WebSocket', value: 'wss://studio-api.genlayer.com/ws', copyable: true }, + { label: 'Chain ID', value: '61_999', copyable: false }, + ], + }, + explorers: { + title: 'Explorers & Tools', + items: [ + { label: 'GenLayer Explorer', url: 'https://explorer.genlayer.com' }, + { label: 'GenLayer Studio', url: 'https://studio.genlayer.com' }, + { label: 'GenLayer Simulator', url: 'https://simulator.genlayer.com' }, + ], + }, + sdks: { + title: 'SDKs & CLI', + items: [ + { + label: 'GenLayer JS SDK', + package: 'genlayer-js', + version: 'latest', + installCommand: 'npm install genlayer-js', + url: 'https://www.npmjs.com/package/genlayer-js', + }, + { + label: 'GenLayer Python SDK', + package: 'genlayer', + version: 'latest', + installCommand: 'pip install genlayer', + url: 'https://pypi.org/project/genlayer/', + }, + { + label: 'GenLayer CLI', + package: 'genlayer-cli', + version: 'latest', + installCommand: 'npm install -g genlayer-cli', + url: 'https://www.npmjs.com/package/genlayer-cli', + }, + ], + }, +}; + +export const ecosystemProjects = [ + { + title: 'Internet Court', + description: 'Decentralized arbitration platform for dispute resolution with AI-evaluated evidence.', + url: 'https://internetcourt.org', + track: 'Onchain Justice', + }, + { + title: 'MergeProof', + description: 'Verified code contribution tracking and proof-of-work for open source developers.', + url: 'https://mergeproof.com', + track: 'Future of Work', + }, + { + title: 'Molly.fun', + description: 'Social AI agent platform exploring agent-to-agent economic interactions.', + url: 'https://molly.fun', + track: 'Agentic Economy', + }, + { + title: 'COFI Bets', + description: 'On-chain prediction market with AI-resolved outcomes through validator consensus.', + url: 'https://bet.courtofinternet.com/', + track: 'Prediction Markets', + }, + { + title: 'Rally.fun', + description: 'Collaborative funding platform for community-driven initiatives and bounties.', + url: 'https://rally.fun', + track: 'Future of Work', + }, + { + title: 'Argue.fun', + description: 'Structured debate platform with AI-judged arguments and community voting.', + url: 'https://argue.fun', + track: 'AI Governance', + }, + { + title: 'P2P Betting', + description: 'Peer-to-peer betting platform with trustless settlement via Intelligent Contracts.', + url: 'https://p2p-betting-mu.vercel.app/create', + track: 'Prediction Markets', + }, + { + title: 'Prediction Market Kit', + description: 'Open-source toolkit for building custom prediction markets on GenLayer.', + url: 'https://pmkit.courtofinternet.com/', + track: 'Prediction Markets', + }, + { + title: 'Unstoppable', + description: 'Multiplayer coordination game exploring social dynamics and consensus mechanisms.', + url: 'https://unstoppable.fun/', + track: 'AI Gaming', + }, + { + title: 'Mochi Quest', + description: 'Interactive AI game where players navigate consensus-based challenges together.', + url: 'https://guess-picture.onrender.com/mochi-quest', + track: 'AI Gaming', + }, + { + title: 'Bridge Boilerplate', + description: 'Connect GenLayer Intelligent Contracts with EVM chains via LayerZero V2. Offload AI reasoning to GenLayer while keeping users and liquidity on Base/Ethereum.', + url: 'https://github.com/genlayer-foundation/genlayer-studio-bridge-boilerplate', + track: 'Infrastructure', + }, + { + title: 'PM Kit', + description: 'Fully on-chain prediction market kit with cross-chain bridging, escrow, and trustless resolution via GenLayer validator consensus. Like Limitless, but decentralized.', + url: 'https://github.com/courtofinternet/pm-kit', + track: 'Prediction Markets', + }, +]; + +export const hackathon = { + title: 'Testnet Bradbury Hackathon', + subtitle: 'Build Intelligent Contracts with AI consensus', + highlights: [ + 'Builder Points + $5,000 in prizes', + '6 tracks to choose from', + 'Mentorship & funding opportunities', + ], + ctaLabel: 'View Hackathon Details', + ctaPath: '/hackathon', +}; + +export const portalInfo = [ + { + step: 1, + title: 'Sign Up', + description: 'Connect your wallet and create your builder profile on the GenLayer Portal.', + color: 'from-[#f8b93d] to-[#ee8d24]', // orange + }, + { + step: 2, + title: 'Build & Deploy', + description: 'Write Intelligent Contracts, deploy to Bradbury testnet, and build your frontend.', + color: 'from-[#6da7f3] to-[#387de8]', // blue + }, + { + step: 3, + title: 'Submit', + description: 'Submit your contributions through the portal to earn points and climb the leaderboard.', + color: 'from-[#a77fee] to-[#7f52e1]', // purple + }, + { + step: 4, + title: 'Earn & Rank', + description: 'Accumulate points, unlock contributions, and rise through the builder ranks.', + color: 'from-[#3eb359] to-[#2d9a46]', // green + }, +]; + +export const tracksAndIdeas = [ + { + title: 'Agentic Economy Infrastructure', + description: 'Build the infrastructure for AI agents to interact, transact, and coordinate autonomously.', + gradient: 'from-[#f8b93d] to-[#ee8d24]', + }, + { + title: 'AI Governance', + description: 'AI-driven decisions and coordination between humans and autonomous agents.', + gradient: 'from-[#6da7f3] to-[#387de8]', + }, + { + title: 'Prediction Markets & P2P Betting', + description: 'Bet, predict, and trade on future outcomes with on-chain markets powered by AI consensus.', + gradient: 'from-[#a77fee] to-[#7f52e1]', + }, + { + title: 'AI Gaming', + description: 'Multiplayer games exploring coordination, social dynamics, and consensus mechanics.', + gradient: 'from-[#e85d75] to-[#c94058]', + }, + { + title: 'Future of Work', + description: 'AI-verified deliverables, reputation tracking, and outcome-based payment systems.', + gradient: 'from-[#3eb359] to-[#2d9a46]', + }, + { + title: 'Onchain Justice', + description: 'Decentralized arbitration with AI-evaluated evidence and fair dispute resolution.', + gradient: 'from-[#f0923b] to-[#d6721e]', + }, +]; diff --git a/frontend/src/routes/Hackathon.svelte b/frontend/src/routes/Hackathon.svelte index 459d6d9d..e6c818a8 100644 --- a/frontend/src/routes/Hackathon.svelte +++ b/frontend/src/routes/Hackathon.svelte @@ -23,8 +23,19 @@ import arrowRightDark from '../assets/hackathon/arrow-right-dark.svg'; import pointsGradientIcon from '../assets/hackathon/points-gradient-icon.svg'; import glSymbolSmall from '../assets/hackathon/gl-symbol-small.svg'; + import sponsorChutes from '../assets/hackathon/sponsors/chutes.webp'; + import sponsorPathrock from '../assets/hackathon/sponsors/pathrock.png'; + import sponsorStakeme from '../assets/hackathon/sponsors/stakeme.png'; + import sponsorCrouton from '../assets/hackathon/sponsors/crouton.webp'; import FeaturedBuilds from '../components/portal/FeaturedBuilds.svelte'; + const sponsors = [ + { name: 'CHUTES', logo: sponsorChutes, url: 'https://chutes.ai', bg: '#131313' }, + { name: 'Pathrock Network', logo: sponsorPathrock, url: 'https://pathrocknetwork.org', bg: '#ffffff' }, + { name: 'Stakeme', logo: sponsorStakeme, url: 'https://stakeme.pro', bg: '#ffffff' }, + { name: 'Crouton Digital', logo: sponsorCrouton, url: 'https://crouton.digital', bg: '#ffffff' }, + ].sort(() => Math.random() - 0.5); + const heroTrack = { icon: trackPrediction, title: 'Agentic Economy Infrastructure', @@ -128,17 +139,22 @@
- -
-
- -
- Video coming soon + +
+
-
+

Build Once, Earn Forever

@@ -277,78 +293,87 @@
- -
-
-

Requirements

-

Your project must include an Intelligent Contract with:

-
-
- - Optimistic Democracy consensus -
-
- - Equivalence Principle + +
+
+ +
+
+

Requirements

+

Your project must include an Intelligent Contract with:

+
+
+ + Optimistic Democracy consensus +
+
+ + Equivalence Principle +
+
-
-
- -
-

Details

-
- -
-
- March 20th - April 3rd -
-
-
-

Hacking Period

-

2 Weeks to build your project

-
-
+ +
+

Timeline

+
+ +
+
+

Hacking Period

+

2 Weeks to build your project

+
+
+ March 20th - April 3rd +
+
+
- - + +
+
+ +
+
+
- -
-
- April 3rd - April 10th -
-
-
-

Judging Process

-

Projects reviewed by expert panel

-
-
+ +
+
+

Judging Process

+

Projects reviewed by expert panel

+
+
+ April 3rd - April 10th +
+
+
- - + +
+
+ +
+
+
- -
-
- April 10th -
-
-
-

Closing Remarks

-

Virtual Demo Day for winners & mentions

+ +
+
+

Closing Remarks

+

Virtual Demo Day for winners & mentions

+
+
+ April 10th +
+
+
@@ -374,10 +399,10 @@

Sponsors

- {#each Array(4) as _, i} -
- Logo -
+ {#each sponsors as sponsor} + + {sponsor.name} + {/each}
@@ -396,7 +421,7 @@ Join the first hackathon where AI participates in blockchain consensus. Register now on DoraHacks.

- Register on DoraHacks diff --git a/frontend/src/routes/Resources.svelte b/frontend/src/routes/Resources.svelte new file mode 100644 index 00000000..ba547fa2 --- /dev/null +++ b/frontend/src/routes/Resources.svelte @@ -0,0 +1,415 @@ + + +
+ + +
+
+

+ {pageHeader.title} +

+

+ {pageHeader.subtitle} +

+
+ + +
+ {#each sections as section} + + {/each} +
+ + +
+
+ + +
+ +
+ + +
+

+ Documentation +

+
+ {#each documentation as doc} +
+ + {doc.tag} + +

+ {doc.title} +

+

+ {doc.description} +

+ + {doc.ctaLabel} + + +
+ {/each} +
+
+ + +
+

+ AI-Assisted Development +

+
+
+ {#each aiDevelopment as tool, toolIdx} +
+ + {tool.tag} + +

+ {tool.title} +

+

+ {tool.description} +

+ + + {#each tool.installSteps as step, stepIdx} +
+ {step.label} +
+ {step.command} + +
+
+ {/each} + + + {tool.ctaLabel} + + +
+ {/each} +
+
+
+ + + + + +
+

+ Ecosystem Projects +

+
+ +
+
+ +
+ {#each ecosystemProjects as project} +
+ + {project.track} + +

+ {project.title} +

+

+ {project.description} +

+ + Visit project + + +
+ {/each} +
+
+
+ + +
+
+ +
+

+ {hackathon.title} +

+

+ {hackathon.subtitle} +

+
+ {#each hackathon.highlights as hl} + + {hl} + + {/each} +
+ +
+
+
+ + +
+

+ How the Portal Works +

+ + + + + +
+ {#each portalInfo as step} +
+
+ {step.step} +
+
+

{step.title}

+

{step.description}

+
+
+ {/each} +
+ + +
+ + +
+

+ Tracks & Ideas +

+
+ {#each tracksAndIdeas as track} +
+ +
+

+ {track.title} +

+

+ {track.description} +

+
+ {/each} +
+
+ +
+ +