feat: add dark mode support, accordion UI, and current combination display
- Update Theme Studio sidebar to use DaisyUI theme-aware classes for dark mode - Convert Customise accordion to native details/summary elements for proper interaction - Add "Current combination" card showing active theme settings - Add SVG recolorer for logo color customization - Add image controller for serving uploaded images - Implement header background image controls (zoom, position) - Add toggle_customise event handler to preserve accordion state across re-renders 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
93
lib/simpleshop_theme_web/controllers/image_controller.ex
Normal file
93
lib/simpleshop_theme_web/controllers/image_controller.ex
Normal file
@@ -0,0 +1,93 @@
|
||||
defmodule SimpleshopThemeWeb.ImageController do
|
||||
use SimpleshopThemeWeb, :controller
|
||||
|
||||
alias SimpleshopTheme.Media
|
||||
alias SimpleshopTheme.Media.SVGRecolorer
|
||||
|
||||
@doc """
|
||||
Serves an image from the database by ID.
|
||||
|
||||
Images are served with aggressive caching headers since they are
|
||||
immutable once uploaded.
|
||||
"""
|
||||
def show(conn, %{"id" => id}) do
|
||||
case Media.get_image(id) do
|
||||
nil ->
|
||||
send_resp(conn, 404, "Image not found")
|
||||
|
||||
image ->
|
||||
conn
|
||||
|> put_resp_content_type(image.content_type)
|
||||
|> put_resp_header("cache-control", "public, max-age=31536000, immutable")
|
||||
|> put_resp_header("etag", ~s("#{image.id}"))
|
||||
|> send_resp(200, image.data)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Serves a thumbnail of an image if available, otherwise falls back to full image.
|
||||
"""
|
||||
def thumbnail(conn, %{"id" => id}) do
|
||||
case Media.get_image(id) do
|
||||
nil ->
|
||||
send_resp(conn, 404, "Image not found")
|
||||
|
||||
%{thumbnail_data: thumbnail_data} = image when not is_nil(thumbnail_data) ->
|
||||
conn
|
||||
|> put_resp_content_type("image/jpeg")
|
||||
|> put_resp_header("cache-control", "public, max-age=31536000, immutable")
|
||||
|> put_resp_header("etag", ~s("#{image.id}-thumb"))
|
||||
|> send_resp(200, thumbnail_data)
|
||||
|
||||
image ->
|
||||
conn
|
||||
|> put_resp_content_type(image.content_type)
|
||||
|> put_resp_header("cache-control", "public, max-age=31536000, immutable")
|
||||
|> put_resp_header("etag", ~s("#{image.id}"))
|
||||
|> send_resp(200, image.data)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Serves an SVG image recolored with the specified color.
|
||||
|
||||
The color should be a hex color code (with or without the leading #).
|
||||
Only works with SVG images.
|
||||
"""
|
||||
def recolored_svg(conn, %{"id" => id, "color" => color}) do
|
||||
clean_color = normalize_color(color)
|
||||
|
||||
with true <- SVGRecolorer.valid_hex_color?(clean_color),
|
||||
%{is_svg: true, svg_content: svg} when not is_nil(svg) <- Media.get_image(id) do
|
||||
recolored = SVGRecolorer.recolor(svg, clean_color)
|
||||
|
||||
conn
|
||||
|> put_resp_content_type("image/svg+xml")
|
||||
|> put_resp_header("cache-control", "public, max-age=3600")
|
||||
|> put_resp_header("etag", ~s("#{id}-#{clean_color}"))
|
||||
|> send_resp(200, recolored)
|
||||
else
|
||||
false ->
|
||||
send_resp(conn, 400, "Invalid color format")
|
||||
|
||||
nil ->
|
||||
send_resp(conn, 404, "Image not found")
|
||||
|
||||
%{is_svg: false} ->
|
||||
send_resp(conn, 400, "Image is not an SVG")
|
||||
|
||||
%{svg_content: nil} ->
|
||||
send_resp(conn, 400, "SVG content not available")
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_color(color) do
|
||||
color = String.trim(color)
|
||||
|
||||
if String.starts_with?(color, "#") do
|
||||
color
|
||||
else
|
||||
"#" <> color
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user