move stuff to docs
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
# Game & Purchase Status Definitions
|
||||
|
||||
## Game Statuses
|
||||
|
||||
Games have a `status` field with the following values:
|
||||
|
||||
| Status | Code | Description |
|
||||
|--------|------|-------------|
|
||||
| **Unplayed** | `u` | Game was purchased but never played |
|
||||
| **Played** | `p` | Game was played but not yet finished |
|
||||
| **Finished** | `f` | Game has been completed |
|
||||
| **Retired** | `r` | Game was intentionally retired (e.g., no longer accessible, collector's item) |
|
||||
| **Abandoned** | `a` | Game was played but the user gave up on it |
|
||||
|
||||
**Setting game status:**
|
||||
- Users explicitly set game status via the UI (finish/drop purchase buttons, status change form)
|
||||
- Status changes are tracked in `GameStatusChange` model
|
||||
- Refunding a purchase always marks its games as abandoned
|
||||
|
||||
---
|
||||
|
||||
## Purchase-Level Status Concepts
|
||||
|
||||
These concepts determine whether a purchase appears in the "unfinished" or "dropped" lists in stats views.
|
||||
|
||||
### Finished
|
||||
|
||||
A purchase is considered **finished** when:
|
||||
|
||||
```
|
||||
Game.status == "f" OR Purchase.games.* has a PlayEvent with an ended date
|
||||
```
|
||||
|
||||
Either signal indicates the game is complete:
|
||||
- **Explicit**: User marked the game as finished (`Game.status = "f"`)
|
||||
- **Implicit**: A PlayEvent exists with `ended` date set (data-driven)
|
||||
|
||||
This uses **OR** logic during a transition period. Later, these signals should be kept in sync so only one source of truth is needed.
|
||||
|
||||
### Dropped
|
||||
|
||||
A purchase is considered **dropped** when:
|
||||
|
||||
```
|
||||
Game.status == "a" OR Purchase.date_refunded IS NOT NULL
|
||||
```
|
||||
|
||||
Either signal indicates the user no longer has an active interest in the game:
|
||||
- **Explicit**: User marked the game as abandoned (`Game.status = "a"`)
|
||||
- **Implicit**: User refunded the purchase (which automatically sets games to abandoned)
|
||||
|
||||
Note: Refunding a purchase always marks its games as abandoned. There is no option to refund without abandoning.
|
||||
|
||||
---
|
||||
|
||||
## Unfinished vs. Dropped
|
||||
|
||||
The stats views categorize purchases into **unfinished** and **dropped** lists.
|
||||
|
||||
### Unfinished
|
||||
|
||||
A purchase is **unfinished** when:
|
||||
1. It was purchased in the relevant time period (this year for yearly stats, all time for all-time stats)
|
||||
2. It was NOT refunded (only counts toward unfinished/backlog)
|
||||
3. It is NOT finished (per the finished definition above)
|
||||
4. It is NOT dropped (per the dropped definition above)
|
||||
5. It is NOT infinite (subscription, etc.)
|
||||
6. It IS a game or DLC (not season passes or battle passes)
|
||||
|
||||
**Unfinished = Active backlog** — games the user may still play.
|
||||
|
||||
### Dropped
|
||||
|
||||
A purchase is **dropped** when:
|
||||
1. It was purchased in the relevant time period
|
||||
2. It is NOT finished (per the finished definition above)
|
||||
3. It matches at least one dropped signal (per the dropped definition above)
|
||||
4. It is NOT infinite
|
||||
5. It IS a game or DLC
|
||||
|
||||
**Dropped = Terminal state** — games the user has given up on or refunded.
|
||||
|
||||
### Summary Table
|
||||
|
||||
| Category | Includes Refunded? | Key Condition |
|
||||
|----------|-------------------|---------------|
|
||||
| **Unfinished** | No | NOT finished, NOT dropped |
|
||||
| **Dropped** | Yes | Finished OR Abandoned/Retired |
|
||||
| **Refunded** | Yes | `date_refunded IS NOT NULL` |
|
||||
| **Infinite** | Yes | `infinite = True` |
|
||||
|
||||
---
|
||||
|
||||
## Query Patterns
|
||||
|
||||
### Checking if a game is finished
|
||||
|
||||
```python
|
||||
game.finished() # Returns True if status="f" or has PlayEvent with ended date
|
||||
```
|
||||
|
||||
### Checking if a game is abandoned
|
||||
|
||||
```python
|
||||
game.abandoned() # Returns True if status="a"
|
||||
```
|
||||
|
||||
### Getting finished purchases
|
||||
|
||||
```python
|
||||
Purchase.objects.finished() # All purchases where games are finished
|
||||
```
|
||||
|
||||
### Getting dropped purchases
|
||||
|
||||
```python
|
||||
Purchase.objects.dropped() # All purchases that are abandoned or refunded
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Transition State
|
||||
|
||||
The system uses **OR logic** for both finished and dropped to catch any mismatch between explicit user actions and data signals:
|
||||
|
||||
- **Finished**: `status="f" OR PlayEvent.ended`
|
||||
- **Dropped**: `status="a" OR date_refunded`
|
||||
|
||||
This bridges the gap between the old model (where `date_finished` and `date_dropped` were on the Purchase model) and the new model (where `Game.status` and `PlayEvent` are the sources of truth).
|
||||
|
||||
**Future:** These signals should be kept in sync. For example:
|
||||
- Setting `Game.status = "f"` should create a PlayEvent with `ended` date
|
||||
- When the sync is reliable, the OR can be simplified to a single check
|
||||
|
||||
Note: Refunding a purchase always automatically sets its games' status to Abandoned. This is not optional — there is no way to refund without abandoning.
|
||||
|
||||
---
|
||||
|
||||
## Edge Cases
|
||||
|
||||
### Unplayed games
|
||||
- Unplayed games (`status="u"`) are considered **unfinished**, not dropped
|
||||
- They appear in the unfinished/backlog list since they are still games the user may play
|
||||
- Unplayed games that are refunded DO count as **dropped** (refund signal overrides)
|
||||
|
||||
### Multiple games per purchase
|
||||
- A purchase can have multiple games via `Purchase.games` (many-to-many)
|
||||
- A purchase is finished if ANY of its games is finished
|
||||
- A purchase is dropped if ANY of its games is abandoned OR the purchase itself is refunded
|
||||
|
||||
### PlayEvents without ended date
|
||||
- A PlayEvent with `started` but no `ended` does NOT count as finished
|
||||
- This represents a game that was started but not completed
|
||||
|
||||
### Retired games
|
||||
- Retired games (`status="r"`) are considered **dropped**
|
||||
- Retirement is for games the user intentionally removed from their collection (collector's items, no longer accessible, etc.)
|
||||
@@ -0,0 +1,398 @@
|
||||
# Form Overhaul Plan
|
||||
|
||||
> Last updated: 2026-05-12
|
||||
> Status: Decided — awaiting implementation
|
||||
>
|
||||
> **Decisions made:**
|
||||
> - All forms (simple and complex) get section headers for consistency
|
||||
> - Two-column layout uses **flexbox** (auto-reflow on different screen sizes)
|
||||
> - `cotton/layouts/add.html` enhanced with **Option A**: `c-section` component slots
|
||||
> - `add_purchase.html` dual-submit **simplified** — remove `<tr><td>`, use same `c-button` pattern as `add_game.html`
|
||||
> - GameStatusChange delete confirmation **converted to modal** (via HTMX trigger)
|
||||
|
||||
## Goal
|
||||
|
||||
Modernize all forms and form-like elements to align with Flowbite design, improve visual consistency, and adopt responsive multi-column layouts for complex forms.
|
||||
|
||||
---
|
||||
|
||||
## Current State Analysis
|
||||
|
||||
### Form Pages (add/edit)
|
||||
|
||||
All use `cotton/layouts/add.html` — single column, `max-w-xl`, `form.as_div`:
|
||||
|
||||
| Page | Form | Fields | Complexity |
|
||||
|---|---|---|---|
|
||||
| Game | `GameForm` | 7 fields: name, sort_name, platform, year, year_orig, status, mastered, wikidata | Medium |
|
||||
| Purchase | `PurchaseForm` | 11 fields: games, platform, dates, price, currency, type, ownership, related, infinite, name | High |
|
||||
| Session | `SessionForm` | 8 fields: game, timestamps, duration, emulated, device, note, checkbox (custom rendering) | High |
|
||||
| Platform | `PlatformForm` | 3 fields: name, icon, group | Low |
|
||||
| Device | `DeviceForm` | 2 fields: name, type | Low |
|
||||
| PlayEvent | `PlayEventForm` | 5 fields: game, dates, note, checkbox | Low |
|
||||
| GameStatusChange | `GameStatusChangeForm` | 4 fields | Low |
|
||||
|
||||
### Other Form-Like Elements
|
||||
|
||||
| Element | Template | Notes |
|
||||
|---|---|---|
|
||||
| Login | `registration/login.html` | Flowbite card, already good |
|
||||
| Search | `cotton/search_field.html` | Reusable, already good |
|
||||
| Delete Game | `partials/delete_game_confirmation.html` | Inline modal, inconsistent button layout |
|
||||
| Delete PlayEvent | `gamestatuschange_confirm_delete.html` | Full-page form, no modal |
|
||||
| Refund Purchase | `partials/refund_purchase_confirmation.html` | Inline modal, inconsistent button layout |
|
||||
| Stats Year Select | `stats.html` | Manual `<select>`, no Flowbite styling |
|
||||
| Status Selector | `partials/gamestatus_selector.html` | Alpine.js dropdown, old Tailwind classes |
|
||||
| Device Selector | `partials/sessiondevice_selector.html` | Alpine.js dropdown, old Tailwind classes |
|
||||
|
||||
---
|
||||
|
||||
## Issues to Fix
|
||||
|
||||
### P0: Broken/Inconsistent
|
||||
|
||||
1. ~~**`modal.html` has a missing `<form>` tag** (line 13: `</form>` with no opening `<form>`)** — *Resolved: rewritten as proper component with form wrapping support, body + footer slots, reusable `close_button` component. Ready for standardizing all inline modals later.*
|
||||
2. **Delete confirmations are inconsistent** — three different patterns (inline modal, full-page form, inline modal)
|
||||
3. **`.errorlist` CSS** has fixed `width: 300px` — too narrow, breaks on mobile. *No scoping needed: Django auto-applies `.errorlist` to form error output only, never used explicitly in templates.*
|
||||
4. **`add_purchase.html` has `<tr><td>`** in a `c-slot` that renders inside a `<div>` — semantic mismatch. **Decision: simplify dual-submit** to match `add_game.html` pattern (use `<c-button>` only).
|
||||
5. **`#button-container` and `.basic-button` in `input.css`** — legacy patterns, unused or dead code
|
||||
|
||||
### P1: Layout & UX
|
||||
|
||||
6. **All add/edit forms are single-column** — PurchaseForm (11 fields) and GameForm (7 fields) would benefit from multi-column
|
||||
7. **No field grouping** — related fields listed flat without visual hierarchy
|
||||
8. **Stats year `<select>`** has no Flowbite styling
|
||||
9. **Search field** is not wrapped in `<form method="get">` — no native clear-on-Enter behavior
|
||||
|
||||
### P2: Styling Consistency
|
||||
|
||||
10. **Status/device selectors** use old Tailwind v3 patterns (`rounded-sm`, `shadow-2xs`, `border-gray-200` without explicit color)
|
||||
11. **`navbar.html` buttons** use `rounded-sm` instead of `rounded-base`
|
||||
12. **`simple_table.html` pagination buttons** use `rounded-s-lg`/`rounded-e-lg` — could be simplified
|
||||
|
||||
---
|
||||
|
||||
## Proposed Improvements
|
||||
|
||||
### 1. Two-Column Layout for Complex Forms (Flexbox)
|
||||
|
||||
**Scope**: `GameForm`, `PurchaseForm`, `PlayEventForm`, `SessionForm`
|
||||
|
||||
Use **flexbox** with wrap behavior so fields auto-reflow on different screen sizes. No fixed column count — fields sit side-by-side on `md:`+ and wrap naturally on smaller screens.
|
||||
|
||||
#### GameForm Layout
|
||||
```
|
||||
┌──────────────────────────────────┐
|
||||
│ Game Details │
|
||||
│ ┌──────────────────┬───────────┐ │
|
||||
│ │ Name │ Platform │ │
|
||||
│ │ Sort Name │ Year │ │
|
||||
│ │ Original Year │ Wikidata │ │
|
||||
│ └──────────────────┴───────────┘ │
|
||||
│ Status │
|
||||
│ ┌──────────────────┬───────────┐ │
|
||||
│ │ Status │ Mastered │ │
|
||||
│ └──────────────────┴───────────┘ │
|
||||
│ [Submit] │
|
||||
└──────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### PurchaseForm Layout (simplified)
|
||||
```
|
||||
┌──────────────────────────────────────────┐
|
||||
│ Purchase Details │
|
||||
│ ┌──────────────────────┬───────────────┐ │
|
||||
│ │ Games (multi-select) │ Platform │ │
|
||||
│ │ Type │ Ownership │ │
|
||||
│ │ Name │ Related Purch │ │
|
||||
│ └──────────────────────┴───────────────┘ │
|
||||
│ Dates │ Price │
|
||||
│ ┌───────────────┬──────┴───────────────┐ │
|
||||
│ │ Date Purch │ Price Curr │ │
|
||||
│ │ Date Refund │ Infinite [ ] │ │
|
||||
│ └───────────────┴──────────────────────┘ │
|
||||
│ [Submit] [Submit + Session] │
|
||||
└──────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Implementation**: `c-section` component accepts `columns="2"` (or `"3"`) which applies `flex flex-wrap gap-4 [&>div]:w-[calc(50%-0.5rem)]` on md+ screens. Each field wraps in a `<div>` inside the section slot.
|
||||
|
||||
**Decision**: Dual-submit in `add_purchase.html` simplified — remove `<tr><td>`, use same `<c-button>` pattern as `add_game.html`.
|
||||
|
||||
### 2. Field Grouping with Card Sections
|
||||
|
||||
**Decision**: ALL forms get section headers for consistency (not just complex forms).
|
||||
|
||||
Group related fields with section headings and subtle borders/backgrounds:
|
||||
|
||||
```html
|
||||
<c-section title="Game Details" columns="2">
|
||||
{{ form.name }}
|
||||
{{ form.platform }}
|
||||
{{ form.sort_name }}
|
||||
{{ form.year_released }}
|
||||
</c-section>
|
||||
```
|
||||
|
||||
Each section renders as:
|
||||
```html
|
||||
<fieldset class="form-section p-5 border-t border-default-medium bg-neutral-primary-soft/30 first-of-type:border-t-0 first-of-type:pt-0">
|
||||
<h3 class="text-sm font-medium text-heading uppercase mb-4">Section Title</h3>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<!-- fields in <div> wrappers, each taking calc(50% - 0.5rem) on md+ -->
|
||||
</div>
|
||||
</fieldset>
|
||||
```
|
||||
|
||||
Each section gets:
|
||||
- Subtle background (`bg-neutral-primary-soft/30`)
|
||||
- Top border with spacing (`border-t border-default-medium`)
|
||||
- Section heading (`text-sm font-medium text-heading uppercase mb-4`)
|
||||
- Flexbox gap for responsive field reflow
|
||||
|
||||
### 1b. `c-section` Component Specification
|
||||
|
||||
New cotton component for the `cotton/` directory:
|
||||
|
||||
```python
|
||||
# games/templates/cotton/section.py (or inline in components.py)
|
||||
from common.components import Div
|
||||
|
||||
def Section(title: str = "", columns: str = "1", children: str = "") -> SafeText:
|
||||
"""Renders a form field section with optional multi-column flexbox layout.
|
||||
|
||||
Args:
|
||||
title: Section heading (renders as uppercase label)
|
||||
columns: "1" (default), "2", or "3" — target column count on md+ screens
|
||||
children: Field markup (each field wrapped in <div> for flex wrapping)
|
||||
"""
|
||||
col_class = {
|
||||
"1": "flex flex-col",
|
||||
"2": "flex flex-wrap gap-4 [&>div]:w-[calc(50%-0.5rem)]",
|
||||
"3": "flex flex-wrap gap-4 [&>div]:w-[calc(33.333%-0.67rem)]",
|
||||
}.get(columns, "flex flex-col")
|
||||
|
||||
return Div(
|
||||
cls=f"form-section p-5 border-t border-default-medium bg-neutral-primary-soft/30 first-of-type:border-t-0 first-of-type:pt-0",
|
||||
children=f"""
|
||||
<h3 class="text-sm font-medium text-heading uppercase mb-4">{title}</h3>
|
||||
<div class="{col_class}">{children}</div>
|
||||
"""
|
||||
)
|
||||
```
|
||||
|
||||
**Template usage:**
|
||||
```django
|
||||
{# add_game.html #}
|
||||
<c-layouts.add title="New Game">
|
||||
<c-section title="Game Details" columns="2">
|
||||
<div>{{ form.name }}</div>
|
||||
<div>{{ form.platform }}</div>
|
||||
<div>{{ form.sort_name }}</div>
|
||||
<div>{{ form.year_released }}</div>
|
||||
<div>{{ form.original_year_released }}</div>
|
||||
<div>{{ form.wikidata }}</div>
|
||||
</c-section>
|
||||
<c-section title="Status" columns="2">
|
||||
<div>{{ form.status }}</div>
|
||||
<div>{{ form.mastered }}</div>
|
||||
</c-section>
|
||||
</c-layouts.add>
|
||||
```
|
||||
|
||||
**`cotton/layouts/add.html` changes:**
|
||||
- Remove hardcoded `{{ form.as_div }}` rendering
|
||||
- Accept optional `sections` variable (list of rendered `c-section` output)
|
||||
- If `sections` provided, render them; otherwise fall back to `{{ form.as_div }}` for simple forms
|
||||
- Keep `additional_row` slot for dual-submit buttons
|
||||
|
||||
### 3. CSS/Style Fixes
|
||||
|
||||
#### `input.css` changes:
|
||||
```css
|
||||
/* Update errorlist */
|
||||
.errorlist {
|
||||
@apply mt-4 mb-1 pl-3 py-2 bg-red-600 text-slate-200 w-full max-w-xl; /* was w-[300px] */
|
||||
}
|
||||
|
||||
/* Remove: #button-container, .basic-button — unused legacy */
|
||||
|
||||
/* Remove: .flowbite-input — custom class is code smell with Tailwind */
|
||||
/* Remove: flowbite-input @apply block (line 229-234) */
|
||||
|
||||
/* Add Flowbite styling for select in stats */
|
||||
#yearSelect {
|
||||
@apply bg-neutral-secondary-medium border border-default-medium text-heading text-sm rounded-base focus:ring-brand focus:border-brand;
|
||||
}
|
||||
```
|
||||
|
||||
**Important**: The styling previously provided by `.flowbite-input` must be preserved. The element-level `@apply` rules for `input`, `select`, and `textarea` in `input.css` (lines 209-219) already provide equivalent styling. These rules automatically apply to all form inputs without needing custom classes:
|
||||
- `input:not([type="checkbox"])` — background, border, text, radius, focus ring, padding
|
||||
- `select` — same base styling as inputs
|
||||
- `textarea` — same base styling with adjusted padding
|
||||
|
||||
**Files to clean up:**
|
||||
- `common/input.css`: Remove `.flowbite-input` class entirely (lines 229-234)
|
||||
- `games/forms.py`: Remove `flowbite_input_widget` and `flowbite_password_widget` (lines 22-23)
|
||||
- `games/forms.py`: Remove `widget=` from `LoginForm` fields (lines 28, 32) — login template uses explicit Tailwind classes already
|
||||
|
||||
#### Rewrite `modal.html`:
|
||||
- Remove stray `</form>` tag and restructure as a proper cotton component
|
||||
- New `c-modal` component with: `modal_id`, `title`, `size="xl"`, `backdrop_close` variables
|
||||
- `{{ slot }}` (cotton default slot) for body content — passed as children of `<c-modal>`, no block tags needed
|
||||
- `{{ footer }}` (optional named slot via `<c-slot name="footer">`) for non-form buttons
|
||||
- Reusable `cotton/close_button.html` via `<c-close-button />`
|
||||
- Size mapping via inline `{% if %}`: `{% if size == 'sm' %}max-w-sm{% elif size == 'lg' %}max-w-lg{% else %}max-w-xl{% endif %}`
|
||||
- Horizontal centering: `mx-auto` on inner container (matching old modal pattern)
|
||||
- Click-to-dismiss backdrop with `event.stopPropagation()` on inner container
|
||||
- Flowbite-style styling: `rounded-lg shadow`, `bg-white dark:bg-gray-800`, `sm:p-5`
|
||||
|
||||
### 4. Unify Delete Confirmations (All Modal)
|
||||
|
||||
**Decision**: GameStatusChange delete confirmation converted from full-page to modal. All three use the same modal pattern.
|
||||
|
||||
**Target**: All confirmation modals use the same pattern:
|
||||
|
||||
```html
|
||||
<div class="fixed inset-0 bg-black/70 dark:bg-gray-600/50 ...">
|
||||
<div class="relative mx-auto p-6 bg-white dark:bg-gray-900 rounded-lg shadow-lg max-w-md w-full">
|
||||
<h2 class="text-xl font-medium text-center">Confirm Action</h2>
|
||||
<p class="text-center mt-4 text-sm text-body">Are you sure...?</p>
|
||||
{% if details %}
|
||||
<ul class="text-center mt-2 text-sm text-body list-disc list-inside">
|
||||
<li>{{ detail }}</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
<p class="text-center mt-3 text-sm font-medium text-red-600">This action cannot be undone.</p>
|
||||
<div class="flex gap-3 mt-6">
|
||||
<c-button color="red" class="w-full" type="submit">Delete</c-button>
|
||||
<c-button color="gray" class="w-full">Cancel</c-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
- **Delete Game** (`partials/delete_game_confirmation.html`): Update template to match standard pattern
|
||||
- **Delete StatusChange** (`gamestatuschange_confirm_delete.html` → `partials/statuschange_delete_confirmation.html`): Adopt the same 2-view pattern as delete-game.
|
||||
- Add `delete_statuschange_confirmation` view (GET → renders modal partial) + URL before the delete URL
|
||||
- Update `partials/history.html` — add `hx-get="{% url 'games:delete_statuschange_confirmation' change.id %}" hx-target="#global-modal-container"` to the Delete link
|
||||
- Create new `partials/statuschange_delete_confirmation.html` using `<c-modal>`, same structure as `delete_game_confirmation.html` (detail list, red warning text, same button layout, `<c-gamestatus>` badge for old status)
|
||||
- Modify `GameStatusChangeDeleteView` to only handle POST (remove its GET-rendered template)
|
||||
- Delete old `gamestatuschange_confirm_delete.html` after migration
|
||||
- **Refund Purchase** (`partials/refund_purchase_confirmation.html`): Update template to match standard pattern
|
||||
|
||||
### 5. Search Form Enhancement
|
||||
|
||||
Wrap `search_field.html` in proper `<form method="get">`:
|
||||
|
||||
```html
|
||||
<form class="max-w-md mx-auto" method="get" x-data x-on:keydown.escape="this.querySelector('input').value=''; this.submit()">
|
||||
<!-- input + button -->
|
||||
</form>
|
||||
```
|
||||
|
||||
This enables:
|
||||
- Native form submission on Enter
|
||||
- Potential for "clear all" functionality
|
||||
- Proper browser form autofill behavior
|
||||
|
||||
### 6. Status/Device Selector Styling
|
||||
|
||||
Update Alpine.js dropdowns to use consistent button classes:
|
||||
- Replace `rounded-lg` with `rounded-base`
|
||||
- Replace `shadow-2xs` with `shadow-xs`
|
||||
- Standardize border colors with `border-default`
|
||||
- Use `text-heading` / `text-body` for dark mode compatibility
|
||||
|
||||
---
|
||||
|
||||
## Templates That Need Changes
|
||||
|
||||
| Template | Change | Effort |
|
||||
|---|---|---|
|
||||
| `cotton/layouts/add.html` | Add `c-section` component support (title, columns, fields slots) | Medium |
|
||||
| `add_game.html` | Multi-column flexbox layout, section headers | Medium |
|
||||
| `add_purchase.html` | Multi-column flexbox layout, simplify dual-submit, section headers | High |
|
||||
| `add_session.html` | Flexbox layout for timestamps+duration, section headers | Low |
|
||||
| `add_playevent.html` | Flexbox layout, section headers | Low |
|
||||
| `add_platform.html` | Section headers (was flat single-column) | Low |
|
||||
| `add_device.html` | Section headers (was flat single-column) | Low |
|
||||
| `partials/delete_game_confirmation.html` | Standardize to shared modal pattern | Low |
|
||||
| `partials/refund_purchase_confirmation.html` | Standardize to shared modal pattern | Low |
|
||||
| `partials/statuschange_delete_confirmation.html` | New — adopt same 2-view pattern as delete-game (modal, `<c-modal>`, HTMX triggers) | Medium |
|
||||
| `gamestatuschange_confirm_delete.html` | Delete (replaced by new partial) | Trivial |
|
||||
| `cotton/modal.html` | Fix missing `<form>` tag | Low |
|
||||
| `stats.html` | Add Flowbite select styling | Low |
|
||||
| `partials/gamestatus_selector.html` | Update button classes | Low |
|
||||
| `partials/sessiondevice_selector.html` | Update button classes | Low |
|
||||
| `cotton/search_field.html` | Wrap in `<form method="get">` | Low |
|
||||
| `common/input.css` | Remove legacy, fix errorlist, add select styles | Low |
|
||||
|
||||
---
|
||||
|
||||
## Implementation Order
|
||||
|
||||
### Phase 1: Quick Wins (low risk, no breaking changes)
|
||||
|
||||
1. **CSS fixes** (`input.css`) — fix errorlist width, remove legacy `.basic-button` / `#button-container`, add select styles
|
||||
2. ~~**`modal.html` rewrite**~~ — add missing `<form>` tag, conditional form wrapper ✓ Implemented (uses `{{ slot }}` cotton default slot, no `{% partial %}` tags; `size` defaults to `"xl"` with inline `{% if %}` mapping)
|
||||
3. **Delete confirmation standardization** — 3 templates → all modal, same pattern (including GameStatusChange: full-page → modal)
|
||||
4. **Search field enhancement** — wrap in `<form method="get">`
|
||||
5. **Stats select styling** — add Flowbite select classes
|
||||
6. **Selector styling updates** — gamestatus + sessiondevice selectors, consistent classes
|
||||
|
||||
### Phase 2: `c-section` Component
|
||||
|
||||
7. **Create `c-section` component** — title, columns, fields slots
|
||||
8. **Update `cotton/layouts/add.html`** — support `sections` variable, fallback to `form.as_div`
|
||||
|
||||
### Phase 3: Form Layout Overhaul (largest change)
|
||||
|
||||
9. **`GameForm`** — section headers + 2-col flexbox (`add_game.html`)
|
||||
10. **`PlayEventForm`** — section headers + 2-col flexbox
|
||||
11. **`PurchaseForm`** — section headers + 2/3-col flexbox + simplify dual-submit (`add_purchase.html`)
|
||||
12. **`SessionForm`** — section headers + flexbox for timestamps+duration (custom rendering already exists)
|
||||
13. **Simple forms** — `add_platform.html`, `add_device.html` get section headers (single column)
|
||||
|
||||
---
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
- Run `make test` after Phase 1 changes to verify nothing broke
|
||||
- `tests/test_paths_return_200.py` — URL-level smoke tests (186 tests). All views must have a `test_*_returns_200` test. Adding new views requires a corresponding test to prevent `TemplateDoesNotExist` regressions.
|
||||
- CSS changes do not require test changes (no test coverage for rendering), but visual verification is recommended
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
- [x] Simple forms section headers? → **All forms get section headers** for consistency
|
||||
- [x] CSS Grid or Flexbox? → **Flexbox** — auto-reflow on different screen sizes
|
||||
- [x] add.html layout variable? → **Option A** — `c-section` cotton component with `title` and `columns` slots
|
||||
- [x] add_purchase.html dual-submit? → **Simplify** — remove `<tr><td>`, use same `<c-button>` pattern as `add_game.html`
|
||||
- [x] GameStatusChange modal or full-page? → **Modal** — trigger via HTMX, same pattern as delete-game
|
||||
- [x] .flowbite-input class? → **Remove entirely** — rely on element-level `@apply` in `input.css`
|
||||
|
||||
## Decision Summary
|
||||
|
||||
| Question | Decision |
|
||||
|---|---|
|
||||
| Section headers on simple forms | Yes, all forms get them |
|
||||
| Layout approach for multi-column | Flexbox with wrap |
|
||||
| Layout mechanism in add.html | Option A: `c-section` cotton component |
|
||||
| Purchase dual-submit | Simplify — single submit button, same as Game |
|
||||
| GameStatusChange delete | Convert to modal (HTMX-triggered) |
|
||||
| .flowbite-input class | Remove — preserve styling via element-level `@apply` in `input.css` |
|
||||
| `modal.html` component | Rewrite with form wrapping, body + footer slots, reusable close button ✓ Implemented
|
||||
|
||||
## Build Step
|
||||
|
||||
After any CSS changes to `common/input.css`, the compiled output must be rebuilt:
|
||||
|
||||
- **`make css`** — one-shot build: `npx @tailwindcss/cli -i ./common/input.css -o ./games/static/base.css`
|
||||
- **`make dev`** — watch mode: Tailwind rebuilds automatically on every `input.css` save
|
||||
|
||||
Running `make dev` is sufficient for development since it concurrently runs Django and the CSS watcher.
|
||||
Only use `make css` if you only want to rebuild CSS without starting the dev server.
|
||||
|
||||
**Important**: Legacy CSS removals (`.basic-button`, `#button-container`, `.flowbite-input`) will only take effect in the browser after a rebuild. The old compiled `base.css` will still contain them until rebuilt.
|
||||
Reference in New Issue
Block a user