# 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 `` - 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
{product.title}
``` Empty state with `hidden only:block`: ```heex
...
``` ### 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"` ## JS/CSS Guidelines - Project is fully Tailwind-free — hand-written CSS with `@layer`, native nesting, `oklch()` - **Never** use `@apply` in CSS - **Never** write inline `