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-26 19:32:50 +00:00
|
|
|
alias Berrypod.{Pages, Settings}
|
2026-02-18 21:23:15 +00:00
|
|
|
alias Berrypod.Media
|
|
|
|
|
alias Berrypod.Theme.{CSSGenerator, Presets, PreviewData}
|
add favicon and site icon generation from uploaded images
Upload a source image (PNG, JPEG, or SVG) and get a complete favicon
setup: PNG variants at 32, 180, 192, 512px served from DB via
FaviconController with ETag caching, SVG favicon for vector sources,
dynamic site.webmanifest, and theme-color meta tag. Theme editor gains
a site icon section with "use logo as icon" toggle, dedicated icon
upload, short name, and background colour picker.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 17:22:15 +00:00
|
|
|
alias Berrypod.Workers.FaviconGeneratorWorker
|
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()
|
add favicon and site icon generation from uploaded images
Upload a source image (PNG, JPEG, or SVG) and get a complete favicon
setup: PNG variants at 32, 180, 192, 512px served from DB via
FaviconController with ETag caching, SVG favicon for vector sources,
dynamic site.webmanifest, and theme-color meta tag. Theme editor gains
a site icon section with "use logo as icon" toggle, dedicated icon
upload, short name, and background colour picker.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 17:22:15 +00:00
|
|
|
icon_image = Media.get_icon()
|
2025-12-31 18:55:44 +00:00
|
|
|
|
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)
|
add favicon and site icon generation from uploaded images
Upload a source image (PNG, JPEG, or SVG) and get a complete favicon
setup: PNG variants at 32, 180, 192, 512px served from DB via
FaviconController with ETag caching, SVG favicon for vector sources,
dynamic site.webmanifest, and theme-color meta tag. Theme editor gains
a site icon section with "use logo as icon" toggle, dedicated icon
upload, short name, and background colour picker.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 17:22:15 +00:00
|
|
|
|> assign(:icon_image, icon_image)
|
2025-12-31 18:55:44 +00:00
|
|
|
|> 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
|
|
|
|
|
)
|
add favicon and site icon generation from uploaded images
Upload a source image (PNG, JPEG, or SVG) and get a complete favicon
setup: PNG variants at 32, 180, 192, 512px served from DB via
FaviconController with ETag caching, SVG favicon for vector sources,
dynamic site.webmanifest, and theme-color meta tag. Theme editor gains
a site icon section with "use logo as icon" toggle, dedicated icon
upload, short name, and background colour picker.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 17:22:15 +00:00
|
|
|
|> allow_upload(:icon_upload,
|
|
|
|
|
accept: ~w(.png .jpg .jpeg .webp .svg),
|
|
|
|
|
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 | _] ->
|
add favicon and site icon generation from uploaded images
Upload a source image (PNG, JPEG, or SVG) and get a complete favicon
setup: PNG variants at 32, 180, 192, 512px served from DB via
FaviconController with ETag caching, SVG favicon for vector sources,
dynamic site.webmanifest, and theme-color meta tag. Theme editor gains
a site icon section with "use logo as icon" toggle, dedicated icon
upload, short name, and background colour picker.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 17:22:15 +00:00
|
|
|
# Trigger favicon generation if using logo as icon
|
|
|
|
|
if socket.assigns.theme_settings.use_logo_as_icon do
|
|
|
|
|
enqueue_favicon_generation(image.id)
|
|
|
|
|
end
|
|
|
|
|
|
2025-12-31 18:55:44 +00:00
|
|
|
{: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
|
|
|
|
|
|
add favicon and site icon generation from uploaded images
Upload a source image (PNG, JPEG, or SVG) and get a complete favicon
setup: PNG variants at 32, 180, 192, 512px served from DB via
FaviconController with ETag caching, SVG favicon for vector sources,
dynamic site.webmanifest, and theme-color meta tag. Theme editor gains
a site icon section with "use logo as icon" toggle, dedicated icon
upload, short name, and background colour picker.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 17:22:15 +00:00
|
|
|
defp handle_progress(:icon_upload, entry, socket) do
|
|
|
|
|
if entry.done? do
|
|
|
|
|
consume_uploaded_entries(socket, :icon_upload, fn %{path: path}, entry ->
|
|
|
|
|
case Media.upload_from_entry(path, entry, "icon") do
|
|
|
|
|
{:ok, image} ->
|
|
|
|
|
Settings.update_theme_settings(%{icon_image_id: image.id})
|
|
|
|
|
{:ok, image}
|
|
|
|
|
|
|
|
|
|
{:error, _} = error ->
|
|
|
|
|
error
|
|
|
|
|
end
|
|
|
|
|
end)
|
|
|
|
|
|> case do
|
|
|
|
|
[image | _] ->
|
|
|
|
|
enqueue_favicon_generation(image.id)
|
|
|
|
|
{:noreply, assign(socket, :icon_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
|
|
|
|
add favicon and site icon generation from uploaded images
Upload a source image (PNG, JPEG, or SVG) and get a complete favicon
setup: PNG variants at 32, 180, 192, 512px served from DB via
FaviconController with ETag caching, SVG favicon for vector sources,
dynamic site.webmanifest, and theme-color meta tag. Theme editor gains
a site icon section with "use logo as icon" toggle, dedicated icon
upload, short name, and background colour picker.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 17:22:15 +00:00
|
|
|
# Trigger favicon regeneration when the icon source changes
|
|
|
|
|
if field_atom == :use_logo_as_icon do
|
|
|
|
|
maybe_enqueue_favicon_from_settings(theme_settings, socket.assigns)
|
|
|
|
|
end
|
|
|
|
|
|
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
|
|
|
|
2026-02-28 17:55:02 +00:00
|
|
|
@impl true
|
|
|
|
|
def handle_event("update_image_alt", %{"image-id" => image_id, "alt" => alt}, socket) do
|
|
|
|
|
case Media.get_image(image_id) do
|
|
|
|
|
nil ->
|
|
|
|
|
{:noreply, socket}
|
|
|
|
|
|
|
|
|
|
image ->
|
|
|
|
|
{:ok, updated} = Media.update_image_metadata(image, %{alt: alt})
|
|
|
|
|
|
|
|
|
|
# Refresh the relevant assign so the template sees the new alt text
|
|
|
|
|
socket =
|
|
|
|
|
cond do
|
|
|
|
|
socket.assigns.logo_image && socket.assigns.logo_image.id == image_id ->
|
|
|
|
|
assign(socket, :logo_image, updated)
|
|
|
|
|
|
|
|
|
|
socket.assigns.header_image && socket.assigns.header_image.id == image_id ->
|
|
|
|
|
assign(socket, :header_image, updated)
|
|
|
|
|
|
|
|
|
|
socket.assigns[:icon_image] && socket.assigns.icon_image &&
|
|
|
|
|
socket.assigns.icon_image.id == image_id ->
|
|
|
|
|
assign(socket, :icon_image, updated)
|
|
|
|
|
|
|
|
|
|
true ->
|
|
|
|
|
socket
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
{:noreply, socket}
|
|
|
|
|
end
|
|
|
|
|
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
|
|
|
|
|
|
add favicon and site icon generation from uploaded images
Upload a source image (PNG, JPEG, or SVG) and get a complete favicon
setup: PNG variants at 32, 180, 192, 512px served from DB via
FaviconController with ETag caching, SVG favicon for vector sources,
dynamic site.webmanifest, and theme-color meta tag. Theme editor gains
a site icon section with "use logo as icon" toggle, dedicated icon
upload, short name, and background colour picker.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 17:22:15 +00:00
|
|
|
@impl true
|
|
|
|
|
def handle_event("remove_icon", _params, socket) do
|
|
|
|
|
if icon = socket.assigns.icon_image do
|
|
|
|
|
Media.delete_image(icon)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
Settings.update_theme_settings(%{icon_image_id: nil})
|
|
|
|
|
|
|
|
|
|
socket =
|
|
|
|
|
socket
|
|
|
|
|
|> assign(:icon_image, nil)
|
|
|
|
|
|> put_flash(:info, "Icon removed")
|
|
|
|
|
|
|
|
|
|
{:noreply, socket}
|
|
|
|
|
end
|
|
|
|
|
|
2025-12-31 18:55:44 +00:00
|
|
|
@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
|
|
|
|
add favicon and site icon generation from uploaded images
Upload a source image (PNG, JPEG, or SVG) and get a complete favicon
setup: PNG variants at 32, 180, 192, 512px served from DB via
FaviconController with ETag caching, SVG favicon for vector sources,
dynamic site.webmanifest, and theme-color meta tag. Theme editor gains
a site icon section with "use logo as icon" toggle, dedicated icon
upload, short name, and background colour picker.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 17:22:15 +00:00
|
|
|
defp enqueue_favicon_generation(source_image_id) do
|
|
|
|
|
%{source_image_id: source_image_id}
|
|
|
|
|
|> FaviconGeneratorWorker.new()
|
|
|
|
|
|> Oban.insert()
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
defp maybe_enqueue_favicon_from_settings(theme_settings, assigns) do
|
|
|
|
|
source_id =
|
|
|
|
|
if theme_settings.use_logo_as_icon do
|
|
|
|
|
case assigns.logo_image do
|
|
|
|
|
%{id: id} -> id
|
|
|
|
|
_ -> nil
|
|
|
|
|
end
|
|
|
|
|
else
|
|
|
|
|
case assigns.icon_image do
|
|
|
|
|
%{id: id} -> id
|
|
|
|
|
_ -> nil
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if source_id, do: enqueue_favicon_generation(source_id)
|
|
|
|
|
end
|
|
|
|
|
|
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,
|
2026-02-28 11:18:37 +00:00
|
|
|
cart_subtotal: "£72.00",
|
|
|
|
|
header_nav_items: BerrypodWeb.ThemeHook.default_header_nav(),
|
|
|
|
|
footer_nav_items: BerrypodWeb.ThemeHook.default_footer_nav()
|
2026-02-08 12:12:39 +00:00
|
|
|
})
|
|
|
|
|
end
|
|
|
|
|
|
2026-02-26 19:32:50 +00:00
|
|
|
# Unified preview — loads page definition, applies context, renders via PageRenderer
|
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
|
|
|
|
2026-02-26 19:32:50 +00:00
|
|
|
defp preview_page(assigns) do
|
|
|
|
|
slug = to_string(assigns.page)
|
|
|
|
|
page = Pages.get_page(slug)
|
|
|
|
|
|
|
|
|
|
assigns =
|
|
|
|
|
assigns
|
|
|
|
|
|> preview_assigns()
|
|
|
|
|
|> assign(:page, page)
|
|
|
|
|
|> preview_page_context(slug)
|
2026-01-17 22:17:59 +00:00
|
|
|
|
2026-02-26 19:32:50 +00:00
|
|
|
extra = Pages.load_block_data(page.blocks, assigns)
|
|
|
|
|
assigns = assign(assigns, extra)
|
|
|
|
|
|
|
|
|
|
~H"<BerrypodWeb.PageRenderer.render_page {assigns} />"
|
2026-01-17 22:17:59 +00:00
|
|
|
end
|
|
|
|
|
|
2026-02-26 19:32:50 +00:00
|
|
|
# Page-context data needed by specific page types in preview mode
|
|
|
|
|
defp preview_page_context(assigns, "pdp") do
|
2026-01-17 22:17:59 +00:00
|
|
|
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
|
|
|
|
2026-02-26 19:32:50 +00:00
|
|
|
assigns
|
|
|
|
|
|> assign(:product, product)
|
|
|
|
|
|> assign(:gallery_images, build_gallery_images(product))
|
|
|
|
|
|> assign(:option_types, option_types)
|
|
|
|
|
|> assign(:selected_options, selected_options)
|
|
|
|
|
|> assign(:available_options, available_options)
|
|
|
|
|
|> assign(:display_price, display_price)
|
|
|
|
|
|> assign(:quantity, 1)
|
|
|
|
|
|> assign(:option_urls, %{})
|
2026-01-17 22:17:59 +00:00
|
|
|
end
|
|
|
|
|
|
2026-02-26 19:32:50 +00:00
|
|
|
defp preview_page_context(assigns, "cart") do
|
2026-01-17 22:17:59 +00:00
|
|
|
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
|
|
|
|
2026-02-26 19:32:50 +00:00
|
|
|
assigns
|
|
|
|
|
|> assign(:cart_page_items, cart_items)
|
|
|
|
|
|> assign(:cart_page_subtotal, subtotal)
|
2026-01-17 22:17:59 +00:00
|
|
|
end
|
|
|
|
|
|
2026-02-26 19:32:50 +00:00
|
|
|
defp preview_page_context(assigns, "about") do
|
|
|
|
|
assign(assigns, :content_blocks, PreviewData.about_content())
|
2026-02-08 10:47:54 +00:00
|
|
|
end
|
|
|
|
|
|
2026-02-26 19:32:50 +00:00
|
|
|
defp preview_page_context(assigns, "delivery") do
|
|
|
|
|
assign(assigns, :content_blocks, PreviewData.delivery_content())
|
2026-02-08 10:47:54 +00:00
|
|
|
end
|
|
|
|
|
|
2026-02-26 19:32:50 +00:00
|
|
|
defp preview_page_context(assigns, "privacy") do
|
|
|
|
|
assign(assigns, :content_blocks, PreviewData.privacy_content())
|
2026-02-08 10:47:54 +00:00
|
|
|
end
|
|
|
|
|
|
2026-02-26 19:32:50 +00:00
|
|
|
defp preview_page_context(assigns, "terms") do
|
|
|
|
|
assign(assigns, :content_blocks, PreviewData.terms_content())
|
2026-01-17 22:17:59 +00:00
|
|
|
end
|
|
|
|
|
|
2026-02-26 19:32:50 +00:00
|
|
|
defp preview_page_context(assigns, "error") do
|
|
|
|
|
assign(assigns, %{
|
|
|
|
|
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-01-17 22:17:59 +00:00
|
|
|
end
|
|
|
|
|
|
2026-02-26 19:32:50 +00:00
|
|
|
defp preview_page_context(assigns, _slug), do: assigns
|
2026-01-17 22:17:59 +00:00
|
|
|
|
|
|
|
|
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
|