From 97981a988431a16d1806a0fd3f8da9978ed0feac Mon Sep 17 00:00:00 2001 From: Jamey Greenwood Date: Sat, 17 Jan 2026 15:37:58 +0000 Subject: [PATCH] refactor: extract remaining PDP components to ShopComponents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add PDP-specific components: - product_gallery with lightbox - product_info (title, price, sale badge) - variant_selector - quantity_selector - add_to_cart_button - product_details accordion - star_rating - trust_badges - reviews_section with review_card Add page layout components: - page_title for consistent h1 styling - cart_layout for cart page structure - rich_text for structured content blocks - accordion_item for generic collapsible sections Update preview pages to be fully component-based: - PDP: 415 → 48 lines (88% reduction) - Cart: 47 → 23 lines - About: 65 → 27 lines Add preview data functions: - reviews() for product reviews - about_content() for rich text blocks Co-Authored-By: Claude Opus 4.5 --- lib/simpleshop_theme/theme/preview_data.ex | 44 + .../components/shop_components.ex | 750 ++++++++++++++++++ .../theme_live/preview_pages/about.html.heex | 40 +- .../theme_live/preview_pages/cart.html.heex | 28 +- .../theme_live/preview_pages/pdp.html.heex | 398 +--------- 5 files changed, 813 insertions(+), 447 deletions(-) diff --git a/lib/simpleshop_theme/theme/preview_data.ex b/lib/simpleshop_theme/theme/preview_data.ex index 93ce095..6b0fe94 100644 --- a/lib/simpleshop_theme/theme/preview_data.ex +++ b/lib/simpleshop_theme/theme/preview_data.ex @@ -57,6 +57,50 @@ defmodule SimpleshopTheme.Theme.PreviewData do mock_testimonials() end + @doc """ + Returns product reviews for preview. + + Always returns mock data formatted for the reviews_section component. + """ + def reviews 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.", + author: "Sarah M.", + date: "2 weeks ago", + verified: true + }, + %{ + rating: 4, + title: "Great gift", + 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 + } + ] + end + + @doc """ + Returns about page content for preview. + + Returns structured content blocks for the rich_text component. + """ + 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: :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: :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: :closing, text: "Thank you for visiting. It means a lot that you're here."} + ] + end + @doc """ Returns categories for preview. diff --git a/lib/simpleshop_theme_web/components/shop_components.ex b/lib/simpleshop_theme_web/components/shop_components.ex index cc3996a..f359ef1 100644 --- a/lib/simpleshop_theme_web/components/shop_components.ex +++ b/lib/simpleshop_theme_web/components/shop_components.ex @@ -1885,4 +1885,754 @@ defmodule SimpleshopThemeWeb.ShopComponents do """ end + + @doc """ + Renders a star rating display. + + ## Attributes + + * `rating` - Required. Number of filled stars (1-5). + * `max` - Optional. Maximum stars to display. Defaults to 5. + * `size` - Optional. Size variant: `:sm` (w-4), `:md` (w-5). Defaults to `:sm`. + * `color` - Optional. Star color. Defaults to "#f59e0b" (amber). + + ## Examples + + <.star_rating rating={5} /> + <.star_rating rating={4} size={:md} /> + """ + attr :rating, :integer, required: true + attr :max, :integer, default: 5 + attr :size, :atom, default: :sm + attr :color, :string, default: "#f59e0b" + + def star_rating(assigns) do + size_class = if assigns.size == :md, do: "w-5 h-5", else: "w-4 h-4" + assigns = assign(assigns, :size_class, size_class) + + ~H""" +
+ <%= for i <- 1..@max do %> + + + + <% end %> +
+ """ + end + + @doc """ + Renders trust badges (e.g., Free Delivery, Easy Returns). + + ## Attributes + + * `items` - Optional. List of badge items. Each item is a map with: + - `icon` - Icon type: `:check` or `:shield` + - `title` - Badge title + - `description` - Badge description + Defaults to Free Delivery and Easy Returns badges. + + ## Examples + + <.trust_badges /> + <.trust_badges items={[%{icon: :check, title: "Custom", description: "Badge text"}]} /> + """ + attr :items, :list, default: [ + %{icon: :check, title: "Free Delivery", description: "On orders over £40"}, + %{icon: :shield, title: "Easy Returns", description: "30-day return policy"} + ] + + def trust_badges(assigns) do + ~H""" +
+ <%= for item <- @items do %> +
+ <.trust_badge_icon icon={item.icon} /> +
+

<%= item.title %>

+

<%= item.description %>

+
+
+ <% end %> +
+ """ + end + + attr :icon, :atom, required: true + + defp trust_badge_icon(%{icon: :check} = assigns) do + ~H""" + + + + """ + end + + defp trust_badge_icon(%{icon: :shield} = assigns) do + ~H""" + + + + """ + end + + defp trust_badge_icon(assigns) do + ~H""" + + + + """ + end + + @doc """ + Renders a customer reviews section with collapsible header and review cards. + + ## Attributes + + * `reviews` - Required. List of review maps with: + - `rating` - Star rating (1-5) + - `title` - Review title + - `body` - Review text + - `author` - Reviewer name + - `date` - Relative date string (e.g., "2 weeks ago") + - `verified` - Boolean, if true shows "Verified purchase" badge + * `average_rating` - Optional. Average rating to show in header. Defaults to 5. + * `total_count` - Optional. Total number of reviews. Defaults to length of reviews list. + * `open` - Optional. Whether section is expanded by default. Defaults to true. + + ## Examples + + <.reviews_section reviews={@product.reviews} average_rating={4.8} total_count={24} /> + """ + attr :reviews, :list, required: true + attr :average_rating, :integer, default: 5 + attr :total_count, :integer, default: nil + attr :open, :boolean, default: true + + def reviews_section(assigns) do + assigns = assign_new(assigns, :display_count, fn -> + assigns.total_count || length(assigns.reviews) + end) + + ~H""" +
+ +
+

+ Customer reviews +

+
+ <.star_rating rating={@average_rating} /> + (<%= @display_count %>) +
+
+ + + +
+ +
+
+ <%= for review <- @reviews do %> + <.review_card review={review} /> + <% end %> +
+ + +
+
+ """ + end + + @doc """ + Renders a single review card. + + ## Attributes + + * `review` - Required. Map with `rating`, `title`, `body`, `author`, `date`, `verified`. + + ## Examples + + <.review_card review={%{rating: 5, title: "Great!", body: "...", author: "Jane", date: "1 week ago", verified: true}} /> + """ + attr :review, :map, required: true + + def review_card(assigns) do + ~H""" +
+
+ <.star_rating rating={@review.rating} /> + <%= @review.date %> +
+

<%= @review.title %>

+

+ <%= @review.body %> +

+
+ <%= @review.author %> + <%= if @review.verified do %> + Verified purchase + <% end %> +
+
+ """ + end + + @doc """ + Renders a product image gallery with thumbnails and lightbox. + + ## Attributes + + * `images` - Required. List of image URLs. + * `product_name` - Required. Product name for alt text. + * `id_prefix` - Optional. Prefix for element IDs. Defaults to "pdp". + + ## Examples + + <.product_gallery images={@product_images} product_name={@product.name} /> + """ + attr :images, :list, required: true + attr :product_name, :string, required: true + attr :id_prefix, :string, default: "pdp" + + def product_gallery(assigns) do + ~H""" +
+
+ {@product_name} +
+
+ + + +
+
+
+
+ <%= for {img_url, idx} <- Enum.with_index(@images) do %> + + <% end %> +
+ + + <.product_lightbox images={@images} product_name={@product_name} id_prefix={@id_prefix} /> +
+ """ + end + + # Private: Renders a product image lightbox dialog used by `product_gallery`. + attr :images, :list, required: true + attr :product_name, :string, required: true + attr :id_prefix, :string, required: true + + defp product_lightbox(assigns) do + ~H""" + + + + """ + end + + @doc """ + Renders product title and price information. + + ## Attributes + + * `product` - Required. Product map with `name`, `price`, `on_sale`, `compare_at_price`. + * `currency` - Optional. Currency symbol. Defaults to "£". + + ## Examples + + <.product_info product={@product} /> + """ + attr :product, :map, required: true + attr :currency, :string, default: "£" + + def product_info(assigns) do + ~H""" +
+

+ <%= @product.name %> +

+ +
+ <%= if @product.on_sale do %> + + <%= @currency %><%= @product.price / 100 %> + + + <%= @currency %><%= @product.compare_at_price / 100 %> + + + SAVE <%= round((@product.compare_at_price - @product.price) / @product.compare_at_price * 100) %>% + + <% else %> + + <%= @currency %><%= @product.price / 100 %> + + <% end %> +
+
+ """ + end + + @doc """ + Renders a variant selector with button options. + + ## Attributes + + * `label` - Required. Label text (e.g., "Size", "Color"). + * `options` - Required. List of option strings. + * `selected` - Optional. Currently selected option. Defaults to first option. + + ## Examples + + <.variant_selector label="Size" options={["S", "M", "L", "XL"]} /> + <.variant_selector label="Color" options={["Red", "Blue", "Green"]} selected="Blue" /> + """ + attr :label, :string, required: true + attr :options, :list, required: true + attr :selected, :string, default: nil + + def variant_selector(assigns) do + assigns = assign_new(assigns, :selected_value, fn -> + assigns.selected || List.first(assigns.options) + end) + + ~H""" +
+ +
+ <%= for option <- @options do %> + + <% end %> +
+
+ """ + end + + @doc """ + Renders a quantity selector with increment/decrement buttons. + + ## Attributes + + * `quantity` - Optional. Current quantity value. Defaults to 1. + * `in_stock` - Optional. Whether the product is in stock. Defaults to true. + * `min` - Optional. Minimum quantity. Defaults to 1. + * `max` - Optional. Maximum quantity. Defaults to 99. + + ## Examples + + <.quantity_selector /> + <.quantity_selector quantity={2} in_stock={false} /> + """ + attr :quantity, :integer, default: 1 + attr :in_stock, :boolean, default: true + attr :min, :integer, default: 1 + attr :max, :integer, default: 99 + + def quantity_selector(assigns) do + ~H""" +
+ +
+
+ + + <%= @quantity %> + + +
+ <%= if @in_stock do %> + In stock + <% else %> + Out of stock + <% end %> +
+
+ """ + end + + @doc """ + Renders the add to cart button. + + ## Attributes + + * `text` - Optional. Button text. Defaults to "Add to basket". + * `disabled` - Optional. Whether button is disabled. Defaults to false. + * `sticky` - Optional. Whether to use sticky positioning on mobile. Defaults to true. + + ## Examples + + <.add_to_cart_button /> + <.add_to_cart_button text="Add to bag" disabled={true} /> + """ + attr :text, :string, default: "Add to basket" + attr :disabled, :boolean, default: false + attr :sticky, :boolean, default: true + + def add_to_cart_button(assigns) do + ~H""" +
+ +
+ """ + end + + @doc """ + Renders a collapsible details/accordion item. + + ## Attributes + + * `title` - Required. Section heading text. + * `open` - Optional. Whether section is expanded by default. Defaults to false. + + ## Slots + + * `inner_block` - Required. Content to show when expanded. + + ## Examples + + <.accordion_item title="Description" open={true}> +

Product description here...

+ + """ + attr :title, :string, required: true + attr :open, :boolean, default: false + + slot :inner_block, required: true + + def accordion_item(assigns) do + ~H""" +
+ + <%= @title %> + + + + +
+ <%= render_slot(@inner_block) %> +
+
+ """ + end + + @doc """ + Renders a product details accordion with Description, Size Guide, and Shipping sections. + + ## Attributes + + * `product` - Required. Product map with `description`. + * `show_size_guide` - Optional. Whether to show size guide. Defaults to true. + * `size_guide` - Optional. Custom size guide data. Uses default if not provided. + + ## Examples + + <.product_details product={@product} /> + <.product_details product={@product} show_size_guide={false} /> + """ + attr :product, :map, required: true + attr :show_size_guide, :boolean, default: true + attr :size_guide, :list, default: nil + + def product_details(assigns) do + assigns = assign_new(assigns, :sizes, fn -> + assigns.size_guide || [ + %{size: "S", chest: "86-91", length: "71"}, + %{size: "M", chest: "91-96", length: "73"}, + %{size: "L", chest: "96-101", length: "75"}, + %{size: "XL", chest: "101-106", length: "77"} + ] + end) + + ~H""" +
+ <.accordion_item title="Description" open={true}> +

<%= @product.description %>. Crafted with attention to detail and quality materials, this product is designed to last. Perfect for everyday use or special occasions.

+ + + <%= if @show_size_guide do %> + <.accordion_item title="Size Guide"> + + + + + + + + + + <%= for {size_row, idx} <- Enum.with_index(@sizes) do %> + + + + + + <% end %> + +
SizeChest (cm)Length (cm)
<%= size_row.size %><%= size_row.chest %><%= size_row.length %>
+ + <% end %> + + <.accordion_item title="Shipping & Returns"> +
+
+

Delivery

+

Free UK delivery on orders over £40. Standard delivery 3-5 working days. Express delivery available at checkout.

+
+
+

Returns

+

We offer a 30-day return policy. Items must be unused and in original packaging. Please contact us to arrange a return.

+
+
+ +
+ """ + end + + @doc """ + Renders a page title heading. + + ## Attributes + + * `text` - Required. The title text. + * `class` - Optional. Additional CSS classes. + + ## Examples + + <.page_title text="Your basket" /> + <.page_title text="Order History" class="mb-4" /> + """ + attr :text, :string, required: true + attr :class, :string, default: "mb-8" + + def page_title(assigns) do + ~H""" +

+ <%= @text %> +

+ """ + end + + @doc """ + Renders a cart items list with order summary layout. + + ## Attributes + + * `items` - Required. List of cart items. + * `subtotal` - Required. Subtotal in pence/cents. + * `currency` - Optional. Currency symbol. Defaults to "£". + * `mode` - Either `:live` (default) or `:preview`. + + ## Examples + + <.cart_layout items={@cart_items} subtotal={3600} mode={:preview} /> + """ + attr :items, :list, required: true + attr :subtotal, :integer, required: true + attr :currency, :string, default: "£" + attr :mode, :atom, default: :live + + def cart_layout(assigns) do + ~H""" +
+
+
+ <%= for item <- @items do %> + <.cart_item item={item} currency={@currency} /> + <% end %> +
+
+ +
+ <.order_summary subtotal={@subtotal} mode={@mode} /> +
+
+ """ + end + + @doc """ + Renders rich text content with themed typography. + + This component renders structured content blocks (paragraphs, headings) + with appropriate theme styling. + + ## Attributes + + * `blocks` - Required. List of content blocks. Each block is a map with: + - `type` - Either `:paragraph`, `:heading`, or `:lead` + - `text` - The text content + - `level` - For headings, the level (2, 3, etc.). Defaults to 2. + + ## Examples + + <.rich_text blocks={[ + %{type: :lead, text: "Introduction paragraph..."}, + %{type: :paragraph, text: "Regular paragraph..."}, + %{type: :heading, text: "Section Title"}, + %{type: :paragraph, text: "More content..."} + ]} /> + """ + attr :blocks, :list, required: true + + def rich_text(assigns) do + ~H""" +
+ <%= for block <- @blocks do %> + <.rich_text_block block={block} /> + <% end %> +
+ """ + end + + attr :block, :map, required: true + + defp rich_text_block(%{block: %{type: :lead}} = assigns) do + ~H""" +

+ <%= @block.text %> +

+ """ + end + + defp rich_text_block(%{block: %{type: :paragraph}} = assigns) do + ~H""" +

+ <%= @block.text %> +

+ """ + end + + defp rich_text_block(%{block: %{type: :heading}} = assigns) do + ~H""" +

+ <%= @block.text %> +

+ """ + end + + defp rich_text_block(%{block: %{type: :closing}} = assigns) do + ~H""" +

+ <%= @block.text %> +

+ """ + end + + defp rich_text_block(assigns) do + ~H""" +

+ <%= @block.text %> +

+ """ + end end diff --git a/lib/simpleshop_theme_web/live/theme_live/preview_pages/about.html.heex b/lib/simpleshop_theme_web/live/theme_live/preview_pages/about.html.heex index fc9859f..a4e5cd0 100644 --- a/lib/simpleshop_theme_web/live/theme_live/preview_pages/about.html.heex +++ b/lib/simpleshop_theme_web/live/theme_live/preview_pages/about.html.heex @@ -1,64 +1,26 @@
- <.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={:preview} cart_count={2} /> -
- <.hero_section title="About the studio" description="Nature photography, printed with care" background={:sunken} /> - <.content_body image_url="/mockups/night-sky-blanket-3.jpg"> -

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

- -

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

- -

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

- -

- Quality you can trust -

-

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

- -

- Printed sustainably -

-

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

- -

- Thank you for visiting. It means a lot that you're here. -

+ <.rich_text blocks={SimpleshopTheme.Theme.PreviewData.about_content()} />
- <.shop_footer theme_settings={@theme_settings} mode={:preview} /> - - <.cart_drawer cart_items={SimpleshopTheme.Theme.PreviewData.cart_drawer_items()} subtotal="£72.00" mode={:preview} /> - <.search_modal hint_text={~s(Try searching for "mountain", "forest", or "ocean")} />
diff --git a/lib/simpleshop_theme_web/live/theme_live/preview_pages/cart.html.heex b/lib/simpleshop_theme_web/live/theme_live/preview_pages/cart.html.heex index ad083d8..d4d2d6a 100644 --- a/lib/simpleshop_theme_web/live/theme_live/preview_pages/cart.html.heex +++ b/lib/simpleshop_theme_web/live/theme_live/preview_pages/cart.html.heex @@ -2,45 +2,21 @@ subtotal = Enum.reduce(@preview_data.cart_items, 0, fn item, acc -> acc + item.product.price * item.quantity end) %>
- <.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={:preview} cart_count={2} />
-

- Your basket -

- -
- -
-
- <%= for item <- @preview_data.cart_items do %> - <.cart_item item={item} currency="$" /> - <% end %> -
-
- - -
- <.order_summary subtotal={subtotal} mode={:preview} /> -
-
+ <.page_title text="Your basket" /> + <.cart_layout items={@preview_data.cart_items} subtotal={subtotal} mode={:preview} />
- <.shop_footer theme_settings={@theme_settings} mode={:preview} /> - - <.cart_drawer cart_items={SimpleshopTheme.Theme.PreviewData.cart_drawer_items()} subtotal="£72.00" mode={:preview} /> - <.search_modal hint_text={~s(Try searching for "mountain", "forest", or "ocean")} />
diff --git a/lib/simpleshop_theme_web/live/theme_live/preview_pages/pdp.html.heex b/lib/simpleshop_theme_web/live/theme_live/preview_pages/pdp.html.heex index 5f56e7f..344a0f5 100644 --- a/lib/simpleshop_theme_web/live/theme_live/preview_pages/pdp.html.heex +++ b/lib/simpleshop_theme_web/live/theme_live/preview_pages/pdp.html.heex @@ -1,20 +1,17 @@ <% product = List.first(@preview_data.products) + gallery_images = [product.image_url, product.hover_image_url, product.image_url, product.hover_image_url] %>
- <.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={:preview} cart_count={2} />
- <.breadcrumb items={[ %{label: "Home", page: "home", href: "/"}, %{label: product.category, page: "collection", href: "/products"}, @@ -22,393 +19,30 @@ ]} mode={:preview} />
- + <.product_gallery images={gallery_images} product_name={product.name} /> +
- <% gallery_images = [product.image_url, product.hover_image_url, product.image_url, product.hover_image_url] %> -
- {product.name} - -
-
- - - -
-
-
-
- <%= for {img_url, idx} <- Enum.with_index(gallery_images) do %> - - <% end %> -
- - - - - - -
- - -
-

- <%= product.name %> -

- -
- <%= if product.on_sale do %> - - £<%= product.price / 100 %> - - - £<%= product.compare_at_price / 100 %> - - - SAVE <%= round((product.compare_at_price - product.price) / product.compare_at_price * 100) %>% - - <% else %> - - £<%= product.price / 100 %> - - <% end %> -
- - -
- -
- <%= for size <- ["S", "M", "L", "XL"] do %> - - <% end %> -
-
- - -
- -
-
- - - 1 - - -
- <%= if product.in_stock do %> - In stock - <% else %> - Out of stock - <% end %> -
-
- - -
- -
- - - <%= if @theme_settings.pdp_trust_badges do %> -
-
- - - -
-

Free Delivery

-

On orders over £40

-
-
-
- - - -
-

Easy Returns

-

30-day return policy

-
-
-
- <% end %> - - -
- -
- - Description - - - - -
-

<%= product.description %>. Crafted with attention to detail and quality materials, this product is designed to last. Perfect for everyday use or special occasions.

-
-
- - -
- - Size Guide - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
SizeChest (cm)Length (cm)
S86-9171
M91-9673
L96-10175
XL101-10677
-
-
- - -
- - Shipping & Returns - - - - -
-
-

Delivery

-

Free UK delivery on orders over £40. Standard delivery 3-5 working days. Express delivery available at checkout.

-
-
-

Returns

-

We offer a 30-day return policy. Items must be unused and in original packaging. Please contact us to arrange a return.

-
-
-
-
+ <.product_info product={product} /> + <.variant_selector label="Size" options={["S", "M", "L", "XL"]} /> + <.quantity_selector quantity={1} in_stock={product.in_stock} /> + <.add_to_cart_button /> + <.trust_badges :if={@theme_settings.pdp_trust_badges} /> + <.product_details product={product} />
- - <%= if @theme_settings.pdp_reviews do %> -
- -
-

- Customer reviews -

-
-
- <%= for _i <- 1..5 do %> - - - - <% end %> -
- (24) -
-
- - - -
+ <.reviews_section :if={@theme_settings.pdp_reviews} reviews={SimpleshopTheme.Theme.PreviewData.reviews()} average_rating={5} total_count={24} /> -
-
- -
-
-
- <%= for _i <- 1..5 do %> - - - - <% end %> -
- 2 weeks ago -
-

Absolutely beautiful

-

- The quality exceeded my expectations. The colours are vibrant and the paper feels premium. It's now pride of place in my living room. -

-
- Sarah M. - Verified purchase -
-
- - -
-
-
- <%= for i <- 1..5 do %> - - - - <% end %> -
- 1 month ago -
-

Great gift

-

- Bought this as a gift and it arrived beautifully packaged. Fast shipping too. Would definitely order again. -

-
- James T. - Verified purchase -
-
-
- - -
-
- <% end %> - - - <%= if @theme_settings.pdp_related_products do %> - <.related_products_section - products={Enum.slice(@preview_data.products, 1, 4)} - theme_settings={@theme_settings} - mode={:preview} - /> - <% end %> + <.related_products_section + :if={@theme_settings.pdp_related_products} + products={Enum.slice(@preview_data.products, 1, 4)} + theme_settings={@theme_settings} + mode={:preview} + />
- <.shop_footer theme_settings={@theme_settings} mode={:preview} /> - - <.cart_drawer cart_items={SimpleshopTheme.Theme.PreviewData.cart_drawer_items()} subtotal="£72.00" mode={:preview} /> - <.search_modal hint_text={~s(Try searching for "mountain", "forest", or "ocean")} />