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
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

View File

@ -31,7 +31,8 @@ defmodule SimpleshopTheme.Images.Optimizer do
{width, _height, _} <- Image.shape(image),
{:ok, resized} <- maybe_resize(image, width),
{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}
end
rescue
@ -191,9 +192,20 @@ defmodule SimpleshopTheme.Images.Optimizer do
widths = applicable_widths(source_width)
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
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
]

View File

@ -59,7 +59,9 @@ defmodule SimpleshopTheme.Images.VariantCache do
if to_process == [] do
Logger.info("[VariantCache] All database image variants up to date")
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 ->
image

View File

@ -119,7 +119,12 @@ defmodule SimpleshopTheme.Media 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
@doc """
@ -132,7 +137,12 @@ defmodule SimpleshopTheme.Media 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
@doc """

View File

@ -46,7 +46,8 @@ defmodule SimpleshopTheme.Media.Image do
defp detect_svg(changeset) do
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
|> put_change(:is_svg, true)
|> maybe_store_svg_content()

View File

@ -23,7 +23,8 @@ defmodule SimpleshopTheme.Media.SVGRecolorer do
"""
@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
|> recolor_fill_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"},
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"},
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"},
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"},
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
# For cover: use the larger scale to ensure full coverage
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)
end
@ -267,21 +275,36 @@ defmodule SimpleshopTheme.Mockups.Generator do
@doc """
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)
variant = hd(variants)
variant_id = variant["id"]
# Get placeholder info
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
placeholder_width = front_placeholder["width"]
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 = %{
title: product_def.name,
@ -439,7 +462,17 @@ defmodule SimpleshopTheme.Mockups.Generator do
image_height = upload["height"],
_ = IO.puts(" Artwork uploaded (ID: #{image_id}, #{image_width}x#{image_height})"),
_ = 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"],
mockup_urls = extract_mockup_urls(product),
_ = IO.puts(" Product created (ID: #{product_id})"),

View File

@ -287,14 +287,17 @@ defmodule SimpleshopTheme.Products do
if MapSet.size(removed_ids) > 0 do
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()
end
# Upsert incoming variants
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)
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.
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
|> Map.values()
|> Enum.join(" / ")

View File

@ -93,7 +93,10 @@ defmodule SimpleshopTheme.Settings.ThemeSettings do
])
|> validate_required([:mood, :typography, :shape, :density])
|> 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(:density, ~w(spacious balanced compact))
|> 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_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_position_x, 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_number(:header_position_x,
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(:card_shadow, ~w(none sm md lg))
|> validate_inclusion(:font_size, ~w(small medium large))

View File

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

View File

@ -67,7 +67,8 @@ defmodule SimpleshopTheme.Theme.PreviewData do
%{
rating: 5,
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.",
date: "2 weeks ago",
verified: true
@ -75,7 +76,8 @@ defmodule SimpleshopTheme.Theme.PreviewData do
%{
rating: 4,
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.",
date: "1 month ago",
verified: true
@ -90,13 +92,33 @@ defmodule SimpleshopTheme.Theme.PreviewData 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: :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: :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: :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: :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: :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."}
]
end
@ -427,42 +449,48 @@ defmodule SimpleshopTheme.Theme.PreviewData do
%{
id: "1",
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,
date: "2025-01-15"
},
%{
id: "2",
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,
date: "2025-01-10"
},
%{
id: "3",
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,
date: "2025-01-05"
},
%{
id: "4",
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,
date: "2024-12-28"
},
%{
id: "5",
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,
date: "2024-12-20"
},
%{
id: "6",
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,
date: "2024-12-15"
}

View File

@ -17,7 +17,8 @@ defmodule SimpleshopThemeWeb do
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
quote do

View File

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

View File

@ -4,8 +4,14 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<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}"} />
<.live_title><%= assigns[:page_title] || @theme_settings.site_name %></.live_title>
<meta
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 -->
<%= for preload <- SimpleshopTheme.Theme.Fonts.preload_links(
@theme_settings.typography,
@ -17,7 +23,9 @@
<script defer phx-track-static src={~p"/assets/js/app.js"}>
</script>
<!-- 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>
<body class="h-full">
<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 />
<%= if @theme_settings.announcement_bar do %>
<.announcement_bar theme_settings={@theme_settings} />
<% end %>
<.shop_header theme_settings={@theme_settings} logo_image={@logo_image} header_image={@header_image} active_page="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);">
<.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 />
<%= if @theme_settings.announcement_bar do %>
<.announcement_bar theme_settings={@theme_settings} />
<% end %>
<.shop_header theme_settings={@theme_settings} logo_image={@logo_image} header_image={@header_image} active_page="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">
<.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 />
<%= if @theme_settings.announcement_bar do %>
<.announcement_bar theme_settings={@theme_settings} />
<% end %>
<.shop_header theme_settings={@theme_settings} logo_image={@logo_image} header_image={@header_image} active_page="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">
<.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 />
<%= if @theme_settings.announcement_bar do %>
<.announcement_bar theme_settings={@theme_settings} />
<% end %>
<.shop_header theme_settings={@theme_settings} logo_image={@logo_image} header_image={@header_image} active_page="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">
<.hero_section
@ -20,11 +30,14 @@
<div class="flex flex-col gap-6">
<.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: "Delivery", value: "3-7 business days after printing"},
%{label: "Returns", value: "Happy to help with faulty or damaged items"}
]} />
]}
/>
<.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 />
<%= if @theme_settings.announcement_bar do %>
<.announcement_bar theme_settings={@theme_settings} />
<% end %>
<.shop_header theme_settings={@theme_settings} logo_image={@logo_image} header_image={@header_image} active_page="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">
<.hero_section
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 />
<%= if @theme_settings.announcement_bar do %>
<.announcement_bar theme_settings={@theme_settings} />
<% end %>
<.shop_header theme_settings={@theme_settings} logo_image={@logo_image} header_image={@header_image} active_page="home" mode={@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">
<.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 />
<%= if @theme_settings.announcement_bar do %>
<.announcement_bar theme_settings={@theme_settings} />
<% end %>
<.shop_header theme_settings={@theme_settings} logo_image={@logo_image} header_image={@header_image} active_page="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">
<.breadcrumb items={[
<.breadcrumb
items={[
%{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}
]} mode={@mode} />
]}
mode={@mode}
/>
<div class="grid grid-cols-1 md:grid-cols-2 gap-12 mb-16">
<.product_gallery images={@gallery_images} product_name={@product.name} />
@ -27,7 +45,12 @@
</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
: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}
def render("404.html", assigns) do
render_error_page(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.")
render_error_page(
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
def render("500.html", assigns) do
render_error_page(assigns, "500", "Server Error",
"Something went wrong on our end. Please try again later or contact support if the problem persists.")
render_error_page(
assigns,
"500",
"Server Error",
"Something went wrong on our end. Please try again later or contact support if the problem persists."
)
end
def render(template, _assigns) do
@ -57,14 +65,15 @@ defmodule SimpleshopThemeWeb.ErrorHTML do
<head>
<meta charset="utf-8" />
<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"} />
<style id="theme-css">
<%= Phoenix.HTML.raw(@generated_css) %>
</style>
</head>
<body class="h-full">
<div class="shop-root themed h-full"
<div
class="shop-root themed h-full"
data-mood={@theme_settings.mood}
data-typography={@theme_settings.typography}
data-shape={@theme_settings.shape}
@ -73,7 +82,8 @@ defmodule SimpleshopThemeWeb.ErrorHTML do
data-header={@theme_settings.header_layout}
data-sticky={to_string(@theme_settings.sticky_header)}
data-layout={@theme_settings.layout_width}
data-shadow={@theme_settings.card_shadow}>
data-shadow={@theme_settings.card_shadow}
>
<SimpleshopThemeWeb.PageTemplates.error
theme_settings={@theme_settings}
logo_image={@logo_image}
@ -96,14 +106,18 @@ defmodule SimpleshopThemeWeb.ErrorHTML do
defp load_theme_data do
try do
theme_settings = Settings.get_theme_settings()
generated_css =
case CSSCache.get() do
{:ok, css} -> css
{:ok, css} ->
css
:miss ->
css = CSSGenerator.generate(theme_settings)
CSSCache.put(css)
css
end
{theme_settings, generated_css}
rescue
_ -> {%ThemeSettings{}, ""}

View File

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

View File

@ -11,7 +11,9 @@ defmodule SimpleshopThemeWeb.ShopLive.Cart do
generated_css =
case CSSCache.get() do
{:ok, css} -> css
{:ok, css} ->
css
:miss ->
css = CSSGenerator.generate(theme_settings)
CSSCache.put(css)
@ -24,7 +26,9 @@ defmodule SimpleshopThemeWeb.ShopLive.Cart do
# For now, use preview data for cart items
# In a real implementation, this would come from session/database
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
end)

View File

@ -20,7 +20,9 @@ defmodule SimpleshopThemeWeb.ShopLive.Collection do
generated_css =
case CSSCache.get() do
{:ok, css} -> css
{:ok, css} ->
css
:miss ->
css = CSSGenerator.generate(theme_settings)
CSSCache.put(css)
@ -82,7 +84,9 @@ defmodule SimpleshopThemeWeb.ShopLive.Collection do
@impl true
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}")}
end
@ -100,7 +104,10 @@ defmodule SimpleshopThemeWeb.ShopLive.Collection do
@impl true
def render(assigns) do
~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 />
<%= if @theme_settings.announcement_bar do %>
@ -145,7 +152,11 @@ defmodule SimpleshopThemeWeb.ShopLive.Collection do
<%= if @products == [] do %>
<div class="text-center py-16" style="color: var(--t-text-secondary);">
<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
</.link>
</div>
@ -155,9 +166,15 @@ defmodule SimpleshopThemeWeb.ShopLive.Collection do
<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} />
</div>
@ -176,10 +193,12 @@ defmodule SimpleshopThemeWeb.ShopLive.Collection do
"px-4 py-2 rounded-full text-sm transition-colors",
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);",
else: "background-color: var(--t-surface-raised); color: var(--t-text-primary);"
)}
)
}
>
All
</.link>
@ -192,10 +211,12 @@ defmodule SimpleshopThemeWeb.ShopLive.Collection do
"px-4 py-2 rounded-full text-sm transition-colors",
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);",
else: "background-color: var(--t-surface-raised); color: var(--t-text-primary);"
)}
)
}
>
{category.name}
</.link>

View File

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

View File

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

View File

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

View File

@ -10,6 +10,7 @@ defmodule SimpleshopThemeWeb.ThemeLive.Index do
theme_settings = Settings.get_theme_settings()
generated_css = CSSGenerator.generate(theme_settings)
active_preset = Presets.detect_preset(theme_settings)
preview_data = %{
products: PreviewData.products(),
cart_items: PreviewData.cart_items(),
@ -361,7 +362,9 @@ defmodule SimpleshopThemeWeb.ThemeLive.Index do
defp preview_page(%{page: :cart} = assigns) do
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

View File

@ -5,7 +5,10 @@
id="theme-sidebar"
class={[
"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 -->
@ -19,7 +22,14 @@
aria-expanded="false"
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>
</svg>
</button>
@ -28,8 +38,12 @@
<!-- Header -->
<div class="mb-6 flex items-start justify-between gap-3">
<div class="flex-1">
<h1 class="text-xl font-semibold tracking-tight mb-2 text-base-content">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>
<h1 class="text-xl font-semibold tracking-tight mb-2 text-base-content">
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>
<button
type="button"
@ -39,7 +53,14 @@
aria-expanded="true"
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>
</svg>
</button>
@ -47,7 +68,9 @@
<!-- Site Name -->
<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">
<input
type="text"
@ -61,7 +84,9 @@
<!-- Branding Section (styled background box) -->
<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 -->
<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-only", "Logo only", "Just your logo (with text built in)"}
] do %>
<label
class={[
<label class={[
"flex items-center gap-3 p-3 bg-base-100 border-2 rounded-lg cursor-pointer transition-all",
if(@theme_settings.logo_mode == value,
do: "border-base-content",
else: "border-transparent hover:border-base-300"
)
]}
>
]}>
<input
type="radio"
name="logo_mode"
@ -98,12 +121,16 @@
]}>
<span class={[
"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")
]}></span>
if(@theme_settings.logo_mode == value,
do: "scale-100 opacity-100",
else: "scale-0 opacity-0"
)
]}>
</span>
</span>
<div class="flex-1">
<div class="text-sm font-medium text-base-content"><%= title %></div>
<div class="text-xs text-base-content/60"><%= desc %></div>
<div class="text-sm font-medium text-base-content">{title}</div>
<div class="text-xs text-base-content/60">{desc}</div>
</div>
</label>
<% end %>
@ -112,7 +139,9 @@
<!-- Logo Upload (for logo-text and logo-only modes) -->
<%= if @theme_settings.logo_mode in ["logo-text", "logo-only"] do %>
<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">
<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">
@ -132,7 +161,9 @@
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"
title="Remove logo"
>×</button>
>
×
</button>
</div>
<% end %>
</div>
@ -140,24 +171,30 @@
<%= for entry <- @uploads.logo_upload.entries do %>
<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="h-full bg-primary transition-all" style={"width: #{entry.progress}%"}></div>
<div
class="h-full bg-primary transition-all"
style={"width: #{entry.progress}%"}
>
</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
type="button"
phx-click="cancel_upload"
phx-value-ref={entry.ref}
phx-value-upload="logo_upload"
class="text-base-content/40 hover:text-base-content/70"
>×</button>
>
×
</button>
</div>
<%= 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 %>
<%= 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 %>
<!-- Logo Size Slider -->
@ -165,7 +202,9 @@
<form phx-change="update_setting" phx-value-field="logo_size" class="mt-3">
<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-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>
<input
type="range"
@ -186,21 +225,31 @@
checked={@theme_settings.logo_recolor}
phx-click="update_setting"
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"
/>
<span class="text-sm text-base-content/70">Recolour logo</span>
</label>
<%= 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
type="color"
name="value"
value={@theme_settings.logo_color}
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>
<% end %>
</div>
@ -218,7 +267,9 @@
checked={@theme_settings.header_background_enabled}
phx-click="update_setting"
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"
/>
<span class="text-sm text-base-content/80">Header background image</span>
@ -228,7 +279,9 @@
<!-- Header Image Upload (only when enabled) -->
<%= if @theme_settings.header_background_enabled do %>
<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">
<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>
@ -248,7 +301,9 @@
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"
title="Remove header background"
>×</button>
>
×
</button>
</div>
<!-- Header Image Controls -->
@ -256,7 +311,9 @@
<form phx-change="update_setting" phx-value-field="header_zoom">
<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-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>
<input
type="range"
@ -269,8 +326,12 @@
</form>
<form phx-change="update_setting" phx-value-field="header_position_x">
<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-mono text-base-content/60"><%= @theme_settings.header_position_x %>%</span>
<span class="text-xs font-medium text-base-content/70">
Horizontal position
</span>
<span class="text-xs font-mono text-base-content/60">
{@theme_settings.header_position_x}%
</span>
</div>
<input
type="range"
@ -283,8 +344,12 @@
</form>
<form phx-change="update_setting" phx-value-field="header_position_y">
<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-mono text-base-content/60"><%= @theme_settings.header_position_y %>%</span>
<span class="text-xs font-medium text-base-content/70">
Vertical position
</span>
<span class="text-xs font-mono text-base-content/60">
{@theme_settings.header_position_y}%
</span>
</div>
<input
type="range"
@ -301,31 +366,39 @@
<%= for entry <- @uploads.header_upload.entries do %>
<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="h-full bg-primary transition-all" style={"width: #{entry.progress}%"}></div>
<div
class="h-full bg-primary transition-all"
style={"width: #{entry.progress}%"}
>
</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
type="button"
phx-click="cancel_upload"
phx-value-ref={entry.ref}
phx-value-upload="header_upload"
class="text-base-content/40 hover:text-base-content/70"
>×</button>
>
×
</button>
</div>
<%= 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 %>
<%= 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 %>
</div>
<% end %>
<!-- Presets Section -->
<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">
<%= for {preset_name, description} <- @presets_with_descriptions do %>
<button
@ -340,8 +413,10 @@
)
]}
>
<div class="font-semibold text-sm capitalize text-base-content"><%= preset_name %></div>
<div class="text-xs text-base-content/60"><%= description %></div>
<div class="font-semibold text-sm capitalize text-base-content">
{preset_name}
</div>
<div class="text-xs text-base-content/60">{description}</div>
</button>
<% end %>
</div>
@ -349,8 +424,15 @@
<!-- Accent Colors (stays in essentials) -->
<div class="mb-6">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Accent colour</label>
<form id="accent-color-form" phx-change="update_color" phx-value-field="accent_color" phx-hook="ColorSync">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">
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">
<input
type="color"
@ -359,14 +441,23 @@
value={@theme_settings.accent_color}
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>
</form>
</div>
<div class="mb-6">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Hover colour</label>
<form id="secondary-accent-color-form" phx-change="update_color" phx-value-field="secondary_accent_color" phx-hook="ColorSync">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">
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">
<input
type="color"
@ -375,14 +466,23 @@
value={@theme_settings.secondary_accent_color}
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>
</form>
</div>
<div class="mb-6">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Sale colour</label>
<form id="sale-color-form" phx-change="update_color" phx-value-field="sale_color" phx-hook="ColorSync">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">
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">
<input
type="color"
@ -391,16 +491,33 @@
value={@theme_settings.sale_color}
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>
</form>
</div>
<!-- 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}>
<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">
<details
class="border-t border-base-300 mt-6 pt-4 group"
id="customise-section"
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>
</svg>
</summary>
@ -409,7 +526,13 @@
<!-- Typography Group -->
<div class="mb-6 pb-6 border-b border-base-200">
<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>
<line x1="9" y1="20" x2="15" y2="20"></line>
<line x1="12" y1="4" x2="12" y2="20"></line>
@ -418,7 +541,9 @@
</div>
<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">
<%= for typo <- ["clean", "editorial", "modern", "classic", "friendly", "minimal"] do %>
<button
@ -430,18 +555,21 @@
"px-3 py-2 text-sm rounded-lg border-2 transition-all capitalize",
if(@theme_settings.typography == typo,
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>
<% end %>
</div>
</div>
<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">
<%= for {value, label} <- [{"small", "Small"}, {"medium", "Medium"}, {"large", "Large"}] do %>
<button
@ -453,18 +581,21 @@
"px-3 py-2 text-sm rounded-lg border-2 transition-all",
if(@theme_settings.font_size == value,
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>
<% end %>
</div>
</div>
<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">
<%= for {value, label} <- [{"regular", "Regular"}, {"medium", "Medium"}, {"bold", "Bold"}] do %>
<button
@ -476,11 +607,12 @@
"px-3 py-2 text-sm rounded-lg border-2 transition-all",
if(@theme_settings.heading_weight == value,
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>
<% end %>
</div>
@ -490,7 +622,13 @@
<!-- Colours Group -->
<div class="mb-6 pb-6 border-b border-base-200">
<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="3"></circle>
</svg>
@ -498,7 +636,9 @@
</div>
<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">
<%= for mood <- ["warm", "neutral", "cool", "dark"] do %>
<button
@ -510,11 +650,12 @@
"px-3 py-2 text-sm rounded-lg border-2 transition-all capitalize",
if(@theme_settings.mood == mood,
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>
<% end %>
</div>
@ -524,7 +665,13 @@
<!-- Layout Group -->
<div class="mb-6 pb-6 border-b border-base-200">
<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>
<line x1="3" y1="9" x2="21" y2="9"></line>
<line x1="9" y1="21" x2="9" y2="9"></line>
@ -533,7 +680,9 @@
</div>
<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">
<%= for cols <- ["2", "3", "4"] do %>
<button
@ -545,18 +694,21 @@
"px-3 py-2 text-sm rounded-lg border-2 transition-all",
if(@theme_settings.grid_columns == cols,
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>
<% end %>
</div>
</div>
<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">
<%= for density <- ["spacious", "balanced", "compact"] do %>
<button
@ -568,18 +720,21 @@
"px-3 py-2 text-sm rounded-lg border-2 transition-all capitalize",
if(@theme_settings.density == density,
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>
<% end %>
</div>
</div>
<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">
<%= for layout <- ["standard", "centered", "left"] do %>
<button
@ -591,11 +746,12 @@
"px-3 py-2 text-sm rounded-lg border-2 transition-all capitalize",
if(@theme_settings.header_layout == layout,
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>
<% end %>
</div>
@ -631,14 +787,22 @@
<!-- Shape Group -->
<div class="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>
</svg>
<span class="text-sm font-semibold text-base-content">Shape</span>
</div>
<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">
<%= for shape <- ["sharp", "soft", "round", "pill"] do %>
<button
@ -650,18 +814,21 @@
"px-3 py-2 text-sm rounded-lg border-2 transition-all capitalize",
if(@theme_settings.shape == shape,
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>
<% end %>
</div>
</div>
<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">
<%= for {value, label} <- [{"none", "None"}, {"sm", "Subtle"}, {"md", "Medium"}, {"lg", "Strong"}] do %>
<button
@ -673,18 +840,21 @@
"px-3 py-2 text-sm rounded-lg border-2 transition-all",
if(@theme_settings.card_shadow == value,
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>
<% end %>
</div>
</div>
<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">
<%= for {value, label} <- [{"filled", "Filled"}, {"outline", "Outline"}, {"soft", "Soft"}] do %>
<button
@ -696,11 +866,12 @@
"px-3 py-2 text-sm rounded-lg border-2 transition-all",
if(@theme_settings.button_style == value,
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>
<% end %>
</div>
@ -710,7 +881,13 @@
<!-- Products Group -->
<div class="mb-6 pb-6 border-b border-base-200">
<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="14" y="3" width="7" height="7"></rect>
<rect x="14" y="14" width="7" height="7"></rect>
@ -720,7 +897,9 @@
</div>
<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">
<%= for width <- ["contained", "wide", "full"] do %>
<button
@ -732,18 +911,21 @@
"px-3 py-2 text-sm rounded-lg border-2 transition-all capitalize",
if(@theme_settings.layout_width == width,
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>
<% end %>
</div>
</div>
<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">
<%= for {value, label} <- [{"square", "Square"}, {"portrait", "Portrait"}, {"landscape", "Landscape"}] do %>
<button
@ -755,18 +937,21 @@
"px-3 py-2 text-sm rounded-lg border-2 transition-all",
if(@theme_settings.image_aspect_ratio == value,
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>
<% end %>
</div>
</div>
<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">
<%= for {value, label} <- [{"left", "Left"}, {"center", "Centre"}] do %>
<button
@ -778,11 +963,12 @@
"px-3 py-2 text-sm rounded-lg border-2 transition-all",
if(@theme_settings.product_text_align == value,
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>
<% end %>
</div>
@ -818,7 +1004,13 @@
<!-- Product Page Group -->
<div class="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>
<line x1="3" y1="9" x2="21" y2="9"></line>
</svg>
@ -869,11 +1061,21 @@
<!-- Current Combination Display -->
<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-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 class="text-xs font-semibold uppercase tracking-wider text-base-content/50 mb-2">
Current combination
</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 class="text-xs text-base-content/40 mt-2">One of 100,000+ possible combinations</div>
</div>
<% end %>
</div>
@ -904,7 +1106,7 @@
)
]}
>
<%= label %>
{label}
</button>
<% end %>
</div>
@ -917,16 +1119,25 @@
<div class="w-3 h-3 rounded-full bg-[#28c940] border border-[#1aab29]"></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]">
<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>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</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>
<!-- 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-typography={@theme_settings.typography}
data-shape={@theme_settings.shape}
@ -936,7 +1147,8 @@
data-sticky={to_string(@theme_settings.sticky_header)}
data-layout={@theme_settings.layout_width}
data-shadow={@theme_settings.card_shadow}
data-button-style={@theme_settings.button_style}>
data-button-style={@theme_settings.button_style}
>
<style>
/* All font faces for theme switching */
<%= Phoenix.HTML.raw(SimpleshopTheme.Theme.Fonts.generate_all_font_faces(

View File

@ -126,6 +126,7 @@ defmodule SimpleshopThemeWeb.UserLive.Login do
end
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

View File

@ -4,7 +4,10 @@ defmodule SimpleshopTheme.Repo.Migrations.CreateProducts do
def change do
create table(:products, primary_key: false) do
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 :title, :string, null: false
add :description, :text

View File

@ -4,7 +4,10 @@ defmodule SimpleshopTheme.Repo.Migrations.CreateProductImages do
def change do
create table(:product_images, primary_key: false) do
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 :position, :integer, default: 0, null: false
add :alt, :string

View File

@ -4,7 +4,10 @@ defmodule SimpleshopTheme.Repo.Migrations.CreateProductVariants do
def change do
create table(:product_variants, primary_key: false) do
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 :title, :string, null: false
add :sku, :string

View File

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

View File

@ -437,7 +437,9 @@ defmodule SimpleshopTheme.ProductsTest do
test "updates existing variants" do
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 = [
%{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
{:ok, updated_css} = CSSCache.get()
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
{:ok, _settings} = Settings.update_theme_settings(%{accent_color: "#0000ff"})
{: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
CSSCache.warm()

View File

@ -61,7 +61,9 @@ defmodule SimpleshopTheme.Sync.ProductSyncWorkerTest do
test "new/2 with scheduled_at creates scheduled job" do
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
end

View File

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

View File

@ -2,7 +2,9 @@ defmodule SimpleshopThemeWeb.ErrorJSONTest do
use SimpleshopThemeWeb.ConnCase, async: true
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
test "renders 500" do