From 29fd14c06e379342bc249f4afcb8356e281a6b17 Mon Sep 17 00:00:00 2001 From: Stephan Kerkman Date: Sun, 31 May 2026 21:09:58 +0200 Subject: [PATCH] docs: add BloomBase redesign implementation plan --- .../plans/2026-05-31-bloombase-redesign.md | 1613 +++++++++++++++++ 1 file changed, 1613 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-31-bloombase-redesign.md diff --git a/docs/superpowers/plans/2026-05-31-bloombase-redesign.md b/docs/superpowers/plans/2026-05-31-bloombase-redesign.md new file mode 100644 index 0000000..c1e4b91 --- /dev/null +++ b/docs/superpowers/plans/2026-05-31-bloombase-redesign.md @@ -0,0 +1,1613 @@ +# BloomBase UI Redesign — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace Bootstrap 5 with Tailwind CSS, add a 5-tab bottom navigation, extend `Location` with a cover photo field, restyle all existing screens, and add a new Area Detail page — producing the vibrant organic aesthetic from Naomi's mockup. + +**Architecture:** New `bloombase.css` holds fonts + CSS blob/flower decorations; `base.html` is fully rewritten with Tailwind CDN + bottom nav; all child templates are rewritten to use Tailwind utilities; one new view + template (`location_detail`) handles the Area Detail screen; `Location` model gains a nullable `cover_photo` ImageField with its own migration. + +**Tech Stack:** Django 5.x, HTMX 1.9, Tailwind CSS Play CDN (development), Google Fonts (Great Vibes + Nunito + Pacifico), SQLite + +--- + +## File Map + +| File | Action | +|---|---| +| `plants/static/plants/css/bloombase.css` | **Create** — fonts, blob shapes, canvas flowers | +| `plants/templates/plants/base.html` | **Rewrite** — Tailwind, bottom nav, decorations | +| `plants/models.py` | **Modify** — add `cover_photo` to `Location` | +| `plants/migrations/XXXX_location_cover_photo.py` | **Create** via `makemigrations` | +| `plants/forms.py` | **Modify** — add `LocationForm` with `cover_photo` | +| `plants/views/locations.py` | **Modify** — use `LocationForm`; add `location_detail` view | +| `plants/urls.py` | **Modify** — add `location_detail` URL | +| `plants/templates/plants/locations.html` | **Rewrite** — My Garden screen | +| `plants/templates/plants/location_detail.html` | **Create** — Area Detail screen | +| `plants/templates/plants/location_edit.html` | **Rewrite** — add cover photo field | +| `plants/templates/plants/dashboard.html` | **Rewrite** — camera-first Home screen | +| `plants/templates/plants/plant_list.html` | **Rewrite** — search + filter chips | +| `plants/templates/plants/plant_detail.html` | **Rewrite** — hero + care + CTA | +| `plants/templates/plants/identify_upload.html` | **Rewrite** — viewfinder camera UI | +| `plants/templates/plants/identify_confirm.html` | **Rewrite** — match list restyle | +| `plants/tests/test_location_redesign.py` | **Create** — model + view tests | + +--- + +## Task 1: Static CSS — bloombase.css + +Creates the custom stylesheet: Google Fonts import, CSS blob background shapes, and fixed-position SVG flower decorations. + +**Files:** +- Create: `plants/static/plants/css/bloombase.css` + +- [ ] **Step 1.1: Create the static directory and file** + +```bash +mkdir -p plants/static/plants/css +``` + +- [ ] **Step 1.2: Write bloombase.css** + +```css +/* plants/static/plants/css/bloombase.css */ + +@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800;900&family=Great+Vibes&family=Pacifico&display=swap'); + +/* ── Body canvas ── */ +body { + background-color: #FFF8F2; + font-family: 'Nunito', sans-serif; + min-height: 100vh; + position: relative; + overflow-x: hidden; +} + +/* ── Background blob shapes ── */ +.bb-blob-tl { + position: fixed; top: -100px; left: -80px; + width: 380px; height: 380px; + background: radial-gradient(ellipse at 45% 45%, #F9C090 0%, #F07040 55%, #E05020 100%); + border-radius: 62% 38% 54% 46% / 58% 44% 56% 42%; + opacity: 0.18; + pointer-events: none; z-index: 0; +} +.bb-blob-br { + position: fixed; bottom: -80px; right: -60px; + width: 340px; height: 340px; + background: radial-gradient(ellipse at 55% 55%, #FFD080 0%, #F8A040 60%, #F07040 100%); + border-radius: 44% 56% 38% 62% / 52% 48% 52% 48%; + opacity: 0.16; + pointer-events: none; z-index: 0; +} + +/* ── Canvas SVG flowers ── */ +.bb-flower { position: fixed; pointer-events: none; z-index: 0; } +.bb-flower-tr { top: 16px; right: 24px; } +.bb-flower-ml { top: 38%; left: -4px; } +.bb-flower-br { bottom: 72px; right: 48px; } +.bb-flower-bl { bottom: 88px; left: 32px; } + +/* ── Brand fonts ── */ +.font-script { font-family: 'Great Vibes', cursive; } +.font-pacifico { font-family: 'Pacifico', cursive; } +.font-nunito { font-family: 'Nunito', sans-serif; } + +/* ── Bottom nav stays above content ── */ +#bottom-nav { + position: fixed; bottom: 0; left: 0; right: 0; + background: #fff; + border-top: 1px solid #F0E8DF; + height: 62px; + display: flex; align-items: center; justify-content: space-around; + z-index: 50; + padding-bottom: env(safe-area-inset-bottom, 0); +} + +/* ── FAB inside nav ── */ +.bb-fab { + width: 44px; height: 44px; border-radius: 50%; + background: linear-gradient(145deg, #F58040, #E84020); + box-shadow: 0 4px 16px rgba(240, 80, 40, 0.5); + display: flex; align-items: center; justify-content: center; + color: #fff; font-size: 24px; line-height: 1; font-weight: 300; + flex-shrink: 0; +} + +/* ── Gradient CTA button ── */ +.btn-bloom { + display: block; width: 100%; + background: linear-gradient(135deg, #F58040, #E84020); + color: #fff; border: none; border-radius: 50px; + padding: 12px 24px; font-family: 'Nunito', sans-serif; + font-weight: 800; font-size: 15px; text-align: center; + box-shadow: 0 4px 14px rgba(240,80,40,0.4); cursor: pointer; + text-decoration: none; +} +.btn-bloom-outline { + display: block; width: 100%; + background: transparent; color: #1A1208; + border: 1.5px solid #E0D4C8; border-radius: 50px; + padding: 11px 24px; font-family: 'Nunito', sans-serif; + font-weight: 700; font-size: 15px; text-align: center; + cursor: pointer; text-decoration: none; +} + +/* ── White pill button (hero overlays) ── */ +.btn-pill { + background: rgba(255,255,255,0.88); + border: none; border-radius: 50px; + padding: 6px 14px; font-size: 13px; font-weight: 700; + color: #1A1208; backdrop-filter: blur(4px); cursor: pointer; + text-decoration: none; +} + +/* ── Confidence bar ── */ +.conf-track { height: 5px; background: #F0E4D8; border-radius: 3px; overflow: hidden; } +.conf-fill { height: 100%; background: linear-gradient(90deg, #F8A060, #E84020); border-radius: 3px; } + +/* ── Area thumbnail gradient placeholders ── */ +.at-placeholder-1 { background: linear-gradient(135deg, #88C860, #E85878, #A0D060); } +.at-placeholder-2 { background: linear-gradient(150deg, #B8E060, #F0A840, #E87830); } +.at-placeholder-3 { background: linear-gradient(120deg, #F8B840, #88C060, #F0C860); } +.at-placeholder-4 { background: linear-gradient(140deg, #D0A8E8, #68B858, #C888E0); } +.at-placeholder-5 { background: linear-gradient(130deg, #78D8C8, #E8F0A0, #60C8A8); } +.at-placeholder-6 { background: linear-gradient(125deg, #F8A0B0, #B8E060, #F09060); } + +/* ── Viewfinder corners ── */ +.vf-corner { position: absolute; width: 18px; height: 18px; border-color: rgba(255,255,255,0.9); border-style: solid; } +.vf-tl { top: 10px; left: 10px; border-width: 2.5px 0 0 2.5px; border-radius: 2px 0 0 0; } +.vf-tr { top: 10px; right: 10px; border-width: 2.5px 2.5px 0 0; border-radius: 0 2px 0 0; } +.vf-bl { bottom: 10px; left: 10px; border-width: 0 0 2.5px 2.5px; border-radius: 0 0 0 2px; } +.vf-br { bottom: 10px; right: 10px; border-width: 0 2.5px 2.5px 0; border-radius: 0 0 2px 0; } +``` + +- [ ] **Step 1.3: Commit** + +```bash +git add plants/static/plants/css/bloombase.css +git commit -m "feat: add bloombase.css — fonts, blobs, canvas flowers, shared components" +``` + +--- + +## Task 2: Rewrite base.html + +Full replacement of the Bootstrap top-nav shell with Tailwind CDN + 5-tab bottom nav + background canvas decorations. + +**Files:** +- Modify: `plants/templates/plants/base.html` + +- [ ] **Step 2.1: Replace base.html** + +```html +{# plants/templates/plants/base.html #} + + + + + + {% block title %}BloomBase{% endblock %} + {% load static %} + + + + + {% block extra_head %}{% endblock %} + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ {% block content %}{% endblock %} +
+ + + + + + +``` + +- [ ] **Step 2.2: Verify the app still loads (no crash)** + +```bash +cd /home/stephan/devel/python/plantdb +python manage.py check +``` + +Expected: `System check identified no issues (0 silenced).` + +- [ ] **Step 2.3: Commit** + +```bash +git add plants/templates/plants/base.html +git commit -m "feat: rewrite base.html — Tailwind CDN, 5-tab bottom nav, canvas decorations" +``` + +--- + +## Task 3: Location model — add cover_photo + migration + +**Files:** +- Modify: `plants/models.py` +- Create: migration (via `makemigrations`) +- Create: `plants/tests/test_location_redesign.py` + +- [ ] **Step 3.1: Write failing test first** + +```python +# plants/tests/test_location_redesign.py +import pytest +from django.core.files.uploadedfile import SimpleUploadedFile +from plants.models import Location + + +@pytest.mark.django_db +def test_location_cover_photo_field_exists(): + """Location can be saved with and without a cover_photo.""" + loc = Location.objects.create(name='Test Garden') + assert loc.cover_photo.name == '' or loc.cover_photo.name is None + + fake_img = SimpleUploadedFile('cover.jpg', b'fake-image-data', content_type='image/jpeg') + loc.cover_photo = fake_img + loc.save() + loc.refresh_from_db() + assert loc.cover_photo.name.startswith('locations/') +``` + +- [ ] **Step 3.2: Run test — expect failure** + +```bash +python -m pytest plants/tests/test_location_redesign.py::test_location_cover_photo_field_exists -v +``` + +Expected: `FAILED` — `AttributeError: 'Location' object has no attribute 'cover_photo'` + +- [ ] **Step 3.3: Add cover_photo to Location model** + +In `plants/models.py`, change: + +```python +class Location(models.Model): + name = models.CharField(max_length=200, unique=True) +``` + +to: + +```python +class Location(models.Model): + name = models.CharField(max_length=200, unique=True) + cover_photo = models.ImageField(upload_to='locations/', blank=True, null=True) +``` + +- [ ] **Step 3.4: Create migration** + +```bash +python manage.py makemigrations plants --name location_cover_photo +``` + +Expected output: `Migrations for 'plants': plants/migrations/XXXX_location_cover_photo.py` + +- [ ] **Step 3.5: Apply migration** + +```bash +python manage.py migrate +``` + +Expected: `Applying plants.XXXX_location_cover_photo... OK` + +- [ ] **Step 3.6: Run test — expect pass** + +```bash +python -m pytest plants/tests/test_location_redesign.py::test_location_cover_photo_field_exists -v +``` + +Expected: `PASSED` + +- [ ] **Step 3.7: Commit** + +```bash +git add plants/models.py plants/migrations/ plants/tests/test_location_redesign.py +git commit -m "feat: add Location.cover_photo ImageField + migration + test" +``` + +--- + +## Task 4: LocationForm + updated views + new location_detail + URL + +**Files:** +- Modify: `plants/forms.py` +- Modify: `plants/views/locations.py` +- Modify: `plants/urls.py` +- Modify: `plants/tests/test_location_redesign.py` + +- [ ] **Step 4.1: Write failing tests** + +Append to `plants/tests/test_location_redesign.py`: + +```python +from django.test import Client +from django.urls import reverse + + +@pytest.fixture +def client(): + return Client() + + +@pytest.mark.django_db +def test_location_detail_view_returns_200(client): + loc = Location.objects.create(name='My Terrace') + resp = client.get(reverse('location_detail', kwargs={'pk': loc.pk})) + assert resp.status_code == 200 + assert b'My Terrace' in resp.content + + +@pytest.mark.django_db +def test_location_edit_with_cover_photo(client): + loc = Location.objects.create(name='Balcony') + fake_img = SimpleUploadedFile('cover.jpg', b'data', content_type='image/jpeg') + resp = client.post( + reverse('location_edit', kwargs={'pk': loc.pk}), + {'name': 'Balcony Updated', 'cover_photo': fake_img}, + ) + assert resp.status_code == 302 + loc.refresh_from_db() + assert loc.name == 'Balcony Updated' + assert loc.cover_photo.name.startswith('locations/') +``` + +- [ ] **Step 4.2: Run tests — expect failure** + +```bash +python -m pytest plants/tests/test_location_redesign.py -v -k "detail or edit_with_cover" +``` + +Expected: `FAILED` — `NoReverseMatch: location_detail` and related errors. + +- [ ] **Step 4.3: Add LocationForm to forms.py** + +Append to `plants/forms.py`: + +```python +class LocationForm(forms.ModelForm): + class Meta: + model = Location + fields = ['name', 'cover_photo'] +``` + +- [ ] **Step 4.4: Rewrite views/locations.py** + +```python +# plants/views/locations.py +from django.shortcuts import render, get_object_or_404, redirect +from django.views.decorators.http import require_POST +from django.db.models import Count +from plants.models import Location, Plant +from plants.forms import LocationForm + + +def location_list(request): + if request.method == 'POST': + name = request.POST.get('name', '').strip() + if name: + Location.objects.get_or_create(name=name) + return redirect('location_list') + + locations = Location.objects.annotate(plant_count=Count('plants')) + return render(request, 'plants/locations.html', { + 'locations': locations, + 'active_tab': 'garden', + }) + + +def location_detail(request, pk): + location = get_object_or_404(Location, pk=pk) + plants = location.plants.select_related('species').prefetch_related('photos').all() + return render(request, 'plants/location_detail.html', { + 'location': location, + 'plants': plants, + 'active_tab': 'garden', + }) + + +def location_edit(request, pk): + location = get_object_or_404(Location, pk=pk) + if request.method == 'POST': + form = LocationForm(request.POST, request.FILES, instance=location) + if form.is_valid(): + form.save() + return redirect('location_list') + else: + form = LocationForm(instance=location) + return render(request, 'plants/location_edit.html', { + 'location': location, + 'form': form, + 'active_tab': 'garden', + }) + + +@require_POST +def location_delete(request, pk): + location = get_object_or_404(Location, pk=pk) + Plant.objects.filter(location=location).update(location=None) + location.delete() + return redirect('location_list') +``` + +- [ ] **Step 4.5: Add location_detail URL** + +In `plants/urls.py`, add after the existing `locations/` URL: + +```python +path('locations//', locations.location_detail, name='location_detail'), +``` + +The full relevant section of urlpatterns should look like: + +```python +path('locations/', locations.location_list, name='location_list'), +path('locations//', locations.location_detail, name='location_detail'), +path('locations//edit/', locations.location_edit, name='location_edit'), +path('locations//delete/', locations.location_delete, name='location_delete'), +``` + +- [ ] **Step 4.6: Run tests — expect pass** + +```bash +python -m pytest plants/tests/test_location_redesign.py -v +``` + +Expected: all `PASSED` + +- [ ] **Step 4.7: Commit** + +```bash +git add plants/forms.py plants/views/locations.py plants/urls.py plants/tests/test_location_redesign.py +git commit -m "feat: add LocationForm with cover_photo, location_detail view, update location_edit" +``` + +--- + +## Task 5: locations.html — My Garden screen + +**Files:** +- Modify: `plants/templates/plants/locations.html` + +- [ ] **Step 5.1: Rewrite locations.html** + +```html +{# plants/templates/plants/locations.html #} +{% extends "plants/base.html" %} +{% block title %}My Garden — BloomBase{% endblock %} +{% block content %} +{% load static %} + +
+ + +
+

My Garden

+ Edit +
+ + +
+ + + + Search plants or areas +
+ + +
+ {% for loc in locations %} + {% with idx=forloop.counter0 %} + + +
+ {% if loc.cover_photo %} + {{ loc.name }} + {% else %} +
+ {% endif %} +
+ +
+
{{ loc.name }}
+
+ {{ loc.plant_count }} plant{{ loc.plant_count|pluralize }} +
+
+ + +
+ {% endwith %} + {% empty %} +

+ No areas yet — add one below! +

+ {% endfor %} +
+ + +
+ {% csrf_token %} + + +
+ +
+{% endblock %} +``` + +- [ ] **Step 5.2: Verify page renders** + +```bash +python manage.py runserver 0.0.0.0:8000 +``` + +Open http://localhost:8000/locations/ — expect My Garden screen with area rows and + form. + +- [ ] **Step 5.3: Commit** + +```bash +git add plants/templates/plants/locations.html +git commit -m "feat: restyle locations.html as My Garden screen with cover photo thumbnails" +``` + +--- + +## Task 6: location_detail.html — Area Detail screen + +**Files:** +- Create: `plants/templates/plants/location_detail.html` + +- [ ] **Step 6.1: Create location_detail.html** + +```html +{# plants/templates/plants/location_detail.html #} +{% extends "plants/base.html" %} +{% block title %}{{ location.name }} — BloomBase{% endblock %} +{% block content %} + + +
+ {% if location.cover_photo %} + {{ location.name }} + {% else %} +
+ {% endif %} + +
+ + ← Back + + Edit +
+ +
+ + +

{{ location.name }}

+

+ {{ plants.count }} plant{{ plants.count|pluralize }} +

+ + + {% if plants %} + + {% else %} + +
+
🪴
+

No plants here yet.

+ Identify a plant +
+ {% endif %} + +
+{% endblock %} +``` + +- [ ] **Step 6.2: Verify page renders** + +Open http://localhost:8000/locations/1/ (replace 1 with a real pk) — expect hero + plant grid. + +- [ ] **Step 6.3: Commit** + +```bash +git add plants/templates/plants/location_detail.html +git commit -m "feat: add location_detail.html — area detail with cover photo header + plant grid" +``` + +--- + +## Task 7: location_edit.html — add cover photo upload + +**Files:** +- Modify: `plants/templates/plants/location_edit.html` + +- [ ] **Step 7.1: Rewrite location_edit.html** + +```html +{# plants/templates/plants/location_edit.html #} +{% extends "plants/base.html" %} +{% block title %}Edit Area — BloomBase{% endblock %} +{% block content %} + +
+ +
+ +

Edit area

+
+ +
+ {% csrf_token %} + + +
+ + +
+ + +
+ + {% if location.cover_photo %} +
+ Current cover +
+ {% endif %} + + +
+ + +
+ + Cancel +
+
+ +
+{% endblock %} +``` + +- [ ] **Step 7.2: Verify form submits with photo** + +Open http://localhost:8000/locations/1/edit/ — fill name, attach image, save. Confirm redirect to /locations/ with updated thumbnail. + +- [ ] **Step 7.3: Commit** + +```bash +git add plants/templates/plants/location_edit.html +git commit -m "feat: restyle location_edit.html with cover photo upload field" +``` + +--- + +## Task 8: dashboard.html — camera-first Home screen + +Also update `dashboard` view to pass `active_tab`. + +**Files:** +- Modify: `plants/views/dashboard.py` +- Modify: `plants/templates/plants/dashboard.html` + +- [ ] **Step 8.1: Update dashboard view to pass active_tab and location count** + +```python +# plants/views/dashboard.py +from datetime import date +from django.shortcuts import render +from plants.models import Plant, Location + + +def dashboard(request): + today = date.today() + all_plants = list(Plant.objects.select_related('species').all()) + blooming_now = [p for p in all_plants if today.month in (p.bloom_months or [])] + latest_plants = Plant.objects.select_related('species', 'location').prefetch_related('photos').order_by('-pk')[:6] + location_count = Location.objects.count() + + return render(request, 'plants/dashboard.html', { + 'total_plants': len(all_plants), + 'blooming_now': blooming_now, + 'blooming_now_count': len(blooming_now), + 'today': today, + 'latest_plants': latest_plants, + 'location_count': location_count, + 'active_tab': 'home', + }) +``` + +- [ ] **Step 8.2: Rewrite dashboard.html** + +```html +{# plants/templates/plants/dashboard.html #} +{% extends "plants/base.html" %} +{% load plant_extras %} +{% block title %}BloomBase{% endblock %} +{% block content %} + + +
+ + +
+ +
+ + +

Hello, Plant Lover! 🌿

+

+ What are we growing today? +

+

+ Identify a plant and add it to Bloombase. +

+ + + + +
+ +
+
+
+
+ +
+
+ + + +
+
+
+
+ + + + 🪴 +
+
My Garden
+
+ {{ total_plants }} Plant{{ total_plants|pluralize }} · {{ location_count }} Area{{ location_count|pluralize }} +
+
+ +
+ + + {% if latest_plants %} +

🌿 Recently added

+ + {% endif %} + +
+{% endblock %} +``` + +- [ ] **Step 8.3: Verify home screen** + +Open http://localhost:8000/ — expect camera viewfinder + My Garden pill + recent plants. + +- [ ] **Step 8.4: Commit** + +```bash +git add plants/views/dashboard.py plants/templates/plants/dashboard.html +git commit -m "feat: restyle dashboard as camera-first Home screen" +``` + +--- + +## Task 9: plant_list.html — search + location filter chips + +Also update the `plant_list` view to pass `active_tab`. + +**Files:** +- Modify: `plants/views/plants.py` (just add `active_tab` to plant_list context) +- Modify: `plants/templates/plants/plant_list.html` + +- [ ] **Step 9.1: Add active_tab to plant_list view** + +In `plants/views/plants.py`, find the `plant_list` view and add `'active_tab': 'garden'` to the context dict it passes to `render`. (Check the current return statement and add the key alongside existing context keys.) + +- [ ] **Step 9.2: Rewrite plant_list.html** + +```html +{# plants/templates/plants/plant_list.html #} +{% extends "plants/base.html" %} +{% block title %}Plants — BloomBase{% endblock %} +{% block content %} + +
+ + +
+

All Plants

+ + +
+ + +
+ + + + +
+ + +
+ + All + + {% for loc in locations %} + + {{ loc.name }} + + {% endfor %} +
+ + +
+ {% include "plants/partials/plant_list_results.html" %} +
+ +
+{% endblock %} +``` + +- [ ] **Step 9.3: Update plant_list_results partial** + +Open `plants/templates/plants/partials/plant_list_results.html` and check its current content. Update it to use Tailwind card style: + +```html +{# plants/templates/plants/partials/plant_list_results.html #} +{% for plant in plants %} + + {% if plant.species and plant.species.api_image_url %} + + {% elif plant.thumbnail_url %} + + {% else %} +
🌿
+ {% endif %} +
+
{{ plant.name }}
+ {% if plant.location %} + + {{ plant.location.name }} + + {% endif %} +
+ +
+{% empty %} +

No plants found.

+{% endfor %} +``` + +- [ ] **Step 9.4: Verify plant list** + +Open http://localhost:8000/plants/ — expect search + chips + card list. + +- [ ] **Step 9.5: Commit** + +```bash +git add plants/views/plants.py plants/templates/plants/plant_list.html plants/templates/plants/partials/plant_list_results.html +git commit -m "feat: restyle plant_list with Tailwind — search, filter chips, plant cards" +``` + +--- + +## Task 10: plant_detail.html — hero + care + CTA + +Also add `active_tab` to the `plant_detail` view. + +**Files:** +- Modify: `plants/views/plants.py` (add `active_tab` to plant_detail context) +- Modify: `plants/templates/plants/plant_detail.html` + +- [ ] **Step 10.1: Add active_tab to plant_detail view context** + +In `plants/views/plants.py`, find the `plant_detail` view and add `'active_tab': 'garden'` to the context. + +- [ ] **Step 10.2: Rewrite plant_detail.html** + +```html +{# plants/templates/plants/plant_detail.html #} +{% extends "plants/base.html" %} +{% load plant_extras %} +{% block title %}{{ plant.name }} — BloomBase{% endblock %} +{% block content %} + + +
+ {% if plant.species and plant.species.api_image_url %} + {{ plant.name }} + {% elif plant.thumbnail_url %} + {{ plant.name }} + {% else %} +
🌿
+ {% endif %} +
+ + ← Back + + Edit + + +
+ +
+ + +

{{ plant.name }}

+ {% if plant.species %} +
+ {{ plant.species.scientific_name }} + + + +
+ {% endif %} + + {% if plant.location %} +

+ 📍 {{ plant.location.name }} +

+ {% endif %} + + + {% if plant.watering or plant.sunlight or plant.species %} +
+ {% if plant.watering %} +
+ 💧 + {{ plant.get_watering_display }} +
+ {% elif plant.species and plant.species.watering %} +
+ 💧 + {{ plant.species.watering }} +
+ {% endif %} + {% if plant.sunlight %} +
+ ☀️ + {{ plant.get_sunlight_display }} +
+ {% elif plant.species and plant.species.sunlight %} +
+ ☀️ + {{ plant.species.sunlight }} +
+ {% endif %} + {% if plant.species and plant.species.frost_hardiness_c is not None %} +
+ 🌡️ + {{ plant.species.frost_hardiness_c }}°C min +
+ {% endif %} + {% if plant.species and plant.species.max_height_cm %} +
+ 📏 + {{ plant.species.max_height_cm }} cm +
+ {% endif %} +
+ {% endif %} + + + {% if plant.bloom_months %} +
+

🌸 Bloom months

+ {% month_strip plant.bloom_months %} +
+ {% endif %} + + + {% if plant.notes %} +
+

{{ plant.notes|linebreaksbr }}

+
+ {% endif %} + + + {% if plant.pruning_months %} + {% include "plants/partials/pruning_strip.html" %} + {% endif %} + + + + + + + +
+{% endblock %} +``` + +- [ ] **Step 10.3: Verify plant detail** + +Open http://localhost:8000/plants/1/ — expect hero + care row + CTA buttons. + +- [ ] **Step 10.4: Commit** + +```bash +git add plants/views/plants.py plants/templates/plants/plant_detail.html +git commit -m "feat: restyle plant_detail — hero, care row, gradient CTA buttons" +``` + +--- + +## Task 11: identify_upload.html — viewfinder camera UI + +Also add `active_tab` to `identify_upload` view. + +**Files:** +- Modify: `plants/views/identify.py` (add `active_tab`) +- Modify: `plants/templates/plants/identify_upload.html` + +- [ ] **Step 11.1: Add active_tab to identify_upload view** + +In `plants/views/identify.py`, find the `identify_upload` view. Add `'active_tab': 'home'` to the context dict passed to render (for the GET response). + +- [ ] **Step 11.2: Rewrite identify_upload.html** + +```html +{# plants/templates/plants/identify_upload.html #} +{% extends "plants/base.html" %} +{% block title %}Identify plant — BloomBase{% endblock %} +{% block content %} + +
+ + +
+ +

Identify plant

+
+ +

+ Add one or more photos, tag what each shows, then tap Identify. +

+ + {% if error %} +
+ {{ error }} +
+ {% endif %} + +
+ {% csrf_token %} + + + +
+
+
+
+
+
+ +
+ + +
+
+ + +
+ + +
+ +
+ + +
+ + + + + + + +
+ + +{% endblock %} +``` + +- [ ] **Step 11.3: Verify identify screen** + +Open http://localhost:8000/identify/ — expect viewfinder box + shutter button + organ tags. + +- [ ] **Step 11.4: Commit** + +```bash +git add plants/views/identify.py plants/templates/plants/identify_upload.html +git commit -m "feat: restyle identify_upload as camera-first viewfinder screen" +``` + +--- + +## Task 12: identify_confirm.html — match list restyle + +**Files:** +- Modify: `plants/templates/plants/identify_confirm.html` + +- [ ] **Step 12.1: Rewrite identify_confirm.html** + +```html +{# plants/templates/plants/identify_confirm.html #} +{% extends "plants/base.html" %} +{% block title %}Confirm species — BloomBase{% endblock %} +{% block content %} + +
+ +
+ +

Is this your plant?

+
+

+ Tap the correct species, then confirm. +

+ +
+ {% csrf_token %} + + +
+ {% for m in matches %} +
+ {% if m.image_url %} + {{ m.scientific_name }} + {% else %} +
🌿
+ {% endif %} +
+
{{ m.scientific_name }}
+ {% if m.common_names %} +
+ {{ m.common_names|join:", " }} +
+ {% endif %} +
{{ m.family }}
+
+ + {% widthratio m.score 1 100 %}% + +
+ {% endfor %} +
+ + +
+ +
+ + +{% endblock %} +``` + +- [ ] **Step 12.2: Verify identify confirm** + +Run a real identification or view the confirm page directly — expect match cards with confidence badges and gradient confirm button. + +- [ ] **Step 12.3: Commit** + +```bash +git add plants/templates/plants/identify_confirm.html +git commit -m "feat: restyle identify_confirm — match cards with confidence badges" +``` + +--- + +## Self-Review + +**Spec coverage check:** + +| Spec item | Task | +|---|---| +| Replace Bootstrap 5 with Tailwind CSS | Task 2 (base.html) | +| 5-tab bottom nav (Home, Garden, +, Care, Profile) | Task 2 | +| `Location.cover_photo` + migration | Task 3 | +| `LocationForm` with cover_photo | Task 4 | +| `location_detail` view + URL | Task 4 | +| locations.html My Garden screen | Task 5 | +| location_detail.html Area Detail | Task 6 | +| location_edit.html with cover photo | Task 7 | +| dashboard.html camera-first | Task 8 | +| plant_list.html | Task 9 | +| plant_detail.html | Task 10 | +| identify_upload.html | Task 11 | +| identify_confirm.html | Task 12 | +| Canvas blob decorations + SVG flowers | Task 1 + Task 2 | +| Great Vibes + Nunito + Pacifico fonts | Task 1 | +| Gradient CTA buttons | Task 1 (.btn-bloom) | +| `active_tab` nav active state | Tasks 4, 8, 9, 10, 11 | +| Care + Profile stubs | Task 2 (href="#") | + +**Gaps identified and resolved:** +- `plant_list_results.html` partial was missing — added in Task 9.3 +- `plant_list` and `plant_detail` views need `active_tab` in context — noted in Tasks 9 and 10 +- `identify_upload` view needs `active_tab` — noted in Task 11 + +**Type/name consistency:** +- `active_tab` values: `'home'` (dashboard, identify), `'garden'` (locations, plant_list, plant_detail, location_detail, location_edit) +- CSS class names used consistently: `.btn-bloom`, `.btn-bloom-outline`, `.btn-pill`, `.bb-fab`, `.vf-corner .vf-tl/tr/bl/br` +- `at-placeholder-N` classes 1–6 defined in bloombase.css and used in locations.html + location_detail.html + +**No placeholders:** All template code is complete. All CSS is complete.