From 88636db9d2d86841a627e3592f48b8b7cd513ef0 Mon Sep 17 00:00:00 2001 From: Jamey Greenwood Date: Sat, 17 Jan 2026 16:19:35 +0000 Subject: [PATCH] feat: add shop storefront with optimized theme CSS - Create LoadTheme plug for loading theme settings - Add shop layout with inline CSS injection (~400 bytes vs 17KB full) - Create ShopLive.Home at / using shared ShopComponents - Wire up CSS cache invalidation when theme settings change - Replace Phoenix welcome page with themed shop home page The shop layout injects only the active theme CSS variables inline, achieving a 97% reduction compared to the full variants file used by the theme editor. Co-Authored-By: Claude Opus 4.5 --- lib/simpleshop_theme/settings.ex | 7 +++ .../components/layouts/shop.html.heex | 30 +++++++++++++ .../live/shop_live/home.ex | 41 ++++++++++++++++++ .../live/shop_live/home.html.heex | 43 +++++++++++++++++++ lib/simpleshop_theme_web/plugs/load_theme.ex | 39 +++++++++++++++++ lib/simpleshop_theme_web/router.ex | 13 ++++-- .../controllers/page_controller_test.exs | 4 +- 7 files changed, 172 insertions(+), 5 deletions(-) create mode 100644 lib/simpleshop_theme_web/components/layouts/shop.html.heex create mode 100644 lib/simpleshop_theme_web/live/shop_live/home.ex create mode 100644 lib/simpleshop_theme_web/live/shop_live/home.html.heex create mode 100644 lib/simpleshop_theme_web/plugs/load_theme.ex diff --git a/lib/simpleshop_theme/settings.ex b/lib/simpleshop_theme/settings.ex index cdc25bc..ea84ce7 100644 --- a/lib/simpleshop_theme/settings.ex +++ b/lib/simpleshop_theme/settings.ex @@ -83,6 +83,13 @@ defmodule SimpleshopTheme.Settings do settings = Ecto.Changeset.apply_changes(changeset) json = Jason.encode!(settings) put_setting("theme_settings", json, "json") + + # Invalidate and rewarm CSS cache + alias SimpleshopTheme.Theme.{CSSCache, CSSGenerator} + CSSCache.invalidate() + css = CSSGenerator.generate(settings) + CSSCache.put(css) + {:ok, settings} else {:error, changeset} diff --git a/lib/simpleshop_theme_web/components/layouts/shop.html.heex b/lib/simpleshop_theme_web/components/layouts/shop.html.heex new file mode 100644 index 0000000..3091f49 --- /dev/null +++ b/lib/simpleshop_theme_web/components/layouts/shop.html.heex @@ -0,0 +1,30 @@ + + + + + + + <.live_title><%= assigns[:page_title] || @theme_settings.site_name %> + + + + + + +
+ <%= @inner_content %> +
+ + diff --git a/lib/simpleshop_theme_web/live/shop_live/home.ex b/lib/simpleshop_theme_web/live/shop_live/home.ex new file mode 100644 index 0000000..3ca9f80 --- /dev/null +++ b/lib/simpleshop_theme_web/live/shop_live/home.ex @@ -0,0 +1,41 @@ +defmodule SimpleshopThemeWeb.ShopLive.Home do + use SimpleshopThemeWeb, :live_view + + alias SimpleshopTheme.Settings + alias SimpleshopTheme.Media + alias SimpleshopTheme.Theme.{CSSCache, CSSGenerator, PreviewData} + + @impl true + def mount(_params, _session, socket) do + # Load theme settings (cached CSS for performance) + 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 + + logo_image = Media.get_logo() + header_image = Media.get_header() + + preview_data = %{ + products: PreviewData.products(), + categories: PreviewData.categories() + } + + socket = + socket + |> assign(:page_title, "Home") + |> assign(:theme_settings, theme_settings) + |> assign(:generated_css, generated_css) + |> assign(:logo_image, logo_image) + |> assign(:header_image, header_image) + |> assign(:preview_data, preview_data) + + {:ok, socket} + end +end diff --git a/lib/simpleshop_theme_web/live/shop_live/home.html.heex b/lib/simpleshop_theme_web/live/shop_live/home.html.heex new file mode 100644 index 0000000..aed1647 --- /dev/null +++ b/lib/simpleshop_theme_web/live/shop_live/home.html.heex @@ -0,0 +1,43 @@ +
+ <.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} header_image={@header_image} active_page="home" mode={:shop} cart_count={0} /> + +
+ <.hero_section + title="Original designs, printed on demand" + description="From art prints to apparel – unique products created by independent artists and delivered straight to your door." + cta_text="Shop the collection" + cta_page="collection" + mode={:shop} + /> + + <.category_nav categories={@preview_data.categories} mode={:shop} /> + + <.featured_products_section + title="Featured products" + products={@preview_data.products} + theme_settings={@theme_settings} + mode={:shop} + /> + + <.image_text_section + title="Made with passion, printed with care" + description="Every design starts with an idea. We work with quality print partners to bring those ideas to life on premium products – from gallery-quality art prints to everyday essentials." + image_url="/mockups/mountain-sunrise-print-3.jpg" + link_text="Learn more about the studio →" + link_page="about" + mode={:shop} + /> +
+ + <.shop_footer theme_settings={@theme_settings} mode={:shop} /> + + <.cart_drawer cart_items={[]} subtotal="£0.00" mode={:shop} /> + + <.search_modal hint_text={~s(Try searching for "mountain", "forest", or "ocean")} /> +
diff --git a/lib/simpleshop_theme_web/plugs/load_theme.ex b/lib/simpleshop_theme_web/plugs/load_theme.ex new file mode 100644 index 0000000..78183ec --- /dev/null +++ b/lib/simpleshop_theme_web/plugs/load_theme.ex @@ -0,0 +1,39 @@ +defmodule SimpleshopThemeWeb.Plugs.LoadTheme do + @moduledoc """ + Plug that loads theme settings and generated CSS for public shop pages. + + This plug: + 1. Checks the ETS cache for pre-generated CSS + 2. Falls back to generating CSS from theme settings on cache miss + 3. Assigns both `theme_settings` and `generated_css` to the connection + + The generated CSS contains only the active theme values (not all variants), + making it much smaller than the full theme-layer2-attributes.css file used + by the theme editor for live preview switching. + """ + + import Plug.Conn + + alias SimpleshopTheme.Settings + alias SimpleshopTheme.Theme.{CSSGenerator, CSSCache} + + def init(opts), do: opts + + def call(conn, _opts) do + {theme_settings, generated_css} = + case CSSCache.get() do + {:ok, css} -> + {Settings.get_theme_settings(), css} + + :miss -> + settings = Settings.get_theme_settings() + css = CSSGenerator.generate(settings) + CSSCache.put(css) + {settings, css} + end + + conn + |> assign(:theme_settings, theme_settings) + |> assign(:generated_css, generated_css) + end +end diff --git a/lib/simpleshop_theme_web/router.ex b/lib/simpleshop_theme_web/router.ex index 5333159..ce03774 100644 --- a/lib/simpleshop_theme_web/router.ex +++ b/lib/simpleshop_theme_web/router.ex @@ -17,10 +17,17 @@ defmodule SimpleshopThemeWeb.Router do plug :accepts, ["json"] end - scope "/", SimpleshopThemeWeb do - pipe_through :browser + pipeline :shop do + plug SimpleshopThemeWeb.Plugs.LoadTheme + end - get "/", PageController, :home + # Public storefront (root level) + scope "/", SimpleshopThemeWeb do + pipe_through [:browser, :shop] + + live_session :public_shop, layout: {SimpleshopThemeWeb.Layouts, :shop} do + live "/", ShopLive.Home, :index + end end # Image serving routes (public, no auth required) diff --git a/test/simpleshop_theme_web/controllers/page_controller_test.exs b/test/simpleshop_theme_web/controllers/page_controller_test.exs index 736ebca..c792246 100644 --- a/test/simpleshop_theme_web/controllers/page_controller_test.exs +++ b/test/simpleshop_theme_web/controllers/page_controller_test.exs @@ -1,8 +1,8 @@ defmodule SimpleshopThemeWeb.PageControllerTest do use SimpleshopThemeWeb.ConnCase - test "GET /", %{conn: conn} do + test "GET / renders the shop home page", %{conn: conn} do conn = get(conn, ~p"/") - assert html_response(conn, 200) =~ "Peace of mind from prototype to production" + assert html_response(conn, 200) =~ "Original designs, printed on demand" end end