+
+
+
- {% trans "Welcome to your new Wagtail site!" %}
- {% trans " by Wagtail Shop Kit" %}
+ Welcome to Wagtail Shop
+ Discover amazing products for your needs
-
-
-
- archive-content
-
-
-
-
-
-
-
- Docker Development Environment
-
-
-
- database
-
-
-
-
+ {% comment %} below can be dynamic later {% endcomment %}
+ Shop Now
+
+
+
+
+
+
+ Featured Products
+
+
+
+
+
+
- Postgresql, Mysql or Sqlite3 Database
-
-
-
- cycle
-
-
-
-
+
+
+
Classic Watch
+
Elegant timepiece for any occasion
+
$129.99
+
Add to Cart
+
+
+
+
+
+
+
+
- Frontend Node SASS and Javascript compilation
-
-
-
- style
-
-
-
-
-
-
-
+
+
+
Leather Bag
+
Premium quality everyday carry
+
$89.99
+
Add to Cart
+
+
+
+
+
+
+
+
- Pico CSS for almost classless styling
-
-
-
- questionnaire
-
-
-
-
-
-
+
+
+
Wireless Headphones
+
Crystal clear sound quality
+
$159.99
+
Add to Cart
+
+
+
+
+
+
+
+
+
- esbuild javascript bundler
-
-
-
- patch-19
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
Desk Lamp
+
Modern minimalist lighting
+
$49.99
+
Add to Cart
+
+
+
+
+
+
+
+ Shop by Category
+ {% if featured_categories %}
+
-
-
-
-
+
+ {% else %}
+
+ No categories available yet. Run python manage.py create_sample_categories to create sample categories.
+
+ {% endif %}
+
-
-
diff --git a/app/home/tests.py b/app/home/tests.py
index 5b9f502..cf8ed8a 100644
--- a/app/home/tests.py
+++ b/app/home/tests.py
@@ -30,7 +30,7 @@ def test_home_frontend_returns_200(self):
"""Test that the home page frontend returns 200 OK."""
response = self.client.get("/")
self.assertEqual(response.status_code, 200)
- self.assertContains(response, "Welcome to your new Wagtail site!")
+ self.assertContains(response, "Welcome to Wagtail Shop")
self.assertTemplateUsed(response, "home/home_page.html")
def test_home_admin_edit_returns_200(self):
diff --git a/app/settings/base.py b/app/settings/base.py
index d0cf7b9..dc75e65 100644
--- a/app/settings/base.py
+++ b/app/settings/base.py
@@ -27,6 +27,7 @@
INSTALLED_APPS = [
"app.home",
"app.search",
+ "app.shop",
"wagtail.contrib.forms",
"wagtail.contrib.redirects",
"wagtail.contrib.table_block",
diff --git a/app/shop/__init__.py b/app/shop/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/shop/admin.py b/app/shop/admin.py
new file mode 100644
index 0000000..846f6b4
--- /dev/null
+++ b/app/shop/admin.py
@@ -0,0 +1 @@
+# Register your models here.
diff --git a/app/shop/apps.py b/app/shop/apps.py
new file mode 100644
index 0000000..f0eefef
--- /dev/null
+++ b/app/shop/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class ShopConfig(AppConfig):
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "app.shop"
diff --git a/app/shop/management/__init__.py b/app/shop/management/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/shop/management/commands/__init__.py b/app/shop/management/commands/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/shop/management/commands/create_sample_categories.py b/app/shop/management/commands/create_sample_categories.py
new file mode 100644
index 0000000..e70148e
--- /dev/null
+++ b/app/shop/management/commands/create_sample_categories.py
@@ -0,0 +1,271 @@
+import random
+
+from django.core.management.base import BaseCommand
+from wagtail.images.models import Image
+from wagtail.models import Page
+
+from app.shop.models import ShopCategoryPage, ShopIndexPage
+
+
+class Command(BaseCommand):
+ help = "Creates sample category pages for the shop under a ShopIndexPage"
+
+ def add_arguments(self, parser):
+ parser.add_argument(
+ "--reset",
+ action="store_true",
+ help="Delete all existing ShopCategoryPage instances before creating new ones",
+ )
+ parser.add_argument(
+ "--no-icons",
+ action="store_true",
+ help="Skip assigning icons to categories",
+ )
+ parser.add_argument(
+ "--home-slug",
+ type=str,
+ default="home",
+ help="Slug of the home page (default: home)",
+ )
+ parser.add_argument(
+ "--shop-title",
+ type=str,
+ default="Shop",
+ help="Title for the shop index page (default: Shop)",
+ )
+ parser.add_argument(
+ "--shop-slug",
+ type=str,
+ default="shop",
+ help="Slug for the shop index page (default: shop)",
+ )
+
+ def handle(self, *args, **options):
+ # Find home page
+ try:
+ home_page = Page.objects.get(slug=options["home_slug"])
+ except Page.DoesNotExist:
+ self.stdout.write(
+ self.style.ERROR(
+ f"Home page with slug '{options['home_slug']}' not found"
+ )
+ )
+ return
+
+ # Find or create ShopIndexPage
+ shop_index = ShopIndexPage.objects.filter(slug=options["shop_slug"]).first()
+
+ if shop_index:
+ self.stdout.write(
+ self.style.SUCCESS(
+ f"Found existing ShopIndexPage: '{shop_index.title}' at {shop_index.get_url()}"
+ )
+ )
+ else:
+ self.stdout.write("Creating ShopIndexPage...")
+ shop_index = ShopIndexPage(
+ title=options["shop_title"],
+ slug=options["shop_slug"],
+ intro="
Browse our collection of quality products across various categories.
",
+ show_in_menus=True,
+ )
+ home_page.add_child(instance=shop_index)
+ rev = shop_index.save_revision()
+ rev.publish()
+ self.stdout.write(
+ self.style.SUCCESS(
+ f"✓ Created ShopIndexPage: '{shop_index.title}' at {shop_index.get_url()}"
+ )
+ )
+
+ # Reset if requested
+ if options["reset"]:
+ self.stdout.write("Deleting existing category pages...")
+ all_categories = shop_index.get_children().type(ShopCategoryPage)
+ deleted_count = all_categories.count()
+
+ # Delete all at once instead of iterating
+ for category in all_categories:
+ category.delete()
+
+ # Refresh shop index from database to update tree structure
+ shop_index.refresh_from_db()
+
+ self.stdout.write(
+ self.style.SUCCESS(f"Deleted {deleted_count} category pages")
+ )
+
+ # Query available images (unless --no-icons is specified)
+ available_images = []
+ if not options["no_icons"]:
+ available_images = list(Image.objects.all().order_by("?")) # Random order
+ if not available_images:
+ self.stdout.write(
+ self.style.WARNING(
+ "No images found in database. Run 'python manage.py create_sample_media' first."
+ )
+ )
+ self.stdout.write(
+ self.style.WARNING("Creating categories without icons...")
+ )
+ else:
+ self.stdout.write(
+ self.style.SUCCESS(
+ f"Found {len(available_images)} images available for category icons"
+ )
+ )
+
+ # Category data matching the home page
+ categories = [
+ {
+ "title": "Electronics",
+ "slug": "electronics",
+ "description": """
+
Discover the latest gadgets and tech products. From smartphones and laptops to
+ smart home devices and wearables, find everything you need to stay connected and
+ productive in the digital age.
+
Our electronics collection features cutting-edge technology from leading brands,
+ ensuring quality and innovation in every product.
+ """,
+ "featured": True,
+ },
+ {
+ "title": "Fashion",
+ "slug": "fashion",
+ "description": """
+
Explore our curated collection of clothing and accessories for every style and
+ occasion. From casual everyday wear to elegant formal attire, express your unique
+ personality through fashion.
+
We offer a diverse range of sizes, styles, and trends to suit everyone's taste.
+ """,
+ "featured": True,
+ },
+ {
+ "title": "Home & Living",
+ "slug": "home-living",
+ "description": """
+
Transform your living space with our selection of furniture and decor. Create a
+ comfortable, stylish home that reflects your personality and meets your lifestyle needs.
+
From modern minimalist designs to classic traditional pieces, we have everything
+ to make your house a home.
+ """,
+ "featured": True,
+ },
+ {
+ "title": "Beauty",
+ "slug": "beauty",
+ "description": """
+
Discover premium skincare and cosmetics to enhance your natural beauty. Our
+ carefully selected products help you look and feel your best every day.
+
From daily essentials to special occasion makeup, we offer quality beauty products
+ for all skin types and preferences.
+ """,
+ "featured": False,
+ },
+ {
+ "title": "Sports",
+ "slug": "sports",
+ "description": """
+
Get active with our range of fitness and outdoor equipment. Whether you're a
+ seasoned athlete or just starting your fitness journey, we have the gear you need
+ to reach your goals.
+
Explore workout equipment, outdoor gear, and athletic wear designed for
+ performance and comfort.
+ """,
+ "featured": False,
+ },
+ {
+ "title": "Books",
+ "slug": "books",
+ "description": """
+
Feed your mind with our diverse collection of books. From bestselling novels and
+ educational resources to inspiring biographies and practical guides, discover your
+ next great read.
+
We offer books across all genres and subjects, perfect for readers of all ages
+ and interests.
+ """,
+ "featured": False,
+ },
+ ]
+
+ created_count = 0
+
+ # Shuffle images for random assignment
+ if available_images:
+ random.shuffle(available_images)
+
+ for idx, category_data in enumerate(categories):
+ # Check if category already exists under shop index
+ existing = (
+ shop_index.get_children()
+ .type(ShopCategoryPage)
+ .filter(slug=category_data["slug"])
+ .first()
+ )
+ if existing:
+ self.stdout.write(
+ self.style.WARNING(
+ f"Category '{category_data['title']}' already exists, skipping..."
+ )
+ )
+ continue
+
+ # Create category page
+ category_page = ShopCategoryPage(
+ title=category_data["title"],
+ slug=category_data["slug"],
+ description=category_data["description"],
+ featured=category_data["featured"],
+ show_in_menus=True, # Make categories appear in menus
+ )
+
+ # Assign icon from shuffled image list
+ if available_images:
+ # Use modulo to cycle through images if we have fewer images than categories
+ icon_index = idx % len(available_images)
+ category_page.icon = available_images[icon_index]
+ self.stdout.write(
+ f" Assigning icon: {category_page.icon.title} (ID: {category_page.icon.id})"
+ )
+
+ # Add as child of shop index page
+ shop_index.add_child(instance=category_page)
+ rev = category_page.save_revision()
+ rev.publish()
+
+ created_count += 1
+ icon_status = (
+ f" 🖼️ (icon: {category_page.icon.title[:30]}...)"
+ if category_page.icon
+ else " (no icon)"
+ )
+ featured_status = " ⭐ [FEATURED]" if category_page.featured else ""
+ self.stdout.write(
+ self.style.SUCCESS(
+ f" ✓ Created: {category_page.title}{icon_status}{featured_status}"
+ )
+ )
+
+ # Summary
+ self.stdout.write("\n" + "=" * 60)
+ if created_count > 0:
+ self.stdout.write(
+ self.style.SUCCESS(
+ f"Successfully created {created_count} category pages under '{shop_index.title}'"
+ )
+ )
+ else:
+ self.stdout.write(self.style.WARNING("No new category pages were created"))
+
+ # Display URLs
+ if created_count > 0:
+ self.stdout.write("\nCategory URLs:")
+ for category in (
+ shop_index.get_children()
+ .type(ShopCategoryPage)
+ .live()
+ .order_by("title")
+ ):
+ self.stdout.write(f" → {category.get_url()} - {category.title}")
+
+ self.stdout.write("=" * 60)
diff --git a/app/shop/management/commands/create_sample_products.py b/app/shop/management/commands/create_sample_products.py
new file mode 100644
index 0000000..ddf5004
--- /dev/null
+++ b/app/shop/management/commands/create_sample_products.py
@@ -0,0 +1,354 @@
+import random
+from decimal import Decimal
+
+from django.core.management.base import BaseCommand
+from faker import Faker
+from wagtail.images.models import Image
+from wagtail.models import Page
+
+from app.shop.models import ProductImage, ProductPage, ShopCategoryPage
+
+fake = Faker()
+
+
+class Command(BaseCommand):
+ help = "Creates sample product pages for testing under existing category pages"
+
+ def add_arguments(self, parser):
+ parser.add_argument(
+ "--count",
+ type=int,
+ default=20,
+ help="Number of products to create per category (default: 20)",
+ )
+ parser.add_argument(
+ "--reset",
+ action="store_true",
+ help="Delete all existing ProductPage instances before creating new ones",
+ )
+ parser.add_argument(
+ "--with-images",
+ action="store_true",
+ help="Generate sample product images and gallery",
+ )
+ parser.add_argument(
+ "--featured",
+ type=int,
+ default=3,
+ help="Number of products per category to mark as featured (default: 3)",
+ )
+
+ def handle(self, *args, **options):
+ # Fix tree structure first (in case it's corrupted)
+ self.stdout.write("Fixing page tree structure...")
+ Page.fix_tree()
+
+ # Get all category pages
+ categories = ShopCategoryPage.objects.live()
+
+ if not categories.exists():
+ self.stdout.write(
+ self.style.ERROR(
+ "No category pages found. Run 'python manage.py create_sample_categories' first."
+ )
+ )
+ return
+
+ self.stdout.write(
+ self.style.SUCCESS(f"Found {categories.count()} category pages")
+ )
+
+ # Reset if requested
+ if options["reset"]:
+ self.stdout.write("Deleting existing product pages...")
+ all_products = ProductPage.objects.all()
+ deleted_count = all_products.count()
+ all_products.delete()
+
+ # Refresh all categories from database to update tree structure
+ for category in categories:
+ category.refresh_from_db()
+
+ self.stdout.write(
+ self.style.SUCCESS(f"Deleted {deleted_count} product pages")
+ )
+
+ # Get available images for products
+ available_images = []
+ if options["with_images"]:
+ available_images = list(Image.objects.all().order_by("?")) # Random order
+ if not available_images:
+ self.stdout.write(
+ self.style.WARNING(
+ "No images found in database. Run 'python manage.py create_sample_media' first."
+ )
+ )
+ self.stdout.write(
+ self.style.WARNING("Creating products without images...")
+ )
+ else:
+ self.stdout.write(
+ self.style.SUCCESS(
+ f"Found {len(available_images)} images available for products"
+ )
+ )
+
+ # Product name templates by category
+ product_templates = {
+ "electronics": [
+ "Wireless Bluetooth Headphones",
+ "Smart Watch Series {n}",
+ "Portable Bluetooth Speaker",
+ "USB-C Fast Charger",
+ "Wireless Mouse",
+ "Mechanical Keyboard",
+ "HD Webcam",
+ "Laptop Stand",
+ "Phone Case",
+ "Screen Protector",
+ "Power Bank 20000mAh",
+ "Wireless Earbuds",
+ "HDMI Cable",
+ "Gaming Controller",
+ "LED Desk Lamp",
+ ],
+ "fashion": [
+ "Cotton T-Shirt",
+ "Denim Jeans",
+ "Leather Jacket",
+ "Running Shoes",
+ "Canvas Sneakers",
+ "Wool Sweater",
+ "Summer Dress",
+ "Casual Shorts",
+ "Baseball Cap",
+ "Leather Belt",
+ "Cotton Socks (3-Pack)",
+ "Winter Coat",
+ "Scarf",
+ "Gloves",
+ "Sunglasses",
+ ],
+ "home-living": [
+ "Ceramic Coffee Mug",
+ "Throw Pillow",
+ "Floor Lamp",
+ "Wall Clock",
+ "Photo Frame",
+ "Desk Organizer",
+ "Storage Box",
+ "Area Rug",
+ "Curtain Panel",
+ "Table Lamp",
+ "Decorative Vase",
+ "Wall Art Print",
+ "Couch Cover",
+ "Bedding Set",
+ "Kitchen Mat",
+ ],
+ "beauty": [
+ "Facial Cleanser",
+ "Moisturizing Cream",
+ "Lip Balm",
+ "Face Mask Set",
+ "Makeup Brush Set",
+ "Eyeshadow Palette",
+ "Liquid Foundation",
+ "Hair Serum",
+ "Nail Polish Set",
+ "Body Lotion",
+ "Sunscreen SPF 50",
+ "Anti-Aging Serum",
+ "Exfoliating Scrub",
+ "Makeup Remover",
+ "Hair Straightener",
+ ],
+ "sports": [
+ "Yoga Mat",
+ "Resistance Bands Set",
+ "Dumbbell Set",
+ "Water Bottle",
+ "Running Shorts",
+ "Sports Bra",
+ "Fitness Tracker",
+ "Jump Rope",
+ "Gym Bag",
+ "Foam Roller",
+ "Athletic Socks",
+ "Compression Sleeves",
+ "Exercise Ball",
+ "Knee Support",
+ "Sweat Towel",
+ ],
+ "books": [
+ "The Art of Programming",
+ "Mystery Novel Collection",
+ "Cookbook: Healthy Recipes",
+ "Self-Help Guide",
+ "Science Fiction Novel",
+ "Biography: Inspiring Lives",
+ "Children's Story Book",
+ "Travel Guide",
+ "Business Strategy Book",
+ "Poetry Collection",
+ "History Encyclopedia",
+ "Graphic Novel",
+ "Language Learning Book",
+ "Philosophy Reader",
+ "DIY Home Improvement",
+ ],
+ }
+
+ total_created = 0
+ total_images_created = 0
+
+ for category in categories:
+ self.stdout.write(f"\nProcessing category: {category.title}")
+ self.stdout.write("-" * 60)
+
+ # Get product templates for this category
+ category_slug = category.slug
+ templates = product_templates.get(
+ category_slug, ["Product {n}"] * options["count"]
+ )
+
+ # Ensure we have enough product names
+ if len(templates) < options["count"]:
+ # Cycle through templates if we need more
+ templates = templates * ((options["count"] // len(templates)) + 1)
+
+ # Shuffle templates for variety
+ random.shuffle(templates)
+
+ for i in range(options["count"]):
+ # Generate product title
+ product_title = templates[i].format(n=i + 1)
+
+ # Check if product already exists
+ existing = (
+ category.get_children()
+ .type(ProductPage)
+ .filter(title=product_title)
+ )
+ if existing.exists():
+ self.stdout.write(
+ self.style.WARNING(
+ f" Product '{product_title}' already exists, skipping..."
+ )
+ )
+ continue
+
+ # Generate realistic price (10-500 range with .99 endings)
+ price_base = random.randint(10, 500)
+ price = Decimal(f"{price_base}.99")
+
+ # Generate rich text description
+ description = self._generate_description(product_title, category.title)
+
+ # Random stock status (80% in stock)
+ in_stock = random.random() < 0.8
+
+ # Determine if featured (first N products per category)
+ featured = i < options["featured"]
+
+ # Generate SKU with category prefix and random suffix
+ from datetime import datetime
+
+ category_prefix = category.slug[:4].upper()
+ timestamp = datetime.now().strftime("%m%d%H%M%S%f")[
+ :14
+ ] # Include microseconds
+ sku = f"{category_prefix}-{timestamp}"
+
+ # Create product page
+ product = ProductPage(
+ title=product_title,
+ price=price,
+ sku=sku, # Explicitly set SKU
+ description=description,
+ in_stock=in_stock,
+ featured=featured,
+ show_in_menus=False,
+ )
+
+ # Assign main image if available
+ if available_images:
+ product.main_image = random.choice(available_images)
+
+ # Add as child of category
+ category.add_child(instance=product)
+ rev = product.save_revision()
+ rev.publish()
+
+ # Add gallery images (2-4 images per product)
+ if available_images and options["with_images"]:
+ num_gallery_images = random.randint(2, 4)
+ # Get random images, ensuring no duplicates
+ gallery_images = random.sample(
+ available_images, min(num_gallery_images, len(available_images))
+ )
+
+ for img in gallery_images:
+ ProductImage.objects.create(
+ product=product,
+ image=img,
+ caption=f"{product_title} - {fake.sentence(nb_words=4)}",
+ sort_order=gallery_images.index(img),
+ )
+ total_images_created += 1
+
+ total_created += 1
+
+ # Status indicators
+ status_indicators = []
+ if product.main_image:
+ status_indicators.append("🖼️ ")
+ if product.featured:
+ status_indicators.append("⭐")
+ if not product.in_stock:
+ status_indicators.append("📦")
+
+ status = " ".join(status_indicators)
+
+ self.stdout.write(
+ self.style.SUCCESS(
+ f" ✓ {product.title} - ${product.price} {status}"
+ )
+ )
+
+ # Summary
+ self.stdout.write("\n" + "=" * 60)
+ self.stdout.write(
+ self.style.SUCCESS(
+ f"Successfully created {total_created} product pages across {categories.count()} categories"
+ )
+ )
+ if options["with_images"] and total_images_created > 0:
+ self.stdout.write(
+ self.style.SUCCESS(
+ f"Added {total_images_created} gallery images to products"
+ )
+ )
+
+ self.stdout.write("\nLegend:")
+ self.stdout.write(" 🖼️ = Has main image")
+ self.stdout.write(" ⭐ = Featured product")
+ self.stdout.write(" 📦 = Out of stock")
+ self.stdout.write("=" * 60)
+
+ def _generate_description(self, product_title, category_title):
+ """Generate realistic product description using Faker"""
+ intro = fake.paragraph(nb_sentences=2)
+ features = "\n".join(
+ [f"
{fake.sentence()} " for _ in range(random.randint(3, 5))]
+ )
+ closing = fake.paragraph(nb_sentences=1)
+
+ return f"""
+
{intro}
+
Key Features:
+
+
{closing}
+ """
diff --git a/app/shop/migrations/0001_initial.py b/app/shop/migrations/0001_initial.py
new file mode 100644
index 0000000..c6543cd
--- /dev/null
+++ b/app/shop/migrations/0001_initial.py
@@ -0,0 +1,31 @@
+# Generated by Django 5.2.9 on 2026-01-02 17:18
+
+import django.db.models.deletion
+import wagtail.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('wagtailcore', '0096_referenceindex_referenceindex_source_object_and_more'),
+ ('wagtailimages', '0027_image_description'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='ShopCategoryPage',
+ fields=[
+ ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
+ ('description', wagtail.fields.RichTextField(blank=True)),
+ ('featured', models.BooleanField(default=False)),
+ ('icon', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.image')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=('wagtailcore.page',),
+ ),
+ ]
diff --git a/app/shop/migrations/0002_shopindexpage.py b/app/shop/migrations/0002_shopindexpage.py
new file mode 100644
index 0000000..e810a3a
--- /dev/null
+++ b/app/shop/migrations/0002_shopindexpage.py
@@ -0,0 +1,27 @@
+# Generated by Django 5.2.9 on 2026-01-02 22:01
+
+import django.db.models.deletion
+import wagtail.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('shop', '0001_initial'),
+ ('wagtailcore', '0096_referenceindex_referenceindex_source_object_and_more'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='ShopIndexPage',
+ fields=[
+ ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
+ ('intro', wagtail.fields.RichTextField(blank=True, help_text='Optional introduction text for the shop')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=('wagtailcore.page',),
+ ),
+ ]
diff --git a/app/shop/migrations/0003_productpage_productimage.py b/app/shop/migrations/0003_productpage_productimage.py
new file mode 100644
index 0000000..8ecd4bd
--- /dev/null
+++ b/app/shop/migrations/0003_productpage_productimage.py
@@ -0,0 +1,48 @@
+# Generated by Django 5.2.9 on 2026-01-02 22:42
+
+import django.db.models.deletion
+import modelcluster.fields
+import wagtail.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('shop', '0002_shopindexpage'),
+ ('wagtailcore', '0096_referenceindex_referenceindex_source_object_and_more'),
+ ('wagtailimages', '0027_image_description'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='ProductPage',
+ fields=[
+ ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
+ ('price', models.DecimalField(decimal_places=2, help_text='Product price in dollars', max_digits=10)),
+ ('sku', models.CharField(blank=True, help_text='Stock Keeping Unit - leave blank to auto-generate', max_length=100, unique=True)),
+ ('description', wagtail.fields.RichTextField(help_text='Product description and details')),
+ ('in_stock', models.BooleanField(default=True, help_text='Whether the product is currently available')),
+ ('featured', models.BooleanField(default=False, help_text='Show this product in featured sections')),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('main_image', models.ForeignKey(blank=True, help_text='Main product image', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.image')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=('wagtailcore.page',),
+ ),
+ migrations.CreateModel(
+ name='ProductImage',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('sort_order', models.IntegerField(blank=True, editable=False, null=True)),
+ ('caption', models.CharField(blank=True, max_length=250)),
+ ('image', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='wagtailimages.image')),
+ ('product', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='gallery_images', to='shop.productpage')),
+ ],
+ options={
+ 'ordering': ['sort_order'],
+ },
+ ),
+ ]
diff --git a/app/shop/migrations/__init__.py b/app/shop/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/shop/models.py b/app/shop/models.py
new file mode 100644
index 0000000..f75507b
--- /dev/null
+++ b/app/shop/models.py
@@ -0,0 +1,202 @@
+from django.core.paginator import Paginator
+from django.db import models
+from modelcluster.fields import ParentalKey
+from wagtail.admin.panels import FieldPanel, InlinePanel
+from wagtail.fields import RichTextField
+from wagtail.models import Orderable, Page
+
+"""
+**Create ShopCategoryPage model**
+ - File: `app/shop/models.py`
+ - Extends: `wagtail.models.Page`
+ - Fields:
+ - `description` (RichTextField, blank=True)
+ - `icon` (ForeignKey to wagtailimages.Image, optional)
+ - `featured` (BooleanField, default=False)
+ - Content panels:
+ - Basic: title (inherited from Page), description, icon, featured
+ - Settings panels:
+ - Promote tab: slug (auto-populating), SEO fields
+ - Parent page types: `HomePage` or self (for subcategories)
+ - Subpage types: `ProductPage`, `CategoryPage` (allow subcategories)
+ - Methods:
+ - `get_products()`: Returns child ProductPage objects
+ - `get_context()`: Add products and pagination to template
+ """
+
+
+class ShopIndexPage(Page):
+ """
+ Main shop page that displays all categories.
+ Should be created once as a child of HomePage.
+ """
+
+ intro = RichTextField(
+ blank=True, help_text="Optional introduction text for the shop"
+ )
+
+ content_panels = Page.content_panels + [
+ FieldPanel("intro"),
+ ]
+
+ parent_page_types = ["home.HomePage"]
+ subpage_types = ["shop.ShopCategoryPage"]
+ max_count = 1 # Only one shop index page allowed
+
+ def get_context(self, request):
+ context = super().get_context(request)
+ # Get all category pages that are children of this shop index
+ categories = ShopCategoryPage.objects.child_of(self).live().order_by("title")
+ context["categories"] = categories
+ return context
+
+
+class ShopCategoryPage(Page):
+ description = RichTextField(blank=True)
+ icon = models.ForeignKey(
+ "wagtailimages.Image",
+ null=True,
+ blank=True,
+ on_delete=models.SET_NULL,
+ related_name="+",
+ )
+ featured = models.BooleanField(default=False)
+
+ content_panels = Page.content_panels + [
+ FieldPanel("description"),
+ FieldPanel("icon"),
+ FieldPanel("featured"),
+ ]
+
+ parent_page_types = ["shop.ShopIndexPage"]
+ subpage_types = ["shop.ProductPage"]
+
+ def get_products(self):
+ """Returns child ProductPage objects"""
+ return ProductPage.objects.child_of(self).live().order_by("-first_published_at")
+
+ def get_context(self, request):
+ context = super().get_context(request)
+ products = self.get_products()
+ paginator = Paginator(products, 12) # Show 12 products per page
+ page_number = request.GET.get("page")
+ page_obj = paginator.get_page(page_number)
+ context["products"] = page_obj
+ return context
+
+
+class ProductImage(Orderable):
+ """
+ Inline image gallery for products.
+ Allows multiple images per product with captions and ordering.
+ """
+
+ product = ParentalKey(
+ "ProductPage", on_delete=models.CASCADE, related_name="gallery_images"
+ )
+ image = models.ForeignKey(
+ "wagtailimages.Image",
+ on_delete=models.CASCADE,
+ related_name="+",
+ )
+ caption = models.CharField(max_length=250, blank=True)
+
+ panels = [
+ FieldPanel("image"),
+ FieldPanel("caption"),
+ ]
+
+ class Meta:
+ ordering = ["sort_order"]
+
+ def __str__(self):
+ return f"Image for {self.product.title}"
+
+
+class ProductPage(Page):
+ """
+ Individual product detail page.
+ Products are children of CategoryPage in the page tree.
+ """
+
+ # Basic product info
+ price = models.DecimalField(
+ max_digits=10,
+ decimal_places=2,
+ help_text="Product price in dollars",
+ )
+ sku = models.CharField(
+ max_length=100,
+ unique=True,
+ blank=True,
+ help_text="Stock Keeping Unit - leave blank to auto-generate",
+ )
+
+ # Product content
+ description = RichTextField(help_text="Product description and details")
+ main_image = models.ForeignKey(
+ "wagtailimages.Image",
+ null=True,
+ blank=True,
+ on_delete=models.SET_NULL,
+ related_name="+",
+ help_text="Main product image",
+ )
+
+ # Product status
+ in_stock = models.BooleanField(
+ default=True, help_text="Whether the product is currently available"
+ )
+ featured = models.BooleanField(
+ default=False, help_text="Show this product in featured sections"
+ )
+
+ # Timestamps
+ created_at = models.DateTimeField(auto_now_add=True)
+
+ content_panels = Page.content_panels + [
+ FieldPanel("sku"),
+ FieldPanel("price"),
+ FieldPanel("main_image"),
+ FieldPanel("description"),
+ InlinePanel("gallery_images", label="Gallery Images"),
+ FieldPanel("in_stock"),
+ FieldPanel("featured"),
+ ]
+
+ parent_page_types = ["shop.ShopCategoryPage"]
+ subpage_types = [] # Products are leaf nodes
+
+ search_fields = (
+ Page.search_fields
+ + [
+ # Inherit Page.search_fields (title, etc.)
+ ]
+ )
+
+ def get_category(self):
+ """Returns the parent CategoryPage"""
+ return self.get_parent().specific
+
+ def save(self, *args, **kwargs):
+ """Auto-generate SKU if not provided"""
+ if not self.sku:
+ # Generate SKU based on category and timestamp
+ category = self.get_parent()
+ if category:
+ category_prefix = category.slug[:4].upper()
+ # Use ID if available, otherwise use timestamp + random suffix
+ if self.id:
+ self.sku = f"{category_prefix}-{self.id:04d}"
+ else:
+ # For new products, use a timestamp-based SKU with random suffix
+ import random
+ from datetime import datetime
+
+ timestamp = datetime.now().strftime("%m%d%H%M%S")
+ random_suffix = random.randint(1000, 9999)
+ self.sku = f"{category_prefix}-{timestamp}-{random_suffix}"
+ super().save(*args, **kwargs)
+
+ def __str__(self):
+ return self.title
diff --git a/app/shop/templates/shop/product_page.html b/app/shop/templates/shop/product_page.html
new file mode 100644
index 0000000..36450a0
--- /dev/null
+++ b/app/shop/templates/shop/product_page.html
@@ -0,0 +1,103 @@
+{% extends "base.html" %}
+{% load static wagtailcore_tags wagtailimages_tags %}
+
+{% block body_class %}template-productpage{% endblock %}
+
+{% block content %}
+
+
+
+
+
+
+
+
+
+
+
+ {% if page.main_image %}
+ {% image page.main_image fill-800x800 as main_img %}
+
+ {% else %}
+
+
+ No image
+
+ {% endif %}
+
+
+
+ {% if page.gallery_images.all %}
+
+ {% for gallery_image in page.gallery_images.all %}
+
+ {% image gallery_image.image fill-150x150 as thumb_img %}
+
+
+ {% endfor %}
+
+ {% endif %}
+
+
+
+
+
{{ page.title }}
+
+
+
+
+
+ {% if page.sku %}
+
SKU: {{ page.sku }}
+ {% endif %}
+
+
+
+ {{ page.description|richtext }}
+
+
+
+
+
+
+
+
+
+
+ Add to Cart (Coming Soon)
+
+
Shopping cart functionality will be available in a future update.
+
+
+
+
+
+
+
+{% endblock content %}
diff --git a/app/shop/templates/shop/shop_category_page.html b/app/shop/templates/shop/shop_category_page.html
new file mode 100644
index 0000000..63aeff6
--- /dev/null
+++ b/app/shop/templates/shop/shop_category_page.html
@@ -0,0 +1,130 @@
+{% extends "base.html" %}
+{% load static wagtailcore_tags wagtailimages_tags %}
+
+{% block body_class %}template-shopcategorypage{% endblock %}
+
+{% block content %}
+
+
+
+
+
+
+
+
+
+
+ {% if subcategories %}
+
+ Browse Subcategories
+
+
+ {% endif %}
+
+
+ {% if products %}
+
+ {% if subcategories %}All Products{% else %}Products{% endif %}
+
+
+
+
+ {% if products.paginator.num_pages > 1 %}
+
+ {% endif %}
+
+ {% elif not subcategories %}
+
+
+
No products yet
+
This category doesn't have any products at the moment.
+
+ {% endif %}
+
+{% endblock content %}
diff --git a/app/shop/templates/shop/shop_index_page.html b/app/shop/templates/shop/shop_index_page.html
new file mode 100644
index 0000000..4925629
--- /dev/null
+++ b/app/shop/templates/shop/shop_index_page.html
@@ -0,0 +1,60 @@
+{% extends "base.html" %}
+{% load static wagtailcore_tags wagtailimages_tags %}
+
+{% block body_class %}template-shopindexpage{% endblock %}
+
+{% block content %}
+
+
+
+
+
+ {% if categories %}
+
+ {% else %}
+
+
+
No categories yet
+
Categories will appear here once they are created.
+
+ Run python manage.py create_sample_categories to create sample categories.
+
+
+ {% endif %}
+
+{% endblock content %}
diff --git a/app/shop/tests.py b/app/shop/tests.py
new file mode 100644
index 0000000..f1bb0d8
--- /dev/null
+++ b/app/shop/tests.py
@@ -0,0 +1,338 @@
+from decimal import Decimal
+
+from django.test import TestCase
+from wagtail.images.tests.utils import get_test_image_file
+from wagtail.test.utils import WagtailPageTestCase
+
+from app.home.models import HomePage
+from app.shop.models import ProductImage, ProductPage, ShopCategoryPage, ShopIndexPage
+
+
+class ShopIndexPageTest(WagtailPageTestCase):
+ """Tests for ShopIndexPage model"""
+
+ def setUp(self):
+ self.home_page = HomePage.objects.first()
+
+ def test_can_create_shop_index_page(self):
+ """Test that ShopIndexPage can be created under HomePage"""
+ self.assertCanCreateAt(HomePage, ShopIndexPage)
+
+ def test_shop_index_page_only_allows_category_children(self):
+ """Test that ShopIndexPage only allows ShopCategoryPage as children"""
+ self.assertAllowedSubpageTypes(ShopIndexPage, {ShopCategoryPage})
+
+ def test_shop_index_page_max_count(self):
+ """Test that only one ShopIndexPage can be created"""
+ # Create first shop index
+ shop_index = ShopIndexPage(title="Shop", slug="shop", intro="
Test
")
+ self.home_page.add_child(instance=shop_index)
+
+ # Check max_count is enforced
+ self.assertEqual(ShopIndexPage.max_count, 1)
+
+
+class ShopCategoryPageTest(WagtailPageTestCase):
+ """Tests for ShopCategoryPage model"""
+
+ def setUp(self):
+ self.home_page = HomePage.objects.first()
+ self.shop_index = ShopIndexPage(
+ title="Shop", slug="shop", intro="
Browse our products
"
+ )
+ self.home_page.add_child(instance=self.shop_index)
+
+ def test_can_create_category_page(self):
+ """Test that ShopCategoryPage can be created under ShopIndexPage"""
+ self.assertCanCreateAt(ShopIndexPage, ShopCategoryPage)
+
+ def test_category_page_allows_product_children(self):
+ """Test that ShopCategoryPage allows ProductPage as children"""
+ self.assertAllowedSubpageTypes(ShopCategoryPage, {ProductPage})
+
+ def test_category_page_creation(self):
+ """Test creating a category page with all fields"""
+ category = ShopCategoryPage(
+ title="Electronics",
+ slug="electronics",
+ description="
Electronic products
",
+ featured=True,
+ )
+ self.shop_index.add_child(instance=category)
+
+ # Verify the category was created
+ self.assertEqual(category.title, "Electronics")
+ self.assertEqual(category.slug, "electronics")
+ self.assertTrue(category.featured)
+
+ def test_get_products_method(self):
+ """Test that get_products() returns child ProductPage objects"""
+ # Create category
+ category = ShopCategoryPage(title="Electronics", slug="electronics")
+ self.shop_index.add_child(instance=category)
+
+ # Create some products
+ for i in range(3):
+ product = ProductPage(
+ title=f"Product {i}",
+ slug=f"product-{i}",
+ price=Decimal("99.99"),
+ description="
Test product
",
+ sku=f"TEST-{i}",
+ )
+ category.add_child(instance=product)
+
+ # Test get_products method
+ products = category.get_products()
+ self.assertEqual(products.count(), 3)
+
+
+class ProductPageTest(WagtailPageTestCase):
+ """Tests for ProductPage model"""
+
+ def setUp(self):
+ self.home_page = HomePage.objects.first()
+
+ # Create shop structure
+ self.shop_index = ShopIndexPage(title="Shop", slug="shop")
+ self.home_page.add_child(instance=self.shop_index)
+
+ self.category = ShopCategoryPage(
+ title="Electronics",
+ slug="electronics",
+ description="
Electronic products
",
+ )
+ self.shop_index.add_child(instance=self.category)
+
+ def test_can_create_product_page(self):
+ """Test that ProductPage can be created under ShopCategoryPage"""
+ self.assertCanCreateAt(ShopCategoryPage, ProductPage)
+
+ def test_product_page_parent_types(self):
+ """Test that ProductPage can only be created under ShopCategoryPage"""
+ self.assertAllowedParentPageTypes(ProductPage, {ShopCategoryPage})
+
+ def test_product_page_has_no_subpages(self):
+ """Test that ProductPage is a leaf node (no subpages allowed)"""
+ self.assertAllowedSubpageTypes(ProductPage, {})
+
+ def test_product_page_creation_with_all_fields(self):
+ """Test creating a product page with all required and optional fields"""
+ product = ProductPage(
+ title="Wireless Headphones",
+ slug="wireless-headphones",
+ price=Decimal("129.99"),
+ sku="ELEC-001",
+ description="
High quality wireless headphones
",
+ in_stock=True,
+ featured=True,
+ )
+ self.category.add_child(instance=product)
+
+ # Verify all fields
+ self.assertEqual(product.title, "Wireless Headphones")
+ self.assertEqual(product.price, Decimal("129.99"))
+ self.assertEqual(product.sku, "ELEC-001")
+ self.assertTrue(product.in_stock)
+ self.assertTrue(product.featured)
+ self.assertIsNotNone(product.created_at)
+
+ def test_product_sku_auto_generation(self):
+ """Test that SKU is auto-generated when not provided"""
+ product = ProductPage(
+ title="Test Product",
+ slug="test-product",
+ price=Decimal("49.99"),
+ description="
Test
",
+ )
+ self.category.add_child(instance=product)
+
+ # SKU should be auto-generated
+ self.assertIsNotNone(product.sku)
+ self.assertTrue(product.sku.startswith("ELEC-"))
+
+ def test_product_sku_uniqueness(self):
+ """Test that SKU field enforces uniqueness"""
+ # Create first product
+ product1 = ProductPage(
+ title="Product 1",
+ slug="product-1",
+ price=Decimal("99.99"),
+ sku="TEST-123",
+ description="
Test
",
+ )
+ self.category.add_child(instance=product1)
+
+ # Try to create another product with same SKU
+ product2 = ProductPage(
+ title="Product 2",
+ slug="product-2",
+ price=Decimal("99.99"),
+ sku="TEST-123", # Same SKU
+ description="
Test
",
+ )
+
+ # This should raise a validation error
+ from django.core.exceptions import ValidationError
+
+ with self.assertRaises(ValidationError):
+ self.category.add_child(instance=product2)
+
+ def test_get_category_method(self):
+ """Test that get_category() returns the parent category"""
+ product = ProductPage(
+ title="Test Product",
+ slug="test-product",
+ price=Decimal("99.99"),
+ sku="TEST-001",
+ description="
Test
",
+ )
+ self.category.add_child(instance=product)
+
+ # Get category
+ parent_category = product.get_category()
+ self.assertEqual(parent_category.id, self.category.id)
+ self.assertIsInstance(parent_category, ShopCategoryPage)
+
+ def test_product_page_str_method(self):
+ """Test the string representation of ProductPage"""
+ product = ProductPage(
+ title="Test Product",
+ slug="test-product",
+ price=Decimal("99.99"),
+ sku="TEST-001",
+ description="
Test
",
+ )
+ self.category.add_child(instance=product)
+
+ self.assertEqual(str(product), "Test Product")
+
+
+class ProductImageTest(TestCase):
+ """Tests for ProductImage inline model"""
+
+ def setUp(self):
+ from wagtail.images.models import Image
+
+ self.home_page = HomePage.objects.first()
+
+ # Create shop structure
+ self.shop_index = ShopIndexPage(title="Shop", slug="shop")
+ self.home_page.add_child(instance=self.shop_index)
+
+ self.category = ShopCategoryPage(title="Electronics", slug="electronics")
+ self.shop_index.add_child(instance=self.category)
+
+ self.product = ProductPage(
+ title="Test Product",
+ slug="test-product",
+ price=Decimal("99.99"),
+ sku="TEST-001",
+ description="
Test
",
+ )
+ self.category.add_child(instance=self.product)
+
+ # Create test image
+ self.image = Image.objects.create(
+ title="Test image", file=get_test_image_file()
+ )
+
+ def test_product_image_creation(self):
+ """Test creating a ProductImage"""
+ product_image = ProductImage.objects.create(
+ product=self.product, image=self.image, caption="Test caption"
+ )
+
+ self.assertEqual(product_image.product, self.product)
+ self.assertEqual(product_image.image, self.image)
+ self.assertEqual(product_image.caption, "Test caption")
+
+ def test_product_image_gallery_relationship(self):
+ """Test that product images are accessible via gallery_images"""
+ # Create multiple gallery images
+ for i in range(3):
+ ProductImage.objects.create(
+ product=self.product,
+ image=self.image,
+ caption=f"Gallery image {i}",
+ sort_order=i,
+ )
+
+ # Check that all images are accessible
+ gallery_images = self.product.gallery_images.all()
+ self.assertEqual(gallery_images.count(), 3)
+
+ def test_product_image_ordering(self):
+ """Test that product images are ordered by sort_order"""
+ # Create images with specific sort orders
+ ProductImage.objects.create(
+ product=self.product, image=self.image, caption="Third", sort_order=3
+ )
+ ProductImage.objects.create(
+ product=self.product, image=self.image, caption="First", sort_order=1
+ )
+ ProductImage.objects.create(
+ product=self.product, image=self.image, caption="Second", sort_order=2
+ )
+
+ # Get ordered images
+ ordered_images = self.product.gallery_images.all()
+ self.assertEqual(ordered_images[0].caption, "First")
+ self.assertEqual(ordered_images[1].caption, "Second")
+ self.assertEqual(ordered_images[2].caption, "Third")
+
+ def test_product_image_str_method(self):
+ """Test the string representation of ProductImage"""
+ product_image = ProductImage.objects.create(
+ product=self.product, image=self.image, caption="Test"
+ )
+
+ self.assertEqual(str(product_image), "Image for Test Product")
+
+
+class ProductPageTemplateTest(TestCase):
+ """Tests for ProductPage template rendering"""
+
+ def setUp(self):
+ self.home_page = HomePage.objects.first()
+
+ # Create shop structure
+ self.shop_index = ShopIndexPage(title="Shop", slug="shop")
+ self.home_page.add_child(instance=self.shop_index)
+
+ self.category = ShopCategoryPage(title="Electronics", slug="electronics")
+ self.shop_index.add_child(instance=self.category)
+
+ self.product = ProductPage(
+ title="Test Product",
+ slug="test-product",
+ price=Decimal("99.99"),
+ sku="TEST-001",
+ description="
This is a test product
",
+ in_stock=True,
+ )
+ self.category.add_child(instance=self.product)
+
+ # Publish the pages
+ revision = self.product.save_revision()
+ revision.publish()
+
+ def test_product_page_renders(self):
+ """Test that product page returns 200 status"""
+ response = self.client.get(self.product.url)
+ self.assertEqual(response.status_code, 200)
+
+ def test_product_page_uses_correct_template(self):
+ """Test that product page uses the correct template"""
+ response = self.client.get(self.product.url)
+ self.assertTemplateUsed(response, "shop/product_page.html")
+
+ def test_product_page_context(self):
+ """Test that product page has correct context"""
+ response = self.client.get(self.product.url)
+
+ # Check that page object is in context
+ self.assertEqual(response.context["page"], self.product)
+ self.assertIn("Test Product", response.content.decode())
+ self.assertIn("$99.99", response.content.decode())
+ self.assertIn("In Stock", response.content.decode())
diff --git a/app/shop/views.py b/app/shop/views.py
new file mode 100644
index 0000000..60f00ef
--- /dev/null
+++ b/app/shop/views.py
@@ -0,0 +1 @@
+# Create your views here.
diff --git a/app/templates/base.html b/app/templates/base.html
index 92e1099..6673616 100644
--- a/app/templates/base.html
+++ b/app/templates/base.html
@@ -34,8 +34,12 @@
{% wagtailuserbar %}
+ {% include "includes/base_header.html" %}
+
{% block content %}{% endblock %}
+ {% include "includes/base_footer.html" %}
+
{# Global javascript #}
diff --git a/app/templates/includes/base_footer.html b/app/templates/includes/base_footer.html
new file mode 100644
index 0000000..5fe6898
--- /dev/null
+++ b/app/templates/includes/base_footer.html
@@ -0,0 +1,36 @@
+
diff --git a/app/templates/includes/base_header.html b/app/templates/includes/base_header.html
new file mode 100644
index 0000000..78cba28
--- /dev/null
+++ b/app/templates/includes/base_header.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+ {% comment %} Can be dynamic later {% endcomment %}
+ Shop
+ About
+ Cart
+
+
+
+
diff --git a/docs/README.md b/docs/README.md
index 4715b42..23a81a1 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -4,5 +4,7 @@ This is the docs folder. It contains all the documentation for the project.
## Table of contents
-- [Backend Documentation](./backend-development.md)
-- [Frontend Documentation](./frontend-development.md)
+- [Installation Guide](./installation.md)
+- [Backend Development](./backend-development.md)
+- [Frontend Development](./frontend-development.md)
+- [Management Commands](./management-commands.md)
diff --git a/docs/development-progress.md b/docs/development-progress.md
new file mode 100644
index 0000000..8e61838
--- /dev/null
+++ b/docs/development-progress.md
@@ -0,0 +1,316 @@
+# E-commerce Development Progress Tracker
+
+> **Quick reference checklist for tracking development progress**
+>
+> For detailed specifications, see [E-commerce Development Plan](./e-commerce-development-plan.md)
+
+**Last Updated**: Not started
+
+**Current Phase**: Setup
+
+---
+
+## Setup & Prerequisites
+
+- [x] Docker environment running (`make up`)
+- [x] Database selected (SQLite/PostgreSQL/MySQL)
+- [x] Superuser created (`make superuser`)
+- [x] Frontend assets compiled (`npm run build`)
+
+---
+
+## Phase 1: Category Pages
+
+**Goal**: Create category pages as foundation of page tree
+
+**Architecture**: Categories as Wagtail Pages (not snippets) - Products will be children
+
+### Core Tasks
+- [x] Create shop app (`python manage.py startapp shop`)
+- [x] Add shop to INSTALLED_APPS in `app/settings/base.py`
+- [x] Create CategoryPage model (extends wagtail.models.Page)
+ - Fields: description (RichTextField), icon, featured
+ - Parent types: HomePage, self (subcategories)
+ - Subpage types: ProductPage, CategoryPage
+ - Methods: get_products(), get_context()
+- [x] Create category page template (`app/shop/templates/shop/category_page.html`)
+ - Category header, description, icon
+
+ - Product grid showing child products
+
+- [x] Create a ShopIndexPage (extends wagtail.models.Page)
+ - Parent types: HomePage ONLY
+ - Subpage types: CategoryPage ONLY
+ - Template: `app/shop/templates/shop/shop_index_page.html`
+ - Displays all CategoryPages as cards with icon, name, description
+- [x] Add styling in extra_css block
+- [x] Run migrations
+- [x] Create sample categories command
+ - Command: `python manage.py create_sample_categories`
+ - Creates ShopIndexPage under HomePage (if not exists)
+ - Creates 6 CategoryPage instances under ShopIndexPage
+ - Test with: `--reset`, `--no-icons`, `--shop-slug=shop`, `--shop-title="Shop"`
+
+- [x] Verify in Wagtail admin: Category pages in page tree under HomePage
+
+**Completion Criteria**:
+- [x] All tasks checked above
+- [x] Category pages visible in Wagtail page tree
+- [x] Categories browsable at shop/electronics/, shop/fashion/, etc.
+- [x] Management command creates all 6 categories
+
+
+---
+
+## Phase 2: Product Detail Pages
+
+**Goal**: Individual product pages as children of categories
+
+**Architecture**: Products are children of CategoryPages in page tree
+
+### Core Tasks
+- [ ] Create ProductPage model (extends wagtail.models.Page)
+ - Fields: price, sku, description, main_image, in_stock, featured, created_at
+ - Parent types: CategoryPage ONLY (enforced)
+ - Subpage types: None (leaf node)
+ - Search fields configured
+ - Methods: get_category() returns parent
+- [ ] Create ProductImage inline model (gallery)
+ - ParentalKey to ProductPage
+ - Fields: image, caption, display_order
+- [ ] Create product detail template (`app/shop/templates/shop/product_page.html`)
+ - Product image, title, breadcrumb (Home > Category > Product)
+ - Price display, description
+ - Stock status, "Add to Cart" placeholder
+ - Image gallery (thumbnail strip)
+- [ ] Add styling in extra_css block
+- [ ] Run migrations
+- [ ] Create sample products command
+ - Command: `python manage.py create_sample_products --count=20 --with-images`
+ - Creates products as children of existing CategoryPages
+ - Distributes products evenly across categories
+ - Auto-generates SKUs
+- [ ] Write tests for products and images
+ - Test parent type restriction
+ - Test get_category() method
+ - Test SKU auto-generation
+- [ ] Verify: Products browsable in page tree under categories
+
+**Completion Criteria**:
+- [ ] All tasks checked above
+- [ ] Product pages render correctly at /electronics/wireless-headphones/
+- [ ] Products properly nested under categories in page tree
+- [ ] Images display properly
+- [ ] Management command creates realistic products
+- [ ] Tests passing
+
+---
+
+## Phase 3: Category Browse Pages
+
+**Goal**: Browse products by category
+
+### Core Tasks
+- [ ] Create CategoryPage model (extends wagtail.models.Page)
+ - Fields: category (FK to ProductCategory), show_featured_only, products_per_page
+ - Methods: get_products(), get_context() with pagination
+- [ ] Create category page template (`app/shop/templates/shop/category_page.html`)
+ - Category header (name, description, icon)
+ - Product grid (reuse product card styling)
+ - Pagination controls
+- [ ] Update navigation
+ - Link category cards on home page to CategoryPages
+ - Add category menu to header
+- [ ] Run migrations
+- [ ] Create category pages command
+ - Command: `python manage.py create_category_pages`
+ - Test with: `--reset`
+- [ ] Write tests for category pages and filtering
+- [ ] Verify: Category pages show filtered products
+
+**Completion Criteria**:
+- [ ] All tasks checked above
+- [ ] Category pages display correctly
+- [ ] Product filtering works
+- [ ] Pagination functional
+- [ ] Management command creates all pages
+- [ ] Tests passing
+
+---
+
+## Phase 4: Product Listing/Index Page
+
+**Goal**: Main shop page with filtering and search
+
+### Core Tasks
+- [ ] Create ProductIndexPage model (extends wagtail.models.Page)
+ - Fields: intro, products_per_page, show_filters
+ - Methods: get_products(), get_context() with filtering
+- [ ] Add filtering functionality
+ - GET parameters: category, min_price, max_price, in_stock, featured, sort
+ - Pagination support
+- [ ] Create product index template (`app/shop/templates/shop/product_index_page.html`)
+ - Filter sidebar (category, price range, stock, sort)
+ - Product grid
+ - Result count, pagination
+- [ ] Integrate with search
+ - Update `app/search/views.py` to show ProductPages
+ - Add search box to header
+- [ ] Run migrations
+- [ ] Create product index page (via admin or command)
+ - Command: `python manage.py setup_product_index` (optional)
+ - Slug: `/shop/` or `/products/`
+- [ ] Update header navigation to link to shop page
+- [ ] Write tests for filtering, sorting, pagination, search
+- [ ] Verify: All products browsable with filters working
+
+**Completion Criteria**:
+- [ ] All tasks checked above
+- [ ] Filtering works correctly
+- [ ] Search returns product results
+- [ ] Sorting functional
+- [ ] Tests passing
+
+---
+
+## Phase 5: Product Enhancements
+
+**Goal**: Variants, specs, reviews, related products
+
+### Core Tasks
+- [ ] Create ProductVariant model (inline)
+ - Fields: name, variant_type, price_modifier, sku_suffix, in_stock
+ - Add to ProductPage as InlinePanel
+- [ ] Add product specifications
+ - Choose approach: StreamField, TableBlock, or JSONField
+ - Update template to display specs
+- [ ] Add related products field
+ - ParentalManyToManyField to ProductPage
+ - Display in template (3-4 related products)
+- [ ] Create ProductReview snippet
+ - Fields: author_name, rating, comment, created_at, is_approved
+ - Add review submission form
+ - Display approved reviews on product page
+- [ ] Add breadcrumbs
+ - Template tag or include
+ - Show: Home > Category > Product
+- [ ] Optimize images
+ - Create renditions: 200x200, 400x400, 800x800
+ - Update templates to use renditions
+- [ ] Create enhancement command
+ - Command: `python manage.py enhance_sample_products`
+ - Options: `--variants-only`, `--reviews-only`, `--related-only`, `--reset`
+- [ ] Run migrations
+- [ ] Write tests for variants, specs, reviews, related products
+- [ ] Verify: All enhancements working
+
+**Completion Criteria**:
+- [ ] All tasks checked above
+- [ ] Variants selectable
+- [ ] Reviews display and submission works
+- [ ] Related products show
+- [ ] Breadcrumbs appear
+- [ ] Management command enhances products
+- [ ] Tests passing
+
+---
+
+## Phase 6: Supporting Content Pages
+
+**Goal**: Info pages and contact form
+
+### Core Tasks
+- [ ] Create StandardPage model (StreamField for body)
+- [ ] Create ContactFormPage model (wagtail.contrib.forms)
+ - Fields: name, email, subject, message
+ - Email notification setup
+- [ ] Create templates for content pages
+- [ ] Create specific pages:
+ - [ ] About Us
+ - [ ] FAQ
+ - [ ] Shipping & Returns
+ - [ ] Terms & Conditions
+ - [ ] Privacy Policy
+ - [ ] Contact page with form
+- [ ] Update footer links to actual pages
+- [ ] Create content pages command
+ - Command: `python manage.py create_content_pages`
+ - Options: `--reset`, `--pages=about,faq`
+- [ ] Write tests for content pages and contact form
+- [ ] Verify: All info pages accessible, contact form works
+
+**Completion Criteria**:
+- [ ] All tasks checked above
+- [ ] All info pages created
+- [ ] Contact form submits successfully
+- [ ] Footer links functional
+- [ ] Management command creates pages
+- [ ] Tests passing
+
+---
+
+## Quick Commands Reference
+
+### Database Reset
+```bash
+make down && make up && make migrate
+make superuser
+```
+
+### Run All Phase Commands
+```bash
+# Phase 1
+python manage.py create_sample_categories
+
+# Phase 2
+python manage.py create_sample_products --count=50 --with-images
+
+# Phase 3
+python manage.py create_category_pages
+
+# Phase 4
+python manage.py setup_product_index
+
+# Phase 5
+python manage.py enhance_sample_products
+
+# Phase 6
+python manage.py create_content_pages
+```
+
+### Testing
+```bash
+make test # Run all tests
+make sh # Enter container shell
+python manage.py test app.shop.tests # Test shop app only
+```
+
+### Frontend
+```bash
+npm run build # Build production assets
+npm start # Watch mode for development
+```
+
+---
+
+## Notes & Decisions
+
+_Use this section to track important decisions, blockers, or notes as you work_
+
+- **[Date]**: Decision/Note here
+- **[Date]**: Issue encountered and resolution
+
+---
+
+## Next Steps
+
+When resuming work:
+1. Check which phase is current (see "Current Phase" at top)
+2. Review unchecked tasks in that phase
+3. Run `make test` to verify current state
+4. Continue with next unchecked task
+
+For detailed implementation specs, always refer to [E-commerce Development Plan](./e-commerce-development-plan.md)
diff --git a/docs/e-commerce-development-plan.md b/docs/e-commerce-development-plan.md
new file mode 100644
index 0000000..039e3ef
--- /dev/null
+++ b/docs/e-commerce-development-plan.md
@@ -0,0 +1,802 @@
+# Wagtail Shop Kit - E-commerce Development Plan
+
+## Overview
+
+Incremental development plan for building out e-commerce functionality in Wagtail Shop Kit. This plan focuses on establishing the content foundation (categories, products, pages) before adding transactional features (cart, checkout, users).
+
+## Development Philosophy
+
+- **Start with foundation**: Categories → Products → Listings
+- **Content before transactions**: Build browsable catalog before cart/checkout
+- **Wagtail-native patterns**: Use Pages for URLs, Snippets for reusable content
+- **Test as you go**: Write tests for each phase
+- **Keep it simple**: Start with MVP features, iterate later
+- **Reproducible content**: Create management commands and/or fixtures for each phase to enable UI testing and quick site resets
+
+---
+
+## Data Management & Fixtures Strategy
+
+**Goal**: Every phase should produce reproducible sample data for testing and development.
+
+### Approach
+
+For each phase of development:
+
+1. **Management Commands** (Preferred for complex data):
+ - Create command in `app/shop/management/commands/`
+ - Follow pattern from existing `create_sample_media.py`
+ - Generate realistic sample data with randomization
+ - Support options: `--count`, `--reset`, `--clear`
+ - Make idempotent (safe to run multiple times)
+ - Log progress clearly
+
+2. **Fixtures** (For simple, static data):
+ - Create JSON fixtures in `app/shop/fixtures/`
+ - Use for consistent reference data (categories, sample users)
+ - Load with `python manage.py loaddata
`
+ - Keep minimal and focused
+
+### Benefits
+
+- **UI Testing**: Quickly populate site with realistic content for visual testing
+- **Demo Environments**: Spin up demo sites with full content in seconds
+- **Onboarding**: New developers can see working examples immediately
+- **CI/CD**: Automated testing with predictable data
+- **Site Resets**: Recover from mistakes or experiments quickly
+
+### Naming Convention
+
+Management commands should follow this pattern:
+- Phase 1: `setup_phase1_categories.py` or `create_sample_categories.py`
+- Phase 2: `setup_phase2_products.py` or `create_sample_products.py`
+- Phase 3: `setup_phase3_category_pages.py`
+- Phase 4: `setup_phase4_product_index.py`
+- Or create cumulative: `setup_shop_demo.py --phase=1` to `--phase=6`
+
+### Quick Setup Workflow
+
+After implementing each phase:
+```bash
+# Reset database
+make down && make up && make migrate
+
+# Create superuser
+make superuser
+
+# Run phase setup commands
+python manage.py setup_phase1_categories
+python manage.py setup_phase2_products --count=50
+python manage.py setup_phase3_category_pages
+python manage.py setup_phase4_product_index
+
+# Or use all-in-one command
+python manage.py setup_shop_demo --all-phases
+```
+
+---
+
+## Phase 1: Category Pages
+
+**Goal**: Create category pages as the foundation of the page tree hierarchy
+
+### Architecture Decision
+
+Categories are implemented as **Wagtail Pages** (not snippets) to leverage Wagtail's page tree for hierarchical navigation. Products will be children of category pages, creating a natural URL structure like `/electronics/wireless-headphones/`.
+
+**Trade-offs:**
+- ✅ Natural URL hierarchy and breadcrumbs
+- ✅ Built-in Wagtail admin page management
+- ✅ Easy to add category-specific content and templates
+- ⚠️ Products can only belong to one category (parent page relationship)
+- ⚠️ Moving products between categories requires page tree operations
+
+### Tasks
+
+1. **Create shop app**
+ ```bash
+ python manage.py startapp shop
+ ```
+ - Add to `INSTALLED_APPS` in `app/settings/base.py`
+ - Create `app/shop/templates/shop/` directory structure
+ - Create initial `__init__.py`, `models.py`, `admin.py`, `tests.py`
+
+2. **Create CategoryPage model**
+ - File: `app/shop/models.py`
+ - Extends: `wagtail.models.Page`
+ - Fields:
+ - `description` (RichTextField, blank=True)
+ - `icon` (ForeignKey to wagtailimages.Image, optional)
+ - `featured` (BooleanField, default=False)
+ - Content panels:
+ - Basic: title (inherited from Page), description, icon, featured
+ - Settings panels:
+ - Promote tab: slug (auto-populating), SEO fields
+ - Parent page types: `HomePage` or self (for subcategories)
+ - Subpage types: `ProductPage`, `CategoryPage` (allow subcategories)
+ - Methods:
+ - `get_products()`: Returns child ProductPage objects
+ - `get_context()`: Add products and pagination to template
+
+3. **Create category page template**
+ - File: `app/shop/templates/shop/category_page.html`
+ - Extends: `base.html`
+ - Sections:
+ - Category header (title, description, icon)
+ - Child categories (if any)
+ - Product grid showing child products
+ - Pagination
+ - Responsive design using Pico CSS
+
+4. **Run migrations**
+ ```bash
+ python manage.py makemigrations shop
+ python manage.py migrate
+ ```
+
+5. **Create sample categories command**
+ - **Management command**: `create_sample_categories.py`
+ - Creates 6 category pages as children of HomePage:
+ - Electronics, Fashion, Home & Living, Beauty, Sports, Books
+ - Options:
+ - `--reset`: Delete existing category pages
+ - `--with-icons`: Create icon images
+ - `--parent-slug=home`: Specify parent page (default: HomePage)
+ - **Purpose**: Reproducible category page structure for demos
+
+6. **Write tests**
+ - File: `app/shop/tests.py`
+ - Test CategoryPage creation and page tree hierarchy
+ - Test get_products() method
+ - Test category page rendering
+ - Test admin page operations (create, edit, move)
+
+**Deliverable**: CategoryPage model working in Wagtail page tree, ready to contain products
+
+---
+
+## Phase 2: Product Detail Pages
+
+**Goal**: Create individual product pages as children of categories
+
+### Tasks
+
+1. **Create ProductPage model**
+ - File: `app/shop/models.py`
+ - Extends: `wagtail.models.Page`
+ - Fields:
+ - `price` (DecimalField, max_digits=10, decimal_places=2)
+ - `sku` (CharField, unique, optional, help_text for auto-generation)
+ - `description` (RichTextField)
+ - `main_image` (ForeignKey to wagtailimages.Image)
+ - `in_stock` (BooleanField, default=True)
+ - `featured` (BooleanField, default=False)
+ - `created_at` (DateTimeField, auto_now_add=True)
+ - Content panels:
+ - Basic info: title, sku, price
+ - Content: description (rich text), main_image
+ - Settings: in_stock, featured
+ - Parent page types: `CategoryPage` (products must be under a category)
+ - Subpage types: None (leaf node)
+ - Search fields: title, description, sku
+ - Methods:
+ - `get_category()`: Returns parent CategoryPage
+ - `save()`: Auto-generate SKU if not provided
+
+2. **Create product detail template**
+ - File: `app/shop/templates/shop/product_page.html`
+ - Extends: `base.html`
+ - Sections:
+ - Product image (large, responsive)
+ - Product title and category breadcrumb
+ - Price display (prominent, styled)
+ - Description (rich text)
+ - Stock status indicator
+ - "Add to Cart" placeholder button (non-functional for now)
+ - Related products section (future)
+ - Styling in `{% block extra_css %}` using Pico CSS
+
+3. **Enhance ProductPage with image gallery**
+ - Create `ProductImage` model (inline):
+ - ForeignKey to ProductPage (ParentalKey for Wagtail)
+ - ForeignKey to wagtailimages.Image
+ - `caption` (CharField, optional)
+ - `display_order` (IntegerField)
+ - Use `InlinePanel` in ProductPage
+ - Add gallery to template (thumbnail strip + lightbox)
+
+4. **Run migrations**
+ ```bash
+ python manage.py makemigrations shop
+ python manage.py migrate
+ ```
+
+5. **Create sample products command**
+ - **Management command**: `create_sample_products.py`
+ - **Required for UI testing**: Generate realistic browsable products
+ - Options:
+ - `--count=20`: Number of products per category (default: 20)
+ - `--with-images`: Generate sample product images
+ - `--reset`: Clear existing product pages first
+ - `--featured=5`: Mark N products per category as featured
+ - Generate realistic product data using Faker:
+ - Product names (e.g., "Wireless Bluetooth Speaker", "Cotton T-Shirt")
+ - Rich text descriptions (2-3 paragraphs)
+ - Prices: $10-$500 range with .99 endings
+ - SKUs: Auto-generated (e.g., "ELEC-001", "FASH-042")
+ - Random in_stock status (80% in stock)
+ - Create products as children of CategoryPage instances
+ - Distribute evenly across all existing categories
+ - Generate 2-4 images per product using `create_sample_media` pattern
+ - **Purpose**: Reproducible product catalog for testing all product features
+
+6. **Write tests**
+ - Test ProductPage creation with all fields
+ - Test image gallery inline
+ - Test get_category() method returns parent
+ - Test parent page type restriction (must be CategoryPage)
+ - Test template rendering (200 status)
+ - Test search indexing
+ - Test SKU auto-generation
+
+**Deliverable**: Individual product pages with images, descriptions, browsable via Wagtail page tree under categories
+
+---
+
+## Phase 3: Product Listing/Shop Index Page
+
+**Goal**: Create a main shop page showing all products with filtering
+
+### Architecture Note
+
+Since products are organized under CategoryPages in the tree, we need a separate page type that can list and filter products from across all categories. This provides a "browse all products" view independent of the category hierarchy.
+
+### Tasks
+
+1. **Create ProductIndexPage model**
+ - File: `app/shop/models.py`
+ - Extends: `wagtail.models.Page`
+ - Fields:
+ - `intro` (RichTextField, blank=True)
+ - `products_per_page` (IntegerField, default=12)
+ - `show_filters` (BooleanField, default=True)
+ - Content panels:
+ - Basic: title, intro, products_per_page, show_filters
+ - Parent page types: `HomePage`
+ - Subpage types: None (leaf node)
+ - Max count: 1 (only one shop index)
+ - Methods:
+ - `get_products()`: Returns all ProductPage objects (live, ordered)
+ - `get_categories()`: Returns all CategoryPage objects for filter sidebar
+ - `get_context()`: Add products, categories, filters, pagination to template
+
+2. **Add filtering functionality to ProductIndexPage**
+ - Handle GET parameters in `get_context()`:
+ - `category`: Filter by CategoryPage slug
+ - `min_price`, `max_price`: Price range
+ - `in_stock`: Show only available
+ - `featured`: Show featured products
+ - `sort`: Order by (price_asc, price_desc, newest, name)
+ - Add pagination support (Django Paginator)
+
+3. **Create product index template**
+ - File: `app/shop/templates/shop/product_index_page.html`
+ - Extends: `base.html`
+ - Sections:
+ - Page intro/description
+ - Filter sidebar/panel:
+ - Category filter (links to CategoryPages)
+ - Price range sliders
+ - In stock toggle
+ - Featured toggle
+ - Sort dropdown
+ - Product grid (same styling as category page)
+ - Result count ("Showing X of Y products")
+ - Pagination controls
+ - Responsive: Filters collapse to dropdown on mobile
+
+4. **Integrate with search**
+ - Update `app/search/views.py`:
+ - Filter results to show ProductPages
+ - Add product-specific result template
+ - Add search box to header navigation
+ - Link from ProductIndexPage
+
+5. **Update navigation**
+ - Update `app/home/templates/home/welcome_page.html`:
+ - Link "Shop Now" CTA to ProductIndexPage
+ - Link category cards to CategoryPage URLs
+ - Add "Shop" link to header navigation pointing to ProductIndexPage
+
+6. **Run migrations**
+ ```bash
+ python manage.py makemigrations shop
+ python manage.py migrate
+ ```
+
+7. **Create product index page** (via admin or command)
+ - **Optional management command**: `setup_product_index.py`
+ - Create ProductIndexPage under HomePage programmatically
+ - Slug: `/shop/` or `/products/`
+ - Set intro text, products_per_page, show_filters
+ - **Alternative**: Document manual creation via Wagtail admin
+ - **Purpose**: Quick setup of main shop entry point
+
+8. **Write tests**
+ - Test ProductIndexPage creation
+ - Test get_products() returns all products
+ - Test get_categories() returns all categories
+ - Test filtering by category, price, stock
+ - Test sorting options
+ - Test pagination
+ - Test search integration
+ - Test template rendering
+
+**Deliverable**: Main shop page with all products, filtering, sorting, and search
+
+---
+
+## Phase 4: Product Enhancements
+
+**Goal**: Create a page showing all products with filtering and search
+
+### Tasks
+
+1. **Create ProductIndexPage model**
+ - File: `app/shop/models.py`
+ - Extends: `wagtail.models.Page`
+ - Fields:
+ - `intro` (RichTextField, optional)
+ - `products_per_page` (IntegerField, default=12)
+ - `show_filters` (BooleanField, default=True)
+ - Methods:
+ - `get_products()`: Returns all ProductPages
+ - `get_context()`: Add products, categories, filters to context
+ - Max count: 1 (only one shop index page)
+ - Parent page types: HomePage
+ - Subpage types: ProductPage (can create products under it)
+
+2. **Add filtering functionality**
+ - Update `get_context()` to handle GET parameters:
+ - `category`: Filter by category ID/slug
+ - `min_price`, `max_price`: Price range
+ - `in_stock`: Show only available
+ - `featured`: Show featured products
+ - `sort`: Order by (price_asc, price_desc, newest, name)
+ - Add pagination
+
+3. **Create product index template**
+ - File: `app/shop/templates/shop/product_index_page.html`
+ - Extends: `base.html`
+ - Sections:
+ - Page intro/description
+ - Filter sidebar/panel:
+ - Category checkboxes
+ - Price range sliders
+ - In stock toggle
+ - Featured toggle
+ - Sort dropdown
+ - Product grid (same styling as category page)
+ - Pagination
+ - Result count ("Showing X of Y products")
+ - Responsive: Filters collapse to dropdown on mobile
+
+4. **Integrate with search**
+ - Update `app/search/views.py`:
+ - Filter results to show ProductPages
+ - Add product-specific result template
+ - Add search box to header navigation
+ - Consider: Autocomplete/suggestions (future enhancement)
+
+5. **Run migrations**
+ ```bash
+ python manage.py makemigrations shop
+ python manage.py migrate
+ ```
+
+6. **Create product index page command**
+ - **Management command**: `setup_product_index.py` (optional)
+ - Create ProductIndexPage under HomePage programmatically
+ - Slug: `/shop/` or `/products/`
+ - Set intro text, products_per_page, show_filters
+ - **Alternative**: Document manual creation via Wagtail admin
+ - **Purpose**: Reproducible main shop page for testing filtering/search
+
+7. **Write tests**
+ - Test ProductIndexPage creation
+ - Test filtering by category, price, stock
+ - Test sorting
+ - Test pagination
+ - Test search integration
+ - Test template rendering
+
+**Deliverable**: Main shop page with all products, filtering, sorting, and search
+
+---
+
+## Phase 5: Product Enhancements
+
+**Goal**: Add features to make products more appealing and useful
+
+### Tasks
+
+1. **Add product variants**
+ - Create `ProductVariant` model:
+ - ParentalKey to ProductPage
+ - `name` (CharField: "Small", "Red", etc.)
+ - `variant_type` (CharField: "Size", "Color")
+ - `price_modifier` (DecimalField, can be positive/negative)
+ - `sku_suffix` (CharField)
+ - `in_stock` (BooleanField)
+ - Add InlinePanel to ProductPage
+ - Update template to show variant selector
+ - JavaScript for variant selection (update price display)
+
+2. **Add product specifications**
+ - Use StreamField or TableBlock for specifications:
+ - Option A: RichTextField with table block
+ - Option B: JSONField with structured data
+ - Option C: StructBlock in StreamField
+ - Display as formatted table in template
+ - Examples: Dimensions, Weight, Materials, Care Instructions
+
+3. **Add related products**
+ - Add `related_products` field to ProductPage:
+ - ParentalManyToManyField to ProductPage
+ - Display in product detail template
+ - Show 3-4 related product cards
+ - Auto-suggest: Same category products (fallback if none manually selected)
+
+4. **Add product reviews (simple version)**
+ - Create `ProductReview` snippet:
+ - ForeignKey to ProductPage
+ - `author_name` (CharField)
+ - `rating` (IntegerField, 1-5)
+ - `comment` (TextField)
+ - `created_at` (DateTimeField)
+ - `is_approved` (BooleanField, default=False)
+ - Display approved reviews on product page
+ - Show average rating
+ - Add simple review submission form (no auth required yet)
+
+5. **Add breadcrumbs**
+ - Create breadcrumb template tag or include
+ - Show: Home > Category > Product
+ - Add to all shop templates
+
+6. **Optimize images**
+ - Create image renditions for common sizes:
+ - Thumbnail: 200x200
+ - Product card: 400x400
+ - Product detail: 800x800
+ - Update templates to use renditions
+ - Consider: WebP format for better performance
+
+7. **Create sample variants, reviews, and related products**
+ - **Management command**: `enhance_sample_products.py`
+ - Add variants to existing products:
+ - Sizes for clothing (S, M, L, XL)
+ - Colors for applicable products
+ - Price modifiers (+$5 for Large, etc.)
+ - Generate sample reviews using Faker:
+ - 3-5 reviews per product
+ - Ratings 3-5 stars (weighted toward higher)
+ - Realistic review text
+ - Mix of approved/unapproved
+ - Link related products:
+ - Same category products
+ - 3-4 related per product
+ - Options:
+ - `--variants-only`
+ - `--reviews-only`
+ - `--related-only`
+ - `--reset`
+ - **Purpose**: Reproducible enhanced product data for full feature testing
+
+8. **Run migrations**
+ ```bash
+ python manage.py makemigrations shop
+ python manage.py migrate
+ ```
+
+9. **Write tests**
+ - Test variants creation and display
+ - Test specifications rendering
+ - Test related products logic
+ - Test review submission and approval
+ - Test breadcrumbs generation
+
+**Deliverable**: Enhanced product pages with variants, specs, reviews, and related products
+
+---
+
+## Phase 6: Supporting Content Pages
+
+**Goal**: Create standard e-commerce pages for information and support
+
+### Tasks
+
+1. **Create StandardPage model** (if not exists)
+ - File: `app/shop/models.py` or new `app/content/models.py`
+ - Extends: `wagtail.models.Page`
+ - Fields:
+ - `body` (StreamField with rich text, images, headings)
+ - Parent: HomePage
+ - Use for: About, Contact, FAQ, Shipping Info, Returns Policy
+
+2. **Create specific pages**
+ - About Us page: Company story, mission
+ - Contact page: Form with email submission
+ - FAQ page: Accordion-style questions
+ - Shipping & Returns: Policy details
+ - Terms & Conditions: Legal text
+ - Privacy Policy: GDPR compliance
+
+3. **Update footer links**
+ - File: `app/home/templates/home/welcome_page.html`
+ - Link footer sections to actual pages:
+ - Customer Support → FAQ, Contact
+ - Shop → ProductIndexPage, Categories
+ - Account → Login (placeholder for Phase 7+)
+
+4. **Create contact form**
+ - Use `wagtail.contrib.forms`:
+ - Create ContactFormPage model
+ - Fields: name, email, subject, message
+ - Email notification on submission
+ - Template with styled form
+ - Success message after submission
+
+5. **Create sample content pages command**
+ - **Management command**: `create_content_pages.py`
+ - Create standard pages:
+ - About Us (with rich text content)
+ - FAQ (with questions and answers)
+ - Shipping & Returns (policy text)
+ - Terms & Conditions
+ - Privacy Policy
+ - Create ContactFormPage under HomePage
+ - Options:
+ - `--reset`: Delete existing content pages
+ - `--pages=about,faq`: Create specific pages only
+ - **Purpose**: Reproducible informational pages for complete site testing
+
+6. **Write tests**
+ - Test StandardPage creation
+ - Test contact form submission
+ - Test email sending (use Django test email backend)
+
+**Deliverable**: Complete informational pages linked from footer, contact form functional
+
+---
+
+## Phase 7+: Future Enhancements (Not in Current Scope)
+
+These will be planned separately after core shop functionality is complete:
+
+### User Management & Authentication
+- User registration and login
+- User profiles
+- Order history
+- Wishlist
+- Saved addresses
+
+### Shopping Cart
+- Add to cart functionality
+- Cart page with item list
+- Update quantities
+- Remove items
+- Cart persistence (session/database)
+
+### Checkout Process
+- Shipping address form
+- Billing address form
+- Shipping method selection
+- Payment integration (Stripe, PayPal)
+- Order confirmation page
+- Order confirmation emails
+
+### Order Management
+- Order model
+- Order items model
+- Order status tracking
+- Admin order management
+- Customer order history
+
+### Advanced Features
+- Inventory management
+- Product stock alerts
+- Discount codes/coupons
+- Gift cards
+- Product recommendations (ML)
+- Analytics integration
+
+---
+
+## Technical Guidelines
+
+### Wagtail Best Practices
+
+1. **Use Snippets for reusable content**: Categories, variants, shipping methods
+2. **Use Pages for browsable content**: Products, categories, listings
+3. **Use StreamFields for flexible content**: Product descriptions, page bodies
+4. **Add search_fields to all Page models**: Improve search functionality
+5. **Use ParentalKey for inline relationships**: Images, variants
+6. **Test admin access**: Ensure all models editable via Wagtail admin
+
+### Documentation References
+
+When implementing Wagtail features, refer to the official documentation:
+
+**Wagtail CMS Documentation**: https://docs.wagtail.org/en/stable/
+
+Particularly relevant sections for e-commerce development:
+- [Page Models & Fields](https://docs.wagtail.org/en/stable/topics/pages.html) - Creating ProductPage, CategoryPage, etc.
+- [Snippets](https://docs.wagtail.org/en/stable/topics/snippets.html) - ProductCategory, ProductVariant snippets
+- [Images & Renditions](https://docs.wagtail.org/en/stable/topics/images.html) - Product image handling
+- [Search](https://docs.wagtail.org/en/stable/topics/search/index.html) - Product search functionality
+- [Forms](https://docs.wagtail.org/en/stable/reference/contrib/forms.html) - Contact form implementation
+- [Testing](https://docs.wagtail.org/en/stable/advanced_topics/testing.html) - Writing tests for page types
+
+**Django Documentation**: https://docs.djangoproject.com/en/5.2/
+
+### Database Considerations
+
+- Start with PostgreSQL or MySQL (not SQLite) for production readiness
+- Add indexes to frequently queried fields: category, price, sku
+- Use select_related/prefetch_related for efficient queries
+- Consider: Database views for complex product filtering
+
+### Frontend Guidelines
+
+- Reuse home page product card styling for consistency
+- Keep extra_css in templates for page-specific styles
+- Use Pico CSS variables: `--pico-primary`, `--pico-muted-color`
+- Ensure responsive design at 768px, 1024px, 1280px breakpoints
+- Optimize images: Use Wagtail renditions, serve WebP where supported
+
+### Testing Strategy
+
+- Test model creation and relationships
+- Test page rendering (200 status codes)
+- Test Wagtail admin CRUD operations
+- Test filtering and search functionality
+- Test template context variables
+- Run tests after each phase: `make test`
+
+### Migration Management
+
+- Create migrations after each model change
+- Test migrations on fresh database
+- Document any data migrations needed
+- Keep migrations small and focused
+
+### Management Commands & Fixtures
+
+**Management Commands** (for complex, randomized data):
+- Follow pattern in `create_sample_media.py`
+- **Must create for each phase** to generate sample content
+- Add `--help` text for all options
+- Support options:
+ - `--count=N`: Number of items to create
+ - `--reset`: Clear existing data first
+ - `--clear`: Delete all data without creating new
+ - `--phase=N`: For cumulative setup commands
+- Support idempotent operations (safe to run multiple times)
+- Log progress and success messages with clear output
+- Use Faker library for realistic data (names, descriptions, prices)
+
+**Fixtures** (for static reference data):
+- Create JSON fixtures in `app/shop/fixtures/`
+- Use `python manage.py dumpdata` to create from existing data
+- Load with `python manage.py loaddata `
+- Good for: Categories, sample users, consistent test data
+- Version control fixtures for reproducibility
+
+**Example Command Structure**:
+```python
+# app/shop/management/commands/create_sample_products.py
+from django.core.management.base import BaseCommand
+from faker import Faker
+from app.shop.models import ProductPage, ProductCategory
+from app.home.models import HomePage
+
+class Command(BaseCommand):
+ help = "Create sample products for testing"
+
+ def add_arguments(self, parser):
+ parser.add_argument('--count', type=int, default=20)
+ parser.add_argument('--reset', action='store_true')
+ parser.add_argument('--clear', action='store_true')
+
+ def handle(self, *args, **options):
+ # Implementation
+ pass
+```
+
+**Why This Matters**:
+- Enables quick UI testing after each development phase
+- Allows easy site resets during development
+- Provides realistic demo content for stakeholders
+- Speeds up onboarding for new developers
+- Makes CI/CD testing more reliable
+
+---
+
+## Success Criteria by Phase
+
+### Phase 1 ✓
+- CategoryPage model created and working
+- Category pages browsable in Wagtail page tree
+- Category template displays products and subcategories
+- Sample categories created via management command
+- Tests passing
+
+### Phase 2 ✓
+- Individual products browsable via Wagtail tree
+- Product detail pages render with images
+- Sample products created
+- Management command creates realistic product catalog
+- Tests passing
+
+### Phase 3 ✓
+- Category pages show filtered products
+- Category links work from home page
+- Pagination functional
+- Management command creates all category pages
+- Tests passing
+
+### Phase 4 ✓
+- Product index page shows all products
+- Filtering by category, price, stock works
+- Search returns product results
+- Sort options functional
+- Product index page setup is reproducible
+- Tests passing
+
+### Phase 5 ✓
+- Product variants selectable
+- Specifications display in tables
+- Related products show
+- Reviews submission works
+- Breadcrumbs appear
+- Management command enhances products with variants/reviews
+- Tests passing
+
+### Phase 6 ✓
+- All info pages created and linked
+- Contact form submits successfully
+- Footer links functional
+- Management command creates all content pages
+- Tests passing
+
+---
+
+## Implementation Order Summary
+
+```
+1. CategoryPage (page) ← Foundation - categories as pages in tree
+2. ProductPage (page) ← Core content - products as children of categories
+3. ProductIndexPage (page) ← Browse all products with filtering
+4. ProductVariant, ProductImage (inlines) ← Product enhancements
+5. ProductReview (snippet) ← Social proof
+6. StandardPage, ContactFormPage (pages) ← Supporting content
+```
+
+**Page Tree Structure:**
+```
+HomePage
+├── CategoryPage (Electronics)
+│ ├── ProductPage (Wireless Headphones)
+│ ├── ProductPage (Bluetooth Speaker)
+│ └── ...
+├── CategoryPage (Fashion)
+│ ├── ProductPage (Cotton T-Shirt)
+│ └── ...
+├── ProductIndexPage (Shop)
+└── StandardPage (About, Contact, etc.)
+```
+
+**Later**: Users → Cart → Checkout → Orders
+
+This order ensures each phase builds on the previous, with clear dependencies and testable milestones. The page-based architecture provides natural URL hierarchy and leverages Wagtail's built-in page management.
diff --git a/docs/installation.md b/docs/installation.md
new file mode 100644
index 0000000..846dbfd
--- /dev/null
+++ b/docs/installation.md
@@ -0,0 +1,67 @@
+# Installation Guide
+
+This guide will help you set up the Wagtail Shop Kit development environment on your local machine.
+
+## Requirements
+
+### Required
+
+- [Python >= 3.10](https://www.python.org/downloads/) (development and deployment)
+- [Docker](https://www.docker.com/) (for local development)
+- [Docker Compose](https://docs.docker.com/compose/) (for local development)
+- [Node.js](https://nodejs.org/en/) (for frontend build tools in development)
+
+### Optional
+
+- [Git](https://git-scm.com/) (optional, for version control)
+- [Make](https://www.gnu.org/software/make/) (optional, for running commands)
+- [NVM](https://github.com/nvm-sh/nvm) (optional, for managing Node versions)
+- [pre-commit](https://pre-commit.com/) (optional, for running code checks)
+- [UV](https://github.com/astral-sh/uv) (optional, for managing Python dependencies)
+
+## Getting Started
+
+Follow these steps to set up the project:
+
+1. Clone this repository to a location on your computer
+2. Change into the project directory
+3. Copy `.env.example` to `.env` and choose your database (default is SQLite). Set `DATABASE=sqlite|postgres|mysql`.
+4. Run `make build` to build the Docker containers
+5. Run `make up` to start the Docker containers
+6. Run `make migrate` to apply database migrations
+7. Run `make superuser` to create a superuser
+8. Run `make run` to start the Django development server
+
+**Note:** `.env` is required. `make build` and `make up` validate environment variables via `make check-env` and will fail if required values are missing. See `.env.example` for all keys.
+
+## Quick Start
+
+There is a make command to run most of the steps above in one go:
+
+```bash
+make quickstart
+```
+
+You'll need to run `make superuser` separately to create your admin user.
+
+## Default Configuration
+
+If you haven't changed `.env` the app will default to:
+
+- Use SQLite as the database
+- A mail utility will be available at [http://localhost:8025](http://localhost:8025)
+- A database management utility will be available at [http://localhost:8080](http://localhost:8080)
+
+## Accessing the Site
+
+Once the development server is running:
+
+- **Main site**: [http://localhost:8000](http://localhost:8000)
+- **Wagtail admin**: [http://localhost:8000/admin](http://localhost:8000/admin)
+- **Style guide**: [http://localhost:8000/style-guide](http://localhost:8000/style-guide) (only available in debug mode)
+
+## Next Steps
+
+- See [Backend Development](./backend-development.md) for information about database configuration and backend development
+- See [Frontend Development](./frontend-development.md) for information about building CSS and JavaScript assets
+- See [Management Commands](./management-commands.md) for helpful commands to generate sample content and manage data
diff --git a/docs/management-commands.md b/docs/management-commands.md
index 507394b..5f8da98 100644
--- a/docs/management-commands.md
+++ b/docs/management-commands.md
@@ -5,6 +5,7 @@ This document provides detailed information about the custom Django management c
## Table of Contents
- [create_sample_media](#create_sample_media)
+- [create_sample_categories](#create_sample_categories)
- [Future Commands](#future-commands)
---
@@ -127,6 +128,170 @@ docker exec -it wagtail-shop-kit-app-1 python manage.py create_sample_media --re
---
+## create_sample_categories
+
+**Location**: `app/shop/management/commands/create_sample_categories.py`
+
+**Purpose**: Creates a shop index page and sample category pages for the e-commerce section of your Wagtail site.
+
+### Description
+
+The `create_sample_categories` command sets up the foundational structure for your online shop by:
+
+1. **Creating or finding a ShopIndexPage**: Automatically creates a main "Shop" page under the home page if it doesn't already exist
+2. **Creating Category Pages**: Generates 6 sample category pages (Electronics, Fashion, Home & Living, Beauty, Sports, Books) as children of the shop index page
+3. **Assigning Icons**: Optionally assigns random images from your media library as category icons
+4. **Publishing Content**: All pages are automatically published and ready to view
+
+This command is perfect for:
+- Setting up a new e-commerce site structure
+- Demonstrating shop browsing functionality
+- Creating realistic category data for development
+- Testing the shop page hierarchy
+
+### Usage
+
+```bash
+# Basic usage (creates shop index and 6 categories with icons)
+python manage.py create_sample_categories
+
+# Reset and recreate all categories
+python manage.py create_sample_categories --reset
+
+# Create categories without icons
+python manage.py create_sample_categories --no-icons
+
+# Custom shop page configuration
+python manage.py create_sample_categories --shop-title "Store" --shop-slug "store"
+
+# Use a different home page
+python manage.py create_sample_categories --home-slug "homepage"
+```
+
+### Options
+
+| Option | Type | Default | Description |
+|--------|------|---------|-------------|
+| `--reset` | Flag | False | Delete all existing category pages before creating new ones |
+| `--no-icons` | Flag | False | Skip assigning icons to categories |
+| `--home-slug` | String | "home" | Slug of the home page where shop index will be created |
+| `--shop-title` | String | "Shop" | Title for the shop index page |
+| `--shop-slug` | String | "shop" | Slug for the shop index page |
+
+### Generated Content Details
+
+#### ShopIndexPage
+- **Location**: Created as a child of the home page
+- **Default URL**: `/shop/`
+- **Properties**:
+ - Title: "Shop" (customizable)
+ - Intro text: "Browse our collection of quality products across various categories."
+ - Visible in menus: Yes
+ - Max count: 1 (only one shop index page allowed)
+
+#### Category Pages
+The command creates 6 category pages with the following structure:
+
+| Category | Slug | Featured | Description Theme |
+|----------|------|----------|-------------------|
+| Electronics | electronics | Yes | Gadgets, tech products, smartphones, laptops |
+| Fashion | fashion | Yes | Clothing, accessories, styles, trends |
+| Home & Living | home-living | Yes | Furniture, decor, interior design |
+| Beauty | beauty | No | Skincare, cosmetics, beauty products |
+| Sports | sports | No | Fitness equipment, outdoor gear |
+| Books | books | No | Novels, educational resources, guides |
+
+Each category includes:
+- **Rich text description**: HTML formatted text with multiple paragraphs
+- **Icon**: Random image from your media library (if available and not using `--no-icons`)
+- **Featured flag**: First 3 categories are marked as featured
+- **Menu visibility**: All categories are visible in navigation menus
+
+### Page Hierarchy
+
+The command creates the following page structure:
+
+```
+Home
+└── Shop (ShopIndexPage)
+ ├── Beauty (ShopCategoryPage)
+ ├── Books (ShopCategoryPage)
+ ├── Electronics (ShopCategoryPage) ⭐ Featured
+ ├── Fashion (ShopCategoryPage) ⭐ Featured
+ ├── Home & Living (ShopCategoryPage) ⭐ Featured
+ └── Sports (ShopCategoryPage)
+```
+
+### Examples
+
+#### Creating categories in Docker
+```bash
+docker exec -it wagtail-shop-kit-app-1 python manage.py create_sample_categories
+```
+
+#### Creating categories with make command
+```bash
+make sh
+python manage.py create_sample_categories
+```
+
+#### Resetting and recreating with different configuration
+```bash
+docker exec -it wagtail-shop-kit-app-1 python manage.py create_sample_categories \
+ --reset \
+ --shop-title "Store" \
+ --shop-slug "store"
+```
+
+#### Creating without icons (faster, no media dependencies)
+```bash
+docker exec -it wagtail-shop-kit-app-1 python manage.py create_sample_categories --no-icons
+```
+
+### Prerequisites
+
+- A home page must exist in your Wagtail site (default slug: "home")
+- For icons: Run `python manage.py create_sample_media` first to generate sample images
+
+### Output Example
+
+```
+Found existing ShopIndexPage: 'Shop' at /shop/
+Found 75 images available for category icons
+ Assigning icon: Sharp City Skyline #469 (ID: 29)
+ ✓ Created: Electronics 🖼️ (icon: Sharp City Skyline #469...) ⭐ [FEATURED]
+ Assigning icon: Handcrafted Garden Abstract Art #415 (ID: 44)
+ ✓ Created: Fashion 🖼️ (icon: Handcrafted Garden Abstract Ar...) ⭐ [FEATURED]
+ ...
+
+============================================================
+Successfully created 6 category pages under 'Shop'
+
+Category URLs:
+ → /shop/beauty/ - Beauty
+ → /shop/books/ - Books
+ → /shop/electronics/ - Electronics
+ → /shop/fashion/ - Fashion
+ → /shop/home-living/ - Home & Living
+ → /shop/sports/ - Sports
+============================================================
+```
+
+### Technical Implementation
+
+#### Dependencies
+- **Wagtail Page Models**: Uses `ShopIndexPage` and `ShopCategoryPage` from `app.shop.models`
+- **Wagtail Images**: Optionally uses `wagtail.images.models.Image` for category icons
+- **Treebeard**: Leverages Wagtail's page tree structure for parent-child relationships
+
+#### Key Features
+- **Idempotent**: Safe to run multiple times; checks for existing pages before creating
+- **Tree Cache Management**: Properly refreshes page tree after deletions to prevent cache issues
+- **Random Icon Assignment**: Shuffles available images to assign unique icons to each category
+- **Revision Management**: Creates and publishes page revisions automatically
+
+---
+
## Future Commands
This section will be expanded as additional management commands are added to the project.
diff --git a/pyproject.toml b/pyproject.toml
index a26a123..485d606 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -9,6 +9,7 @@ requires-python = ">=3.10"
dependencies = [
"django~=5.2",
"django-browser-reload~=1.21",
+ "faker~=33.1",
"wagtail~=7.2",
]
diff --git a/static_src/scss/app.scss b/static_src/scss/app.scss
index 63ebb17..86da46b 100644
--- a/static_src/scss/app.scss
+++ b/static_src/scss/app.scss
@@ -10,4 +10,5 @@
// Your custom styles goes here
-@use 'components/example';
+@use 'components/home';
+@use 'components/shop';
diff --git a/static_src/scss/components/_home.scss b/static_src/scss/components/_home.scss
new file mode 100644
index 0000000..3133a25
--- /dev/null
+++ b/static_src/scss/components/_home.scss
@@ -0,0 +1,295 @@
+// Home Page Styles
+
+/* Base Styling */
+.template-homepage {
+ background-color: var(--pico-color-sand-50);
+ min-height: 100vh;
+}
+
+/* Header Navigation */
+header .welcome-grid {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 2rem;
+}
+
+.shop-branding h1 {
+ margin: 0;
+ font-size: 1.75rem;
+ color: var(--pico-primary);
+}
+
+.shop-branding h1 a {
+ color: var(--pico-primary);
+ text-decoration: none;
+ transition: opacity 0.2s ease;
+}
+
+.shop-branding h1 a:hover {
+ opacity: 0.8;
+}
+
+header nav ul {
+ display: flex;
+ gap: 1.5rem;
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ align-items: center;
+}
+
+header nav li {
+ margin: 0;
+}
+
+header nav a:not([role="button"]) {
+ text-decoration: none;
+ color: var(--pico-color);
+ font-weight: 500;
+}
+
+header nav a:not([role="button"]):hover {
+ color: var(--pico-primary);
+}
+
+/* Hero Banner */
+.hero-banner {
+ text-align: center;
+ padding: 3rem 0;
+ margin: 2rem 0;
+}
+
+.hero-banner h1 {
+ font-size: 2.5rem;
+ margin-bottom: 1rem;
+ color: var(--pico-primary);
+}
+
+.hero-banner hgroup p {
+ font-size: 1.25rem;
+ color: var(--pico-muted-color);
+ margin-bottom: 2rem;
+}
+
+.hero-visual svg {
+ width: 150px;
+ height: 150px;
+ margin: 2rem auto;
+}
+
+.hero-cta {
+ font-size: 1.1rem;
+ padding: 0.75rem 2rem;
+}
+
+/* Featured Products Section */
+.featured-products,
+.product-categories {
+ margin: 4rem 0;
+}
+
+.featured-products h2,
+.product-categories h2 {
+ text-align: center;
+ font-size: 2rem;
+ margin-bottom: 2rem;
+ color: var(--pico-color);
+}
+
+.product-grid {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 2rem;
+ margin-top: 2rem;
+}
+
+.product-card {
+ background: var(--pico-background-color);
+ border-radius: var(--pico-border-radius);
+ overflow: hidden;
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
+}
+
+.product-card:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
+}
+
+.product-image {
+ width: 100%;
+ background: var(--pico-color-sand-50);
+ aspect-ratio: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.product-image svg {
+ width: 100%;
+ height: 100%;
+}
+
+.product-details {
+ padding: 1.5rem;
+}
+
+.product-details h3 {
+ margin: 0 0 0.5rem 0;
+ font-size: 1.25rem;
+}
+
+.product-description {
+ color: var(--pico-muted-color);
+ font-size: 0.9rem;
+ margin: 0.5rem 0;
+}
+
+.product-price {
+ font-size: 1.5rem;
+ font-weight: bold;
+ color: var(--pico-primary);
+ margin: 1rem 0;
+}
+
+.product-details a[role="button"] {
+ width: 100%;
+ margin: 0;
+}
+
+/* Product Categories Section */
+.category-grid {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 1.5rem;
+ margin-top: 2rem;
+}
+
+.category-card {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+ padding: 2rem;
+ background: var(--pico-background-color);
+ border-radius: var(--pico-border-radius);
+ text-decoration: none;
+ color: var(--pico-color);
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
+}
+
+.category-card:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
+ color: var(--pico-primary);
+}
+
+.category-card svg,
+.category-card img {
+ color: var(--pico-primary);
+ margin-bottom: 1rem;
+}
+
+.category-card img {
+ width: 48px;
+ height: 48px;
+ object-fit: contain;
+ border-radius: var(--pico-border-radius);
+}
+
+.category-card h3 {
+ margin: 0.5rem 0;
+ font-size: 1.25rem;
+}
+
+.category-card p {
+ margin: 0.25rem 0 0 0;
+ color: var(--pico-muted-color);
+ font-size: 0.9rem;
+}
+
+/* Footer */
+footer.welcome-grid article {
+ text-align: center;
+}
+
+footer.welcome-grid article h2 {
+ font-size: 1rem;
+ text-decoration: underline;
+}
+
+footer.welcome-grid article a:hover h2,
+footer.welcome-grid article a:hover p {
+ color: var(--pico-primary);
+}
+
+footer a[role="card"] svg {
+ width: 4rem;
+ fill: var(--pico-primary);
+}
+
+/* Responsive Design */
+@media screen and (min-width: 768px) {
+ .shop-branding h1 {
+ font-size: 2rem;
+ }
+
+ .hero-banner {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 2rem;
+ align-items: center;
+ text-align: left;
+ }
+
+ .hero-visual svg {
+ width: 200px;
+ height: 200px;
+ }
+
+ .product-grid {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ .category-grid {
+ grid-template-columns: repeat(2, 1fr);
+ }
+}
+
+@media screen and (min-width: 1024px) {
+ .product-grid {
+ grid-template-columns: repeat(4, 1fr);
+ }
+
+ .category-grid {
+ grid-template-columns: repeat(3, 1fr);
+ }
+
+ footer.welcome-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 20px;
+ }
+
+ footer.welcome-grid svg {
+ width: 2rem;
+ }
+}
+
+@media screen and (min-width: 1280px) {
+ footer.welcome-grid article a {
+ display: grid;
+ grid-template-columns: 1fr 3fr;
+ align-items: center;
+ }
+
+ footer a[role="card"] svg {
+ width: 3rem;
+ }
+
+ footer hgroup {
+ text-align: left;
+ margin: 0;
+ }
+}
diff --git a/static_src/scss/components/_shop.scss b/static_src/scss/components/_shop.scss
new file mode 100644
index 0000000..3e6c068
--- /dev/null
+++ b/static_src/scss/components/_shop.scss
@@ -0,0 +1,596 @@
+// Shop Category Page Styles
+
+/* Base Styling */
+.template-shopcategorypage {
+ background-color: var(--pico-color-sand-50);
+ min-height: 100vh;
+}
+
+/* Category Header */
+.category-header {
+ text-align: center;
+ padding: 3rem 0 2rem;
+ margin-bottom: 2rem;
+}
+
+.category-header .category-icon {
+ margin: 0 auto 1.5rem;
+ width: 100px;
+ height: 100px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.category-header .category-icon img {
+ max-width: 100%;
+ max-height: 100%;
+ object-fit: contain;
+}
+
+.category-header h1 {
+ font-size: 2.5rem;
+ margin-bottom: 1rem;
+ color: var(--pico-primary);
+}
+
+.category-header .category-description {
+ font-size: 1.1rem;
+ color: var(--pico-muted-color);
+ max-width: 800px;
+ margin: 0 auto;
+}
+
+.category-header .category-description p:last-child {
+ margin-bottom: 0;
+}
+
+/* Breadcrumb Navigation */
+nav[aria-label="breadcrumb"] {
+ margin-bottom: 2rem;
+}
+
+nav[aria-label="breadcrumb"] ul {
+ justify-content: flex-start;
+}
+
+/* Subcategories Section */
+.subcategories-section {
+ margin: 3rem 0;
+}
+
+.subcategories-section h2 {
+ font-size: 1.5rem;
+ margin-bottom: 1.5rem;
+ color: var(--pico-color);
+}
+
+.subcategory-grid {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 1.5rem;
+}
+
+.subcategory-card {
+ display: flex;
+ align-items: center;
+ padding: 1.5rem;
+ background: var(--pico-background-color);
+ border-radius: var(--pico-border-radius);
+ text-decoration: none;
+ color: var(--pico-color);
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
+}
+
+.subcategory-card:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ color: var(--pico-primary);
+}
+
+.subcategory-card .subcategory-icon {
+ width: 60px;
+ height: 60px;
+ margin-right: 1.5rem;
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.subcategory-card .subcategory-icon img {
+ max-width: 100%;
+ max-height: 100%;
+ object-fit: contain;
+}
+
+.subcategory-card .subcategory-info h3 {
+ margin: 0 0 0.5rem 0;
+ font-size: 1.1rem;
+}
+
+.subcategory-card .subcategory-info p {
+ margin: 0;
+ color: var(--pico-muted-color);
+ font-size: 0.9rem;
+}
+
+/* Products Section */
+.products-section {
+ margin: 3rem 0;
+}
+
+.products-section h2 {
+ font-size: 1.5rem;
+ margin-bottom: 1.5rem;
+ color: var(--pico-color);
+}
+
+.template-shopcategorypage .product-grid {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 2rem;
+}
+
+.template-shopcategorypage .product-card {
+ background: var(--pico-background-color);
+ border-radius: var(--pico-border-radius);
+ overflow: hidden;
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
+ text-decoration: none;
+ color: var(--pico-color);
+ display: block;
+}
+
+.template-shopcategorypage .product-card:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
+}
+
+.template-shopcategorypage .product-image {
+ width: 100%;
+ background: var(--pico-color-sand-50);
+ aspect-ratio: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+}
+
+.template-shopcategorypage .product-image img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.template-shopcategorypage .product-details {
+ padding: 1.5rem;
+}
+
+.template-shopcategorypage .product-details h3 {
+ margin: 0 0 0.5rem 0;
+ font-size: 1.25rem;
+ color: var(--pico-color);
+}
+
+.template-shopcategorypage .product-description {
+ color: var(--pico-muted-color);
+ font-size: 0.9rem;
+ margin: 0.5rem 0;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+
+.template-shopcategorypage .product-price {
+ font-size: 1.5rem;
+ font-weight: bold;
+ color: var(--pico-primary);
+ margin: 1rem 0 0 0;
+}
+
+.product-stock-status {
+ display: inline-block;
+ padding: 0.25rem 0.75rem;
+ border-radius: var(--pico-border-radius);
+ font-size: 0.85rem;
+ margin-top: 0.5rem;
+}
+
+.product-stock-status.in-stock {
+ background-color: var(--pico-ins-color);
+ color: var(--pico-background-color);
+}
+
+.product-stock-status.out-of-stock {
+ background-color: var(--pico-del-color);
+ color: var(--pico-background-color);
+}
+
+/* Pagination */
+.pagination {
+ margin: 3rem 0;
+ text-align: center;
+}
+
+.pagination nav {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 0.5rem;
+ flex-wrap: wrap;
+}
+
+.pagination nav a,
+.pagination nav span {
+ padding: 0.5rem 1rem;
+ text-decoration: none;
+ color: var(--pico-primary);
+ border: 1px solid var(--pico-primary);
+ border-radius: var(--pico-border-radius);
+ transition: background-color 0.2s ease;
+}
+
+.pagination nav a:hover {
+ background-color: var(--pico-primary);
+ color: var(--pico-background-color);
+}
+
+.pagination nav .current {
+ background-color: var(--pico-primary);
+ color: var(--pico-background-color);
+}
+
+.pagination nav .disabled {
+ opacity: 0.5;
+ pointer-events: none;
+}
+
+/* Responsive Design */
+@media screen and (min-width: 768px) {
+ .category-header h1 {
+ font-size: 3rem;
+ }
+
+ .subcategory-grid {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ .template-shopcategorypage .product-grid {
+ grid-template-columns: repeat(2, 1fr);
+ }
+}
+
+@media screen and (min-width: 1024px) {
+ .subcategory-grid {
+ grid-template-columns: repeat(3, 1fr);
+ }
+
+ .template-shopcategorypage .product-grid {
+ grid-template-columns: repeat(3, 1fr);
+ }
+}
+
+@media screen and (min-width: 1280px) {
+ .template-shopcategorypage .product-grid {
+ grid-template-columns: repeat(4, 1fr);
+ }
+}
+
+// Shop Index Page Styles
+.template-shopindexpage {
+ background-color: var(--pico-color-sand-50);
+ min-height: 100vh;
+}
+
+/* Shop Header */
+.shop-header {
+ text-align: center;
+ margin-bottom: 3rem;
+ padding: 2rem 0;
+}
+
+.shop-header h1 {
+ margin-bottom: 1rem;
+}
+
+.shop-intro {
+ max-width: 600px;
+ margin: 0 auto;
+ color: var(--pico-muted-color);
+}
+
+/* Category Grid */
+.category-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+ gap: 1.5rem;
+ margin-bottom: 3rem;
+}
+
+.category-card {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+ padding: 2rem 1.5rem;
+ background: var(--pico-card-background-color);
+ border: 1px solid var(--pico-card-border-color);
+ border-radius: var(--pico-border-radius);
+ text-decoration: none;
+ color: var(--pico-color);
+ transition: all 0.2s ease;
+}
+
+.category-card:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
+ border-color: var(--pico-primary);
+}
+
+.category-card-icon {
+ margin-bottom: 1rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 80px;
+ height: 80px;
+}
+
+.category-card-icon img {
+ max-width: 100%;
+ max-height: 100%;
+ object-fit: contain;
+}
+
+.category-card-icon svg {
+ color: var(--pico-muted-color);
+}
+
+.category-card-content h3 {
+ margin-bottom: 0.5rem;
+ font-size: 1.25rem;
+ color: var(--pico-color);
+}
+
+.category-card-content p {
+ margin: 0;
+ font-size: 0.9rem;
+ color: var(--pico-muted-color);
+ line-height: 1.5;
+}
+
+.category-badge {
+ position: absolute;
+ top: 0.5rem;
+ right: 0.5rem;
+ background: var(--pico-primary);
+ color: white;
+ padding: 0.25rem 0.75rem;
+ border-radius: var(--pico-border-radius);
+ font-size: 0.75rem;
+ font-weight: 600;
+ text-transform: uppercase;
+}
+
+/* Empty State - Shared styles */
+.empty-state {
+ text-align: center;
+ padding: 4rem 2rem;
+ color: var(--pico-muted-color);
+}
+
+.empty-state h3 {
+ color: var(--pico-color);
+ margin-bottom: 0.5rem;
+}
+
+.empty-state code {
+ background: var(--pico-code-background-color);
+ padding: 0.2rem 0.4rem;
+ border-radius: 4px;
+ font-size: 0.85em;
+}
+
+/* Responsive adjustments */
+@media (max-width: 768px) {
+ .category-grid {
+ grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
+ gap: 1rem;
+ }
+
+ .shop-header {
+ padding: 1rem 0;
+ margin-bottom: 2rem;
+ }
+}
+
+// Product Page Styles
+.template-productpage {
+ background-color: var(--pico-color-sand-50);
+ min-height: 100vh;
+}
+
+/* Breadcrumb styling for product page */
+.template-productpage nav[aria-label="breadcrumb"] ul {
+ list-style: none;
+ padding: 0;
+ margin: 1rem 0 2rem 0;
+ display: flex;
+ gap: 0.5rem;
+ flex-wrap: wrap;
+ font-size: 0.9rem;
+}
+
+.template-productpage nav[aria-label="breadcrumb"] li:not(:last-child)::after {
+ content: "›";
+ margin-left: 0.5rem;
+ color: var(--pico-muted-color);
+}
+
+.template-productpage nav[aria-label="breadcrumb"] a {
+ color: var(--pico-primary);
+ text-decoration: none;
+}
+
+.template-productpage nav[aria-label="breadcrumb"] a:hover {
+ text-decoration: underline;
+}
+
+/* Product Detail Layout */
+.product-detail {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 3rem;
+ margin: 2rem 0;
+}
+
+/* Product Images */
+.product-images {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.main-image {
+ width: 100%;
+ border-radius: var(--pico-border-radius);
+ overflow: hidden;
+ background: var(--pico-background-color);
+ border: 1px solid var(--pico-muted-border-color);
+}
+
+.main-image img {
+ width: 100%;
+ height: auto;
+ display: block;
+}
+
+.image-gallery {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
+ gap: 0.5rem;
+}
+
+.gallery-thumbnail {
+ border-radius: var(--pico-border-radius);
+ overflow: hidden;
+ border: 2px solid var(--pico-muted-border-color);
+ cursor: pointer;
+ transition: border-color 0.2s;
+}
+
+.gallery-thumbnail:hover {
+ border-color: var(--pico-primary);
+}
+
+.gallery-thumbnail img {
+ width: 100%;
+ height: auto;
+ display: block;
+}
+
+/* Product Information */
+.product-info h1 {
+ margin-top: 0;
+ font-size: 2rem;
+ color: var(--pico-color);
+}
+
+.product-meta {
+ display: flex;
+ align-items: center;
+ gap: 1.5rem;
+ margin: 1.5rem 0;
+}
+
+.product-price {
+ font-size: 2rem;
+ font-weight: bold;
+ color: var(--pico-primary);
+ margin: 0;
+}
+
+.product-stock-status {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.25rem;
+ padding: 0.5rem 1rem;
+ border-radius: var(--pico-border-radius);
+ font-size: 0.9rem;
+ font-weight: 600;
+}
+
+.product-stock-status.in-stock {
+ background-color: rgba(46, 125, 50, 0.1);
+ color: #2e7d32;
+}
+
+.product-stock-status.out-of-stock {
+ background-color: rgba(211, 47, 47, 0.1);
+ color: #d32f2f;
+}
+
+.product-sku {
+ color: var(--pico-muted-color);
+ font-size: 0.9rem;
+ margin: 0.5rem 0;
+}
+
+.product-sku span {
+ font-family: monospace;
+ color: var(--pico-color);
+}
+
+.product-description {
+ margin: 2rem 0;
+ line-height: 1.8;
+ color: var(--pico-color);
+}
+
+/* Add to Cart Button */
+.product-actions {
+ margin-top: 2rem;
+}
+
+.add-to-cart-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 1rem 2rem;
+ font-size: 1.1rem;
+ font-weight: 600;
+ cursor: not-allowed;
+ opacity: 0.6;
+}
+
+.cart-notice {
+ margin-top: 0.5rem;
+ font-size: 0.85rem;
+ color: var(--pico-muted-color);
+ font-style: italic;
+}
+
+/* Responsive Design for Product Page */
+@media (max-width: 768px) {
+ .product-detail {
+ grid-template-columns: 1fr;
+ gap: 2rem;
+ }
+
+ .product-info h1 {
+ font-size: 1.5rem;
+ }
+
+ .product-price {
+ font-size: 1.5rem;
+ }
+
+ .product-meta {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 1rem;
+ }
+}
diff --git a/static_src/scss/components/example.scss b/static_src/scss/components/example.scss
deleted file mode 100644
index eb0c546..0000000
--- a/static_src/scss/components/example.scss
+++ /dev/null
@@ -1 +0,0 @@
-// example imported/used in app.scss
diff --git a/uv.lock b/uv.lock
index 5317263..31bedaa 100644
--- a/uv.lock
+++ b/uv.lock
@@ -416,6 +416,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" },
]
+[[package]]
+name = "faker"
+version = "33.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "python-dateutil" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9c/50/48ab6ba3f07ee7d0eac367695aeb8bc9eb9c3debc0445a67cd07e2d62b44/faker-33.3.1.tar.gz", hash = "sha256:49dde3b06a5602177bc2ad013149b6f60a290b7154539180d37b6f876ae79b20", size = 1854895, upload-time = "2025-01-10T16:49:37.655Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c2/01/6acc8b4dba4154cd93b444382a9ad3c099557aac577bdc7d66373e0a0c68/Faker-33.3.1-py3-none-any.whl", hash = "sha256:ac4cf2f967ce02c898efa50651c43180bd658a7707cfd676fcc5410ad1482c03", size = 1894842, upload-time = "2025-01-10T16:49:34.261Z" },
+]
+
[[package]]
name = "filelock"
version = "3.20.1"
@@ -677,6 +690,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" },
]
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
+]
+
[[package]]
name = "pyupgrade"
version = "3.21.2"
@@ -794,6 +819,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839, upload-time = "2025-12-18T19:28:48.636Z" },
]
+[[package]]
+name = "six"
+version = "1.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
+]
+
[[package]]
name = "soupsieve"
version = "2.8.1"
@@ -908,6 +942,7 @@ source = { virtual = "." }
dependencies = [
{ name = "django" },
{ name = "django-browser-reload" },
+ { name = "faker" },
{ name = "wagtail" },
]
@@ -924,6 +959,7 @@ dev = [
requires-dist = [
{ name = "django", specifier = "~=5.2" },
{ name = "django-browser-reload", specifier = "~=1.21" },
+ { name = "faker", specifier = "~=33.1" },
{ name = "wagtail", specifier = "~=7.2" },
]