From 95cbd8d2d87306d7d16f866ec1b2957527bf8b13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iva=CC=81n=20Raskovsky?= Date: Mon, 16 Mar 2026 18:36:12 +0000 Subject: [PATCH 1/8] Add builders resources page and fix referral links to portal.genlayer.foundation - Create /builders/resources page with docs, AI tools, Bradbury dev links, ecosystem projects, hackathon CTA, portal steps, and track ideas - Add central data file at src/data/resources.js as single source of truth - Fix referral links from points.genlayer.com to portal.genlayer.foundation - Fix OG/Twitter meta tags from points.genlayer.foundation to portal.genlayer.foundation --- frontend/index.html | 8 +- frontend/src/App.svelte | 2 + .../components/profile/CommunityView.svelte | 2 +- .../src/components/shared/CTABanner.svelte | 2 +- frontend/src/data/resources.js | 264 +++++++++++ frontend/src/routes/Resources.svelte | 415 ++++++++++++++++++ 6 files changed, 687 insertions(+), 6 deletions(-) create mode 100644 frontend/src/data/resources.js create mode 100644 frontend/src/routes/Resources.svelte 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/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/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} +
+ + +
+
+ + +
+
+ +
+
+ +
+ + {gettingStarted.tag} + +

+ {gettingStarted.title} +

+

+ {gettingStarted.description} +

+ + {gettingStarted.ctaLabel} + + +
+
+
+ + +
+

+ 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} +
+
+ +
+ + From c535117ffd275ce7f771321b1695c0c9a262e157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iva=CC=81n=20Raskovsky?= Date: Tue, 17 Mar 2026 11:48:44 +0000 Subject: [PATCH 2/8] Update hackathon page: add video, fix spacing, redesign timeline section - Replace video placeholder with Cloudinary-hosted hackathon ad video - Add 64px vertical spacing to Build Once, Earn Forever section - Combine Requirements and Details into side-by-side layout matching Figma - Update DoraHacks link to genlayer-bradbury - Center video vertically and remove border --- frontend/src/routes/Hackathon.svelte | 160 +++++++++++++++------------ 1 file changed, 87 insertions(+), 73 deletions(-) diff --git a/frontend/src/routes/Hackathon.svelte b/frontend/src/routes/Hackathon.svelte index 459d6d9d..12741a34 100644 --- a/frontend/src/routes/Hackathon.svelte +++ b/frontend/src/routes/Hackathon.svelte @@ -128,17 +128,22 @@ - -
-
- -
- Video coming soon + +
+
-
+

Build Once, Earn Forever

@@ -277,78 +282,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 +
+
+
@@ -396,7 +410,7 @@ Join the first hackathon where AI participates in blockchain consensus. Register now on DoraHacks.

- Register on DoraHacks From 6f55b468a51b1ccdda2096ab8810d6c89cd1d44e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iva=CC=81n=20Raskovsky?= Date: Tue, 17 Mar 2026 12:16:06 +0000 Subject: [PATCH 3/8] Add sponsor logos with links and randomized order - Add Chutes, Pathrock Network, Stakeme, and Crouton Digital logos - Each sponsor card links to their website - Background color matches each logo's background - Sponsors display in random order on each page load - Update DoraHacks link to genlayer-bradbury --- .../src/assets/hackathon/sponsors/chutes.webp | Bin 0 -> 4872 bytes .../assets/hackathon/sponsors/crouton.webp | Bin 0 -> 10836 bytes .../assets/hackathon/sponsors/pathrock.png | Bin 0 -> 15968 bytes .../src/assets/hackathon/sponsors/stakeme.png | Bin 0 -> 9677 bytes frontend/src/routes/Hackathon.svelte | 19 ++++++++++++++---- 5 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 frontend/src/assets/hackathon/sponsors/chutes.webp create mode 100644 frontend/src/assets/hackathon/sponsors/crouton.webp create mode 100644 frontend/src/assets/hackathon/sponsors/pathrock.png create mode 100644 frontend/src/assets/hackathon/sponsors/stakeme.png diff --git a/frontend/src/assets/hackathon/sponsors/chutes.webp b/frontend/src/assets/hackathon/sponsors/chutes.webp new file mode 100644 index 0000000000000000000000000000000000000000..0f58aa174a9ad5d83b2ae7828cb2cfcdcdd768ff GIT binary patch literal 4872 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 zxL!A~$yPq000000000000010A;JX! literal 0 HcmV?d00001 diff --git a/frontend/src/assets/hackathon/sponsors/pathrock.png b/frontend/src/assets/hackathon/sponsors/pathrock.png new file mode 100644 index 0000000000000000000000000000000000000000..0fde196c84462d757a5b56d079068ebd7a530af7 GIT binary patch literal 15968 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 F{{YIgKIH%a literal 0 HcmV?d00001 diff --git a/frontend/src/assets/hackathon/sponsors/stakeme.png b/frontend/src/assets/hackathon/sponsors/stakeme.png new file mode 100644 index 0000000000000000000000000000000000000000..82cee7cd1c2fc20675242a225022fa70c1611d3e GIT binary patch literal 9677 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 z z-)s=0f3CcBziI%0=YsVYVc+ZU&k;V9*^q<1KT}LOgB~jqkm3O3sJlkpbATUBjkD%e zNnZKhda%!#%Zr0414Ci_zg-!hgn{<|wrKpfIR((A|Mu(rFYZ+OeXx4-_j$`;V;TS~ O=!vH8UuEjnum2An$A(t` literal 0 HcmV?d00001 diff --git a/frontend/src/routes/Hackathon.svelte b/frontend/src/routes/Hackathon.svelte index 12741a34..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', @@ -388,10 +399,10 @@ From 0081d54ff22e9591bc3091b0b2300c86f398eccc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iva=CC=81n=20Raskovsky?= Date: Fri, 13 Mar 2026 21:29:39 +0000 Subject: [PATCH 4/8] Add responsive hero banner images for tablet and mobile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hero banner now supports separate images per breakpoint. Content managers can upload tablet and mobile variants in the admin, with automatic fallback to the desktop image. The banner layout also adjusts on mobile — taller height with top-aligned content card for better readability. ## Claude Implementation Notes - backend/contributions/models.py: Added hero_image_tablet, hero_image_mobile ImageFields to FeaturedContent (nullable, fallback help text) - backend/contributions/serializers.py: Extracted shared _build_image_url helper, added hero_image_url_tablet and hero_image_url_mobile fields - backend/contributions/admin.py: Added tablet/mobile fields to Links & Media fieldset with fallback description - backend/contributions/migrations/0039_add_responsive_hero_images.py: Migration for new image fields - frontend/src/components/portal/HeroBanner.svelte: Replaced with element using media queries (1024px, 768px breakpoints). Mobile-first layout: items-start + min-h-[480px] on mobile, items-end + min-h-[300px] on desktop. Applied same responsive changes to validator banner and loading skeleton. --- backend/contributions/admin.py | 4 ++-- .../0039_add_responsive_hero_images.py | 23 ++++++++++++++++++ backend/contributions/models.py | 2 ++ backend/contributions/serializers.py | 24 ++++++++++++++----- .../src/components/portal/HeroBanner.svelte | 18 ++++++++------ 5 files changed, 56 insertions(+), 15 deletions(-) create mode 100644 backend/contributions/migrations/0039_add_responsive_hero_images.py diff --git a/backend/contributions/admin.py b/backend/contributions/admin.py index 6be9af4c..1c6df86a 100644 --- a/backend/contributions/admin.py +++ b/backend/contributions/admin.py @@ -694,8 +694,8 @@ 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', 'hero_image_tablet', 'hero_image_mobile', 'user_profile_image', 'url'), + 'description': 'Upload images directly. Django serves them from the media directory. Tablet/mobile hero images are optional — falls back to the main hero image.' }), ('Metadata', { 'fields': ('created_at', 'updated_at'), 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/models.py b/backend/contributions/models.py index a12c5dfb..ef87a788 100644 --- a/backend/contributions/models.py +++ b/backend/contributions/models.py @@ -664,6 +664,8 @@ class FeaturedContent(BaseModel): related_name='featured_items' ) hero_image = models.ImageField(upload_to='featured/', blank=True, null=True) + hero_image_tablet = models.ImageField(upload_to='featured/', blank=True, null=True, help_text='Tablet variant (768-1023px). Falls back to hero_image if empty.') + hero_image_mobile = models.ImageField(upload_to='featured/', blank=True, null=True, help_text='Mobile variant (<768px). Falls back to hero_image if empty.') user_profile_image = models.ImageField(upload_to='featured/avatars/', blank=True, null=True) url = models.URLField(max_length=500, blank=True) is_active = models.BooleanField(default=True) diff --git a/backend/contributions/serializers.py b/backend/contributions/serializers.py index 702bb394..dcabee69 100644 --- a/backend/contributions/serializers.py +++ b/backend/contributions/serializers.py @@ -723,24 +723,36 @@ class FeaturedContentSerializer(serializers.ModelSerializer): user_address = serializers.CharField(source='user.address', read_only=True) user_profile_image_url = serializers.SerializerMethodField() hero_image_url = serializers.SerializerMethodField() + hero_image_url_tablet = serializers.SerializerMethodField() + hero_image_url_mobile = 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: + def _build_image_url(self, image_field): + """Return absolute URL for an image field if set.""" + if image_field: request = self.context.get('request') if request: - return request.build_absolute_uri(obj.hero_image.url) - return obj.hero_image.url + return request.build_absolute_uri(image_field.url) + return image_field.url return '' + def get_hero_image_url(self, obj): + return self._build_image_url(obj.hero_image) + + def get_hero_image_url_tablet(self, obj): + return self._build_image_url(obj.hero_image_tablet) + + def get_hero_image_url_mobile(self, obj): + return self._build_image_url(obj.hero_image_mobile) + 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: 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} From ba08b77631529bbb330c0cb0ddab9050a5bad917 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iva=CC=81n=20Raskovsky?= Date: Fri, 13 Mar 2026 21:29:43 +0000 Subject: [PATCH 5/8] Add validator API endpoint tests Basic test coverage for the validator profile CRUD endpoints: create, read, update for both Asimov and Bradbury node versions. ## Claude Implementation Notes - backend/validators/tests/test_api.py: New test file with 5 tests covering GET/PATCH /api/v1/validators/me/ for profile creation, update, and retrieval --- backend/validators/tests/test_api.py | 85 ++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 backend/validators/tests/test_api.py 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') From 551f205b12837d531ec4c43797a1c3bb2c4e7a43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iva=CC=81n=20Raskovsky?= Date: Fri, 13 Mar 2026 21:29:50 +0000 Subject: [PATCH 6/8] Add structured commit workflow and changelog Introduces a commit skill that produces 3-layer commit messages (product summary, architecture notes, implementation details) and maintains a human-facing changelog. Also sets git pull to rebase by default for clean linear history. ## Claude Implementation Notes - .claude/commands/commit.md: New commit skill with 3-layer message format, changelog update step, git history management, and learned preferences section - CHANGELOG.md: Initialized with Unreleased section - CLAUDE.md: Replaced attribution section with pointer to commit workflow file --- .claude/commands/commit.md | 122 +++++++++++++++++++++++++++++++++++++ CHANGELOG.md | 5 ++ CLAUDE.md | 8 +-- 3 files changed, 129 insertions(+), 6 deletions(-) create mode 100644 .claude/commands/commit.md create mode 100644 CHANGELOG.md 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..427f66b8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +All notable user-facing changes to this project will be documented in this file. + +## Unreleased 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 From 8aed080aac1b7bf6820d5553309dd0ecaf0526fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iva=CC=81n=20Raskovsky?= Date: Fri, 13 Mar 2026 21:30:01 +0000 Subject: [PATCH 7/8] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 427f66b8..ec19074d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,3 +3,5 @@ All notable user-facing changes to this project will be documented in this file. ## Unreleased + +- Responsive hero banner images for tablet and mobile (e5c01b5) From e8a2769a23877bf3fc0cfeb773a57f6f92128d42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iva=CC=81n=20Raskovsky?= Date: Mon, 16 Mar 2026 20:42:10 +0000 Subject: [PATCH 8/8] Convert featured content images from local storage to Cloudinary The FeaturedContent model previously used Django ImageField for local file storage, inconsistent with the rest of the project which uses Cloudinary. All image fields now use URLField + public_id CharField, matching the User model pattern. ## Claude Implementation Notes - backend/contributions/models.py: Replace 4 ImageFields with URLField + public_id pairs (hero, tablet, mobile, avatar) - backend/contributions/serializers.py: Remove SerializerMethodField helpers, read URL fields directly from model - backend/contributions/admin.py: Show URL fields instead of file uploads, public_ids as read-only collapsed section - backend/contributions/management/commands/seed_featured_content.py: Remove local file copy logic, simplified to just create records - backend/contributions/migrations/0040_convert_featured_images_to_cloudinary.py: Migration for the field conversion --- backend/contributions/admin.py | 15 +++- .../commands/seed_featured_content.py | 83 ++----------------- ...0_convert_featured_images_to_cloudinary.py | 69 +++++++++++++++ backend/contributions/models.py | 12 ++- backend/contributions/serializers.py | 30 +------ 5 files changed, 101 insertions(+), 108 deletions(-) create mode 100644 backend/contributions/migrations/0040_convert_featured_images_to_cloudinary.py diff --git a/backend/contributions/admin.py b/backend/contributions/admin.py index 1c6df86a..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', 'hero_image_tablet', 'hero_image_mobile', 'user_profile_image', 'url'), - 'description': 'Upload images directly. Django serves them from the media directory. Tablet/mobile hero images are optional — falls back to the main hero image.' + '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/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 ef87a788..9af2922c 100644 --- a/backend/contributions/models.py +++ b/backend/contributions/models.py @@ -663,10 +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) - hero_image_tablet = models.ImageField(upload_to='featured/', blank=True, null=True, help_text='Tablet variant (768-1023px). Falls back to hero_image if empty.') - hero_image_mobile = models.ImageField(upload_to='featured/', blank=True, null=True, help_text='Mobile variant (<768px). Falls back to hero_image if empty.') - 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 dcabee69..4605c5bb 100644 --- a/backend/contributions/serializers.py +++ b/backend/contributions/serializers.py @@ -722,9 +722,6 @@ 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() - hero_image_url_tablet = serializers.SerializerMethodField() - hero_image_url_mobile = serializers.SerializerMethodField() link = serializers.SerializerMethodField() class Meta: @@ -735,31 +732,10 @@ class Meta: 'user', 'user_name', 'user_address', 'user_profile_image_url', 'contribution', 'is_active', 'order', 'created_at'] - def _build_image_url(self, image_field): - """Return absolute URL for an image field if set.""" - if image_field: - request = self.context.get('request') - if request: - return request.build_absolute_uri(image_field.url) - return image_field.url - return '' - - def get_hero_image_url(self, obj): - return self._build_image_url(obj.hero_image) - - def get_hero_image_url_tablet(self, obj): - return self._build_image_url(obj.hero_image_tablet) - - def get_hero_image_url_mobile(self, obj): - return self._build_image_url(obj.hero_image_mobile) - 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 ''

Sponsors