From 731845967395c2862507254c1e4bcd564e7221d8 Mon Sep 17 00:00:00 2001 From: Nick Moreton Date: Thu, 1 Jan 2026 22:36:15 +0000 Subject: [PATCH 01/16] Move documentation around --- README.md | 116 ++++++++++--------------------------------- docs/README.md | 6 ++- docs/installation.md | 67 +++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 91 deletions(-) create mode 100644 docs/installation.md diff --git a/README.md b/README.md index d981f5e..ae8556b 100644 --- a/README.md +++ b/README.md @@ -1,110 +1,48 @@ # Wagtail Shop Kit -This is a starter kit for a Wagtail e-commerce project. It includes a Docker setup for local development, a basic project structure, and some useful tools and libraries. - -You can use this project as a starting point for your own Wagtail shop projects and build upon it as needed. +An e-commerce starter kit built with Wagtail CMS and Django. This project provides a foundation for building online shops with a powerful content management system. ## Features -- Docker Development Environment -- Postgresql, Mysql or Sqlite3 Database -- Frontend Node SASS and Javascript compilation -- Pico CSS for almost classless styling -- esbuild javascript bundler -- Wagtail CMS v7.2 -- Django v5.2 +- **Wagtail CMS v7.2** - Powerful, flexible content management +- **Django v5.2** - Robust Python web framework +- **Docker Development Environment** - Containerized setup for consistency +- **Multiple Database Options** - PostgreSQL, MySQL, or SQLite3 +- **Modern Frontend Stack** - SASS, esbuild, and Pico CSS +- **E-commerce Ready** - Built with shop functionality in mind ![Wagtail Shop Kit](./docs/welcome-screen.jpg) -## Requirements - -Required: - -- [Python >= 3.10](https://www.python.org/downloads/) (developemnt 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) (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 - -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: +## Quick Start ```bash -make quickstart -``` - -You'll need to run `make superuser` separately. - -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) - -## Working with the fronend +# Copy environment file and configure +cp .env.example .env -The project uses [Pico CSS](https://picocss.com/) for styling. It's a minmal setup that you can build on. - -When you first run the project you may notice that no styling is applied. This is because the first time you run the project with `make up` the compiled frontend files might not be available. Just run the frontend build script and refresh the page. [Read on](./docs/frontend-development.md) - -## Working with the backend - -The project uses Docker for local development. The Wagtail project is in the `app` directory. The project is set up to use a SQLite database by default. You can change this to use Mysql or Postgres. [Read on](./docs/backend-development.md) - -## Management Commands - -The project includes custom Django management commands to help with development and testing. These commands can be used to generate sample content, manage data, and perform administrative tasks. - -For detailed documentation on all available commands, see [Management Commands Documentation](./docs/management-commands.md). - -### Quick Examples - -```bash -# Create sample images and documents for testing -docker exec -it wagtail-shop-kit-app-1 python manage.py create_sample_media +# Build and start the project +make quickstart -# Reset all sample content -docker exec -it wagtail-shop-kit-app-1 python manage.py create_sample_media --reset +# Create admin user +make superuser ``` -## View the site - -The site will be available at [http://localhost:8000](http://localhost:8000). - -The Wagtail admin interface will be available at [http://localhost:8000/admin](http://localhost:8000/admin). - -## Deployment - -Currently there is no deployment setup included in this project. You could try this [Wagtail deployment guide](https://docs.wagtail.org/en/stable/deployment/index.html) for some ideas. +The site will be available at [http://localhost:8000](http://localhost:8000) -## Deployment Examples +## Documentation -1. How to deploy a Wagtail site to [PythonAnywhere](https://www.nickmoreton.co.uk/articles/deploy-wagtail-cms-to-pythonanywhere/), this does need you to have a paid account with PythonAnywhere. -2. This [example](https://github.com/wagtail-examples/wsk-deploy-python-anywhere) is a fork of this starter kit which has documentation on how to deploy to PythonAnywhere, using a free account. +- **[Installation Guide](./docs/installation.md)** - Detailed setup instructions +- **[Backend Development](./docs/backend-development.md)** - Database configuration and backend development +- **[Frontend Development](./docs/frontend-development.md)** - CSS/JS build process and styling +- **[Management Commands](./docs/management-commands.md)** - Helpful commands for development -## Contributing +## Technology Stack -If you have any suggestions or improvements, please open an issue or a pull request. +- Python 3.10+ +- Django 5.2 +- Wagtail 7.2 +- Docker & Docker Compose +- Node.js (SASS & esbuild) +- Pico CSS ## License 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/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 From 2a63143bdd954834b511858b2aa1390ffe826a9b Mon Sep 17 00:00:00 2001 From: Nick Moreton Date: Thu, 1 Jan 2026 22:38:14 +0000 Subject: [PATCH 02/16] Allow CI --- .github/{temp => workflows}/ci.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/{temp => workflows}/ci.yml (100%) diff --git a/.github/temp/ci.yml b/.github/workflows/ci.yml similarity index 100% rename from .github/temp/ci.yml rename to .github/workflows/ci.yml From 411b7761f2e5f4c87121d0aacb3b0d6f72f74c1b Mon Sep 17 00:00:00 2001 From: nickmoreton Date: Fri, 2 Jan 2026 13:29:40 +0000 Subject: [PATCH 03/16] Welcome page idea --- app/home/templates/home/home_page.html | 232 ++++++++++++++--- app/home/templates/home/welcome_page.html | 289 ++++++++++++---------- 2 files changed, 345 insertions(+), 176 deletions(-) diff --git a/app/home/templates/home/home_page.html b/app/home/templates/home/home_page.html index 0b1dc65..59d85cd 100644 --- a/app/home/templates/home/home_page.html +++ b/app/home/templates/home/home_page.html @@ -4,19 +4,14 @@ {% block body_class %}template-homepage{% endblock %} {% block extra_css %} - -{% comment %} -Delete the line below if you're just getting started and want to remove the welcome screen! -{% endcomment %} -{% endblock extra_css %} - {% block content %} {% comment %} diff --git a/app/home/templates/home/welcome_page.html b/app/home/templates/home/welcome_page.html index c2fa077..926d5b5 100644 --- a/app/home/templates/home/welcome_page.html +++ b/app/home/templates/home/welcome_page.html @@ -1,21 +1,5 @@ {% load i18n wagtailcore_tags %} -
-
-
-

Wagtail Shop

-
- -
-
-
@@ -156,39 +140,3 @@

Books

- - 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/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/__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..88073c3 --- /dev/null +++ b/app/shop/models.py @@ -0,0 +1,58 @@ +from django.db import models +from wagtail.admin.panels import FieldPanel +from wagtail.fields import RichTextField +from wagtail.models import Page + +""" +**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 + """ + + +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 = ["home.HomePage"] + # subpage_types = ["app.shop.ProductPage"] hidden for now + + # implement later + # def get_products(self): + # return ProductPage.objects.child_of(self).live() + + # implement later + # def get_context(self, request): + # context = super().get_context(request) + # products = self.get_products() + # paginator = Paginator(products, 10) # Show 10 products per page + # page_number = request.GET.get("page") + # page_obj = paginator.get_page(page_number) + # context["products"] = page_obj + # return context 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..6a3a5c5 --- /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 page.icon %} +
+ {% image page.icon width-100 as icon_img %} + {{ page.title }} icon +
+ {% endif %} + +

{{ page.title }}

+ + {% if page.description %} +
+ {{ page.description|richtext }} +
+ {% endif %} +
+ + + {% 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/tests.py b/app/shop/tests.py new file mode 100644 index 0000000..a39b155 --- /dev/null +++ b/app/shop/tests.py @@ -0,0 +1 @@ +# Create your tests here. 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..0a71e64 --- /dev/null +++ b/app/templates/includes/base_footer.html @@ -0,0 +1,35 @@ + diff --git a/app/templates/includes/base_header.html b/app/templates/includes/base_header.html new file mode 100644 index 0000000..4316c59 --- /dev/null +++ b/app/templates/includes/base_header.html @@ -0,0 +1,15 @@ +
+
+
+

Wagtail Shop

+
+ +
+
diff --git a/docs/development-progress.md b/docs/development-progress.md index b722e25..2203c39 100644 --- a/docs/development-progress.md +++ b/docs/development-progress.md @@ -12,10 +12,10 @@ ## Setup & Prerequisites -- [ ] Docker environment running (`make up`) -- [ ] Database selected (SQLite/PostgreSQL/MySQL) -- [ ] Superuser created (`make superuser`) -- [ ] Frontend assets compiled (`npm run build`) +- [x] Docker environment running (`make up`) +- [x] Database selected (SQLite/PostgreSQL/MySQL) +- [x] Superuser created (`make superuser`) +- [x] Frontend assets compiled (`npm run build`) --- @@ -26,9 +26,9 @@ **Architecture**: Categories as Wagtail Pages (not snippets) - Products will be children ### Core Tasks -- [ ] Create shop app (`python manage.py startapp shop`) -- [ ] Add shop to INSTALLED_APPS in `app/settings/base.py` -- [ ] Create CategoryPage model (extends wagtail.models.Page) +- [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 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..4409038 --- /dev/null +++ b/static_src/scss/components/_home.scss @@ -0,0 +1,277 @@ +// 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); +} + +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 { + color: var(--pico-primary); + margin-bottom: 1rem; +} + +.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..5c12811 --- /dev/null +++ b/static_src/scss/components/_shop.scss @@ -0,0 +1,307 @@ +// 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 */ +.breadcrumb { + margin-bottom: 2rem; + padding: 0; +} + +.breadcrumb nav { + font-size: 0.9rem; +} + +.breadcrumb nav a { + color: var(--pico-primary); + text-decoration: none; +} + +.breadcrumb nav a:hover { + text-decoration: underline; +} + +.breadcrumb nav span { + color: var(--pico-muted-color); + margin: 0 0.5rem; +} + +/* 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); +} + +/* Empty State */ +.empty-state { + text-align: center; + padding: 4rem 2rem; + color: var(--pico-muted-color); +} + +.empty-state h3 { + font-size: 1.5rem; + margin-bottom: 0.5rem; + color: var(--pico-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); + } +} 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 From 8012e90051ad83b2b7ba0692e50547174cb7e3a7 Mon Sep 17 00:00:00 2001 From: nickmoreton Date: Fri, 2 Jan 2026 17:53:46 +0000 Subject: [PATCH 09/16] List featured categories on the home page --- app/home/models.py | 7 + app/home/templates/home/welcome_page.html | 69 ++---- app/shop/management/__init__.py | 0 app/shop/management/commands/__init__.py | 0 .../commands/create_sample_categories.py | 216 ++++++++++++++++++ static_src/scss/components/_home.scss | 10 +- 6 files changed, 254 insertions(+), 48 deletions(-) create mode 100644 app/shop/management/__init__.py create mode 100644 app/shop/management/commands/__init__.py create mode 100644 app/shop/management/commands/create_sample_categories.py diff --git a/app/home/models.py b/app/home/models.py index a8a0cc2..d74e694 100644 --- a/app/home/models.py +++ b/app/home/models.py @@ -1,9 +1,16 @@ from wagtail import __version__ as WAGTAIL_VERSION from wagtail.models import Page +from app.shop.models import ShopCategoryPage + class HomePage(Page): + def get_featured_categories(self): + """Return featured shop categories""" + return ShopCategoryPage.objects.live().filter(featured=True).order_by("title") + def get_context(self, request): context = super().get_context(request) context["wagtail_version"] = WAGTAIL_VERSION + context["featured_categories"] = self.get_featured_categories() return context diff --git a/app/home/templates/home/welcome_page.html b/app/home/templates/home/welcome_page.html index 926d5b5..7aeb199 100644 --- a/app/home/templates/home/welcome_page.html +++ b/app/home/templates/home/welcome_page.html @@ -1,4 +1,4 @@ -{% load i18n wagtailcore_tags %} +{% load i18n wagtailcore_tags wagtailimages_tags %}
@@ -89,54 +89,29 @@

Desk Lamp

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/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..6d1c7ca --- /dev/null +++ b/app/shop/management/commands/create_sample_categories.py @@ -0,0 +1,216 @@ +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 + + +class Command(BaseCommand): + help = "Creates sample category pages for the shop" + + 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( + "--parent-slug", + type=str, + default="home", + help="Slug of the parent page (default: home)", + ) + + def handle(self, *args, **options): + # Find parent page + try: + parent_page = Page.objects.get(slug=options["parent_slug"]).specific + except Page.DoesNotExist: + self.stdout.write( + self.style.ERROR( + f"Parent page with slug '{options['parent_slug']}' not found" + ) + ) + return + + # Reset if requested + if options["reset"]: + self.stdout.write("Deleting existing category pages...") + deleted_count = ShopCategoryPage.objects.count() + ShopCategoryPage.objects.all().delete() + 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 + existing = ShopCategoryPage.objects.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 parent page + parent_page.add_child(instance=category_page) + category_page.save_revision().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 '{parent_page.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 ShopCategoryPage.objects.all().order_by("title"): + self.stdout.write(f" → {category.get_url()} - {category.title}") + + self.stdout.write("=" * 60) diff --git a/static_src/scss/components/_home.scss b/static_src/scss/components/_home.scss index 4409038..0413d00 100644 --- a/static_src/scss/components/_home.scss +++ b/static_src/scss/components/_home.scss @@ -175,11 +175,19 @@ header nav a:not([role="button"]):hover { color: var(--pico-primary); } -.category-card svg { +.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; From ad42b3afc3398715d67474b006fb9767ff73546f Mon Sep 17 00:00:00 2001 From: nickmoreton Date: Fri, 2 Jan 2026 17:57:41 +0000 Subject: [PATCH 10/16] Brand link as home link --- app/templates/includes/base_header.html | 3 +-- static_src/scss/components/_home.scss | 10 ++++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/templates/includes/base_header.html b/app/templates/includes/base_header.html index 4316c59..4873883 100644 --- a/app/templates/includes/base_header.html +++ b/app/templates/includes/base_header.html @@ -1,12 +1,11 @@
-

Wagtail Shop

+

Wagtail Shop

{{ category.title }}

{% endfor %}
+ {% else %}

No categories available yet. Run python manage.py create_sample_categories to create sample categories. diff --git a/app/shop/management/commands/create_sample_categories.py b/app/shop/management/commands/create_sample_categories.py index 799f8a8..e70148e 100644 --- a/app/shop/management/commands/create_sample_categories.py +++ b/app/shop/management/commands/create_sample_categories.py @@ -4,11 +4,11 @@ from wagtail.images.models import Image from wagtail.models import Page -from app.shop.models import ShopCategoryPage +from app.shop.models import ShopCategoryPage, ShopIndexPage class Command(BaseCommand): - help = "Creates sample category pages for the shop" + help = "Creates sample category pages for the shop under a ShopIndexPage" def add_arguments(self, parser): parser.add_argument( @@ -22,36 +22,74 @@ def add_arguments(self, parser): help="Skip assigning icons to categories", ) parser.add_argument( - "--parent-slug", + "--home-slug", type=str, default="home", - help="Slug of the parent page (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 parent page + # Find home page try: - parent_page = Page.objects.get(slug=options["parent_slug"]) + home_page = Page.objects.get(slug=options["home_slug"]) except Page.DoesNotExist: self.stdout.write( self.style.ERROR( - f"Parent page with slug '{options['parent_slug']}' not found" + 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 = parent_page.get_children().type(ShopCategoryPage) + 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 parent page from database to update tree structure - parent_page.refresh_from_db() + # 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") @@ -157,10 +195,13 @@ def handle(self, *args, **options): random.shuffle(available_images) for idx, category_data in enumerate(categories): - # Check if category already exists - existing = ShopCategoryPage.objects.filter( - slug=category_data["slug"] - ).first() + # 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( @@ -187,8 +228,8 @@ def handle(self, *args, **options): f" Assigning icon: {category_page.icon.title} (ID: {category_page.icon.id})" ) - # Add as child of parent page - parent_page.add_child(instance=category_page) + # Add as child of shop index page + shop_index.add_child(instance=category_page) rev = category_page.save_revision() rev.publish() @@ -210,7 +251,7 @@ def handle(self, *args, **options): if created_count > 0: self.stdout.write( self.style.SUCCESS( - f"Successfully created {created_count} category pages under '{parent_page.title}'" + f"Successfully created {created_count} category pages under '{shop_index.title}'" ) ) else: @@ -219,7 +260,12 @@ def handle(self, *args, **options): # Display URLs if created_count > 0: self.stdout.write("\nCategory URLs:") - for category in ShopCategoryPage.objects.all().order_by("title"): + 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/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/models.py b/app/shop/models.py index c9af564..0924f67 100644 --- a/app/shop/models.py +++ b/app/shop/models.py @@ -23,6 +23,32 @@ """ +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( @@ -40,7 +66,7 @@ class ShopCategoryPage(Page): FieldPanel("featured"), ] - parent_page_types = ["home.HomePage"] + parent_page_types = ["shop.ShopIndexPage"] # subpage_types = ["app.shop.ProductPage"] hidden for now # implement later diff --git a/app/shop/templates/shop/shop_category_page.html b/app/shop/templates/shop/shop_category_page.html index 6a3a5c5..63aeff6 100644 --- a/app/shop/templates/shop/shop_category_page.html +++ b/app/shop/templates/shop/shop_category_page.html @@ -6,13 +6,13 @@ {% block 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 %} +
+ +
+

{{ page.title }}

+ {% if page.intro %} +
+ {{ page.intro|richtext }} +
+ {% endif %} +
+ + + {% 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/templates/includes/base_footer.html b/app/templates/includes/base_footer.html index 0a71e64..5fe6898 100644 --- a/app/templates/includes/base_footer.html +++ b/app/templates/includes/base_footer.html @@ -11,7 +11,8 @@

Customer Support

- + {% comment %} Can be dynamic later {% endcomment %} + diff --git a/app/templates/includes/base_header.html b/app/templates/includes/base_header.html index 4873883..78cba28 100644 --- a/app/templates/includes/base_header.html +++ b/app/templates/includes/base_header.html @@ -5,7 +5,8 @@

Wagtail Shop