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 <noreply@anthropic.com>
This commit is contained in:
parent
97981a9884
commit
88636db9d2
@ -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}
|
||||
|
||||
30
lib/simpleshop_theme_web/components/layouts/shop.html.heex
Normal file
30
lib/simpleshop_theme_web/components/layouts/shop.html.heex
Normal file
@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="h-full">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="csrf-token" content={get_csrf_token()} />
|
||||
<.live_title><%= assigns[:page_title] || @theme_settings.site_name %></.live_title>
|
||||
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
|
||||
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
|
||||
</script>
|
||||
<!-- Generated theme CSS (only active values, not all variants) -->
|
||||
<style id="theme-css"><%= Phoenix.HTML.raw(@generated_css) %></style>
|
||||
</head>
|
||||
<body class="h-full">
|
||||
<div
|
||||
class="shop-root h-full"
|
||||
data-mood={@theme_settings.mood}
|
||||
data-typography={@theme_settings.typography}
|
||||
data-shape={@theme_settings.shape}
|
||||
data-density={@theme_settings.density}
|
||||
data-grid={@theme_settings.grid_columns}
|
||||
data-header={@theme_settings.header_layout}
|
||||
data-sticky={to_string(@theme_settings.sticky_header)}
|
||||
data-layout={@theme_settings.layout_width}
|
||||
data-shadow={@theme_settings.card_shadow}
|
||||
>
|
||||
<%= @inner_content %>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
41
lib/simpleshop_theme_web/live/shop_live/home.ex
Normal file
41
lib/simpleshop_theme_web/live/shop_live/home.ex
Normal file
@ -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
|
||||
43
lib/simpleshop_theme_web/live/shop_live/home.html.heex
Normal file
43
lib/simpleshop_theme_web/live/shop_live/home.html.heex
Normal file
@ -0,0 +1,43 @@
|
||||
<div class="shop-container min-h-screen" style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);">
|
||||
<.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} />
|
||||
|
||||
<main id="main-content">
|
||||
<.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}
|
||||
/>
|
||||
</main>
|
||||
|
||||
<.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")} />
|
||||
</div>
|
||||
39
lib/simpleshop_theme_web/plugs/load_theme.ex
Normal file
39
lib/simpleshop_theme_web/plugs/load_theme.ex
Normal file
@ -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
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user