# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

Berrypod is a customisable e-commerce storefront for print-on-demand sellers, built with Phoenix 1.8 + LiveView 1.1 on Elixir/Erlang. Uses SQLite with BLOB storage for images. Licensed under AGPL-3.0.

## Common Commands

```bash
mix setup              # Install deps, create DB, run migrations, build assets
mix phx.server         # Start dev server at localhost:4000
mix test               # Run all tests
mix test path/to.exs   # Run specific test file
mix test --failed      # Re-run failed tests
mix precommit          # REQUIRED before committing: compile --warning-as-errors, format, test
```

## Architecture

### Core Contexts (lib/berrypod/)

- **Settings** - Theme configuration persistence as JSON
- **Theme** - CSS generation, ETS caching, 8 presets (Gallery, Studio, Boutique, etc.)
- **Products** - Product/variant data synced from Printify/Printful
- **Media/Images** - Image uploads, optimization pipeline (Oban jobs), media library
- **Providers** - Abstraction layer for POD providers (Printify + Printful)
- **Pages** - Database-driven page builder (blocks, cache, defaults, 26 block types)
- **Orders** - Order lifecycle, fulfilment tracking, provider submission
- **Shipping** - Shipping rates, country detection, exchange rates
- **Analytics** - Privacy-first pageview tracking, e-commerce funnel
- **ActivityLog** - System event logging, order timeline
- **Redirects** - URL redirects, 404 monitoring, dead link scanning
- **Newsletter** - Email list collection, campaign sending
- **Search** - FTS5 full-text search index

### Web Layer (lib/berrypod_web/)

- **live/admin/** - Admin LiveViews (orders, pages, media, analytics, etc.)
- **live/shop/** - Shop LiveViews (home, collection, product, cart, custom pages)
- **components/shop_components/** - Reusable shop UI components (split into focused modules)
- **page_renderer.ex** - Generic block-to-component dispatch for all pages

### Three-Layer CSS Architecture

1. **Primitives** (layer1) - Design tokens as CSS custom properties
2. **Attributes** (layer2) - Theme-specific design rules
3. **Semantic** (layer3) - Component styles

Theme switching is instant via CSS custom property injection (no reload).

### Key Routes

| Path | Purpose |
|------|---------|
| `/` | Shop home |
| `/collections/:slug` | Product collection (filtering) |
| `/products/:id` | Product detail |
| `/cart` | Shopping cart |
| `/contact` | Contact + order lookup |
| `/:slug` | Custom CMS pages (catch-all, must be last scope) |
| `/setup` | First-run onboarding |
| `/admin` | Admin dashboard |
| `/admin/orders` | Order management |
| `/admin/pages` | Page editor |
| `/admin/media` | Media library |
| `/admin/analytics` | Analytics dashboard |
| `/admin/activity` | Activity log |
| `/admin/theme` | Theme editor |
| `/admin/settings` | Shop settings |

## Elixir Guidelines

- Use `:req` library for HTTP requests (not httpoison, tesla, httpc)
- Lists don't support index access (`list[i]`), use `Enum.at/2`
- Access changeset fields with `Ecto.Changeset.get_field/2`, not `changeset[:field]`
- Preload associations in queries when needed in templates
- **Rebinding:** Must bind result of `if`/`case` blocks:
  ```elixir
  # WRONG - rebinding inside block is lost
  if connected?(socket), do: socket = assign(socket, :val, val)

  # RIGHT - bind result to variable
  socket = if connected?(socket), do: assign(socket, :val, val), else: socket
  ```

## Phoenix 1.8 Guidelines

- **Always** wrap LiveView templates with `<Layouts.app flash={@flash} ...>`
- Use `@current_scope.user` in templates, never `@current_user`
- Use `to_form/2` for all form handling, access via `@form[:field]`
- Use `<.input>` component from core_components.ex
- Use `<.icon name="hero-x-mark">` for icons, not Heroicons modules

### Auth Routing

Routes requiring auth go in `:require_authenticated_user` live_session:
```elixir
live_session :require_authenticated_user,
  on_mount: [{BerrypodWeb.UserAuth, :require_authenticated}] do
  live "/admin/theme", ThemeLive.Index
end
```

Public routes with optional user go in `:current_user` live_session:
```elixir
live_session :current_user,
  on_mount: [{BerrypodWeb.UserAuth, :mount_current_scope}] do
  live "/", ShopLive.Home
end
```

## HEEx Template Guidelines

- Use `{...}` for interpolation in attributes, `<%= %>` for block constructs in bodies
- Class lists require bracket syntax: `class={["base", @cond && "extra"]}`
- Use `<%!-- comment --%>` for template comments
- Never use `else if` or `elsif` - use `cond` or `case`
- Use `phx-no-curly-interpolation` for literal braces in code blocks
- **Never** use `<% Enum.each %>` - always use `<%= for item <- @items do %>`

## LiveView Guidelines

### Streams (Required for Collections)

Always use streams for lists to prevent memory issues:
```elixir
# Mount
socket |> stream(:products, Products.list_products())

# Add item
socket |> stream_insert(:products, new_product)

# Reset (e.g., filtering)
socket |> stream(:products, filtered_list, reset: true)

# Delete
socket |> stream_delete(:products, product)
```

Template pattern:
```heex
<div id="products" phx-update="stream">
  <div :for={{dom_id, product} <- @streams.products} id={dom_id}>
    {product.title}
  </div>
</div>
```

Empty state with `hidden only:block`:
```heex
<div id="products" phx-update="stream">
  <div class="hidden only:block">No products yet</div>
  <div :for={{dom_id, product} <- @streams.products} id={dom_id}>...</div>
</div>
```

### Form Handling

From params:
```elixir
def handle_event("validate", %{"product" => params}, socket) do
  {:noreply, assign(socket, form: to_form(params, as: :product))}
end
```

From changeset:
```elixir
changeset = Product.changeset(%Product{}, params)
socket |> assign(form: to_form(changeset))
```

### Gotchas

- `phx-hook` with DOM manipulation requires `phx-update="ignore"`
- Avoid LiveComponents unless you have a specific need (isolated state, targeted updates)
- Never use deprecated `phx-update="append"` or `phx-update="prepend"`

## Progressive Enhancement

Build HTML+CSS that works without JS. LiveView layers on top.

**Principle:** If a state change is purely visual (which radio is selected, which panel is shown), handle it in CSS. Don't round-trip to the server for something the browser can do alone.

**Pattern for forms with dynamic sections:**

1. Render ALL possible states in the HTML (e.g. all adapter configs, all option panels)
2. Use CSS `:has(:checked)` to show the matching section and hide the rest
3. Namespace field names to avoid clashes (`email[brevo][api_key]`, not `email[api_key]`)
4. Give the form both `action` (no-JS POST) and `phx-submit` (LiveView)
5. Add a thin controller to handle the no-JS POST path

```css
/* Hide all sections by default, show the one matching the checked radio */
.config-section[data-option] { display: none; }
form:has(#option-foo:checked) [data-option="foo"] { display: block; }
```

**JS-only elements:** Wrap in a container with an ID, hide it via `<noscript><style>`, and provide a form POST alternative:

```heex
<span id="async-action">
  <.button type="button" phx-click="do_thing">Do thing</.button>
</span>
<noscript>
  <style>#async-action { display: none; }</style>
  <.form for={%{}} action={~p"/thing"} method="post" style="display:inline">
    <.button>Do thing</.button>
  </.form>
</noscript>
```

**HEEx and `<style>` tags:** HEEx treats `<style>` content as raw text (no `{@var}` interpolation). To output dynamic CSS, use a helper that returns `Phoenix.HTML.raw/1`:

```elixir
defp dynamic_style(value) do
  Phoenix.HTML.raw("<style>.thing[data-x=\"#{value}\"] { display: block; }</style>")
end
```

## JS/CSS Guidelines

- Project is fully Tailwind-free — hand-written CSS with `@layer`, native nesting, `oklch()`
- **Never** use `@apply` in CSS
- **Never** write inline `<script>` tags - use hooks in assets/js/
- All vendor deps must be imported into app.js/app.css
- Prefer flat selectors or max single-level nesting (esbuild passes CSS through as-is)

## LiveView Testing

- Use `element/2`, `has_element/2` - never test raw HTML
- Reference DOM IDs from templates in tests
- Debug with LazyHTML: `LazyHTML.filter(document, "selector")`

## Writing Style

- Casual, British English tone throughout (code, comments, commits, docs)
- Sentence case always, never title case ("Admin providers" not "Admin Providers")
- Brief and tight in prose. Cut fluff. No waffle.
- Avoid em dashes, semicolons in prose, and overly formal language
- Don't sound like an LLM wrote it (no "straightforward", "robust", "leverage", "comprehensive")
- Comments explain why, not what. Skip obvious ones.

**Code style:**
- Explicit and obvious over clever and terse
- Readable code that anyone can follow beats "smart" one-liners
- Stick to idiomatic Phoenix/Elixir/LiveView/Oban patterns
- Follow conventions from the existing codebase
- When in doubt, check how Phoenix generators do it

## Commits

- Suggest a commit at logical checkpoints (feature complete, tests passing)
- Run `mix precommit` before committing
- Atomic commits with one logical change each
- Commit messages: imperative mood, lowercase, no full stop
  - Good: `add provider connection form validation`
  - Bad: `Added provider connection form validation.`

## Testing

Write tests for new features, but be pragmatic about coverage.

**Do test:**
- Public context functions (the API boundary)
- Critical user flows (auth, checkout, sync)
- Edge cases and error handling
- Complex business logic

**Skip tests for:**
- Trivial getters/setters
- Framework-generated code
- Pure UI tweaks with no logic
- Implementation details that may change

**Approach:**
- Prefer integration tests over isolated unit tests for LiveViews
- One test file per module, colocated in `test/` mirror of `lib/`
- Use fixtures from `test/support/fixtures/` for test data

## Documentation Workflow

**Single source of truth:** [PROGRESS.md](PROGRESS.md)
- Update after completing any feature or task
- Contains current status, next steps, and task breakdown
- Link to plan files for implementation details

**Plan files** (docs/plans/*.md):
- Implementation references, not status trackers
- Mark status at top (e.g., "Status: Complete")
- Keep detailed architecture/design decisions

**Task sizing:**
- Break features into ~1-2 hour tasks
- Each task should fit in one Claude session without context overflow
- Include: files to modify, acceptance criteria, estimate

**Before starting work:**
1. Check PROGRESS.md for current status and next task
2. Read relevant plan file for implementation details
3. Focus on one task at a time
