berrypod/CLAUDE.md
jamey edef628214 tidy docs: condense progress, trim readme, mark plan statuses
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 17:15:18 +00:00

8.9 KiB

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

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:
    # 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:

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:

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:

# 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:

<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:

<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:

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

From changeset:

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"

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

  • 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