simpleshop_theme/docs/plans/dry-refactor.md
jamey 5b08591a55 docs: add DRY refactor plan and update progress
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 11:18:58 +00:00

7.0 KiB

DRY refactor plan

Status: Planned

Codebase analysis identifying repeated patterns and consolidation opportunities. Ordered by impact.

High priority

1. Extract <.shop_layout> wrapper component

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.

Before:

<div id="shop-container" phx-hook="CartPersist" class="shop-container min-h-screen pb-20 md:pb-0"
     style="background-color: var(--t-surface-base); ...">
  <.skip_link />
  <%= if @theme_settings.announcement_bar do %>
    <.announcement_bar theme_settings={@theme_settings} />
  <% end %>
  <.shop_header theme_settings={@theme_settings} logo_image={@logo_image} ... />

  <main id="main-content">
    <!-- unique content -->
  </main>

  <.shop_footer theme_settings={@theme_settings} mode={@mode} />
  <.cart_drawer cart_items={@cart_items} subtotal={@cart_subtotal} ... />
  <.search_modal hint_text={~s(...)} />
  <.mobile_bottom_nav active_page="home" mode={@mode} />
</div>

After:

<.shop_layout {assigns}>
  <main id="main-content">
    <!-- unique content -->
  </main>
</.shop_layout>

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.

2. Extract theme loading into on_mount hook

Problem: 7 shop LiveViews independently load theme settings, check CSS cache, fetch logo/header, assign 5 common values. ~12-15 identical lines per file, ~75 lines total.

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.

Before (every LiveView):

def mount(_params, _session, socket) do
  theme_settings = Settings.get_theme_settings()
  generated_css = case CSSCache.get() do
    {:ok, css} -> css
    :miss -> css = CSSGenerator.generate(theme_settings); CSSCache.put(css); css
  end
  socket =
    socket
    |> assign(:theme_settings, theme_settings)
    |> assign(:generated_css, generated_css)
    |> assign(:logo_image, Media.get_logo())
    |> assign(:header_image, Media.get_header())
    |> assign(:mode, :shop)
  # ... page-specific assigns
end

After:

# Router
live_session :public_shop,
  on_mount: [{CartHook, :mount_cart}, {ThemeHook, :mount_theme}] do
  # ...
end

# LiveView - only page-specific logic remains
def mount(_params, _session, socket) do
  socket = assign(socket, :page_title, "Home")
  {:ok, socket}
end

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.

Files: lib/simpleshop_theme_web/live/theme_live/index.ex

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.

Fix: Split into ~5 modules:

  • ShopLayoutComponents — header, footer, mobile_bottom_nav, skip_link, announcement_bar, shop_layout
  • ShopProductComponents — product_card, product_grid, product_gallery, variant_selector, related_products
  • ShopCartComponents — cart_drawer, cart_item, cart_layout, order_summary
  • ShopContentComponents — rich_text, hero_section, image_text_section, responsive_image
  • ShopFormComponents — contact_form, newsletter_card, search_modal

Keep ShopComponents as a facade that imports all sub-modules for backward compatibility.

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.

Files: cart.ex, cart_hook.ex, checkout_controller.ex

Complexity: Low.

6. Consolidate encryption logic

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.

Files: vault.ex, settings.ex, provider_connection.ex

Complexity: Low-medium.

Low priority

7. Consolidate Settings repo lookups

Problem: get_setting, get_secret, has_secret?, secret_hint each independently call Repo.get_by(Setting, key: key).

Fix: Extract defp fetch_setting(key) that returns {:ok, setting} | :not_found.

Files: settings.ex

Complexity: Trivial.

8. Scalable secrets loading

Problem: Secrets.load_stripe_config/0 hardcodes Stripe key loading. Adding more providers means duplicating the pattern.

Fix: Registry of {settings_key, app, env_key} tuples. load_all/0 iterates.

Files: secrets.ex

Complexity: Trivial but low value until more providers are added.

What's already good

  • CartHook — textbook shared behaviour via attach_hook. Model for ThemeHook.
  • Router — clean pipeline composition, no duplication.
  • CoreComponents vs ShopComponents — proper separation, no overlap.
  • Provider abstraction — clean dispatch with Mox support.
  • Context boundaries — Products, Orders, Settings, Media are well-separated.
  • Page LiveView renders — now all use {assigns} spread (just refactored).

Suggested order

  1. ThemeHook (item 2) — lowest risk, biggest clarity win
  2. shop_layout component (item 1) — biggest line reduction
  3. Preview assigns helper (item 3) — quick win, one file
  4. Cart state builder (item 5) — small but prevents bugs
  5. Split shop_components (item 4) — larger effort, do when convenient
  6. Everything else — opportunistic