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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions .claude/commands/commit.md
Original file line number Diff line number Diff line change
@@ -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:
```
<subject line — product level, imperative, <72 chars>

<body — architecture/component level, what and why, no file paths>

## 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'
<the approved message>
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: `- <what changed for users> (<short hash>)`
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

<!-- Claude updates this section when the user gives feedback about commit style -->
<!-- Examples: "don't mention migration files", "always group frontend/backend" -->

(none yet)
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
8 changes: 2 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <noreply@anthropic.com>

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

Expand Down
15 changes: 12 additions & 3 deletions backend/contributions/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand All @@ -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'),
Expand Down
83 changes: 9 additions & 74 deletions backend/contributions/management/commands/seed_featured_content.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
# ----------------------------------------------------------------
Expand Down Expand Up @@ -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}")
)
Expand All @@ -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,
},
Expand All @@ -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.')
Original file line number Diff line number Diff line change
@@ -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/'),
),
]
Loading
Loading