chore: apply mix format to codebase

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
jamey 2026-01-31 14:24:58 +00:00
parent d97918d66a
commit 336b2bb81d
43 changed files with 2227 additions and 1204 deletions

View File

@ -194,7 +194,9 @@ defmodule Mix.Tasks.GenerateMockups do
""") """)
if failed > 0 do if failed > 0 do
Mix.shell().error("Some products failed to generate. Check the output above for details.") Mix.shell().error(
"Some products failed to generate. Check the output above for details."
)
end end
end end
end end

View File

@ -31,7 +31,8 @@ defmodule SimpleshopTheme.Images.Optimizer do
{width, _height, _} <- Image.shape(image), {width, _height, _} <- Image.shape(image),
{:ok, resized} <- maybe_resize(image, width), {:ok, resized} <- maybe_resize(image, width),
{final_width, final_height, _} <- Image.shape(resized), {final_width, final_height, _} <- Image.shape(resized),
{:ok, webp_data} <- Image.write(resized, :memory, suffix: ".webp", quality: @storage_quality) do {:ok, webp_data} <-
Image.write(resized, :memory, suffix: ".webp", quality: @storage_quality) do
{:ok, webp_data, final_width, final_height} {:ok, webp_data, final_width, final_height}
end end
rescue rescue
@ -191,9 +192,20 @@ defmodule SimpleshopTheme.Images.Optimizer do
widths = applicable_widths(source_width) widths = applicable_widths(source_width)
tasks = [ tasks = [
Task.async(fn -> generate_variant_to_dir(vips_image, output_basename, output_dir, "thumb", :jpg, @thumb_size) end) Task.async(fn ->
generate_variant_to_dir(
vips_image,
output_basename,
output_dir,
"thumb",
:jpg,
@thumb_size
)
end)
| for w <- widths, fmt <- @pregenerated_formats do | for w <- widths, fmt <- @pregenerated_formats do
Task.async(fn -> generate_variant_to_dir(vips_image, output_basename, output_dir, w, fmt, w) end) Task.async(fn ->
generate_variant_to_dir(vips_image, output_basename, output_dir, w, fmt, w)
end)
end end
] ]

View File

@ -59,7 +59,9 @@ defmodule SimpleshopTheme.Images.VariantCache do
if to_process == [] do if to_process == [] do
Logger.info("[VariantCache] All database image variants up to date") Logger.info("[VariantCache] All database image variants up to date")
else else
Logger.info("[VariantCache] Enqueueing #{length(to_process)} database images for processing") Logger.info(
"[VariantCache] Enqueueing #{length(to_process)} database images for processing"
)
Enum.each(to_process, fn image -> Enum.each(to_process, fn image ->
image image

View File

@ -119,7 +119,12 @@ defmodule SimpleshopTheme.Media do
""" """
def get_logo do def get_logo do
Repo.one(from i in ImageSchema, where: i.image_type == "logo", order_by: [desc: i.inserted_at], limit: 1) Repo.one(
from i in ImageSchema,
where: i.image_type == "logo",
order_by: [desc: i.inserted_at],
limit: 1
)
end end
@doc """ @doc """
@ -132,7 +137,12 @@ defmodule SimpleshopTheme.Media do
""" """
def get_header do def get_header do
Repo.one(from i in ImageSchema, where: i.image_type == "header", order_by: [desc: i.inserted_at], limit: 1) Repo.one(
from i in ImageSchema,
where: i.image_type == "header",
order_by: [desc: i.inserted_at],
limit: 1
)
end end
@doc """ @doc """

View File

@ -46,7 +46,8 @@ defmodule SimpleshopTheme.Media.Image do
defp detect_svg(changeset) do defp detect_svg(changeset) do
content_type = get_change(changeset, :content_type) content_type = get_change(changeset, :content_type)
if content_type == "image/svg+xml" or String.ends_with?(get_change(changeset, :filename) || "", ".svg") do if content_type == "image/svg+xml" or
String.ends_with?(get_change(changeset, :filename) || "", ".svg") do
changeset changeset
|> put_change(:is_svg, true) |> put_change(:is_svg, true)
|> maybe_store_svg_content() |> maybe_store_svg_content()

View File

@ -23,7 +23,8 @@ defmodule SimpleshopTheme.Media.SVGRecolorer do
""" """
@spec recolor(String.t(), String.t()) :: String.t() @spec recolor(String.t(), String.t()) :: String.t()
def recolor(svg_content, target_color) when is_binary(svg_content) and is_binary(target_color) do def recolor(svg_content, target_color)
when is_binary(svg_content) and is_binary(target_color) do
svg_content svg_content
|> recolor_fill_attributes(target_color) |> recolor_fill_attributes(target_color)
|> recolor_stroke_attributes(target_color) |> recolor_stroke_attributes(target_color)

View File

@ -163,9 +163,17 @@ defmodule SimpleshopTheme.Mockups.Generator do
hoodie: %{blueprint_id: nil, print_provider_id: nil, search_term: "Pullover Hoodie"}, hoodie: %{blueprint_id: nil, print_provider_id: nil, search_term: "Pullover Hoodie"},
tote: %{blueprint_id: nil, print_provider_id: nil, search_term: "Cotton Tote Bag"}, tote: %{blueprint_id: nil, print_provider_id: nil, search_term: "Cotton Tote Bag"},
mug: %{blueprint_id: nil, print_provider_id: nil, search_term: "Mug 11oz"}, mug: %{blueprint_id: nil, print_provider_id: nil, search_term: "Mug 11oz"},
cushion: %{blueprint_id: nil, print_provider_id: nil, search_term: "Spun Polyester Square Pillow"}, cushion: %{
blueprint_id: nil,
print_provider_id: nil,
search_term: "Spun Polyester Square Pillow"
},
blanket: %{blueprint_id: nil, print_provider_id: nil, search_term: "Sherpa Fleece Blanket"}, blanket: %{blueprint_id: nil, print_provider_id: nil, search_term: "Sherpa Fleece Blanket"},
notebook: %{blueprint_id: nil, print_provider_id: nil, search_term: "Hardcover Journal Matte"}, notebook: %{
blueprint_id: nil,
print_provider_id: nil,
search_term: "Hardcover Journal Matte"
},
phone_case: %{blueprint_id: nil, print_provider_id: nil, search_term: "Tough Phone Cases"}, phone_case: %{blueprint_id: nil, print_provider_id: nil, search_term: "Tough Phone Cases"},
laptop_sleeve: %{blueprint_id: nil, print_provider_id: nil, search_term: "Laptop Sleeve"} laptop_sleeve: %{blueprint_id: nil, print_provider_id: nil, search_term: "Laptop Sleeve"}
} }
@ -258,7 +266,7 @@ defmodule SimpleshopTheme.Mockups.Generator do
placeholder_width > 0 and placeholder_height > 0 do placeholder_width > 0 and placeholder_height > 0 do
# For cover: use the larger scale to ensure full coverage # For cover: use the larger scale to ensure full coverage
width_scale = 1.0 width_scale = 1.0
height_scale = (placeholder_height * artwork_width) / (artwork_height * placeholder_width) height_scale = placeholder_height * artwork_width / (artwork_height * placeholder_width)
max(width_scale, height_scale) max(width_scale, height_scale)
end end
@ -267,21 +275,36 @@ defmodule SimpleshopTheme.Mockups.Generator do
@doc """ @doc """
Create a product with the uploaded artwork. Create a product with the uploaded artwork.
""" """
def create_product(shop_id, product_def, image_id, image_width, image_height, blueprint_id, print_provider_id, variants) do def create_product(
shop_id,
product_def,
image_id,
image_width,
image_height,
blueprint_id,
print_provider_id,
variants
) do
# Get the first variant for simplicity (typically a standard size/color) # Get the first variant for simplicity (typically a standard size/color)
variant = hd(variants) variant = hd(variants)
variant_id = variant["id"] variant_id = variant["id"]
# Get placeholder info # Get placeholder info
placeholders = variant["placeholders"] || [] placeholders = variant["placeholders"] || []
front_placeholder = Enum.find(placeholders, fn p -> p["position"] == "front" end) || hd(placeholders)
front_placeholder =
Enum.find(placeholders, fn p -> p["position"] == "front" end) || hd(placeholders)
# Extract placeholder dimensions and calculate cover scale # Extract placeholder dimensions and calculate cover scale
placeholder_width = front_placeholder["width"] placeholder_width = front_placeholder["width"]
placeholder_height = front_placeholder["height"] placeholder_height = front_placeholder["height"]
scale = calculate_cover_scale(image_width, image_height, placeholder_width, placeholder_height)
IO.puts(" Scale calculation: artwork #{image_width}x#{image_height}, placeholder #{placeholder_width}x#{placeholder_height} -> scale #{Float.round(scale, 3)}") scale =
calculate_cover_scale(image_width, image_height, placeholder_width, placeholder_height)
IO.puts(
" Scale calculation: artwork #{image_width}x#{image_height}, placeholder #{placeholder_width}x#{placeholder_height} -> scale #{Float.round(scale, 3)}"
)
product_data = %{ product_data = %{
title: product_def.name, title: product_def.name,
@ -439,7 +462,17 @@ defmodule SimpleshopTheme.Mockups.Generator do
image_height = upload["height"], image_height = upload["height"],
_ = IO.puts(" Artwork uploaded (ID: #{image_id}, #{image_width}x#{image_height})"), _ = IO.puts(" Artwork uploaded (ID: #{image_id}, #{image_width}x#{image_height})"),
_ = IO.puts(" Creating product..."), _ = IO.puts(" Creating product..."),
{:ok, product} <- create_product(shop_id, product_def, image_id, image_width, image_height, blueprint_id, provider_id, variants), {:ok, product} <-
create_product(
shop_id,
product_def,
image_id,
image_width,
image_height,
blueprint_id,
provider_id,
variants
),
product_id = product["id"], product_id = product["id"],
mockup_urls = extract_mockup_urls(product), mockup_urls = extract_mockup_urls(product),
_ = IO.puts(" Product created (ID: #{product_id})"), _ = IO.puts(" Product created (ID: #{product_id})"),

View File

@ -287,14 +287,17 @@ defmodule SimpleshopTheme.Products do
if MapSet.size(removed_ids) > 0 do if MapSet.size(removed_ids) > 0 do
from(v in ProductVariant, from(v in ProductVariant,
where: v.product_id == ^product_id and v.provider_variant_id in ^MapSet.to_list(removed_ids) where:
v.product_id == ^product_id and v.provider_variant_id in ^MapSet.to_list(removed_ids)
) )
|> Repo.delete_all() |> Repo.delete_all()
end end
# Upsert incoming variants # Upsert incoming variants
Enum.map(variants, fn variant_data -> Enum.map(variants, fn variant_data ->
provider_variant_id = variant_data[:provider_variant_id] || variant_data["provider_variant_id"] provider_variant_id =
variant_data[:provider_variant_id] || variant_data["provider_variant_id"]
attrs = Map.put(variant_data, :product_id, product_id) attrs = Map.put(variant_data, :product_id, product_id)
case get_variant_by_provider(product_id, provider_variant_id) do case get_variant_by_provider(product_id, provider_variant_id) do

View File

@ -88,7 +88,8 @@ defmodule SimpleshopTheme.Products.ProductVariant do
Formats the options as a human-readable title. Formats the options as a human-readable title.
E.g., %{"Size" => "Large", "Color" => "Blue"} -> "Large / Blue" E.g., %{"Size" => "Large", "Color" => "Blue"} -> "Large / Blue"
""" """
def options_title(%__MODULE__{options: options}) when is_map(options) and map_size(options) > 0 do def options_title(%__MODULE__{options: options})
when is_map(options) and map_size(options) > 0 do
options options
|> Map.values() |> Map.values()
|> Enum.join(" / ") |> Enum.join(" / ")

View File

@ -93,7 +93,10 @@ defmodule SimpleshopTheme.Settings.ThemeSettings do
]) ])
|> validate_required([:mood, :typography, :shape, :density]) |> validate_required([:mood, :typography, :shape, :density])
|> validate_inclusion(:mood, ~w(neutral warm cool dark)) |> validate_inclusion(:mood, ~w(neutral warm cool dark))
|> validate_inclusion(:typography, ~w(clean editorial modern classic friendly minimal impulse)) |> validate_inclusion(
:typography,
~w(clean editorial modern classic friendly minimal impulse)
)
|> validate_inclusion(:shape, ~w(sharp soft round pill)) |> validate_inclusion(:shape, ~w(sharp soft round pill))
|> validate_inclusion(:density, ~w(spacious balanced compact)) |> validate_inclusion(:density, ~w(spacious balanced compact))
|> validate_inclusion(:grid_columns, ~w(2 3 4)) |> validate_inclusion(:grid_columns, ~w(2 3 4))
@ -101,8 +104,14 @@ defmodule SimpleshopTheme.Settings.ThemeSettings do
|> validate_inclusion(:logo_mode, ~w(text-only logo-text logo-only)) |> validate_inclusion(:logo_mode, ~w(text-only logo-text logo-only))
|> validate_number(:logo_size, greater_than_or_equal_to: 24, less_than_or_equal_to: 120) |> validate_number(:logo_size, greater_than_or_equal_to: 24, less_than_or_equal_to: 120)
|> validate_number(:header_zoom, greater_than_or_equal_to: 100, less_than_or_equal_to: 200) |> validate_number(:header_zoom, greater_than_or_equal_to: 100, less_than_or_equal_to: 200)
|> validate_number(:header_position_x, greater_than_or_equal_to: 0, less_than_or_equal_to: 100) |> validate_number(:header_position_x,
|> validate_number(:header_position_y, greater_than_or_equal_to: 0, less_than_or_equal_to: 100) greater_than_or_equal_to: 0,
less_than_or_equal_to: 100
)
|> validate_number(:header_position_y,
greater_than_or_equal_to: 0,
less_than_or_equal_to: 100
)
|> validate_inclusion(:layout_width, ~w(contained wide full)) |> validate_inclusion(:layout_width, ~w(contained wide full))
|> validate_inclusion(:card_shadow, ~w(none sm md lg)) |> validate_inclusion(:card_shadow, ~w(none sm md lg))
|> validate_inclusion(:font_size, ~w(small medium large)) |> validate_inclusion(:font_size, ~w(small medium large))

View File

@ -234,5 +234,4 @@ defmodule SimpleshopTheme.Theme.Fonts do
"" ""
end end
end end
end end

View File

@ -67,7 +67,8 @@ defmodule SimpleshopTheme.Theme.PreviewData do
%{ %{
rating: 5, rating: 5,
title: "Absolutely beautiful", title: "Absolutely beautiful",
body: "The quality exceeded my expectations. The colours are vibrant and the paper feels premium. It's now pride of place in my living room.", body:
"The quality exceeded my expectations. The colours are vibrant and the paper feels premium. It's now pride of place in my living room.",
author: "Sarah M.", author: "Sarah M.",
date: "2 weeks ago", date: "2 weeks ago",
verified: true verified: true
@ -75,7 +76,8 @@ defmodule SimpleshopTheme.Theme.PreviewData do
%{ %{
rating: 4, rating: 4,
title: "Great gift", title: "Great gift",
body: "Bought this as a gift and it arrived beautifully packaged. Fast shipping too. Would definitely order again.", body:
"Bought this as a gift and it arrived beautifully packaged. Fast shipping too. Would definitely order again.",
author: "James T.", author: "James T.",
date: "1 month ago", date: "1 month ago",
verified: true verified: true
@ -90,13 +92,33 @@ defmodule SimpleshopTheme.Theme.PreviewData do
""" """
def about_content do def about_content do
[ [
%{type: :lead, text: "I'm Emma, a nature photographer based in the UK. What started as weekend walks with my camera has grown into something I never expected a little shop where I can share my favourite captures with others."}, %{
%{type: :paragraph, text: "Every design in this shop comes from my own photography. Whether it's early morning mist over the hills, autumn leaves in the local woods, or the quiet beauty of wildflower meadows, I'm drawn to the peaceful moments that nature offers."}, type: :lead,
%{type: :paragraph, text: "I work with quality print partners to bring these images to life on products you can actually use and enjoy from art prints for your walls to mugs for your morning tea."}, text:
"I'm Emma, a nature photographer based in the UK. What started as weekend walks with my camera has grown into something I never expected a little shop where I can share my favourite captures with others."
},
%{
type: :paragraph,
text:
"Every design in this shop comes from my own photography. Whether it's early morning mist over the hills, autumn leaves in the local woods, or the quiet beauty of wildflower meadows, I'm drawn to the peaceful moments that nature offers."
},
%{
type: :paragraph,
text:
"I work with quality print partners to bring these images to life on products you can actually use and enjoy from art prints for your walls to mugs for your morning tea."
},
%{type: :heading, text: "Quality you can trust"}, %{type: :heading, text: "Quality you can trust"},
%{type: :paragraph, text: "I've carefully chosen print partners who share my commitment to quality. Every product is made to order using premium materials and printing techniques that ensure vibrant colours and lasting quality."}, %{
type: :paragraph,
text:
"I've carefully chosen print partners who share my commitment to quality. Every product is made to order using premium materials and printing techniques that ensure vibrant colours and lasting quality."
},
%{type: :heading, text: "Printed sustainably"}, %{type: :heading, text: "Printed sustainably"},
%{type: :paragraph, text: "Because each item is printed on demand, there's no waste from unsold stock. My print partners use eco-friendly inks where possible, and products are shipped directly to you to minimise unnecessary handling."}, %{
type: :paragraph,
text:
"Because each item is printed on demand, there's no waste from unsold stock. My print partners use eco-friendly inks where possible, and products are shipped directly to you to minimise unnecessary handling."
},
%{type: :closing, text: "Thank you for visiting. It means a lot that you're here."} %{type: :closing, text: "Thank you for visiting. It means a lot that you're here."}
] ]
end end
@ -427,42 +449,48 @@ defmodule SimpleshopTheme.Theme.PreviewData do
%{ %{
id: "1", id: "1",
author: "Sarah M.", author: "Sarah M.",
content: "The print quality is absolutely stunning - colours are exactly as shown online. My living room looks so much better now!", content:
"The print quality is absolutely stunning - colours are exactly as shown online. My living room looks so much better now!",
rating: 5, rating: 5,
date: "2025-01-15" date: "2025-01-15"
}, },
%{ %{
id: "2", id: "2",
author: "James L.", author: "James L.",
content: "Bought the forest hoodie as a gift. The packaging was lovely and the quality exceeded expectations. Will order again!", content:
"Bought the forest hoodie as a gift. The packaging was lovely and the quality exceeded expectations. Will order again!",
rating: 5, rating: 5,
date: "2025-01-10" date: "2025-01-10"
}, },
%{ %{
id: "3", id: "3",
author: "Emily R.", author: "Emily R.",
content: "My new favourite mug! I love sipping my morning tea while looking at that beautiful fern design.", content:
"My new favourite mug! I love sipping my morning tea while looking at that beautiful fern design.",
rating: 5, rating: 5,
date: "2025-01-05" date: "2025-01-05"
}, },
%{ %{
id: "4", id: "4",
author: "Michael T.", author: "Michael T.",
content: "The tote bag is so sturdy - I use it for everything now. Great print quality that hasn't faded at all.", content:
"The tote bag is so sturdy - I use it for everything now. Great print quality that hasn't faded at all.",
rating: 5, rating: 5,
date: "2024-12-28" date: "2024-12-28"
}, },
%{ %{
id: "5", id: "5",
author: "Lisa K.", author: "Lisa K.",
content: "The night sky blanket is gorgeous and so cosy. Perfect for film nights on the sofa. Highly recommend!", content:
"The night sky blanket is gorgeous and so cosy. Perfect for film nights on the sofa. Highly recommend!",
rating: 5, rating: 5,
date: "2024-12-20" date: "2024-12-20"
}, },
%{ %{
id: "6", id: "6",
author: "David P.", author: "David P.",
content: "Ordered several prints for my new flat. They arrived well packaged and look amazing on the wall.", content:
"Ordered several prints for my new flat. They arrived well packaged and look amazing on the wall.",
rating: 5, rating: 5,
date: "2024-12-15" date: "2024-12-15"
} }

View File

@ -17,7 +17,8 @@ defmodule SimpleshopThemeWeb do
those modules here. those modules here.
""" """
def static_paths, do: ~w(assets css fonts images image_cache mockups favicon.ico robots.txt demo.html) def static_paths,
do: ~w(assets css fonts images image_cache mockups favicon.ico robots.txt demo.html)
def router do def router do
quote do quote do

View File

@ -1 +1 @@
<%= @inner_content %> {@inner_content}

View File

@ -4,8 +4,14 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="csrf-token" content={get_csrf_token()} /> <meta name="csrf-token" content={get_csrf_token()} />
<meta name="description" content={assigns[:page_description] || @theme_settings.site_description || "Welcome to #{@theme_settings.site_name}"} /> <meta
<.live_title><%= assigns[:page_title] || @theme_settings.site_name %></.live_title> name="description"
content={
assigns[:page_description] || @theme_settings.site_description ||
"Welcome to #{@theme_settings.site_name}"
}
/>
<.live_title>{assigns[:page_title] || @theme_settings.site_name}</.live_title>
<!-- Preload critical fonts for the current typography preset --> <!-- Preload critical fonts for the current typography preset -->
<%= for preload <- SimpleshopTheme.Theme.Fonts.preload_links( <%= for preload <- SimpleshopTheme.Theme.Fonts.preload_links(
@theme_settings.typography, @theme_settings.typography,
@ -17,7 +23,9 @@
<script defer phx-track-static src={~p"/assets/js/app.js"}> <script defer phx-track-static src={~p"/assets/js/app.js"}>
</script> </script>
<!-- Generated theme CSS with @font-face declarations --> <!-- Generated theme CSS with @font-face declarations -->
<style id="theme-css"><%= Phoenix.HTML.raw(@generated_css) %></style> <style id="theme-css">
<%= Phoenix.HTML.raw(@generated_css) %>
</style>
</head> </head>
<body class="h-full"> <body class="h-full">
<div <div

View File

@ -1,11 +1,21 @@
<div class="shop-container min-h-screen pb-20 md:pb-0" style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);"> <div
class="shop-container min-h-screen pb-20 md:pb-0"
style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);"
>
<.skip_link /> <.skip_link />
<%= if @theme_settings.announcement_bar do %> <%= if @theme_settings.announcement_bar do %>
<.announcement_bar theme_settings={@theme_settings} /> <.announcement_bar theme_settings={@theme_settings} />
<% end %> <% end %>
<.shop_header theme_settings={@theme_settings} logo_image={@logo_image} header_image={@header_image} active_page="about" mode={@mode} cart_count={@cart_count} /> <.shop_header
theme_settings={@theme_settings}
logo_image={@logo_image}
header_image={@header_image}
active_page="about"
mode={@mode}
cart_count={@cart_count}
/>
<main id="main-content" class="content-page" style="background-color: var(--t-surface-base);"> <main id="main-content" class="content-page" style="background-color: var(--t-surface-base);">
<.hero_section <.hero_section

View File

@ -1,11 +1,21 @@
<div class="shop-container min-h-screen pb-20 md:pb-0" style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);"> <div
class="shop-container min-h-screen pb-20 md:pb-0"
style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);"
>
<.skip_link /> <.skip_link />
<%= if @theme_settings.announcement_bar do %> <%= if @theme_settings.announcement_bar do %>
<.announcement_bar theme_settings={@theme_settings} /> <.announcement_bar theme_settings={@theme_settings} />
<% end %> <% end %>
<.shop_header theme_settings={@theme_settings} logo_image={@logo_image} header_image={@header_image} active_page="cart" mode={@mode} cart_count={@cart_count} /> <.shop_header
theme_settings={@theme_settings}
logo_image={@logo_image}
header_image={@header_image}
active_page="cart"
mode={@mode}
cart_count={@cart_count}
/>
<main id="main-content" class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <main id="main-content" class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<.page_title text="Your basket" /> <.page_title text="Your basket" />

View File

@ -1,11 +1,21 @@
<div class="shop-container min-h-screen pb-20 md:pb-0" style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);"> <div
class="shop-container min-h-screen pb-20 md:pb-0"
style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);"
>
<.skip_link /> <.skip_link />
<%= if @theme_settings.announcement_bar do %> <%= if @theme_settings.announcement_bar do %>
<.announcement_bar theme_settings={@theme_settings} /> <.announcement_bar theme_settings={@theme_settings} />
<% end %> <% end %>
<.shop_header theme_settings={@theme_settings} logo_image={@logo_image} header_image={@header_image} active_page="collection" mode={@mode} cart_count={@cart_count} /> <.shop_header
theme_settings={@theme_settings}
logo_image={@logo_image}
header_image={@header_image}
active_page="collection"
mode={@mode}
cart_count={@cart_count}
/>
<main id="main-content"> <main id="main-content">
<.collection_header title="All Products" product_count={length(@preview_data.products)} /> <.collection_header title="All Products" product_count={length(@preview_data.products)} />

View File

@ -1,11 +1,21 @@
<div class="shop-container min-h-screen pb-20 md:pb-0" style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);"> <div
class="shop-container min-h-screen pb-20 md:pb-0"
style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);"
>
<.skip_link /> <.skip_link />
<%= if @theme_settings.announcement_bar do %> <%= if @theme_settings.announcement_bar do %>
<.announcement_bar theme_settings={@theme_settings} /> <.announcement_bar theme_settings={@theme_settings} />
<% end %> <% end %>
<.shop_header theme_settings={@theme_settings} logo_image={@logo_image} header_image={@header_image} active_page="contact" mode={@mode} cart_count={@cart_count} /> <.shop_header
theme_settings={@theme_settings}
logo_image={@logo_image}
header_image={@header_image}
active_page="contact"
mode={@mode}
cart_count={@cart_count}
/>
<main id="main-content" class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-16"> <main id="main-content" class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
<.hero_section <.hero_section
@ -20,11 +30,14 @@
<div class="flex flex-col gap-6"> <div class="flex flex-col gap-6">
<.order_tracking_card /> <.order_tracking_card />
<.info_card title="Handy to know" items={[ <.info_card
title="Handy to know"
items={[
%{label: "Printing", value: "2-5 business days"}, %{label: "Printing", value: "2-5 business days"},
%{label: "Delivery", value: "3-7 business days after printing"}, %{label: "Delivery", value: "3-7 business days after printing"},
%{label: "Returns", value: "Happy to help with faulty or damaged items"} %{label: "Returns", value: "Happy to help with faulty or damaged items"}
]} /> ]}
/>
<.newsletter_card /> <.newsletter_card />

View File

@ -1,13 +1,27 @@
<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);"> <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 /> <.skip_link />
<%= if @theme_settings.announcement_bar do %> <%= if @theme_settings.announcement_bar do %>
<.announcement_bar theme_settings={@theme_settings} /> <.announcement_bar theme_settings={@theme_settings} />
<% end %> <% end %>
<.shop_header theme_settings={@theme_settings} logo_image={@logo_image} header_image={@header_image} active_page="error" mode={@mode} cart_count={@cart_count} /> <.shop_header
theme_settings={@theme_settings}
logo_image={@logo_image}
header_image={@header_image}
active_page="error"
mode={@mode}
cart_count={@cart_count}
/>
<main id="main-content" class="flex items-center justify-center" style="min-height: calc(100vh - 4rem);"> <main
id="main-content"
class="flex items-center justify-center"
style="min-height: calc(100vh - 4rem);"
>
<div class="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-16"> <div class="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
<.hero_section <.hero_section
variant={:error} variant={:error}

View File

@ -1,11 +1,21 @@
<div class="shop-container min-h-screen pb-20 md:pb-0" style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);"> <div
class="shop-container min-h-screen pb-20 md:pb-0"
style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);"
>
<.skip_link /> <.skip_link />
<%= if @theme_settings.announcement_bar do %> <%= if @theme_settings.announcement_bar do %>
<.announcement_bar theme_settings={@theme_settings} /> <.announcement_bar theme_settings={@theme_settings} />
<% end %> <% end %>
<.shop_header theme_settings={@theme_settings} logo_image={@logo_image} header_image={@header_image} active_page="home" mode={@mode} cart_count={@cart_count} /> <.shop_header
theme_settings={@theme_settings}
logo_image={@logo_image}
header_image={@header_image}
active_page="home"
mode={@mode}
cart_count={@cart_count}
/>
<main id="main-content"> <main id="main-content">
<.hero_section <.hero_section

View File

@ -1,18 +1,36 @@
<div class="shop-container min-h-screen pb-20 md:pb-0" style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);"> <div
class="shop-container min-h-screen pb-20 md:pb-0"
style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);"
>
<.skip_link /> <.skip_link />
<%= if @theme_settings.announcement_bar do %> <%= if @theme_settings.announcement_bar do %>
<.announcement_bar theme_settings={@theme_settings} /> <.announcement_bar theme_settings={@theme_settings} />
<% end %> <% end %>
<.shop_header theme_settings={@theme_settings} logo_image={@logo_image} header_image={@header_image} active_page="pdp" mode={@mode} cart_count={@cart_count} /> <.shop_header
theme_settings={@theme_settings}
logo_image={@logo_image}
header_image={@header_image}
active_page="pdp"
mode={@mode}
cart_count={@cart_count}
/>
<main id="main-content" class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <main id="main-content" class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<.breadcrumb items={[ <.breadcrumb
items={[
%{label: "Home", page: "home", href: "/"}, %{label: "Home", page: "home", href: "/"},
%{label: @product.category, page: "collection", href: "/collections/#{@product.category |> String.downcase() |> String.replace(" ", "-")}"}, %{
label: @product.category,
page: "collection",
href:
"/collections/#{@product.category |> String.downcase() |> String.replace(" ", "-")}"
},
%{label: @product.name, current: true} %{label: @product.name, current: true}
]} mode={@mode} /> ]}
mode={@mode}
/>
<div class="grid grid-cols-1 md:grid-cols-2 gap-12 mb-16"> <div class="grid grid-cols-1 md:grid-cols-2 gap-12 mb-16">
<.product_gallery images={@gallery_images} product_name={@product.name} /> <.product_gallery images={@gallery_images} product_name={@product.name} />
@ -27,7 +45,12 @@
</div> </div>
</div> </div>
<.reviews_section :if={@theme_settings.pdp_reviews} reviews={SimpleshopTheme.Theme.PreviewData.reviews()} average_rating={5} total_count={24} /> <.reviews_section
:if={@theme_settings.pdp_reviews}
reviews={SimpleshopTheme.Theme.PreviewData.reviews()}
average_rating={5}
total_count={24}
/>
<.related_products_section <.related_products_section
:if={@theme_settings.pdp_related_products} :if={@theme_settings.pdp_related_products}

File diff suppressed because it is too large Load Diff

View File

@ -12,13 +12,21 @@ defmodule SimpleshopThemeWeb.ErrorHTML do
alias SimpleshopTheme.Theme.{CSSCache, CSSGenerator, PreviewData} alias SimpleshopTheme.Theme.{CSSCache, CSSGenerator, PreviewData}
def render("404.html", assigns) do def render("404.html", assigns) do
render_error_page(assigns, "404", "Page Not Found", render_error_page(
"Sorry, we couldn't find the page you're looking for. Perhaps you've mistyped the URL or the page has been moved.") assigns,
"404",
"Page Not Found",
"Sorry, we couldn't find the page you're looking for. Perhaps you've mistyped the URL or the page has been moved."
)
end end
def render("500.html", assigns) do def render("500.html", assigns) do
render_error_page(assigns, "500", "Server Error", render_error_page(
"Something went wrong on our end. Please try again later or contact support if the problem persists.") assigns,
"500",
"Server Error",
"Something went wrong on our end. Please try again later or contact support if the problem persists."
)
end end
def render(template, _assigns) do def render(template, _assigns) do
@ -57,14 +65,15 @@ defmodule SimpleshopThemeWeb.ErrorHTML do
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title><%= @error_code %> - <%= @error_title %></title> <title>{@error_code} - {@error_title}</title>
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} /> <link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
<style id="theme-css"> <style id="theme-css">
<%= Phoenix.HTML.raw(@generated_css) %> <%= Phoenix.HTML.raw(@generated_css) %>
</style> </style>
</head> </head>
<body class="h-full"> <body class="h-full">
<div class="shop-root themed h-full" <div
class="shop-root themed h-full"
data-mood={@theme_settings.mood} data-mood={@theme_settings.mood}
data-typography={@theme_settings.typography} data-typography={@theme_settings.typography}
data-shape={@theme_settings.shape} data-shape={@theme_settings.shape}
@ -73,7 +82,8 @@ defmodule SimpleshopThemeWeb.ErrorHTML do
data-header={@theme_settings.header_layout} data-header={@theme_settings.header_layout}
data-sticky={to_string(@theme_settings.sticky_header)} data-sticky={to_string(@theme_settings.sticky_header)}
data-layout={@theme_settings.layout_width} data-layout={@theme_settings.layout_width}
data-shadow={@theme_settings.card_shadow}> data-shadow={@theme_settings.card_shadow}
>
<SimpleshopThemeWeb.PageTemplates.error <SimpleshopThemeWeb.PageTemplates.error
theme_settings={@theme_settings} theme_settings={@theme_settings}
logo_image={@logo_image} logo_image={@logo_image}
@ -96,14 +106,18 @@ defmodule SimpleshopThemeWeb.ErrorHTML do
defp load_theme_data do defp load_theme_data do
try do try do
theme_settings = Settings.get_theme_settings() theme_settings = Settings.get_theme_settings()
generated_css = generated_css =
case CSSCache.get() do case CSSCache.get() do
{:ok, css} -> css {:ok, css} ->
css
:miss -> :miss ->
css = CSSGenerator.generate(theme_settings) css = CSSGenerator.generate(theme_settings)
CSSCache.put(css) CSSCache.put(css)
css css
end end
{theme_settings, generated_css} {theme_settings, generated_css}
rescue rescue
_ -> {%ThemeSettings{}, ""} _ -> {%ThemeSettings{}, ""}

View File

@ -11,7 +11,9 @@ defmodule SimpleshopThemeWeb.ShopLive.About do
generated_css = generated_css =
case CSSCache.get() do case CSSCache.get() do
{:ok, css} -> css {:ok, css} ->
css
:miss -> :miss ->
css = CSSGenerator.generate(theme_settings) css = CSSGenerator.generate(theme_settings)
CSSCache.put(css) CSSCache.put(css)

View File

@ -11,7 +11,9 @@ defmodule SimpleshopThemeWeb.ShopLive.Cart do
generated_css = generated_css =
case CSSCache.get() do case CSSCache.get() do
{:ok, css} -> css {:ok, css} ->
css
:miss -> :miss ->
css = CSSGenerator.generate(theme_settings) css = CSSGenerator.generate(theme_settings)
CSSCache.put(css) CSSCache.put(css)
@ -24,7 +26,9 @@ defmodule SimpleshopThemeWeb.ShopLive.Cart do
# For now, use preview data for cart items # For now, use preview data for cart items
# In a real implementation, this would come from session/database # In a real implementation, this would come from session/database
cart_page_items = PreviewData.cart_items() cart_page_items = PreviewData.cart_items()
cart_page_subtotal = Enum.reduce(cart_page_items, 0, fn item, acc ->
cart_page_subtotal =
Enum.reduce(cart_page_items, 0, fn item, acc ->
acc + item.product.price * item.quantity acc + item.product.price * item.quantity
end) end)

View File

@ -20,7 +20,9 @@ defmodule SimpleshopThemeWeb.ShopLive.Collection do
generated_css = generated_css =
case CSSCache.get() do case CSSCache.get() do
{:ok, css} -> css {:ok, css} ->
css
:miss -> :miss ->
css = CSSGenerator.generate(theme_settings) css = CSSGenerator.generate(theme_settings)
CSSCache.put(css) CSSCache.put(css)
@ -82,7 +84,9 @@ defmodule SimpleshopThemeWeb.ShopLive.Collection do
@impl true @impl true
def handle_event("sort_changed", %{"sort" => sort}, socket) do def handle_event("sort_changed", %{"sort" => sort}, socket) do
slug = if socket.assigns.current_category, do: socket.assigns.current_category.slug, else: "all" slug =
if socket.assigns.current_category, do: socket.assigns.current_category.slug, else: "all"
{:noreply, push_patch(socket, to: ~p"/collections/#{slug}?sort=#{sort}")} {:noreply, push_patch(socket, to: ~p"/collections/#{slug}?sort=#{sort}")}
end end
@ -100,7 +104,10 @@ defmodule SimpleshopThemeWeb.ShopLive.Collection do
@impl true @impl true
def render(assigns) do def render(assigns) do
~H""" ~H"""
<div class="shop-container min-h-screen pb-20 md:pb-0" style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);"> <div
class="shop-container min-h-screen pb-20 md:pb-0"
style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);"
>
<SimpleshopThemeWeb.ShopComponents.skip_link /> <SimpleshopThemeWeb.ShopComponents.skip_link />
<%= if @theme_settings.announcement_bar do %> <%= if @theme_settings.announcement_bar do %>
@ -145,7 +152,11 @@ defmodule SimpleshopThemeWeb.ShopLive.Collection do
<%= if @products == [] do %> <%= if @products == [] do %>
<div class="text-center py-16" style="color: var(--t-text-secondary);"> <div class="text-center py-16" style="color: var(--t-text-secondary);">
<p class="text-lg">No products found in this collection.</p> <p class="text-lg">No products found in this collection.</p>
<.link navigate={~p"/collections/all"} class="mt-4 inline-block underline" style="color: var(--t-text-accent);"> <.link
navigate={~p"/collections/all"}
class="mt-4 inline-block underline"
style="color: var(--t-text-accent);"
>
View all products View all products
</.link> </.link>
</div> </div>
@ -155,9 +166,15 @@ defmodule SimpleshopThemeWeb.ShopLive.Collection do
<SimpleshopThemeWeb.ShopComponents.shop_footer theme_settings={@theme_settings} mode={@mode} /> <SimpleshopThemeWeb.ShopComponents.shop_footer theme_settings={@theme_settings} mode={@mode} />
<SimpleshopThemeWeb.ShopComponents.cart_drawer cart_items={@cart_items} subtotal={@cart_subtotal} mode={@mode} /> <SimpleshopThemeWeb.ShopComponents.cart_drawer
cart_items={@cart_items}
subtotal={@cart_subtotal}
mode={@mode}
/>
<SimpleshopThemeWeb.ShopComponents.search_modal hint_text={~s(Try searching for "mountain", "forest", or "ocean")} /> <SimpleshopThemeWeb.ShopComponents.search_modal hint_text={
~s(Try searching for "mountain", "forest", or "ocean")
} />
<SimpleshopThemeWeb.ShopComponents.mobile_bottom_nav active_page="collection" mode={@mode} /> <SimpleshopThemeWeb.ShopComponents.mobile_bottom_nav active_page="collection" mode={@mode} />
</div> </div>
@ -176,10 +193,12 @@ defmodule SimpleshopThemeWeb.ShopLive.Collection do
"px-4 py-2 rounded-full text-sm transition-colors", "px-4 py-2 rounded-full text-sm transition-colors",
if(@current_slug == nil, do: "font-medium", else: "hover:opacity-80") if(@current_slug == nil, do: "font-medium", else: "hover:opacity-80")
]} ]}
style={if(@current_slug == nil, style={
if(@current_slug == nil,
do: "background-color: var(--t-accent); color: var(--t-text-on-accent);", do: "background-color: var(--t-accent); color: var(--t-text-on-accent);",
else: "background-color: var(--t-surface-raised); color: var(--t-text-primary);" else: "background-color: var(--t-surface-raised); color: var(--t-text-primary);"
)} )
}
> >
All All
</.link> </.link>
@ -192,10 +211,12 @@ defmodule SimpleshopThemeWeb.ShopLive.Collection do
"px-4 py-2 rounded-full text-sm transition-colors", "px-4 py-2 rounded-full text-sm transition-colors",
if(@current_slug == category.slug, do: "font-medium", else: "hover:opacity-80") if(@current_slug == category.slug, do: "font-medium", else: "hover:opacity-80")
]} ]}
style={if(@current_slug == category.slug, style={
if(@current_slug == category.slug,
do: "background-color: var(--t-accent); color: var(--t-text-on-accent);", do: "background-color: var(--t-accent); color: var(--t-text-on-accent);",
else: "background-color: var(--t-surface-raised); color: var(--t-text-primary);" else: "background-color: var(--t-surface-raised); color: var(--t-text-primary);"
)} )
}
> >
{category.name} {category.name}
</.link> </.link>

View File

@ -11,7 +11,9 @@ defmodule SimpleshopThemeWeb.ShopLive.Contact do
generated_css = generated_css =
case CSSCache.get() do case CSSCache.get() do
{:ok, css} -> css {:ok, css} ->
css
:miss -> :miss ->
css = CSSGenerator.generate(theme_settings) css = CSSGenerator.generate(theme_settings)
CSSCache.put(css) CSSCache.put(css)

View File

@ -11,7 +11,9 @@ defmodule SimpleshopThemeWeb.ShopLive.Home do
generated_css = generated_css =
case CSSCache.get() do case CSSCache.get() do
{:ok, css} -> css {:ok, css} ->
css
:miss -> :miss ->
css = CSSGenerator.generate(theme_settings) css = CSSGenerator.generate(theme_settings)
CSSCache.put(css) CSSCache.put(css)

View File

@ -11,7 +11,9 @@ defmodule SimpleshopThemeWeb.ShopLive.ProductShow do
generated_css = generated_css =
case CSSCache.get() do case CSSCache.get() do
{:ok, css} -> css {:ok, css} ->
css
:miss -> :miss ->
css = CSSGenerator.generate(theme_settings) css = CSSGenerator.generate(theme_settings)
CSSCache.put(css) CSSCache.put(css)

View File

@ -10,6 +10,7 @@ defmodule SimpleshopThemeWeb.ThemeLive.Index do
theme_settings = Settings.get_theme_settings() theme_settings = Settings.get_theme_settings()
generated_css = CSSGenerator.generate(theme_settings) generated_css = CSSGenerator.generate(theme_settings)
active_preset = Presets.detect_preset(theme_settings) active_preset = Presets.detect_preset(theme_settings)
preview_data = %{ preview_data = %{
products: PreviewData.products(), products: PreviewData.products(),
cart_items: PreviewData.cart_items(), cart_items: PreviewData.cart_items(),
@ -361,7 +362,9 @@ defmodule SimpleshopThemeWeb.ThemeLive.Index do
defp preview_page(%{page: :cart} = assigns) do defp preview_page(%{page: :cart} = assigns) do
cart_items = assigns.preview_data.cart_items cart_items = assigns.preview_data.cart_items
subtotal = Enum.reduce(cart_items, 0, fn item, acc -> acc + item.product.price * item.quantity end)
subtotal =
Enum.reduce(cart_items, 0, fn item, acc -> acc + item.product.price * item.quantity end)
assigns = assigns =
assigns assigns

View File

@ -5,7 +5,10 @@
id="theme-sidebar" id="theme-sidebar"
class={[ class={[
"bg-base-100 border-r border-base-300 lg:h-screen flex-shrink-0 transition-all duration-300", "bg-base-100 border-r border-base-300 lg:h-screen flex-shrink-0 transition-all duration-300",
if(@sidebar_collapsed, do: "w-12 overflow-hidden", else: "w-full lg:w-[380px] overflow-y-auto p-6") if(@sidebar_collapsed,
do: "w-12 overflow-hidden",
else: "w-full lg:w-[380px] overflow-y-auto p-6"
)
]} ]}
> >
<!-- Collapsed state: just show expand button --> <!-- Collapsed state: just show expand button -->
@ -19,7 +22,14 @@
aria-expanded="false" aria-expanded="false"
aria-controls="theme-sidebar" aria-controls="theme-sidebar"
> >
<svg class="w-5 h-5 text-base-content/70" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"> <svg
class="w-5 h-5 text-base-content/70"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
aria-hidden="true"
>
<polyline points="9 18 15 12 9 6"></polyline> <polyline points="9 18 15 12 9 6"></polyline>
</svg> </svg>
</button> </button>
@ -28,8 +38,12 @@
<!-- Header --> <!-- Header -->
<div class="mb-6 flex items-start justify-between gap-3"> <div class="mb-6 flex items-start justify-between gap-3">
<div class="flex-1"> <div class="flex-1">
<h1 class="text-xl font-semibold tracking-tight mb-2 text-base-content">Theme Studio</h1> <h1 class="text-xl font-semibold tracking-tight mb-2 text-base-content">
<p class="text-sm text-base-content/60 leading-relaxed">One theme, infinite possibilities. Every combination is designed to work beautifully.</p> Theme Studio
</h1>
<p class="text-sm text-base-content/60 leading-relaxed">
One theme, infinite possibilities. Every combination is designed to work beautifully.
</p>
</div> </div>
<button <button
type="button" type="button"
@ -39,7 +53,14 @@
aria-expanded="true" aria-expanded="true"
aria-controls="theme-sidebar" aria-controls="theme-sidebar"
> >
<svg class="w-5 h-5 text-base-content/70" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"> <svg
class="w-5 h-5 text-base-content/70"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
aria-hidden="true"
>
<polyline points="15 18 9 12 15 6"></polyline> <polyline points="15 18 9 12 15 6"></polyline>
</svg> </svg>
</button> </button>
@ -47,7 +68,9 @@
<!-- Site Name --> <!-- Site Name -->
<div class="mb-6"> <div class="mb-6">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Shop name</label> <label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">
Shop name
</label>
<form phx-change="update_setting" phx-value-field="site_name"> <form phx-change="update_setting" phx-value-field="site_name">
<input <input
type="text" type="text"
@ -61,7 +84,9 @@
<!-- Branding Section (styled background box) --> <!-- Branding Section (styled background box) -->
<div class="bg-base-200 rounded-xl p-4 mb-6"> <div class="bg-base-200 rounded-xl p-4 mb-6">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-4">Logo & header</label> <label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-4">
Logo & header
</label>
<!-- Logo Mode Radio Cards --> <!-- Logo Mode Radio Cards -->
<div class="flex flex-col gap-2 mb-4"> <div class="flex flex-col gap-2 mb-4">
@ -70,15 +95,13 @@
{"logo-text", "Logo + shop name", "Your logo image with name beside it"}, {"logo-text", "Logo + shop name", "Your logo image with name beside it"},
{"logo-only", "Logo only", "Just your logo (with text built in)"} {"logo-only", "Logo only", "Just your logo (with text built in)"}
] do %> ] do %>
<label <label class={[
class={[
"flex items-center gap-3 p-3 bg-base-100 border-2 rounded-lg cursor-pointer transition-all", "flex items-center gap-3 p-3 bg-base-100 border-2 rounded-lg cursor-pointer transition-all",
if(@theme_settings.logo_mode == value, if(@theme_settings.logo_mode == value,
do: "border-base-content", do: "border-base-content",
else: "border-transparent hover:border-base-300" else: "border-transparent hover:border-base-300"
) )
]} ]}>
>
<input <input
type="radio" type="radio"
name="logo_mode" name="logo_mode"
@ -98,12 +121,16 @@
]}> ]}>
<span class={[ <span class={[
"w-2 h-2 rounded-full bg-base-content transition-all", "w-2 h-2 rounded-full bg-base-content transition-all",
if(@theme_settings.logo_mode == value, do: "scale-100 opacity-100", else: "scale-0 opacity-0") if(@theme_settings.logo_mode == value,
]}></span> do: "scale-100 opacity-100",
else: "scale-0 opacity-0"
)
]}>
</span>
</span> </span>
<div class="flex-1"> <div class="flex-1">
<div class="text-sm font-medium text-base-content"><%= title %></div> <div class="text-sm font-medium text-base-content">{title}</div>
<div class="text-xs text-base-content/60"><%= desc %></div> <div class="text-xs text-base-content/60">{desc}</div>
</div> </div>
</label> </label>
<% end %> <% end %>
@ -112,7 +139,9 @@
<!-- Logo Upload (for logo-text and logo-only modes) --> <!-- Logo Upload (for logo-text and logo-only modes) -->
<%= if @theme_settings.logo_mode in ["logo-text", "logo-only"] do %> <%= if @theme_settings.logo_mode in ["logo-text", "logo-only"] do %>
<div class="mt-4 pt-4 border-t border-base-300"> <div class="mt-4 pt-4 border-t border-base-300">
<span class="block text-xs font-medium text-base-content/70 mb-2">Upload logo (SVG or PNG)</span> <span class="block text-xs font-medium text-base-content/70 mb-2">
Upload logo (SVG or PNG)
</span>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<form phx-change="noop" phx-submit="noop" class="flex-1"> <form phx-change="noop" phx-submit="noop" class="flex-1">
<label class="flex-1 bg-base-100 border border-dashed border-base-300 rounded-lg p-3 text-sm text-base-content/60 text-center cursor-pointer hover:border-base-content/40 hover:text-base-content/80 transition-all"> <label class="flex-1 bg-base-100 border border-dashed border-base-300 rounded-lg p-3 text-sm text-base-content/60 text-center cursor-pointer hover:border-base-content/40 hover:text-base-content/80 transition-all">
@ -132,7 +161,9 @@
phx-click="remove_logo" phx-click="remove_logo"
class="absolute -top-1.5 -right-1.5 w-[18px] h-[18px] bg-base-content text-base-100 rounded-full text-xs flex items-center justify-center leading-none" class="absolute -top-1.5 -right-1.5 w-[18px] h-[18px] bg-base-content text-base-100 rounded-full text-xs flex items-center justify-center leading-none"
title="Remove logo" title="Remove logo"
>×</button> >
×
</button>
</div> </div>
<% end %> <% end %>
</div> </div>
@ -140,24 +171,30 @@
<%= for entry <- @uploads.logo_upload.entries do %> <%= for entry <- @uploads.logo_upload.entries do %>
<div class="flex items-center gap-2 mt-2"> <div class="flex items-center gap-2 mt-2">
<div class="flex-1 h-1.5 bg-base-300 rounded-full overflow-hidden"> <div class="flex-1 h-1.5 bg-base-300 rounded-full overflow-hidden">
<div class="h-full bg-primary transition-all" style={"width: #{entry.progress}%"}></div> <div
class="h-full bg-primary transition-all"
style={"width: #{entry.progress}%"}
>
</div> </div>
<span class="text-xs text-base-content/60"><%= entry.progress %>%</span> </div>
<span class="text-xs text-base-content/60">{entry.progress}%</span>
<button <button
type="button" type="button"
phx-click="cancel_upload" phx-click="cancel_upload"
phx-value-ref={entry.ref} phx-value-ref={entry.ref}
phx-value-upload="logo_upload" phx-value-upload="logo_upload"
class="text-base-content/40 hover:text-base-content/70" class="text-base-content/40 hover:text-base-content/70"
>×</button> >
×
</button>
</div> </div>
<%= for err <- upload_errors(@uploads.logo_upload, entry) do %> <%= for err <- upload_errors(@uploads.logo_upload, entry) do %>
<p class="text-error text-xs mt-1"><%= error_to_string(err) %></p> <p class="text-error text-xs mt-1">{error_to_string(err)}</p>
<% end %> <% end %>
<% end %> <% end %>
<%= for err <- upload_errors(@uploads.logo_upload) do %> <%= for err <- upload_errors(@uploads.logo_upload) do %>
<p class="text-error text-xs mt-1"><%= error_to_string(err) %></p> <p class="text-error text-xs mt-1">{error_to_string(err)}</p>
<% end %> <% end %>
<!-- Logo Size Slider --> <!-- Logo Size Slider -->
@ -165,7 +202,9 @@
<form phx-change="update_setting" phx-value-field="logo_size" class="mt-3"> <form phx-change="update_setting" phx-value-field="logo_size" class="mt-3">
<div class="flex justify-between items-center mb-2"> <div class="flex justify-between items-center mb-2">
<span class="text-xs font-medium text-base-content/70">Logo size</span> <span class="text-xs font-medium text-base-content/70">Logo size</span>
<span class="text-xs font-mono text-base-content/60"><%= @theme_settings.logo_size %>px</span> <span class="text-xs font-mono text-base-content/60">
{@theme_settings.logo_size}px
</span>
</div> </div>
<input <input
type="range" type="range"
@ -186,21 +225,31 @@
checked={@theme_settings.logo_recolor} checked={@theme_settings.logo_recolor}
phx-click="update_setting" phx-click="update_setting"
phx-value-field="logo_recolor" phx-value-field="logo_recolor"
phx-value-setting_value={if @theme_settings.logo_recolor, do: "false", else: "true"} phx-value-setting_value={
if @theme_settings.logo_recolor, do: "false", else: "true"
}
class="toggle toggle-sm toggle-primary" class="toggle toggle-sm toggle-primary"
/> />
<span class="text-sm text-base-content/70">Recolour logo</span> <span class="text-sm text-base-content/70">Recolour logo</span>
</label> </label>
<%= if @theme_settings.logo_recolor do %> <%= if @theme_settings.logo_recolor do %>
<form id="logo-color-form" phx-change="update_color" phx-value-field="logo_color" phx-hook="ColorSync" class="flex items-center gap-3 mt-2"> <form
id="logo-color-form"
phx-change="update_color"
phx-value-field="logo_color"
phx-hook="ColorSync"
class="flex items-center gap-3 mt-2"
>
<input <input
type="color" type="color"
name="value" name="value"
value={@theme_settings.logo_color} value={@theme_settings.logo_color}
class="w-9 h-9 rounded-lg cursor-pointer border-0 p-0" class="w-9 h-9 rounded-lg cursor-pointer border-0 p-0"
/> />
<span class="font-mono text-sm text-base-content/70"><%= @theme_settings.logo_color %></span> <span class="font-mono text-sm text-base-content/70">
{@theme_settings.logo_color}
</span>
</form> </form>
<% end %> <% end %>
</div> </div>
@ -218,7 +267,9 @@
checked={@theme_settings.header_background_enabled} checked={@theme_settings.header_background_enabled}
phx-click="update_setting" phx-click="update_setting"
phx-value-field="header_background_enabled" phx-value-field="header_background_enabled"
phx-value-setting_value={if @theme_settings.header_background_enabled, do: "false", else: "true"} phx-value-setting_value={
if @theme_settings.header_background_enabled, do: "false", else: "true"
}
class="toggle toggle-sm toggle-primary" class="toggle toggle-sm toggle-primary"
/> />
<span class="text-sm text-base-content/80">Header background image</span> <span class="text-sm text-base-content/80">Header background image</span>
@ -228,7 +279,9 @@
<!-- Header Image Upload (only when enabled) --> <!-- Header Image Upload (only when enabled) -->
<%= if @theme_settings.header_background_enabled do %> <%= if @theme_settings.header_background_enabled do %>
<div class="bg-base-200 rounded-xl p-4 mb-6"> <div class="bg-base-200 rounded-xl p-4 mb-6">
<span class="block text-xs font-medium text-base-content/70 mb-2">Upload header image</span> <span class="block text-xs font-medium text-base-content/70 mb-2">
Upload header image
</span>
<form phx-change="noop" phx-submit="noop"> <form phx-change="noop" phx-submit="noop">
<label class="block bg-base-100 border border-dashed border-base-300 rounded-lg p-3 text-sm text-base-content/60 text-center cursor-pointer hover:border-base-content/40 hover:text-base-content/80 transition-all"> <label class="block bg-base-100 border border-dashed border-base-300 rounded-lg p-3 text-sm text-base-content/60 text-center cursor-pointer hover:border-base-content/40 hover:text-base-content/80 transition-all">
<span>Choose file...</span> <span>Choose file...</span>
@ -248,7 +301,9 @@
phx-click="remove_header" phx-click="remove_header"
class="absolute -top-1.5 -right-1.5 w-[18px] h-[18px] bg-base-content text-base-100 rounded-full text-xs flex items-center justify-center leading-none" class="absolute -top-1.5 -right-1.5 w-[18px] h-[18px] bg-base-content text-base-100 rounded-full text-xs flex items-center justify-center leading-none"
title="Remove header background" title="Remove header background"
>×</button> >
×
</button>
</div> </div>
<!-- Header Image Controls --> <!-- Header Image Controls -->
@ -256,7 +311,9 @@
<form phx-change="update_setting" phx-value-field="header_zoom"> <form phx-change="update_setting" phx-value-field="header_zoom">
<div class="flex justify-between items-center mb-2"> <div class="flex justify-between items-center mb-2">
<span class="text-xs font-medium text-base-content/70">Zoom</span> <span class="text-xs font-medium text-base-content/70">Zoom</span>
<span class="text-xs font-mono text-base-content/60"><%= @theme_settings.header_zoom %>%</span> <span class="text-xs font-mono text-base-content/60">
{@theme_settings.header_zoom}%
</span>
</div> </div>
<input <input
type="range" type="range"
@ -269,8 +326,12 @@
</form> </form>
<form phx-change="update_setting" phx-value-field="header_position_x"> <form phx-change="update_setting" phx-value-field="header_position_x">
<div class="flex justify-between items-center mb-2"> <div class="flex justify-between items-center mb-2">
<span class="text-xs font-medium text-base-content/70">Horizontal position</span> <span class="text-xs font-medium text-base-content/70">
<span class="text-xs font-mono text-base-content/60"><%= @theme_settings.header_position_x %>%</span> Horizontal position
</span>
<span class="text-xs font-mono text-base-content/60">
{@theme_settings.header_position_x}%
</span>
</div> </div>
<input <input
type="range" type="range"
@ -283,8 +344,12 @@
</form> </form>
<form phx-change="update_setting" phx-value-field="header_position_y"> <form phx-change="update_setting" phx-value-field="header_position_y">
<div class="flex justify-between items-center mb-2"> <div class="flex justify-between items-center mb-2">
<span class="text-xs font-medium text-base-content/70">Vertical position</span> <span class="text-xs font-medium text-base-content/70">
<span class="text-xs font-mono text-base-content/60"><%= @theme_settings.header_position_y %>%</span> Vertical position
</span>
<span class="text-xs font-mono text-base-content/60">
{@theme_settings.header_position_y}%
</span>
</div> </div>
<input <input
type="range" type="range"
@ -301,31 +366,39 @@
<%= for entry <- @uploads.header_upload.entries do %> <%= for entry <- @uploads.header_upload.entries do %>
<div class="flex items-center gap-2 mt-2"> <div class="flex items-center gap-2 mt-2">
<div class="flex-1 h-1.5 bg-base-300 rounded-full overflow-hidden"> <div class="flex-1 h-1.5 bg-base-300 rounded-full overflow-hidden">
<div class="h-full bg-primary transition-all" style={"width: #{entry.progress}%"}></div> <div
class="h-full bg-primary transition-all"
style={"width: #{entry.progress}%"}
>
</div> </div>
<span class="text-xs text-base-content/60"><%= entry.progress %>%</span> </div>
<span class="text-xs text-base-content/60">{entry.progress}%</span>
<button <button
type="button" type="button"
phx-click="cancel_upload" phx-click="cancel_upload"
phx-value-ref={entry.ref} phx-value-ref={entry.ref}
phx-value-upload="header_upload" phx-value-upload="header_upload"
class="text-base-content/40 hover:text-base-content/70" class="text-base-content/40 hover:text-base-content/70"
>×</button> >
×
</button>
</div> </div>
<%= for err <- upload_errors(@uploads.header_upload, entry) do %> <%= for err <- upload_errors(@uploads.header_upload, entry) do %>
<p class="text-error text-xs mt-1"><%= error_to_string(err) %></p> <p class="text-error text-xs mt-1">{error_to_string(err)}</p>
<% end %> <% end %>
<% end %> <% end %>
<%= for err <- upload_errors(@uploads.header_upload) do %> <%= for err <- upload_errors(@uploads.header_upload) do %>
<p class="text-error text-xs mt-1"><%= error_to_string(err) %></p> <p class="text-error text-xs mt-1">{error_to_string(err)}</p>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
<!-- Presets Section --> <!-- Presets Section -->
<div class="mb-6"> <div class="mb-6">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Start with a preset</label> <label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">
Start with a preset
</label>
<div class="grid grid-cols-2 gap-2"> <div class="grid grid-cols-2 gap-2">
<%= for {preset_name, description} <- @presets_with_descriptions do %> <%= for {preset_name, description} <- @presets_with_descriptions do %>
<button <button
@ -340,8 +413,10 @@
) )
]} ]}
> >
<div class="font-semibold text-sm capitalize text-base-content"><%= preset_name %></div> <div class="font-semibold text-sm capitalize text-base-content">
<div class="text-xs text-base-content/60"><%= description %></div> {preset_name}
</div>
<div class="text-xs text-base-content/60">{description}</div>
</button> </button>
<% end %> <% end %>
</div> </div>
@ -349,8 +424,15 @@
<!-- Accent Colors (stays in essentials) --> <!-- Accent Colors (stays in essentials) -->
<div class="mb-6"> <div class="mb-6">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Accent colour</label> <label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">
<form id="accent-color-form" phx-change="update_color" phx-value-field="accent_color" phx-hook="ColorSync"> Accent colour
</label>
<form
id="accent-color-form"
phx-change="update_color"
phx-value-field="accent_color"
phx-hook="ColorSync"
>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<input <input
type="color" type="color"
@ -359,14 +441,23 @@
value={@theme_settings.accent_color} value={@theme_settings.accent_color}
class="w-12 h-12 rounded-lg cursor-pointer border-0 p-0" class="w-12 h-12 rounded-lg cursor-pointer border-0 p-0"
/> />
<span class="font-mono text-sm text-base-content/70"><%= @theme_settings.accent_color %></span> <span class="font-mono text-sm text-base-content/70">
{@theme_settings.accent_color}
</span>
</div> </div>
</form> </form>
</div> </div>
<div class="mb-6"> <div class="mb-6">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Hover colour</label> <label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">
<form id="secondary-accent-color-form" phx-change="update_color" phx-value-field="secondary_accent_color" phx-hook="ColorSync"> Hover colour
</label>
<form
id="secondary-accent-color-form"
phx-change="update_color"
phx-value-field="secondary_accent_color"
phx-hook="ColorSync"
>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<input <input
type="color" type="color"
@ -375,14 +466,23 @@
value={@theme_settings.secondary_accent_color} value={@theme_settings.secondary_accent_color}
class="w-12 h-12 rounded-lg cursor-pointer border-0 p-0" class="w-12 h-12 rounded-lg cursor-pointer border-0 p-0"
/> />
<span class="font-mono text-sm text-base-content/70"><%= @theme_settings.secondary_accent_color %></span> <span class="font-mono text-sm text-base-content/70">
{@theme_settings.secondary_accent_color}
</span>
</div> </div>
</form> </form>
</div> </div>
<div class="mb-6"> <div class="mb-6">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Sale colour</label> <label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">
<form id="sale-color-form" phx-change="update_color" phx-value-field="sale_color" phx-hook="ColorSync"> Sale colour
</label>
<form
id="sale-color-form"
phx-change="update_color"
phx-value-field="sale_color"
phx-hook="ColorSync"
>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<input <input
type="color" type="color"
@ -391,16 +491,33 @@
value={@theme_settings.sale_color} value={@theme_settings.sale_color}
class="w-12 h-12 rounded-lg cursor-pointer border-0 p-0" class="w-12 h-12 rounded-lg cursor-pointer border-0 p-0"
/> />
<span class="font-mono text-sm text-base-content/70"><%= @theme_settings.sale_color %></span> <span class="font-mono text-sm text-base-content/70">
{@theme_settings.sale_color}
</span>
</div> </div>
</form> </form>
</div> </div>
<!-- Customise Section (collapsible accordion using native details/summary) --> <!-- Customise Section (collapsible accordion using native details/summary) -->
<details class="border-t border-base-300 mt-6 pt-4 group" id="customise-section" open={@customise_open}> <details
<summary class="flex items-center justify-between w-full py-3 cursor-pointer list-none [&::-webkit-details-marker]:hidden" phx-click="toggle_customise"> class="border-t border-base-300 mt-6 pt-4 group"
<span class="text-sm font-semibold text-base-content/70 group-hover:text-base-content transition-colors">Customise</span> id="customise-section"
<svg class="w-5 h-5 text-base-content/50 transition-transform group-open:rotate-180" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> open={@customise_open}
>
<summary
class="flex items-center justify-between w-full py-3 cursor-pointer list-none [&::-webkit-details-marker]:hidden"
phx-click="toggle_customise"
>
<span class="text-sm font-semibold text-base-content/70 group-hover:text-base-content transition-colors">
Customise
</span>
<svg
class="w-5 h-5 text-base-content/50 transition-transform group-open:rotate-180"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="6 9 12 15 18 9"></polyline> <polyline points="6 9 12 15 18 9"></polyline>
</svg> </svg>
</summary> </summary>
@ -409,7 +526,13 @@
<!-- Typography Group --> <!-- Typography Group -->
<div class="mb-6 pb-6 border-b border-base-200"> <div class="mb-6 pb-6 border-b border-base-200">
<div class="flex items-center gap-2 mb-4"> <div class="flex items-center gap-2 mb-4">
<svg class="w-4 h-4 text-base-content/50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg
class="w-4 h-4 text-base-content/50"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="4 7 4 4 20 4 20 7"></polyline> <polyline points="4 7 4 4 20 4 20 7"></polyline>
<line x1="9" y1="20" x2="15" y2="20"></line> <line x1="9" y1="20" x2="15" y2="20"></line>
<line x1="12" y1="4" x2="12" y2="20"></line> <line x1="12" y1="4" x2="12" y2="20"></line>
@ -418,7 +541,9 @@
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Font style</label> <label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">
Font style
</label>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<%= for typo <- ["clean", "editorial", "modern", "classic", "friendly", "minimal"] do %> <%= for typo <- ["clean", "editorial", "modern", "classic", "friendly", "minimal"] do %>
<button <button
@ -430,18 +555,21 @@
"px-3 py-2 text-sm rounded-lg border-2 transition-all capitalize", "px-3 py-2 text-sm rounded-lg border-2 transition-all capitalize",
if(@theme_settings.typography == typo, if(@theme_settings.typography == typo,
do: "border-base-content bg-base-100 text-base-content", do: "border-base-content bg-base-100 text-base-content",
else: "border-transparent bg-base-200 hover:bg-base-300 text-base-content" else:
"border-transparent bg-base-200 hover:bg-base-300 text-base-content"
) )
]} ]}
> >
<%= typo %> {typo}
</button> </button>
<% end %> <% end %>
</div> </div>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Font size</label> <label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">
Font size
</label>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<%= for {value, label} <- [{"small", "Small"}, {"medium", "Medium"}, {"large", "Large"}] do %> <%= for {value, label} <- [{"small", "Small"}, {"medium", "Medium"}, {"large", "Large"}] do %>
<button <button
@ -453,18 +581,21 @@
"px-3 py-2 text-sm rounded-lg border-2 transition-all", "px-3 py-2 text-sm rounded-lg border-2 transition-all",
if(@theme_settings.font_size == value, if(@theme_settings.font_size == value,
do: "border-base-content bg-base-100 text-base-content", do: "border-base-content bg-base-100 text-base-content",
else: "border-transparent bg-base-200 hover:bg-base-300 text-base-content" else:
"border-transparent bg-base-200 hover:bg-base-300 text-base-content"
) )
]} ]}
> >
<%= label %> {label}
</button> </button>
<% end %> <% end %>
</div> </div>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Heading weight</label> <label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">
Heading weight
</label>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<%= for {value, label} <- [{"regular", "Regular"}, {"medium", "Medium"}, {"bold", "Bold"}] do %> <%= for {value, label} <- [{"regular", "Regular"}, {"medium", "Medium"}, {"bold", "Bold"}] do %>
<button <button
@ -476,11 +607,12 @@
"px-3 py-2 text-sm rounded-lg border-2 transition-all", "px-3 py-2 text-sm rounded-lg border-2 transition-all",
if(@theme_settings.heading_weight == value, if(@theme_settings.heading_weight == value,
do: "border-base-content bg-base-100 text-base-content", do: "border-base-content bg-base-100 text-base-content",
else: "border-transparent bg-base-200 hover:bg-base-300 text-base-content" else:
"border-transparent bg-base-200 hover:bg-base-300 text-base-content"
) )
]} ]}
> >
<%= label %> {label}
</button> </button>
<% end %> <% end %>
</div> </div>
@ -490,7 +622,13 @@
<!-- Colours Group --> <!-- Colours Group -->
<div class="mb-6 pb-6 border-b border-base-200"> <div class="mb-6 pb-6 border-b border-base-200">
<div class="flex items-center gap-2 mb-4"> <div class="flex items-center gap-2 mb-4">
<svg class="w-4 h-4 text-base-content/50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg
class="w-4 h-4 text-base-content/50"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10"></circle> <circle cx="12" cy="12" r="10"></circle>
<circle cx="12" cy="12" r="3"></circle> <circle cx="12" cy="12" r="3"></circle>
</svg> </svg>
@ -498,7 +636,9 @@
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Colour mood</label> <label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">
Colour mood
</label>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<%= for mood <- ["warm", "neutral", "cool", "dark"] do %> <%= for mood <- ["warm", "neutral", "cool", "dark"] do %>
<button <button
@ -510,11 +650,12 @@
"px-3 py-2 text-sm rounded-lg border-2 transition-all capitalize", "px-3 py-2 text-sm rounded-lg border-2 transition-all capitalize",
if(@theme_settings.mood == mood, if(@theme_settings.mood == mood,
do: "border-base-content bg-base-100 text-base-content", do: "border-base-content bg-base-100 text-base-content",
else: "border-transparent bg-base-200 hover:bg-base-300 text-base-content" else:
"border-transparent bg-base-200 hover:bg-base-300 text-base-content"
) )
]} ]}
> >
<%= mood %> {mood}
</button> </button>
<% end %> <% end %>
</div> </div>
@ -524,7 +665,13 @@
<!-- Layout Group --> <!-- Layout Group -->
<div class="mb-6 pb-6 border-b border-base-200"> <div class="mb-6 pb-6 border-b border-base-200">
<div class="flex items-center gap-2 mb-4"> <div class="flex items-center gap-2 mb-4">
<svg class="w-4 h-4 text-base-content/50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg
class="w-4 h-4 text-base-content/50"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect> <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<line x1="3" y1="9" x2="21" y2="9"></line> <line x1="3" y1="9" x2="21" y2="9"></line>
<line x1="9" y1="21" x2="9" y2="9"></line> <line x1="9" y1="21" x2="9" y2="9"></line>
@ -533,7 +680,9 @@
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Product grid</label> <label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">
Product grid
</label>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<%= for cols <- ["2", "3", "4"] do %> <%= for cols <- ["2", "3", "4"] do %>
<button <button
@ -545,18 +694,21 @@
"px-3 py-2 text-sm rounded-lg border-2 transition-all", "px-3 py-2 text-sm rounded-lg border-2 transition-all",
if(@theme_settings.grid_columns == cols, if(@theme_settings.grid_columns == cols,
do: "border-base-content bg-base-100 text-base-content", do: "border-base-content bg-base-100 text-base-content",
else: "border-transparent bg-base-200 hover:bg-base-300 text-base-content" else:
"border-transparent bg-base-200 hover:bg-base-300 text-base-content"
) )
]} ]}
> >
<%= cols %> columns {cols} columns
</button> </button>
<% end %> <% end %>
</div> </div>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Density</label> <label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">
Density
</label>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<%= for density <- ["spacious", "balanced", "compact"] do %> <%= for density <- ["spacious", "balanced", "compact"] do %>
<button <button
@ -568,18 +720,21 @@
"px-3 py-2 text-sm rounded-lg border-2 transition-all capitalize", "px-3 py-2 text-sm rounded-lg border-2 transition-all capitalize",
if(@theme_settings.density == density, if(@theme_settings.density == density,
do: "border-base-content bg-base-100 text-base-content", do: "border-base-content bg-base-100 text-base-content",
else: "border-transparent bg-base-200 hover:bg-base-300 text-base-content" else:
"border-transparent bg-base-200 hover:bg-base-300 text-base-content"
) )
]} ]}
> >
<%= density %> {density}
</button> </button>
<% end %> <% end %>
</div> </div>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Header layout</label> <label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">
Header layout
</label>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<%= for layout <- ["standard", "centered", "left"] do %> <%= for layout <- ["standard", "centered", "left"] do %>
<button <button
@ -591,11 +746,12 @@
"px-3 py-2 text-sm rounded-lg border-2 transition-all capitalize", "px-3 py-2 text-sm rounded-lg border-2 transition-all capitalize",
if(@theme_settings.header_layout == layout, if(@theme_settings.header_layout == layout,
do: "border-base-content bg-base-100 text-base-content", do: "border-base-content bg-base-100 text-base-content",
else: "border-transparent bg-base-200 hover:bg-base-300 text-base-content" else:
"border-transparent bg-base-200 hover:bg-base-300 text-base-content"
) )
]} ]}
> >
<%= layout %> {layout}
</button> </button>
<% end %> <% end %>
</div> </div>
@ -631,14 +787,22 @@
<!-- Shape Group --> <!-- Shape Group -->
<div class="mb-4"> <div class="mb-4">
<div class="flex items-center gap-2 mb-4"> <div class="flex items-center gap-2 mb-4">
<svg class="w-4 h-4 text-base-content/50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg
class="w-4 h-4 text-base-content/50"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect> <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
</svg> </svg>
<span class="text-sm font-semibold text-base-content">Shape</span> <span class="text-sm font-semibold text-base-content">Shape</span>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Corner style</label> <label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">
Corner style
</label>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<%= for shape <- ["sharp", "soft", "round", "pill"] do %> <%= for shape <- ["sharp", "soft", "round", "pill"] do %>
<button <button
@ -650,18 +814,21 @@
"px-3 py-2 text-sm rounded-lg border-2 transition-all capitalize", "px-3 py-2 text-sm rounded-lg border-2 transition-all capitalize",
if(@theme_settings.shape == shape, if(@theme_settings.shape == shape,
do: "border-base-content bg-base-100 text-base-content", do: "border-base-content bg-base-100 text-base-content",
else: "border-transparent bg-base-200 hover:bg-base-300 text-base-content" else:
"border-transparent bg-base-200 hover:bg-base-300 text-base-content"
) )
]} ]}
> >
<%= shape %> {shape}
</button> </button>
<% end %> <% end %>
</div> </div>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Card shadow</label> <label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">
Card shadow
</label>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<%= for {value, label} <- [{"none", "None"}, {"sm", "Subtle"}, {"md", "Medium"}, {"lg", "Strong"}] do %> <%= for {value, label} <- [{"none", "None"}, {"sm", "Subtle"}, {"md", "Medium"}, {"lg", "Strong"}] do %>
<button <button
@ -673,18 +840,21 @@
"px-3 py-2 text-sm rounded-lg border-2 transition-all", "px-3 py-2 text-sm rounded-lg border-2 transition-all",
if(@theme_settings.card_shadow == value, if(@theme_settings.card_shadow == value,
do: "border-base-content bg-base-100 text-base-content", do: "border-base-content bg-base-100 text-base-content",
else: "border-transparent bg-base-200 hover:bg-base-300 text-base-content" else:
"border-transparent bg-base-200 hover:bg-base-300 text-base-content"
) )
]} ]}
> >
<%= label %> {label}
</button> </button>
<% end %> <% end %>
</div> </div>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Button style</label> <label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">
Button style
</label>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<%= for {value, label} <- [{"filled", "Filled"}, {"outline", "Outline"}, {"soft", "Soft"}] do %> <%= for {value, label} <- [{"filled", "Filled"}, {"outline", "Outline"}, {"soft", "Soft"}] do %>
<button <button
@ -696,11 +866,12 @@
"px-3 py-2 text-sm rounded-lg border-2 transition-all", "px-3 py-2 text-sm rounded-lg border-2 transition-all",
if(@theme_settings.button_style == value, if(@theme_settings.button_style == value,
do: "border-base-content bg-base-100 text-base-content", do: "border-base-content bg-base-100 text-base-content",
else: "border-transparent bg-base-200 hover:bg-base-300 text-base-content" else:
"border-transparent bg-base-200 hover:bg-base-300 text-base-content"
) )
]} ]}
> >
<%= label %> {label}
</button> </button>
<% end %> <% end %>
</div> </div>
@ -710,7 +881,13 @@
<!-- Products Group --> <!-- Products Group -->
<div class="mb-6 pb-6 border-b border-base-200"> <div class="mb-6 pb-6 border-b border-base-200">
<div class="flex items-center gap-2 mb-4"> <div class="flex items-center gap-2 mb-4">
<svg class="w-4 h-4 text-base-content/50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg
class="w-4 h-4 text-base-content/50"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect x="3" y="3" width="7" height="7"></rect> <rect x="3" y="3" width="7" height="7"></rect>
<rect x="14" y="3" width="7" height="7"></rect> <rect x="14" y="3" width="7" height="7"></rect>
<rect x="14" y="14" width="7" height="7"></rect> <rect x="14" y="14" width="7" height="7"></rect>
@ -720,7 +897,9 @@
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Content width</label> <label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">
Content width
</label>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<%= for width <- ["contained", "wide", "full"] do %> <%= for width <- ["contained", "wide", "full"] do %>
<button <button
@ -732,18 +911,21 @@
"px-3 py-2 text-sm rounded-lg border-2 transition-all capitalize", "px-3 py-2 text-sm rounded-lg border-2 transition-all capitalize",
if(@theme_settings.layout_width == width, if(@theme_settings.layout_width == width,
do: "border-base-content bg-base-100 text-base-content", do: "border-base-content bg-base-100 text-base-content",
else: "border-transparent bg-base-200 hover:bg-base-300 text-base-content" else:
"border-transparent bg-base-200 hover:bg-base-300 text-base-content"
) )
]} ]}
> >
<%= width %> {width}
</button> </button>
<% end %> <% end %>
</div> </div>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Image aspect ratio</label> <label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">
Image aspect ratio
</label>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<%= for {value, label} <- [{"square", "Square"}, {"portrait", "Portrait"}, {"landscape", "Landscape"}] do %> <%= for {value, label} <- [{"square", "Square"}, {"portrait", "Portrait"}, {"landscape", "Landscape"}] do %>
<button <button
@ -755,18 +937,21 @@
"px-3 py-2 text-sm rounded-lg border-2 transition-all", "px-3 py-2 text-sm rounded-lg border-2 transition-all",
if(@theme_settings.image_aspect_ratio == value, if(@theme_settings.image_aspect_ratio == value,
do: "border-base-content bg-base-100 text-base-content", do: "border-base-content bg-base-100 text-base-content",
else: "border-transparent bg-base-200 hover:bg-base-300 text-base-content" else:
"border-transparent bg-base-200 hover:bg-base-300 text-base-content"
) )
]} ]}
> >
<%= label %> {label}
</button> </button>
<% end %> <% end %>
</div> </div>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Product text alignment</label> <label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">
Product text alignment
</label>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<%= for {value, label} <- [{"left", "Left"}, {"center", "Centre"}] do %> <%= for {value, label} <- [{"left", "Left"}, {"center", "Centre"}] do %>
<button <button
@ -778,11 +963,12 @@
"px-3 py-2 text-sm rounded-lg border-2 transition-all", "px-3 py-2 text-sm rounded-lg border-2 transition-all",
if(@theme_settings.product_text_align == value, if(@theme_settings.product_text_align == value,
do: "border-base-content bg-base-100 text-base-content", do: "border-base-content bg-base-100 text-base-content",
else: "border-transparent bg-base-200 hover:bg-base-300 text-base-content" else:
"border-transparent bg-base-200 hover:bg-base-300 text-base-content"
) )
]} ]}
> >
<%= label %> {label}
</button> </button>
<% end %> <% end %>
</div> </div>
@ -818,7 +1004,13 @@
<!-- Product Page Group --> <!-- Product Page Group -->
<div class="mb-4"> <div class="mb-4">
<div class="flex items-center gap-2 mb-4"> <div class="flex items-center gap-2 mb-4">
<svg class="w-4 h-4 text-base-content/50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg
class="w-4 h-4 text-base-content/50"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect> <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<line x1="3" y1="9" x2="21" y2="9"></line> <line x1="3" y1="9" x2="21" y2="9"></line>
</svg> </svg>
@ -869,11 +1061,21 @@
<!-- Current Combination Display --> <!-- Current Combination Display -->
<div class="bg-base-200 rounded-xl p-4 mt-6"> <div class="bg-base-200 rounded-xl p-4 mt-6">
<div class="text-xs font-semibold uppercase tracking-wider text-base-content/50 mb-2">Current combination</div> <div class="text-xs font-semibold uppercase tracking-wider text-base-content/50 mb-2">
<div class="text-sm text-base-content leading-relaxed"> Current combination
<%= String.capitalize(@theme_settings.mood) %> · <%= String.capitalize(@theme_settings.typography) %> · <%= String.capitalize(@theme_settings.shape) %> · <%= String.capitalize(@theme_settings.density) %> · <%= @theme_settings.grid_columns %>-up · <%= String.capitalize(@theme_settings.header_layout) %> </div>
<div class="text-sm text-base-content leading-relaxed">
{String.capitalize(@theme_settings.mood)} · {String.capitalize(
@theme_settings.typography
)} · {String.capitalize(@theme_settings.shape)} · {String.capitalize(
@theme_settings.density
)} · {@theme_settings.grid_columns}-up · {String.capitalize(
@theme_settings.header_layout
)}
</div>
<div class="text-xs text-base-content/40 mt-2">
One of 100,000+ possible combinations
</div> </div>
<div class="text-xs text-base-content/40 mt-2">One of 100,000+ possible combinations</div>
</div> </div>
<% end %> <% end %>
</div> </div>
@ -904,7 +1106,7 @@
) )
]} ]}
> >
<%= label %> {label}
</button> </button>
<% end %> <% end %>
</div> </div>
@ -917,16 +1119,25 @@
<div class="w-3 h-3 rounded-full bg-[#28c940] border border-[#1aab29]"></div> <div class="w-3 h-3 rounded-full bg-[#28c940] border border-[#1aab29]"></div>
</div> </div>
<div class="flex-1 flex items-center gap-2 bg-base-100 border border-base-content/20 rounded-md px-3 py-[5px]"> <div class="flex-1 flex items-center gap-2 bg-base-100 border border-base-content/20 rounded-md px-3 py-[5px]">
<svg class="w-[14px] h-[14px] text-base-content/50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg
class="w-[14px] h-[14px] text-base-content/50"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect> <rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path> <path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg> </svg>
<span class="text-sm text-base-content/60 truncate"><%= @theme_settings.site_name |> String.downcase() |> String.replace(" ", "") %>.myshopify.com</span> <span class="text-sm text-base-content/60 truncate">
{@theme_settings.site_name |> String.downcase() |> String.replace(" ", "")}.myshopify.com
</span>
</div> </div>
</div> </div>
<!-- Preview Frame --> <!-- Preview Frame -->
<div class="themed preview-frame bg-white overflow-auto flex-1 rounded-b-lg border border-t-0 border-base-content/20" <div
class="themed preview-frame bg-white overflow-auto flex-1 rounded-b-lg border border-t-0 border-base-content/20"
data-mood={@theme_settings.mood} data-mood={@theme_settings.mood}
data-typography={@theme_settings.typography} data-typography={@theme_settings.typography}
data-shape={@theme_settings.shape} data-shape={@theme_settings.shape}
@ -936,7 +1147,8 @@
data-sticky={to_string(@theme_settings.sticky_header)} data-sticky={to_string(@theme_settings.sticky_header)}
data-layout={@theme_settings.layout_width} data-layout={@theme_settings.layout_width}
data-shadow={@theme_settings.card_shadow} data-shadow={@theme_settings.card_shadow}
data-button-style={@theme_settings.button_style}> data-button-style={@theme_settings.button_style}
>
<style> <style>
/* All font faces for theme switching */ /* All font faces for theme switching */
<%= Phoenix.HTML.raw(SimpleshopTheme.Theme.Fonts.generate_all_font_faces( <%= Phoenix.HTML.raw(SimpleshopTheme.Theme.Fonts.generate_all_font_faces(

View File

@ -126,6 +126,7 @@ defmodule SimpleshopThemeWeb.UserLive.Login do
end end
defp local_mail_adapter? do defp local_mail_adapter? do
Application.get_env(:simpleshop_theme, SimpleshopTheme.Mailer)[:adapter] == Swoosh.Adapters.Local Application.get_env(:simpleshop_theme, SimpleshopTheme.Mailer)[:adapter] ==
Swoosh.Adapters.Local
end end
end end

View File

@ -4,7 +4,10 @@ defmodule SimpleshopTheme.Repo.Migrations.CreateProducts do
def change do def change do
create table(:products, primary_key: false) do create table(:products, primary_key: false) do
add :id, :binary_id, primary_key: true add :id, :binary_id, primary_key: true
add :provider_connection_id, references(:provider_connections, type: :binary_id, on_delete: :delete_all), null: false
add :provider_connection_id,
references(:provider_connections, type: :binary_id, on_delete: :delete_all), null: false
add :provider_product_id, :string, null: false add :provider_product_id, :string, null: false
add :title, :string, null: false add :title, :string, null: false
add :description, :text add :description, :text

View File

@ -4,7 +4,10 @@ defmodule SimpleshopTheme.Repo.Migrations.CreateProductImages do
def change do def change do
create table(:product_images, primary_key: false) do create table(:product_images, primary_key: false) do
add :id, :binary_id, primary_key: true add :id, :binary_id, primary_key: true
add :product_id, references(:products, type: :binary_id, on_delete: :delete_all), null: false
add :product_id, references(:products, type: :binary_id, on_delete: :delete_all),
null: false
add :src, :string, null: false add :src, :string, null: false
add :position, :integer, default: 0, null: false add :position, :integer, default: 0, null: false
add :alt, :string add :alt, :string

View File

@ -4,7 +4,10 @@ defmodule SimpleshopTheme.Repo.Migrations.CreateProductVariants do
def change do def change do
create table(:product_variants, primary_key: false) do create table(:product_variants, primary_key: false) do
add :id, :binary_id, primary_key: true add :id, :binary_id, primary_key: true
add :product_id, references(:products, type: :binary_id, on_delete: :delete_all), null: false
add :product_id, references(:products, type: :binary_id, on_delete: :delete_all),
null: false
add :provider_variant_id, :string, null: false add :provider_variant_id, :string, null: false
add :title, :string, null: false add :title, :string, null: false
add :sku, :string add :sku, :string

View File

@ -70,6 +70,7 @@ defmodule SimpleshopTheme.Media.SVGRecolorerTest do
svg = """ svg = """
<svg><style>.st0{fill:#FFFFFF;}.st1{fill:#EF1D1D;stroke:#000000;}</style><path class="st0"/></svg> <svg><style>.st0{fill:#FFFFFF;}.st1{fill:#EF1D1D;stroke:#000000;}</style><path class="st0"/></svg>
""" """
result = SVGRecolorer.recolor(svg, "#ff6600") result = SVGRecolorer.recolor(svg, "#ff6600")
assert result =~ "fill:#ff6600" assert result =~ "fill:#ff6600"
refute result =~ "#FFFFFF" refute result =~ "#FFFFFF"
@ -80,6 +81,7 @@ defmodule SimpleshopTheme.Media.SVGRecolorerTest do
svg = """ svg = """
<svg><style>.st1{stroke:#000000;stroke-miterlimit:10;}</style><path class="st1"/></svg> <svg><style>.st1{stroke:#000000;stroke-miterlimit:10;}</style><path class="st1"/></svg>
""" """
result = SVGRecolorer.recolor(svg, "#ff6600") result = SVGRecolorer.recolor(svg, "#ff6600")
assert result =~ "stroke:#ff6600" assert result =~ "stroke:#ff6600"
refute result =~ "#000000" refute result =~ "#000000"
@ -89,6 +91,7 @@ defmodule SimpleshopTheme.Media.SVGRecolorerTest do
svg = """ svg = """
<svg><style>.st0{fill:none;stroke:#000;}</style><path class="st0"/></svg> <svg><style>.st0{fill:none;stroke:#000;}</style><path class="st0"/></svg>
""" """
result = SVGRecolorer.recolor(svg, "#ff6600") result = SVGRecolorer.recolor(svg, "#ff6600")
assert result =~ "fill:none" assert result =~ "fill:none"
assert result =~ "stroke:#ff6600" assert result =~ "stroke:#ff6600"
@ -98,6 +101,7 @@ defmodule SimpleshopTheme.Media.SVGRecolorerTest do
svg = """ svg = """
<svg><style>.icon{fill:black;stroke:white;}</style><path class="icon"/></svg> <svg><style>.icon{fill:black;stroke:white;}</style><path class="icon"/></svg>
""" """
result = SVGRecolorer.recolor(svg, "#ff6600") result = SVGRecolorer.recolor(svg, "#ff6600")
assert result =~ "fill:#ff6600" assert result =~ "fill:#ff6600"
assert result =~ "stroke:#ff6600" assert result =~ "stroke:#ff6600"

View File

@ -96,7 +96,10 @@ defmodule SimpleshopTheme.Products.ProductTest do
test "stores provider_data as map", %{conn: conn} do test "stores provider_data as map", %{conn: conn} do
provider_data = %{"blueprint_id" => 145, "print_provider_id" => 29, "extra" => "value"} provider_data = %{"blueprint_id" => 145, "print_provider_id" => 29, "extra" => "value"}
attrs = valid_product_attrs(%{provider_connection_id: conn.id, provider_data: provider_data})
attrs =
valid_product_attrs(%{provider_connection_id: conn.id, provider_data: provider_data})
changeset = Product.changeset(%Product{}, attrs) changeset = Product.changeset(%Product{}, attrs)
assert changeset.valid? assert changeset.valid?

View File

@ -437,7 +437,9 @@ defmodule SimpleshopTheme.ProductsTest do
test "updates existing variants" do test "updates existing variants" do
product = product_fixture() product = product_fixture()
existing = product_variant_fixture(%{product: product, provider_variant_id: "v1", price: 2000})
existing =
product_variant_fixture(%{product: product, provider_variant_id: "v1", price: 2000})
variants = [ variants = [
%{provider_variant_id: "v1", title: "Small Updated", price: 2200} %{provider_variant_id: "v1", title: "Small Updated", price: 2200}

View File

@ -76,13 +76,15 @@ defmodule SimpleshopTheme.SettingsTest do
# Cache should now contain new CSS with the red accent color # Cache should now contain new CSS with the red accent color
{:ok, updated_css} = CSSCache.get() {:ok, updated_css} = CSSCache.get()
assert updated_css =~ ".themed {" assert updated_css =~ ".themed {"
assert updated_css =~ "--t-accent-h: 0" # Red = hue 0 # Red = hue 0
assert updated_css =~ "--t-accent-h: 0"
# Change to blue # Change to blue
{:ok, _settings} = Settings.update_theme_settings(%{accent_color: "#0000ff"}) {:ok, _settings} = Settings.update_theme_settings(%{accent_color: "#0000ff"})
{:ok, blue_css} = CSSCache.get() {:ok, blue_css} = CSSCache.get()
assert blue_css =~ "--t-accent-h: 240" # Blue = hue 240 # Blue = hue 240
assert blue_css =~ "--t-accent-h: 240"
# Restore default # Restore default
CSSCache.warm() CSSCache.warm()

View File

@ -61,7 +61,9 @@ defmodule SimpleshopTheme.Sync.ProductSyncWorkerTest do
test "new/2 with scheduled_at creates scheduled job" do test "new/2 with scheduled_at creates scheduled job" do
future = DateTime.add(DateTime.utc_now(), 60, :second) future = DateTime.add(DateTime.utc_now(), 60, :second)
changeset = ProductSyncWorker.new(%{provider_connection_id: "test-id"}, scheduled_at: future)
changeset =
ProductSyncWorker.new(%{provider_connection_id: "test-id"}, scheduled_at: future)
assert changeset.changes.scheduled_at == future assert changeset.changes.scheduled_at == future
end end

View File

@ -51,6 +51,7 @@ defmodule SimpleshopTheme.Theme.PreviewDataTest do
if product.hover_image_url do if product.hover_image_url do
assert is_binary(product.hover_image_url) assert is_binary(product.hover_image_url)
assert String.starts_with?(product.hover_image_url, "/") or assert String.starts_with?(product.hover_image_url, "/") or
String.starts_with?(product.hover_image_url, "http") String.starts_with?(product.hover_image_url, "http")
end end

View File

@ -2,7 +2,9 @@ defmodule SimpleshopThemeWeb.ErrorJSONTest do
use SimpleshopThemeWeb.ConnCase, async: true use SimpleshopThemeWeb.ConnCase, async: true
test "renders 404" do test "renders 404" do
assert SimpleshopThemeWeb.ErrorJSON.render("404.json", %{}) == %{errors: %{detail: "Not Found"}} assert SimpleshopThemeWeb.ErrorJSON.render("404.json", %{}) == %{
errors: %{detail: "Not Found"}
}
end end
test "renders 500" do test "renders 500" do