-`shop_components.ex` alone is 4,424 lines — 24% of all Elixir source
- Web layer (9,576) slightly larger than contexts (7,950)
- Top 3 files (`shop_components.ex`, `index.html.heex`, `preview_data.ex`) account for 6,650 lines — 36% of source
### What matters
Every LOC is maintenance cost, attack surface, and cognitive load. The goal isn't minimising LOC for its own sake — a 50-line module that does one thing clearly beats a 20-line clever abstraction. But 224 lines of copy-pasted template wrapper is the kind of LOC worth eliminating.
Contexts are well-sized (200-550 lines each, good boundaries). LiveViews average ~157 lines — thin wrappers over contexts, which is right. The problems are in the shared component layer and the repeated boilerplate.
### Worth questioning
-`preview_data.ex` (1,049 lines) and `mockups/generator.ex` (522 lines) total 1,570 lines of hard-coded demo content. Should this be seed data or fixtures rather than compiled code? Could be moved behind `Mix.env()` guards or into a mix task to reduce the production runtime footprint by ~8%.
- Test-to-source ratio is 0.37:1 (lines). Reasonable for a template-heavy app, but new features should aim to keep this steady or improve it.
**Problem:** Every page template repeats ~28 lines of shell boilerplate: `#shop-container` div, `skip_link`, `announcement_bar`, `shop_header`, `shop_footer`, `cart_drawer`, `search_modal`, `mobile_bottom_nav`. That's ~224 lines of copy-paste across 8 templates.
**Fix:** Create a `shop_layout/1` component in `shop_components.ex` that accepts an inner block and common attrs. Templates become just their unique `<main>` content.
**Files:** `shop_components.ex` (new component), all 8 page templates, `collection.ex` inline render.
**Complexity:** Medium. The error template has slight variations (no `phx-hook`, different class). Handle with an optional attr or keep error as a special case.
**Fix:** Create a `ThemeHook` (like `CartHook`) using `on_mount` that assigns `theme_settings`, `generated_css`, `logo_image`, `header_image`, `mode`. Add it to the `:public_shop` live_session alongside `CartHook`.
**Net saving:** ~55 lines removed (75 duplicated minus ~20 for the new hook module). Also removes 7 sets of aliases (Settings, Media, CSSCache, CSSGenerator) from LiveViews.
**Files:** New `theme_hook.ex`, `router.ex` (add to on_mount), all 7 shop LiveViews (remove boilerplate).
**Complexity:** Low. Follows established `CartHook` pattern exactly.
### 3. Extract common preview assigns helper
**Problem:** 10 `preview_page` clauses in `theme_live/index.ex` each pass the same 8 common attrs (theme_settings, logo_image, header_image, mode, cart_items, cart_count, cart_subtotal, cart_drawer_open). ~80 lines of duplication.
**Fix:** Extract a private `common_preview_assigns/1` function. Each clause merges page-specific attrs on top.
**Complexity:** Low. Pure refactor within one file.
## Medium priority
### 4. Split `shop_components.ex` into focused modules
**Problem:** 4,400 lines, 55 public functions. Covers navigation, products, cart, contact, content rendering, forms. Hard to navigate, slow to compile.
**Net saving:** ~0 lines (redistribution, not reduction). Value is in navigability, compile speed, and cognitive load — no single file over ~1,000 lines.
**Files:** `shop_components.ex` split into 5 new files.
**Complexity:** Medium. Mechanical but touches many imports. Need to update `page_templates.ex` and any direct references.
### 5. Unify cart hydration between CartHook and CheckoutController
**Problem:** `CheckoutController` manually calls `Cart.hydrate()` and `Enum.reduce` for subtotal instead of using `Cart.calculate_subtotal/1`. Duplicates logic from `CartHook.update_cart_assigns/2`.
**Fix:** Extract a `Cart.build_state/1` function that returns `%{items: hydrated, count: n, subtotal: formatted}`. Both CartHook and CheckoutController use it.
**Problem:** Both `Settings.put_secret/2` and `ProviderConnection` changeset independently call `Vault.encrypt()` with different error handling patterns.
**Fix:** Add `Vault.encrypt!/1` that raises on failure (for changeset use) and keep `Vault.encrypt/1` for tuple returns. Or create an `Ecto.Type` for encrypted fields.
| | **Total** | **~355** | | **~1.7% of total source** |
Combined with the structural improvement of splitting `shop_components.ex` (4,424 lines into 5 modules of ~900 lines each), the largest file drops from 24% of all source to under 5%.