2026-02-18 21:23:15 +00:00
|
|
|
|
defmodule BerrypodWeb.Admin.Theme.Index do
|
|
|
|
|
|
use BerrypodWeb, :live_view
|
feat: add Theme LiveView with preset switching
Implement basic theme editor interface with live preview:
- ThemeLive.Index LiveView with mount and event handlers
- Two-column layout: controls sidebar + preview area
- Display all 9 presets as clickable buttons
- Apply preset and regenerate CSS on click
- Show current theme settings (mood, typography, shape, density, color)
- Preview page switcher (7 pages: home, collection, product, cart, about, contact, 404)
- Inline <style> tag with generated CSS for instant preview
- Basic preview frame showing theme variables in action
- Authentication required via :require_authenticated_user pipeline
- Theme navigation link added to user menu
- 9 comprehensive LiveView tests
All tests passing (197 total).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-30 21:53:52 +00:00
|
|
|
|
|
2026-02-18 21:23:15 +00:00
|
|
|
|
alias Berrypod.Settings
|
|
|
|
|
|
alias Berrypod.Media
|
|
|
|
|
|
alias Berrypod.Theme.{CSSGenerator, Presets, PreviewData}
|
feat: add Theme LiveView with preset switching
Implement basic theme editor interface with live preview:
- ThemeLive.Index LiveView with mount and event handlers
- Two-column layout: controls sidebar + preview area
- Display all 9 presets as clickable buttons
- Apply preset and regenerate CSS on click
- Show current theme settings (mood, typography, shape, density, color)
- Preview page switcher (7 pages: home, collection, product, cart, about, contact, 404)
- Inline <style> tag with generated CSS for instant preview
- Basic preview frame showing theme variables in action
- Authentication required via :require_authenticated_user pipeline
- Theme navigation link added to user menu
- 9 comprehensive LiveView tests
All tests passing (197 total).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-30 21:53:52 +00:00
|
|
|
|
|
|
|
|
|
|
@impl true
|
|
|
|
|
|
def mount(_params, _session, socket) do
|
|
|
|
|
|
theme_settings = Settings.get_theme_settings()
|
|
|
|
|
|
generated_css = CSSGenerator.generate(theme_settings)
|
2025-12-31 18:55:44 +00:00
|
|
|
|
active_preset = Presets.detect_preset(theme_settings)
|
2026-01-31 14:24:58 +00:00
|
|
|
|
|
feat: add preview page templates with theme styling
Implement all 7 preview pages showcasing theme customization:
- Home page: hero, featured products, testimonials, categories
- Collection page: product grid with filters and sorting
- Product detail page (PDP): gallery, variants, add to cart
- Cart page: cart items with quantity controls and order summary
- About page: company story and values
- Contact page: contact form and business information
- 404 error page: error message with product suggestions
Features:
- All pages use CSS custom properties for theming
- Preview data from PreviewData module (mock products, testimonials, categories)
- Responsive layouts with Tailwind utilities
- Grid columns respect theme settings
- Colors, typography, shapes, and spacing all theme-aware
- Components created as embed_templates for clean separation
Technical implementation:
- Created PreviewPages component module with embed_templates
- Wired up preview_data in LiveView mount
- Updated index.html.heex to render preview pages based on @preview_page
- All pages styled with inline styles using CSS variables
- Scrollable preview frame with max-height
All tests passing (197 total).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-30 22:06:04 +00:00
|
|
|
|
preview_data = %{
|
|
|
|
|
|
products: PreviewData.products(),
|
|
|
|
|
|
cart_items: PreviewData.cart_items(),
|
|
|
|
|
|
testimonials: PreviewData.testimonials(),
|
|
|
|
|
|
categories: PreviewData.categories()
|
|
|
|
|
|
}
|
feat: add Theme LiveView with preset switching
Implement basic theme editor interface with live preview:
- ThemeLive.Index LiveView with mount and event handlers
- Two-column layout: controls sidebar + preview area
- Display all 9 presets as clickable buttons
- Apply preset and regenerate CSS on click
- Show current theme settings (mood, typography, shape, density, color)
- Preview page switcher (7 pages: home, collection, product, cart, about, contact, 404)
- Inline <style> tag with generated CSS for instant preview
- Basic preview frame showing theme variables in action
- Authentication required via :require_authenticated_user pipeline
- Theme navigation link added to user menu
- 9 comprehensive LiveView tests
All tests passing (197 total).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-30 21:53:52 +00:00
|
|
|
|
|
2025-12-31 18:55:44 +00:00
|
|
|
|
logo_image = Media.get_logo()
|
|
|
|
|
|
header_image = Media.get_header()
|
|
|
|
|
|
|
feat: add Theme LiveView with preset switching
Implement basic theme editor interface with live preview:
- ThemeLive.Index LiveView with mount and event handlers
- Two-column layout: controls sidebar + preview area
- Display all 9 presets as clickable buttons
- Apply preset and regenerate CSS on click
- Show current theme settings (mood, typography, shape, density, color)
- Preview page switcher (7 pages: home, collection, product, cart, about, contact, 404)
- Inline <style> tag with generated CSS for instant preview
- Basic preview frame showing theme variables in action
- Authentication required via :require_authenticated_user pipeline
- Theme navigation link added to user menu
- 9 comprehensive LiveView tests
All tests passing (197 total).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-30 21:53:52 +00:00
|
|
|
|
socket =
|
|
|
|
|
|
socket
|
|
|
|
|
|
|> assign(:theme_settings, theme_settings)
|
|
|
|
|
|
|> assign(:generated_css, generated_css)
|
|
|
|
|
|
|> assign(:preview_page, :home)
|
2025-12-31 18:55:44 +00:00
|
|
|
|
|> assign(:presets_with_descriptions, Presets.all_with_descriptions())
|
|
|
|
|
|
|> assign(:active_preset, active_preset)
|
feat: add preview page templates with theme styling
Implement all 7 preview pages showcasing theme customization:
- Home page: hero, featured products, testimonials, categories
- Collection page: product grid with filters and sorting
- Product detail page (PDP): gallery, variants, add to cart
- Cart page: cart items with quantity controls and order summary
- About page: company story and values
- Contact page: contact form and business information
- 404 error page: error message with product suggestions
Features:
- All pages use CSS custom properties for theming
- Preview data from PreviewData module (mock products, testimonials, categories)
- Responsive layouts with Tailwind utilities
- Grid columns respect theme settings
- Colors, typography, shapes, and spacing all theme-aware
- Components created as embed_templates for clean separation
Technical implementation:
- Created PreviewPages component module with embed_templates
- Wired up preview_data in LiveView mount
- Updated index.html.heex to render preview pages based on @preview_page
- All pages styled with inline styles using CSS variables
- Scrollable preview frame with max-height
All tests passing (197 total).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-30 22:06:04 +00:00
|
|
|
|
|> assign(:preview_data, preview_data)
|
2025-12-31 18:55:44 +00:00
|
|
|
|
|> assign(:logo_image, logo_image)
|
|
|
|
|
|
|> assign(:header_image, header_image)
|
|
|
|
|
|
|> assign(:customise_open, false)
|
2026-01-19 21:37:34 +00:00
|
|
|
|
|> assign(:sidebar_collapsed, false)
|
2026-02-05 22:11:16 +00:00
|
|
|
|
|> assign(:cart_drawer_open, false)
|
2025-12-31 18:55:44 +00:00
|
|
|
|
|> allow_upload(:logo_upload,
|
|
|
|
|
|
accept: ~w(.png .jpg .jpeg .webp .svg),
|
|
|
|
|
|
max_entries: 1,
|
|
|
|
|
|
max_file_size: 2_000_000,
|
|
|
|
|
|
auto_upload: true,
|
|
|
|
|
|
progress: &handle_progress/3
|
|
|
|
|
|
)
|
|
|
|
|
|
|> allow_upload(:header_upload,
|
|
|
|
|
|
accept: ~w(.png .jpg .jpeg .webp),
|
|
|
|
|
|
max_entries: 1,
|
|
|
|
|
|
max_file_size: 5_000_000,
|
|
|
|
|
|
auto_upload: true,
|
|
|
|
|
|
progress: &handle_progress/3
|
|
|
|
|
|
)
|
feat: add Theme LiveView with preset switching
Implement basic theme editor interface with live preview:
- ThemeLive.Index LiveView with mount and event handlers
- Two-column layout: controls sidebar + preview area
- Display all 9 presets as clickable buttons
- Apply preset and regenerate CSS on click
- Show current theme settings (mood, typography, shape, density, color)
- Preview page switcher (7 pages: home, collection, product, cart, about, contact, 404)
- Inline <style> tag with generated CSS for instant preview
- Basic preview frame showing theme variables in action
- Authentication required via :require_authenticated_user pipeline
- Theme navigation link added to user menu
- 9 comprehensive LiveView tests
All tests passing (197 total).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-30 21:53:52 +00:00
|
|
|
|
|
|
|
|
|
|
{:ok, socket}
|
|
|
|
|
|
end
|
|
|
|
|
|
|
2025-12-31 18:55:44 +00:00
|
|
|
|
defp handle_progress(:logo_upload, entry, socket) do
|
|
|
|
|
|
if entry.done? do
|
|
|
|
|
|
consume_uploaded_entries(socket, :logo_upload, fn %{path: path}, entry ->
|
|
|
|
|
|
case Media.upload_from_entry(path, entry, "logo") do
|
|
|
|
|
|
{:ok, image} ->
|
|
|
|
|
|
Settings.update_theme_settings(%{logo_image_id: image.id})
|
|
|
|
|
|
{:ok, image}
|
|
|
|
|
|
|
|
|
|
|
|
{:error, _} = error ->
|
|
|
|
|
|
error
|
|
|
|
|
|
end
|
|
|
|
|
|
end)
|
|
|
|
|
|
|> case do
|
|
|
|
|
|
[image | _] ->
|
|
|
|
|
|
{:noreply, assign(socket, :logo_image, image)}
|
|
|
|
|
|
|
|
|
|
|
|
_ ->
|
|
|
|
|
|
{:noreply, socket}
|
|
|
|
|
|
end
|
|
|
|
|
|
else
|
|
|
|
|
|
{:noreply, socket}
|
|
|
|
|
|
end
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
defp handle_progress(:header_upload, entry, socket) do
|
|
|
|
|
|
if entry.done? do
|
|
|
|
|
|
consume_uploaded_entries(socket, :header_upload, fn %{path: path}, entry ->
|
|
|
|
|
|
case Media.upload_from_entry(path, entry, "header") do
|
|
|
|
|
|
{:ok, image} ->
|
|
|
|
|
|
Settings.update_theme_settings(%{header_image_id: image.id})
|
|
|
|
|
|
{:ok, image}
|
|
|
|
|
|
|
|
|
|
|
|
{:error, _} = error ->
|
|
|
|
|
|
error
|
|
|
|
|
|
end
|
|
|
|
|
|
end)
|
|
|
|
|
|
|> case do
|
|
|
|
|
|
[image | _] ->
|
|
|
|
|
|
{:noreply, assign(socket, :header_image, image)}
|
|
|
|
|
|
|
|
|
|
|
|
_ ->
|
|
|
|
|
|
{:noreply, socket}
|
|
|
|
|
|
end
|
|
|
|
|
|
else
|
|
|
|
|
|
{:noreply, socket}
|
|
|
|
|
|
end
|
|
|
|
|
|
end
|
|
|
|
|
|
|
feat: add Theme LiveView with preset switching
Implement basic theme editor interface with live preview:
- ThemeLive.Index LiveView with mount and event handlers
- Two-column layout: controls sidebar + preview area
- Display all 9 presets as clickable buttons
- Apply preset and regenerate CSS on click
- Show current theme settings (mood, typography, shape, density, color)
- Preview page switcher (7 pages: home, collection, product, cart, about, contact, 404)
- Inline <style> tag with generated CSS for instant preview
- Basic preview frame showing theme variables in action
- Authentication required via :require_authenticated_user pipeline
- Theme navigation link added to user menu
- 9 comprehensive LiveView tests
All tests passing (197 total).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-30 21:53:52 +00:00
|
|
|
|
@impl true
|
|
|
|
|
|
def handle_event("apply_preset", %{"preset" => preset_name}, socket) do
|
|
|
|
|
|
preset_atom = String.to_existing_atom(preset_name)
|
|
|
|
|
|
|
|
|
|
|
|
case Settings.apply_preset(preset_atom) do
|
|
|
|
|
|
{:ok, theme_settings} ->
|
|
|
|
|
|
generated_css = CSSGenerator.generate(theme_settings)
|
|
|
|
|
|
|
|
|
|
|
|
socket =
|
|
|
|
|
|
socket
|
|
|
|
|
|
|> assign(:theme_settings, theme_settings)
|
|
|
|
|
|
|> assign(:generated_css, generated_css)
|
2025-12-31 18:55:44 +00:00
|
|
|
|
|> assign(:active_preset, preset_atom)
|
feat: add Theme LiveView with preset switching
Implement basic theme editor interface with live preview:
- ThemeLive.Index LiveView with mount and event handlers
- Two-column layout: controls sidebar + preview area
- Display all 9 presets as clickable buttons
- Apply preset and regenerate CSS on click
- Show current theme settings (mood, typography, shape, density, color)
- Preview page switcher (7 pages: home, collection, product, cart, about, contact, 404)
- Inline <style> tag with generated CSS for instant preview
- Basic preview frame showing theme variables in action
- Authentication required via :require_authenticated_user pipeline
- Theme navigation link added to user menu
- 9 comprehensive LiveView tests
All tests passing (197 total).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-30 21:53:52 +00:00
|
|
|
|
|> put_flash(:info, "Applied #{preset_name} preset")
|
|
|
|
|
|
|
|
|
|
|
|
{:noreply, socket}
|
|
|
|
|
|
|
|
|
|
|
|
{:error, _} ->
|
|
|
|
|
|
{:noreply, put_flash(socket, :error, "Failed to apply preset")}
|
|
|
|
|
|
end
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
@impl true
|
|
|
|
|
|
def handle_event("change_preview_page", %{"page" => page_name}, socket) do
|
|
|
|
|
|
page_atom = String.to_existing_atom(page_name)
|
2026-01-02 13:48:03 +00:00
|
|
|
|
|
|
|
|
|
|
socket =
|
|
|
|
|
|
socket
|
|
|
|
|
|
|> assign(:preview_page, page_atom)
|
|
|
|
|
|
|> push_event("scroll-preview-top", %{})
|
|
|
|
|
|
|
|
|
|
|
|
{:noreply, socket}
|
feat: add Theme LiveView with preset switching
Implement basic theme editor interface with live preview:
- ThemeLive.Index LiveView with mount and event handlers
- Two-column layout: controls sidebar + preview area
- Display all 9 presets as clickable buttons
- Apply preset and regenerate CSS on click
- Show current theme settings (mood, typography, shape, density, color)
- Preview page switcher (7 pages: home, collection, product, cart, about, contact, 404)
- Inline <style> tag with generated CSS for instant preview
- Basic preview frame showing theme variables in action
- Authentication required via :require_authenticated_user pipeline
- Theme navigation link added to user menu
- 9 comprehensive LiveView tests
All tests passing (197 total).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-30 21:53:52 +00:00
|
|
|
|
end
|
|
|
|
|
|
|
2025-12-31 00:24:53 +00:00
|
|
|
|
@impl true
|
|
|
|
|
|
def handle_event("update_setting", %{"field" => field, "setting_value" => value}, socket) do
|
|
|
|
|
|
field_atom = String.to_existing_atom(field)
|
|
|
|
|
|
attrs = %{field_atom => value}
|
|
|
|
|
|
|
|
|
|
|
|
case Settings.update_theme_settings(attrs) do
|
|
|
|
|
|
{:ok, theme_settings} ->
|
|
|
|
|
|
generated_css = CSSGenerator.generate(theme_settings)
|
2025-12-31 18:55:44 +00:00
|
|
|
|
active_preset = Presets.detect_preset(theme_settings)
|
2025-12-31 00:24:53 +00:00
|
|
|
|
|
|
|
|
|
|
socket =
|
|
|
|
|
|
socket
|
|
|
|
|
|
|> assign(:theme_settings, theme_settings)
|
|
|
|
|
|
|> assign(:generated_css, generated_css)
|
2025-12-31 18:55:44 +00:00
|
|
|
|
|> assign(:active_preset, active_preset)
|
2025-12-31 00:24:53 +00:00
|
|
|
|
|
|
|
|
|
|
{:noreply, socket}
|
|
|
|
|
|
|
|
|
|
|
|
{:error, _} ->
|
|
|
|
|
|
{:noreply, socket}
|
|
|
|
|
|
end
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
@impl true
|
|
|
|
|
|
def handle_event("update_setting", %{"field" => field} = params, socket) do
|
|
|
|
|
|
# For phx-change events from select/input elements, the value comes from the name attribute
|
|
|
|
|
|
value = params[field] || params["#{field}_text"] || params["value"]
|
|
|
|
|
|
|
|
|
|
|
|
if value do
|
|
|
|
|
|
field_atom = String.to_existing_atom(field)
|
|
|
|
|
|
attrs = %{field_atom => value}
|
|
|
|
|
|
|
|
|
|
|
|
case Settings.update_theme_settings(attrs) do
|
|
|
|
|
|
{:ok, theme_settings} ->
|
|
|
|
|
|
generated_css = CSSGenerator.generate(theme_settings)
|
2025-12-31 18:55:44 +00:00
|
|
|
|
active_preset = Presets.detect_preset(theme_settings)
|
2025-12-31 00:24:53 +00:00
|
|
|
|
|
|
|
|
|
|
socket =
|
|
|
|
|
|
socket
|
|
|
|
|
|
|> assign(:theme_settings, theme_settings)
|
|
|
|
|
|
|> assign(:generated_css, generated_css)
|
2025-12-31 18:55:44 +00:00
|
|
|
|
|> assign(:active_preset, active_preset)
|
2025-12-31 00:24:53 +00:00
|
|
|
|
|
|
|
|
|
|
{:noreply, socket}
|
|
|
|
|
|
|
|
|
|
|
|
{:error, _} ->
|
|
|
|
|
|
{:noreply, socket}
|
|
|
|
|
|
end
|
|
|
|
|
|
else
|
|
|
|
|
|
{:noreply, socket}
|
|
|
|
|
|
end
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
@impl true
|
|
|
|
|
|
def handle_event("update_color", %{"field" => field, "value" => value}, socket) do
|
|
|
|
|
|
field_atom = String.to_existing_atom(field)
|
|
|
|
|
|
attrs = %{field_atom => value}
|
|
|
|
|
|
|
|
|
|
|
|
case Settings.update_theme_settings(attrs) do
|
2026-01-01 16:16:05 +00:00
|
|
|
|
{:ok, theme_settings} ->
|
|
|
|
|
|
generated_css = CSSGenerator.generate(theme_settings)
|
|
|
|
|
|
active_preset = Presets.detect_preset(theme_settings)
|
|
|
|
|
|
|
|
|
|
|
|
socket =
|
|
|
|
|
|
socket
|
|
|
|
|
|
|> assign(:theme_settings, theme_settings)
|
|
|
|
|
|
|> assign(:generated_css, generated_css)
|
|
|
|
|
|
|> assign(:active_preset, active_preset)
|
|
|
|
|
|
|
|
|
|
|
|
{:noreply, socket}
|
|
|
|
|
|
|
|
|
|
|
|
{:error, _} ->
|
|
|
|
|
|
{:noreply, socket}
|
|
|
|
|
|
end
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
@impl true
|
|
|
|
|
|
def handle_event("toggle_setting", %{"field" => field}, socket) do
|
|
|
|
|
|
field_atom = String.to_existing_atom(field)
|
|
|
|
|
|
current_value = Map.get(socket.assigns.theme_settings, field_atom)
|
|
|
|
|
|
attrs = %{field_atom => !current_value}
|
|
|
|
|
|
|
|
|
|
|
|
case Settings.update_theme_settings(attrs) do
|
2025-12-31 00:24:53 +00:00
|
|
|
|
{:ok, theme_settings} ->
|
|
|
|
|
|
generated_css = CSSGenerator.generate(theme_settings)
|
2025-12-31 18:55:44 +00:00
|
|
|
|
active_preset = Presets.detect_preset(theme_settings)
|
2025-12-31 00:24:53 +00:00
|
|
|
|
|
|
|
|
|
|
socket =
|
|
|
|
|
|
socket
|
|
|
|
|
|
|> assign(:theme_settings, theme_settings)
|
|
|
|
|
|
|> assign(:generated_css, generated_css)
|
2025-12-31 18:55:44 +00:00
|
|
|
|
|> assign(:active_preset, active_preset)
|
2025-12-31 00:24:53 +00:00
|
|
|
|
|
|
|
|
|
|
{:noreply, socket}
|
|
|
|
|
|
|
|
|
|
|
|
{:error, _} ->
|
|
|
|
|
|
{:noreply, socket}
|
|
|
|
|
|
end
|
|
|
|
|
|
end
|
|
|
|
|
|
|
feat: add Theme LiveView with preset switching
Implement basic theme editor interface with live preview:
- ThemeLive.Index LiveView with mount and event handlers
- Two-column layout: controls sidebar + preview area
- Display all 9 presets as clickable buttons
- Apply preset and regenerate CSS on click
- Show current theme settings (mood, typography, shape, density, color)
- Preview page switcher (7 pages: home, collection, product, cart, about, contact, 404)
- Inline <style> tag with generated CSS for instant preview
- Basic preview frame showing theme variables in action
- Authentication required via :require_authenticated_user pipeline
- Theme navigation link added to user menu
- 9 comprehensive LiveView tests
All tests passing (197 total).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-30 21:53:52 +00:00
|
|
|
|
@impl true
|
|
|
|
|
|
def handle_event("save_theme", _params, socket) do
|
|
|
|
|
|
socket = put_flash(socket, :info, "Theme saved successfully")
|
|
|
|
|
|
{:noreply, socket}
|
|
|
|
|
|
end
|
2025-12-31 18:55:44 +00:00
|
|
|
|
|
|
|
|
|
|
@impl true
|
|
|
|
|
|
def handle_event("remove_logo", _params, socket) do
|
|
|
|
|
|
if logo = socket.assigns.logo_image do
|
|
|
|
|
|
Media.delete_image(logo)
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
Settings.update_theme_settings(%{logo_image_id: nil})
|
|
|
|
|
|
|
|
|
|
|
|
socket =
|
|
|
|
|
|
socket
|
|
|
|
|
|
|> assign(:logo_image, nil)
|
|
|
|
|
|
|> put_flash(:info, "Logo removed")
|
|
|
|
|
|
|
|
|
|
|
|
{:noreply, socket}
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
@impl true
|
|
|
|
|
|
def handle_event("remove_header", _params, socket) do
|
|
|
|
|
|
if header = socket.assigns.header_image do
|
|
|
|
|
|
Media.delete_image(header)
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
Settings.update_theme_settings(%{header_image_id: nil})
|
|
|
|
|
|
|
|
|
|
|
|
socket =
|
|
|
|
|
|
socket
|
|
|
|
|
|
|> assign(:header_image, nil)
|
|
|
|
|
|
|> put_flash(:info, "Header image removed")
|
|
|
|
|
|
|
|
|
|
|
|
{:noreply, socket}
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
@impl true
|
|
|
|
|
|
def handle_event("cancel_upload", %{"ref" => ref, "upload" => upload_name}, socket) do
|
|
|
|
|
|
upload_atom = String.to_existing_atom(upload_name)
|
|
|
|
|
|
{:noreply, cancel_upload(socket, upload_atom, ref)}
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
@impl true
|
|
|
|
|
|
def handle_event("toggle_customise", _params, socket) do
|
|
|
|
|
|
{:noreply, assign(socket, :customise_open, !socket.assigns.customise_open)}
|
|
|
|
|
|
end
|
|
|
|
|
|
|
2026-01-19 21:37:34 +00:00
|
|
|
|
@impl true
|
|
|
|
|
|
def handle_event("toggle_sidebar", _params, socket) do
|
|
|
|
|
|
{:noreply, assign(socket, :sidebar_collapsed, !socket.assigns.sidebar_collapsed)}
|
|
|
|
|
|
end
|
|
|
|
|
|
|
2026-02-05 22:11:16 +00:00
|
|
|
|
@impl true
|
|
|
|
|
|
def handle_event("open_cart_drawer", _params, socket) do
|
|
|
|
|
|
{:noreply, assign(socket, :cart_drawer_open, true)}
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
@impl true
|
|
|
|
|
|
def handle_event("close_cart_drawer", _params, socket) do
|
|
|
|
|
|
{:noreply, assign(socket, :cart_drawer_open, false)}
|
|
|
|
|
|
end
|
|
|
|
|
|
|
2025-12-31 18:55:44 +00:00
|
|
|
|
@impl true
|
|
|
|
|
|
def handle_event("noop", _params, socket) do
|
|
|
|
|
|
{:noreply, socket}
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
def error_to_string(:too_large), do: "File is too large"
|
|
|
|
|
|
def error_to_string(:too_many_files), do: "Too many files"
|
|
|
|
|
|
def error_to_string(:not_accepted), do: "File type not accepted"
|
|
|
|
|
|
def error_to_string(err), do: inspect(err)
|
2026-01-17 22:17:59 +00:00
|
|
|
|
|
2026-02-08 12:12:39 +00:00
|
|
|
|
defp preview_assigns(assigns) do
|
|
|
|
|
|
assign(assigns, %{
|
|
|
|
|
|
mode: :preview,
|
2026-02-13 08:27:26 +00:00
|
|
|
|
products: assigns.preview_data.products,
|
|
|
|
|
|
categories: assigns.preview_data.categories,
|
2026-02-08 12:12:39 +00:00
|
|
|
|
cart_items: PreviewData.cart_drawer_items(),
|
|
|
|
|
|
cart_count: 2,
|
|
|
|
|
|
cart_subtotal: "£72.00"
|
|
|
|
|
|
})
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
# Preview page component — delegates to shared PageTemplates with preview-specific assigns
|
2026-01-17 22:17:59 +00:00
|
|
|
|
attr :page, :atom, required: true
|
|
|
|
|
|
attr :preview_data, :map, required: true
|
|
|
|
|
|
attr :theme_settings, :map, required: true
|
|
|
|
|
|
attr :logo_image, :any, required: true
|
|
|
|
|
|
attr :header_image, :any, required: true
|
2026-02-05 22:11:16 +00:00
|
|
|
|
attr :cart_drawer_open, :boolean, default: false
|
2026-01-17 22:17:59 +00:00
|
|
|
|
|
|
|
|
|
|
defp preview_page(%{page: :home} = assigns) do
|
2026-02-08 12:12:39 +00:00
|
|
|
|
assigns = preview_assigns(assigns)
|
2026-02-18 21:23:15 +00:00
|
|
|
|
~H"<BerrypodWeb.PageTemplates.home {assigns} />"
|
2026-01-17 22:17:59 +00:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
defp preview_page(%{page: :collection} = assigns) do
|
2026-02-08 12:12:39 +00:00
|
|
|
|
assigns = preview_assigns(assigns)
|
2026-02-18 21:23:15 +00:00
|
|
|
|
~H"<BerrypodWeb.PageTemplates.collection {assigns} />"
|
2026-01-17 22:17:59 +00:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
defp preview_page(%{page: :pdp} = assigns) do
|
|
|
|
|
|
product = List.first(assigns.preview_data.products)
|
2026-02-13 07:29:19 +00:00
|
|
|
|
option_types = Map.get(product, :option_types) || []
|
|
|
|
|
|
variants = Map.get(product, :variants) || []
|
2026-02-03 22:17:48 +00:00
|
|
|
|
|
|
|
|
|
|
{selected_options, selected_variant} =
|
|
|
|
|
|
case variants do
|
|
|
|
|
|
[first | _] -> {first.options, first}
|
|
|
|
|
|
[] -> {%{}, nil}
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
available_options =
|
|
|
|
|
|
Enum.reduce(option_types, %{}, fn opt, acc ->
|
|
|
|
|
|
values = Enum.map(opt.values, & &1.title)
|
|
|
|
|
|
Map.put(acc, opt.name, values)
|
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
|
|
display_price =
|
add denormalized product fields and use Product structs throughout
Adds cheapest_price, compare_at_price, in_stock, on_sale columns to
products table (recomputed from variants after each sync). Shop
components now work with Product structs directly instead of plain
maps from PreviewData. Renames .name to .title, adds Product display
helpers (primary_image, hover_image, option_types) and ProductImage
helpers (display_url, direct_url, source_width). Adds Products context
query functions for storefront use (list_visible_products,
get_visible_product, list_categories with DB-level sort/filter).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 01:26:39 +00:00
|
|
|
|
if selected_variant, do: selected_variant.price, else: product.cheapest_price
|
2026-01-17 22:17:59 +00:00
|
|
|
|
|
|
|
|
|
|
assigns =
|
|
|
|
|
|
assigns
|
2026-02-08 12:12:39 +00:00
|
|
|
|
|> preview_assigns()
|
2026-01-17 22:17:59 +00:00
|
|
|
|
|> assign(:product, product)
|
|
|
|
|
|
|> assign(:gallery_images, build_gallery_images(product))
|
|
|
|
|
|
|> assign(:related_products, Enum.slice(assigns.preview_data.products, 1, 4))
|
2026-02-03 22:17:48 +00:00
|
|
|
|
|> assign(:option_types, option_types)
|
|
|
|
|
|
|> assign(:selected_options, selected_options)
|
|
|
|
|
|
|> assign(:available_options, available_options)
|
|
|
|
|
|
|> assign(:display_price, display_price)
|
2026-02-08 12:12:39 +00:00
|
|
|
|
|> assign(:quantity, 1)
|
2026-01-17 22:17:59 +00:00
|
|
|
|
|
2026-02-18 21:23:15 +00:00
|
|
|
|
~H"<BerrypodWeb.PageTemplates.pdp {assigns} />"
|
2026-01-17 22:17:59 +00:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
defp preview_page(%{page: :cart} = assigns) do
|
|
|
|
|
|
cart_items = assigns.preview_data.cart_items
|
2026-01-31 14:24:58 +00:00
|
|
|
|
|
|
|
|
|
|
subtotal =
|
add denormalized product fields and use Product structs throughout
Adds cheapest_price, compare_at_price, in_stock, on_sale columns to
products table (recomputed from variants after each sync). Shop
components now work with Product structs directly instead of plain
maps from PreviewData. Renames .name to .title, adds Product display
helpers (primary_image, hover_image, option_types) and ProductImage
helpers (display_url, direct_url, source_width). Adds Products context
query functions for storefront use (list_visible_products,
get_visible_product, list_categories with DB-level sort/filter).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 01:26:39 +00:00
|
|
|
|
Enum.reduce(cart_items, 0, fn item, acc ->
|
|
|
|
|
|
acc + item.product.cheapest_price * item.quantity
|
|
|
|
|
|
end)
|
2026-01-17 22:17:59 +00:00
|
|
|
|
|
|
|
|
|
|
assigns =
|
|
|
|
|
|
assigns
|
2026-02-08 12:12:39 +00:00
|
|
|
|
|> preview_assigns()
|
2026-01-17 22:17:59 +00:00
|
|
|
|
|> assign(:cart_page_items, cart_items)
|
|
|
|
|
|
|> assign(:cart_page_subtotal, subtotal)
|
|
|
|
|
|
|
2026-02-18 21:23:15 +00:00
|
|
|
|
~H"<BerrypodWeb.PageTemplates.cart {assigns} />"
|
2026-01-17 22:17:59 +00:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
defp preview_page(%{page: :about} = assigns) do
|
2026-02-08 12:12:39 +00:00
|
|
|
|
assigns =
|
|
|
|
|
|
assigns
|
|
|
|
|
|
|> preview_assigns()
|
|
|
|
|
|
|> assign(%{
|
|
|
|
|
|
active_page: "about",
|
|
|
|
|
|
hero_title: "About the studio",
|
|
|
|
|
|
hero_description: "Your story goes here – this is sample content for the demo shop",
|
|
|
|
|
|
hero_background: :sunken,
|
|
|
|
|
|
image_src: "/mockups/night-sky-blanket-3",
|
|
|
|
|
|
image_alt: "Night sky blanket draped over a chair",
|
|
|
|
|
|
content_blocks: PreviewData.about_content()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-02-18 21:23:15 +00:00
|
|
|
|
~H"<BerrypodWeb.PageTemplates.content {assigns} />"
|
2026-02-08 10:47:54 +00:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
defp preview_page(%{page: :delivery} = assigns) do
|
2026-02-08 12:12:39 +00:00
|
|
|
|
assigns =
|
|
|
|
|
|
assigns
|
|
|
|
|
|
|> preview_assigns()
|
|
|
|
|
|
|> assign(%{
|
|
|
|
|
|
active_page: "delivery",
|
|
|
|
|
|
hero_title: "Delivery & returns",
|
|
|
|
|
|
hero_description: "Everything you need to know about shipping and returns",
|
|
|
|
|
|
content_blocks: PreviewData.delivery_content()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-02-18 21:23:15 +00:00
|
|
|
|
~H"<BerrypodWeb.PageTemplates.content {assigns} />"
|
2026-02-08 10:47:54 +00:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
defp preview_page(%{page: :privacy} = assigns) do
|
2026-02-08 12:12:39 +00:00
|
|
|
|
assigns =
|
|
|
|
|
|
assigns
|
|
|
|
|
|
|> preview_assigns()
|
|
|
|
|
|
|> assign(%{
|
|
|
|
|
|
active_page: "privacy",
|
|
|
|
|
|
hero_title: "Privacy policy",
|
|
|
|
|
|
hero_description: "How we handle your personal information",
|
|
|
|
|
|
content_blocks: PreviewData.privacy_content()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-02-18 21:23:15 +00:00
|
|
|
|
~H"<BerrypodWeb.PageTemplates.content {assigns} />"
|
2026-02-08 10:47:54 +00:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
defp preview_page(%{page: :terms} = assigns) do
|
2026-02-08 12:12:39 +00:00
|
|
|
|
assigns =
|
|
|
|
|
|
assigns
|
|
|
|
|
|
|> preview_assigns()
|
|
|
|
|
|
|> assign(%{
|
|
|
|
|
|
active_page: "terms",
|
|
|
|
|
|
hero_title: "Terms of service",
|
|
|
|
|
|
hero_description: "The legal bits",
|
|
|
|
|
|
content_blocks: PreviewData.terms_content()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-02-18 21:23:15 +00:00
|
|
|
|
~H"<BerrypodWeb.PageTemplates.content {assigns} />"
|
2026-01-17 22:17:59 +00:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
defp preview_page(%{page: :contact} = assigns) do
|
2026-02-08 12:12:39 +00:00
|
|
|
|
assigns = preview_assigns(assigns)
|
2026-02-18 21:23:15 +00:00
|
|
|
|
~H"<BerrypodWeb.PageTemplates.contact {assigns} />"
|
2026-01-17 22:17:59 +00:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
defp preview_page(%{page: :error} = assigns) do
|
2026-02-08 12:12:39 +00:00
|
|
|
|
assigns =
|
|
|
|
|
|
assigns
|
|
|
|
|
|
|> preview_assigns()
|
|
|
|
|
|
|> assign(%{
|
|
|
|
|
|
error_code: "404",
|
|
|
|
|
|
error_title: "Page Not Found",
|
|
|
|
|
|
error_description:
|
|
|
|
|
|
"Sorry, we couldn't find the page you're looking for. Perhaps you've mistyped the URL or the page has been moved."
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-02-18 21:23:15 +00:00
|
|
|
|
~H"<BerrypodWeb.PageTemplates.error {assigns} />"
|
2026-01-17 22:17:59 +00:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
defp build_gallery_images(product) do
|
2026-02-18 21:23:15 +00:00
|
|
|
|
alias Berrypod.Products.ProductImage
|
add denormalized product fields and use Product structs throughout
Adds cheapest_price, compare_at_price, in_stock, on_sale columns to
products table (recomputed from variants after each sync). Shop
components now work with Product structs directly instead of plain
maps from PreviewData. Renames .name to .title, adds Product display
helpers (primary_image, hover_image, option_types) and ProductImage
helpers (display_url, direct_url, source_width). Adds Products context
query functions for storefront use (list_visible_products,
get_visible_product, list_categories with DB-level sort/filter).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 01:26:39 +00:00
|
|
|
|
|
2026-02-13 07:29:19 +00:00
|
|
|
|
(Map.get(product, :images) || [])
|
add denormalized product fields and use Product structs throughout
Adds cheapest_price, compare_at_price, in_stock, on_sale columns to
products table (recomputed from variants after each sync). Shop
components now work with Product structs directly instead of plain
maps from PreviewData. Renames .name to .title, adds Product display
helpers (primary_image, hover_image, option_types) and ProductImage
helpers (display_url, direct_url, source_width). Adds Products context
query functions for storefront use (list_visible_products,
get_visible_product, list_categories with DB-level sort/filter).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 01:26:39 +00:00
|
|
|
|
|> Enum.sort_by(& &1.position)
|
2026-02-16 17:47:41 +00:00
|
|
|
|
|> Enum.map(fn img -> ProductImage.url(img, 1200) end)
|
add denormalized product fields and use Product structs throughout
Adds cheapest_price, compare_at_price, in_stock, on_sale columns to
products table (recomputed from variants after each sync). Shop
components now work with Product structs directly instead of plain
maps from PreviewData. Renames .name to .title, adds Product display
helpers (primary_image, hover_image, option_types) and ProductImage
helpers (display_url, direct_url, source_width). Adds Products context
query functions for storefront use (list_visible_products,
get_visible_product, list_categories with DB-level sort/filter).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 01:26:39 +00:00
|
|
|
|
|> Enum.reject(&is_nil/1)
|
|
|
|
|
|
|> case do
|
|
|
|
|
|
[] -> []
|
|
|
|
|
|
urls -> urls
|
|
|
|
|
|
end
|
2026-01-17 22:17:59 +00:00
|
|
|
|
end
|
feat: add Theme LiveView with preset switching
Implement basic theme editor interface with live preview:
- ThemeLive.Index LiveView with mount and event handlers
- Two-column layout: controls sidebar + preview area
- Display all 9 presets as clickable buttons
- Apply preset and regenerate CSS on click
- Show current theme settings (mood, typography, shape, density, color)
- Preview page switcher (7 pages: home, collection, product, cart, about, contact, 404)
- Inline <style> tag with generated CSS for instant preview
- Basic preview frame showing theme variables in action
- Authentication required via :require_authenticated_user pipeline
- Theme navigation link added to user menu
- 9 comprehensive LiveView tests
All tests passing (197 total).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-30 21:53:52 +00:00
|
|
|
|
end
|