mix ci alias: compile --warning-as-errors, format --check-formatted, credo, dialyzer, test. Credo configured with sensible defaults. Dialyzer ignore file for false positives (Stripe types, Mix tasks, ExUnit internals). Credo fixes: map_join, filter consolidation, nesting extraction, cond→if simplification. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1048 lines
32 KiB
Elixir
1048 lines
32 KiB
Elixir
defmodule SimpleshopTheme.Theme.PreviewData do
|
||
@moduledoc """
|
||
Provides preview data for theme customization.
|
||
|
||
Uses real product data when available, falls back to mock data otherwise.
|
||
This allows users to preview themes before adding products to their shop.
|
||
"""
|
||
|
||
alias SimpleshopTheme.Products
|
||
|
||
@doc """
|
||
Returns products for preview.
|
||
|
||
Uses real products if available, otherwise returns mock product data.
|
||
"""
|
||
def products do
|
||
if has_real_products?() do
|
||
get_real_products()
|
||
else
|
||
mock_products()
|
||
end
|
||
end
|
||
|
||
@doc """
|
||
Returns cart items for preview.
|
||
|
||
Always returns mock data as cart is session-specific.
|
||
"""
|
||
def cart_items do
|
||
mock_cart_items()
|
||
end
|
||
|
||
@doc """
|
||
Returns cart drawer items formatted for the cart drawer component.
|
||
|
||
Format matches Cart.hydrate/1 output for consistency between preview and live modes.
|
||
"""
|
||
def cart_drawer_items do
|
||
[
|
||
%{
|
||
variant_id: "preview-1",
|
||
product_id: "preview-product-1",
|
||
name: "Mountain Sunrise Art Print",
|
||
variant: "12″ x 18″ / Matte",
|
||
price: 2400,
|
||
quantity: 1,
|
||
image: "/mockups/mountain-sunrise-print-1-400.webp"
|
||
},
|
||
%{
|
||
variant_id: "preview-2",
|
||
product_id: "preview-product-2",
|
||
name: "Fern Leaf Mug",
|
||
variant: "11oz / White",
|
||
price: 1499,
|
||
quantity: 2,
|
||
image: "/mockups/fern-leaf-mug-1-400.webp"
|
||
}
|
||
]
|
||
end
|
||
|
||
@doc """
|
||
Returns testimonials for preview.
|
||
|
||
Always returns mock data.
|
||
"""
|
||
def testimonials 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: "Sample review – five stars",
|
||
body:
|
||
"This is an example review for the demo store. Real reviews from your customers will appear here once you start getting orders.",
|
||
author: "Demo Customer",
|
||
date: "2 weeks ago",
|
||
verified: true
|
||
},
|
||
%{
|
||
rating: 4,
|
||
title: "Sample review – four stars",
|
||
body:
|
||
"Another example review to show how the layout handles multiple entries. You can customise the review section in the theme settings.",
|
||
author: "Demo Shopper",
|
||
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:
|
||
"This is a sample about page for your SimpleShop store. You're reading it as Robin, a fictional nature photographer – but this is where your own story goes."
|
||
},
|
||
%{
|
||
type: :paragraph,
|
||
text:
|
||
"Tell your customers who you are and what inspires your work. Maybe it started as a hobby that grew into something bigger, or maybe you've been creating for years. Either way, this is the place to share it."
|
||
},
|
||
%{
|
||
type: :paragraph,
|
||
text:
|
||
"In Robin's version, every design comes from their own photography – early morning mist over the hills, autumn leaves in the local woods, the quiet beauty of wildflower meadows. Your story will be different, and that's the point."
|
||
},
|
||
%{type: :heading, text: "Quality you can trust"},
|
||
%{
|
||
type: :paragraph,
|
||
text:
|
||
"This section is a good place to talk about your products and what makes them worth buying. Print on demand means every item is made to order using quality materials and printing techniques – tell your customers about that."
|
||
},
|
||
%{type: :heading, text: "Printed sustainably"},
|
||
%{
|
||
type: :paragraph,
|
||
text:
|
||
"And here's where you can mention anything else that matters to you – sustainability, packaging, your creative process, whatever sets your shop apart. No unsold stock means no waste, and your customers will appreciate knowing that."
|
||
},
|
||
%{type: :closing, text: "Thanks for checking out the demo. Make this page your own."}
|
||
]
|
||
end
|
||
|
||
@doc """
|
||
Returns delivery & returns page content for preview.
|
||
"""
|
||
def delivery_content do
|
||
[
|
||
%{
|
||
type: :lead,
|
||
text:
|
||
"This is sample content for the demo shop. Replace it with your own delivery and returns information."
|
||
},
|
||
%{type: :heading, text: "Shipping"},
|
||
%{
|
||
type: :paragraph,
|
||
text:
|
||
"All products are printed on demand and shipped directly from the print facility. Typical delivery times depend on your location:"
|
||
},
|
||
%{
|
||
type: :list,
|
||
items: [
|
||
"United Kingdom: 5–8 business days",
|
||
"Europe: 8–12 business days",
|
||
"United States & Canada: 8–14 business days",
|
||
"Rest of world: 10–20 business days"
|
||
]
|
||
},
|
||
%{
|
||
type: :paragraph,
|
||
text:
|
||
"You'll receive a shipping confirmation email with tracking details once your order has been dispatched."
|
||
},
|
||
%{type: :heading, text: "Returns & exchanges"},
|
||
%{
|
||
type: :paragraph,
|
||
text:
|
||
"Because every item is made to order, we can't accept returns for change of mind. However, if your order arrives damaged or with a printing defect, we'll sort it out — just get in touch within 14 days of delivery."
|
||
},
|
||
%{
|
||
type: :paragraph,
|
||
text:
|
||
"Please include your order number and a photo of the issue so we can resolve things quickly."
|
||
},
|
||
%{type: :heading, text: "Cancellations"},
|
||
%{
|
||
type: :paragraph,
|
||
text:
|
||
"Orders can be cancelled within 2 hours of being placed. After that, production will have started and we won't be able to make changes. Contact us as soon as possible if you need to cancel."
|
||
}
|
||
]
|
||
end
|
||
|
||
@doc """
|
||
Returns privacy policy page content for preview.
|
||
"""
|
||
def privacy_content do
|
||
[
|
||
%{
|
||
type: :lead,
|
||
text: "This is sample content for the demo shop. Replace it with your own privacy policy."
|
||
},
|
||
%{type: :heading, text: "What we collect"},
|
||
%{
|
||
type: :paragraph,
|
||
text:
|
||
"When you place an order, we collect the information needed to fulfil it — your name, email address, shipping address, and payment details. Payment is processed securely by Stripe; we never see or store your card number."
|
||
},
|
||
%{type: :heading, text: "How we use it"},
|
||
%{
|
||
type: :paragraph,
|
||
text: "We use your information to:"
|
||
},
|
||
%{
|
||
type: :list,
|
||
items: [
|
||
"Process and deliver your order",
|
||
"Send order confirmation and shipping updates",
|
||
"Respond to any queries or issues you raise"
|
||
]
|
||
},
|
||
%{
|
||
type: :paragraph,
|
||
text:
|
||
"We won't send you marketing emails unless you've opted in, and we'll never sell your data to third parties."
|
||
},
|
||
%{type: :heading, text: "Third parties"},
|
||
%{
|
||
type: :paragraph,
|
||
text:
|
||
"To fulfil orders, we share your shipping details with our print-on-demand provider. Payment is handled by Stripe. Both process data in accordance with their own privacy policies."
|
||
},
|
||
%{type: :heading, text: "Your rights"},
|
||
%{
|
||
type: :paragraph,
|
||
text:
|
||
"You can request a copy of your data, ask us to correct it, or ask us to delete it at any time. Just get in touch and we'll sort it out."
|
||
},
|
||
%{type: :heading, text: "Cookies"},
|
||
%{
|
||
type: :paragraph,
|
||
text:
|
||
"We use essential cookies to keep your session and cart working. We don't use tracking cookies or third-party analytics that follow you around the web."
|
||
}
|
||
]
|
||
end
|
||
|
||
@doc """
|
||
Returns terms of service page content for preview.
|
||
"""
|
||
def terms_content do
|
||
[
|
||
%{
|
||
type: :lead,
|
||
text:
|
||
"This is sample content for the demo shop. Replace it with your own terms of service."
|
||
},
|
||
%{type: :heading, text: "Overview"},
|
||
%{
|
||
type: :paragraph,
|
||
text:
|
||
"By placing an order through this shop, you agree to the following terms. Please read them before making a purchase."
|
||
},
|
||
%{type: :heading, text: "Products"},
|
||
%{
|
||
type: :paragraph,
|
||
text:
|
||
"All products are made to order using print-on-demand services. Colours may vary slightly between screens and the finished product. We do our best to represent products accurately, but minor differences are normal."
|
||
},
|
||
%{type: :heading, text: "Orders & payment"},
|
||
%{
|
||
type: :paragraph,
|
||
text:
|
||
"Payment is taken at the time of purchase via Stripe. Once an order is confirmed and production has started, it cannot be modified. Prices include VAT where applicable."
|
||
},
|
||
%{type: :heading, text: "Delivery"},
|
||
%{
|
||
type: :paragraph,
|
||
text:
|
||
"Delivery times are estimates and may vary. We're not responsible for delays caused by postal services or customs. See our delivery page for current timeframes."
|
||
},
|
||
%{type: :heading, text: "Returns"},
|
||
%{
|
||
type: :paragraph,
|
||
text:
|
||
"As items are made to order, we don't accept returns for change of mind. If an item arrives damaged or defective, contact us within 14 days for a replacement. See our delivery & returns page for full details."
|
||
},
|
||
%{type: :heading, text: "Intellectual property"},
|
||
%{
|
||
type: :paragraph,
|
||
text:
|
||
"All designs, images, and content on this site are owned by the shop owner and may not be reproduced without permission."
|
||
},
|
||
%{type: :heading, text: "Liability"},
|
||
%{
|
||
type: :paragraph,
|
||
text:
|
||
"We do our best to keep the shop running smoothly, but we can't guarantee uninterrupted service. Our liability is limited to the value of your order."
|
||
},
|
||
%{type: :heading, text: "Changes"},
|
||
%{
|
||
type: :paragraph,
|
||
text:
|
||
"We may update these terms from time to time. Any changes apply to orders placed after the update."
|
||
}
|
||
]
|
||
end
|
||
|
||
@doc """
|
||
Returns categories for preview.
|
||
|
||
Uses real categories if available, otherwise returns mock data.
|
||
"""
|
||
def categories do
|
||
if has_real_categories?() do
|
||
get_real_categories()
|
||
else
|
||
mock_categories()
|
||
end
|
||
end
|
||
|
||
@doc """
|
||
Returns a category by its slug.
|
||
|
||
Returns nil if not found.
|
||
"""
|
||
def category_by_slug(slug) do
|
||
Enum.find(categories(), fn cat -> cat.slug == slug end)
|
||
end
|
||
|
||
@doc """
|
||
Returns products filtered by category slug.
|
||
|
||
If slug is nil or "all", returns all products.
|
||
"""
|
||
def products_by_category(nil), do: products()
|
||
def products_by_category("all"), do: products()
|
||
|
||
def products_by_category(slug) do
|
||
case category_by_slug(slug) do
|
||
nil ->
|
||
[]
|
||
|
||
category ->
|
||
products()
|
||
|> Enum.filter(fn product -> product.category == category.name end)
|
||
end
|
||
end
|
||
|
||
@doc """
|
||
Checks if the shop has real products.
|
||
|
||
Returns true if at least one visible, active product exists in the database.
|
||
Returns false if the database is unavailable (e.g., in tests without sandbox).
|
||
"""
|
||
def has_real_products? do
|
||
try do
|
||
Products.list_products(visible: true, status: "active") |> Enum.any?()
|
||
rescue
|
||
DBConnection.OwnershipError -> false
|
||
end
|
||
end
|
||
|
||
defp has_real_categories? do
|
||
has_real_products?()
|
||
end
|
||
|
||
defp get_real_products do
|
||
Products.list_products(
|
||
visible: true,
|
||
status: "active",
|
||
preload: [images: :image, variants: []]
|
||
)
|
||
|> Enum.map(&product_to_map/1)
|
||
end
|
||
|
||
defp get_real_categories do
|
||
Products.list_products(visible: true, status: "active")
|
||
|> Enum.map(& &1.category)
|
||
|> Enum.reject(&is_nil/1)
|
||
|> Enum.frequencies()
|
||
|> Enum.map(fn {name, count} ->
|
||
%{
|
||
id: Slug.slugify(name),
|
||
name: name,
|
||
slug: Slug.slugify(name),
|
||
product_count: count,
|
||
image_url: nil
|
||
}
|
||
end)
|
||
|> Enum.sort_by(& &1.name)
|
||
end
|
||
|
||
# Transform a Product struct to the map format expected by shop components
|
||
defp product_to_map(product) do
|
||
# Get images sorted by position
|
||
images = Enum.sort_by(product.images, & &1.position)
|
||
first_image = List.first(images)
|
||
second_image = Enum.at(images, 1)
|
||
|
||
# Get available variants for pricing
|
||
available_variants =
|
||
product.variants
|
||
|> Enum.filter(&(&1.is_enabled and &1.is_available))
|
||
|
||
# Get the cheapest available variant for display price
|
||
cheapest_variant =
|
||
available_variants
|
||
|> Enum.min_by(& &1.price, fn -> nil end)
|
||
|
||
# Determine stock and sale status
|
||
in_stock = Enum.any?(available_variants)
|
||
on_sale = Enum.any?(product.variants, &SimpleshopTheme.Products.ProductVariant.on_sale?/1)
|
||
|
||
# Use local image if available, fall back to CDN URL
|
||
{image_url, image_id, source_width} = image_attrs(first_image)
|
||
{hover_image_url, hover_image_id, hover_source_width} = image_attrs(second_image)
|
||
|
||
# Map variants to frontend format (only enabled/published ones)
|
||
variants =
|
||
product.variants
|
||
|> Enum.filter(& &1.is_enabled)
|
||
|> Enum.map(fn v ->
|
||
%{
|
||
id: v.id,
|
||
provider_variant_id: v.provider_variant_id,
|
||
title: v.title,
|
||
price: v.price,
|
||
compare_at_price: v.compare_at_price,
|
||
options: v.options,
|
||
is_available: v.is_available
|
||
}
|
||
end)
|
||
|
||
# Extract option types, filtered to only values present in enabled variants
|
||
option_types =
|
||
SimpleshopTheme.Providers.Printify.extract_option_types(product.provider_data)
|
||
|> filter_option_types_by_variants(variants)
|
||
|
||
%{
|
||
id: product.slug,
|
||
name: product.title,
|
||
description: product.description,
|
||
price: if(cheapest_variant, do: cheapest_variant.price, else: 0),
|
||
compare_at_price: if(cheapest_variant, do: cheapest_variant.compare_at_price, else: nil),
|
||
image_url: image_url,
|
||
image_id: image_id,
|
||
hover_image_url: hover_image_url,
|
||
hover_image_id: hover_image_id,
|
||
source_width: source_width,
|
||
hover_source_width: hover_source_width,
|
||
category: product.category,
|
||
slug: product.slug,
|
||
in_stock: in_stock,
|
||
on_sale: on_sale,
|
||
inserted_at: product.inserted_at,
|
||
option_types: option_types,
|
||
variants: variants
|
||
}
|
||
end
|
||
|
||
# Extract image attributes, preferring local Media.Image when available
|
||
defp image_attrs(nil), do: {nil, nil, nil}
|
||
|
||
defp image_attrs(%{image_id: image_id, image: %{source_width: source_width}})
|
||
when not is_nil(image_id) do
|
||
# Local image available - use image_id for responsive <picture> element
|
||
{nil, image_id, source_width}
|
||
end
|
||
|
||
defp image_attrs(%{src: src}) do
|
||
# Fall back to CDN URL
|
||
{src, nil, nil}
|
||
end
|
||
|
||
# Filter option types to only include values present in enabled variants
|
||
# This ensures we don't show unpublished options from the Printify catalog
|
||
defp filter_option_types_by_variants(option_types, variants) do
|
||
# Collect all option values present in the enabled variants
|
||
values_in_use =
|
||
Enum.reduce(variants, %{}, fn variant, acc ->
|
||
Enum.reduce(variant.options, acc, fn {opt_name, opt_value}, inner_acc ->
|
||
Map.update(inner_acc, opt_name, MapSet.new([opt_value]), &MapSet.put(&1, opt_value))
|
||
end)
|
||
end)
|
||
|
||
# Filter each option type's values to only those in use
|
||
option_types
|
||
|> Enum.map(fn opt_type ->
|
||
used_values = Map.get(values_in_use, opt_type.name, MapSet.new())
|
||
|
||
filtered_values =
|
||
opt_type.values
|
||
|> Enum.filter(fn v -> MapSet.member?(used_values, v.title) end)
|
||
|
||
%{opt_type | values: filtered_values}
|
||
end)
|
||
|> Enum.reject(fn opt_type -> opt_type.values == [] end)
|
||
end
|
||
|
||
# Default source width for mockup variants (max generated size)
|
||
@mockup_source_width 1200
|
||
|
||
defp mock_products do
|
||
[
|
||
# Art Prints
|
||
%{
|
||
id: "1",
|
||
name: "Mountain Sunrise Art Print",
|
||
description: "Capture the magic of dawn with this stunning mountain landscape print",
|
||
price: 1999,
|
||
compare_at_price: nil,
|
||
image_url: "/mockups/mountain-sunrise-print-1",
|
||
hover_image_url: "/mockups/mountain-sunrise-print-2",
|
||
source_width: @mockup_source_width,
|
||
hover_source_width: @mockup_source_width,
|
||
category: "Art Prints",
|
||
in_stock: true,
|
||
on_sale: false,
|
||
option_types: [
|
||
%{
|
||
name: "Size",
|
||
type: :size,
|
||
values: [
|
||
%{id: 1, title: "8×10"},
|
||
%{id: 2, title: "12×18"},
|
||
%{id: 3, title: "18×24"}
|
||
]
|
||
}
|
||
],
|
||
variants: [
|
||
%{
|
||
id: "p1",
|
||
title: "8×10",
|
||
price: 1999,
|
||
options: %{"Size" => "8×10"},
|
||
is_available: true
|
||
},
|
||
%{
|
||
id: "p2",
|
||
title: "12×18",
|
||
price: 2400,
|
||
options: %{"Size" => "12×18"},
|
||
is_available: true
|
||
},
|
||
%{
|
||
id: "p3",
|
||
title: "18×24",
|
||
price: 3200,
|
||
options: %{"Size" => "18×24"},
|
||
is_available: true
|
||
}
|
||
]
|
||
},
|
||
%{
|
||
id: "2",
|
||
name: "Ocean Waves Art Print",
|
||
description: "A calming sunset over ocean waves to bring peace to any room",
|
||
price: 2400,
|
||
compare_at_price: nil,
|
||
image_url: "/mockups/ocean-waves-print-1",
|
||
hover_image_url: "/mockups/ocean-waves-print-2",
|
||
source_width: @mockup_source_width,
|
||
hover_source_width: @mockup_source_width,
|
||
category: "Art Prints",
|
||
in_stock: true,
|
||
on_sale: false
|
||
},
|
||
%{
|
||
id: "3",
|
||
name: "Wildflower Meadow Art Print",
|
||
description: "Beautiful wildflower meadow captured in the summer sunshine",
|
||
price: 2400,
|
||
compare_at_price: nil,
|
||
image_url: "/mockups/wildflower-meadow-print-1",
|
||
hover_image_url: "/mockups/wildflower-meadow-print-2",
|
||
source_width: @mockup_source_width,
|
||
hover_source_width: @mockup_source_width,
|
||
category: "Art Prints",
|
||
in_stock: true,
|
||
on_sale: false
|
||
},
|
||
%{
|
||
id: "4",
|
||
name: "Geometric Abstract Art Print",
|
||
description: "Modern minimalist design with bold geometric shapes",
|
||
price: 2800,
|
||
compare_at_price: 3200,
|
||
image_url: "/mockups/geometric-abstract-print-1",
|
||
hover_image_url: "/mockups/geometric-abstract-print-2",
|
||
source_width: @mockup_source_width,
|
||
hover_source_width: @mockup_source_width,
|
||
category: "Art Prints",
|
||
in_stock: true,
|
||
on_sale: true
|
||
},
|
||
%{
|
||
id: "5",
|
||
name: "Botanical Illustration Print",
|
||
description: "Vintage-inspired botanical drawing with intricate detail",
|
||
price: 2400,
|
||
compare_at_price: nil,
|
||
image_url: "/mockups/botanical-illustration-print-1",
|
||
hover_image_url: "/mockups/botanical-illustration-print-2",
|
||
source_width: @mockup_source_width,
|
||
hover_source_width: @mockup_source_width,
|
||
category: "Art Prints",
|
||
in_stock: true,
|
||
on_sale: false
|
||
},
|
||
# Apparel
|
||
%{
|
||
id: "6",
|
||
name: "Forest Silhouette T-Shirt",
|
||
description: "Soft cotton tee featuring a peaceful forest silhouette design",
|
||
price: 2999,
|
||
compare_at_price: nil,
|
||
image_url: "/mockups/forest-silhouette-tshirt-1",
|
||
hover_image_url: "/mockups/forest-silhouette-tshirt-2",
|
||
source_width: @mockup_source_width,
|
||
hover_source_width: @mockup_source_width,
|
||
category: "Apparel",
|
||
in_stock: true,
|
||
on_sale: false,
|
||
option_types: [
|
||
%{
|
||
name: "Color",
|
||
type: :color,
|
||
values: [
|
||
%{id: 1, title: "Black", hex: "#000000"},
|
||
%{id: 2, title: "Navy", hex: "#1A2237"},
|
||
%{id: 3, title: "White", hex: "#FFFFFF"},
|
||
%{id: 4, title: "Sport Grey", hex: "#9CA3AF"}
|
||
]
|
||
},
|
||
%{
|
||
name: "Size",
|
||
type: :size,
|
||
values: [
|
||
%{id: 10, title: "S"},
|
||
%{id: 11, title: "M"},
|
||
%{id: 12, title: "L"},
|
||
%{id: 13, title: "XL"},
|
||
%{id: 14, title: "2XL"}
|
||
]
|
||
}
|
||
],
|
||
variants: [
|
||
# Black variants
|
||
%{
|
||
id: "t1",
|
||
title: "Black / S",
|
||
price: 2999,
|
||
options: %{"Color" => "Black", "Size" => "S"},
|
||
is_available: true
|
||
},
|
||
%{
|
||
id: "t2",
|
||
title: "Black / M",
|
||
price: 2999,
|
||
options: %{"Color" => "Black", "Size" => "M"},
|
||
is_available: true
|
||
},
|
||
%{
|
||
id: "t3",
|
||
title: "Black / L",
|
||
price: 2999,
|
||
options: %{"Color" => "Black", "Size" => "L"},
|
||
is_available: true
|
||
},
|
||
%{
|
||
id: "t4",
|
||
title: "Black / XL",
|
||
price: 2999,
|
||
options: %{"Color" => "Black", "Size" => "XL"},
|
||
is_available: true
|
||
},
|
||
%{
|
||
id: "t5",
|
||
title: "Black / 2XL",
|
||
price: 3299,
|
||
options: %{"Color" => "Black", "Size" => "2XL"},
|
||
is_available: true
|
||
},
|
||
# Navy variants
|
||
%{
|
||
id: "t6",
|
||
title: "Navy / S",
|
||
price: 2999,
|
||
options: %{"Color" => "Navy", "Size" => "S"},
|
||
is_available: true
|
||
},
|
||
%{
|
||
id: "t7",
|
||
title: "Navy / M",
|
||
price: 2999,
|
||
options: %{"Color" => "Navy", "Size" => "M"},
|
||
is_available: true
|
||
},
|
||
%{
|
||
id: "t8",
|
||
title: "Navy / L",
|
||
price: 2999,
|
||
options: %{"Color" => "Navy", "Size" => "L"},
|
||
is_available: true
|
||
},
|
||
%{
|
||
id: "t9",
|
||
title: "Navy / XL",
|
||
price: 2999,
|
||
options: %{"Color" => "Navy", "Size" => "XL"},
|
||
is_available: true
|
||
},
|
||
%{
|
||
id: "t10",
|
||
title: "Navy / 2XL",
|
||
price: 3299,
|
||
options: %{"Color" => "Navy", "Size" => "2XL"},
|
||
is_available: false
|
||
},
|
||
# White variants
|
||
%{
|
||
id: "t11",
|
||
title: "White / S",
|
||
price: 2999,
|
||
options: %{"Color" => "White", "Size" => "S"},
|
||
is_available: true
|
||
},
|
||
%{
|
||
id: "t12",
|
||
title: "White / M",
|
||
price: 2999,
|
||
options: %{"Color" => "White", "Size" => "M"},
|
||
is_available: true
|
||
},
|
||
%{
|
||
id: "t13",
|
||
title: "White / L",
|
||
price: 2999,
|
||
options: %{"Color" => "White", "Size" => "L"},
|
||
is_available: true
|
||
},
|
||
%{
|
||
id: "t14",
|
||
title: "White / XL",
|
||
price: 2999,
|
||
options: %{"Color" => "White", "Size" => "XL"},
|
||
is_available: false
|
||
},
|
||
%{
|
||
id: "t15",
|
||
title: "White / 2XL",
|
||
price: 3299,
|
||
options: %{"Color" => "White", "Size" => "2XL"},
|
||
is_available: false
|
||
},
|
||
# Sport Grey variants
|
||
%{
|
||
id: "t16",
|
||
title: "Sport Grey / S",
|
||
price: 2999,
|
||
options: %{"Color" => "Sport Grey", "Size" => "S"},
|
||
is_available: true
|
||
},
|
||
%{
|
||
id: "t17",
|
||
title: "Sport Grey / M",
|
||
price: 2999,
|
||
options: %{"Color" => "Sport Grey", "Size" => "M"},
|
||
is_available: true
|
||
},
|
||
%{
|
||
id: "t18",
|
||
title: "Sport Grey / L",
|
||
price: 2999,
|
||
options: %{"Color" => "Sport Grey", "Size" => "L"},
|
||
is_available: true
|
||
},
|
||
%{
|
||
id: "t19",
|
||
title: "Sport Grey / XL",
|
||
price: 2999,
|
||
options: %{"Color" => "Sport Grey", "Size" => "XL"},
|
||
is_available: true
|
||
},
|
||
%{
|
||
id: "t20",
|
||
title: "Sport Grey / 2XL",
|
||
price: 3299,
|
||
options: %{"Color" => "Sport Grey", "Size" => "2XL"},
|
||
is_available: true
|
||
}
|
||
]
|
||
},
|
||
%{
|
||
id: "7",
|
||
name: "Forest Light Hoodie",
|
||
description: "Cosy fleece hoodie with stunning forest light photography",
|
||
price: 4499,
|
||
compare_at_price: 4999,
|
||
image_url: "/mockups/forest-light-hoodie-1",
|
||
hover_image_url: "/mockups/forest-light-hoodie-2",
|
||
source_width: @mockup_source_width,
|
||
hover_source_width: @mockup_source_width,
|
||
category: "Apparel",
|
||
in_stock: true,
|
||
on_sale: true
|
||
},
|
||
%{
|
||
id: "8",
|
||
name: "Wildflower Meadow Tote Bag",
|
||
description: "Sturdy cotton tote bag with vibrant wildflower design",
|
||
price: 1999,
|
||
compare_at_price: nil,
|
||
image_url: "/mockups/wildflower-meadow-tote-1",
|
||
hover_image_url: "/mockups/wildflower-meadow-tote-2",
|
||
source_width: @mockup_source_width,
|
||
hover_source_width: @mockup_source_width,
|
||
category: "Apparel",
|
||
in_stock: true,
|
||
on_sale: false
|
||
},
|
||
%{
|
||
id: "9",
|
||
name: "Sunset Gradient Tote Bag",
|
||
description: "Beautiful ocean sunset printed on durable canvas tote",
|
||
price: 1999,
|
||
compare_at_price: nil,
|
||
image_url: "/mockups/sunset-gradient-tote-1",
|
||
hover_image_url: "/mockups/sunset-gradient-tote-2",
|
||
source_width: @mockup_source_width,
|
||
hover_source_width: @mockup_source_width,
|
||
category: "Apparel",
|
||
in_stock: true,
|
||
on_sale: false
|
||
},
|
||
# Homewares
|
||
%{
|
||
id: "10",
|
||
name: "Fern Leaf Mug",
|
||
description: "Start your morning right with this nature-inspired ceramic mug",
|
||
price: 1499,
|
||
compare_at_price: nil,
|
||
image_url: "/mockups/fern-leaf-mug-1",
|
||
hover_image_url: "/mockups/fern-leaf-mug-2",
|
||
source_width: @mockup_source_width,
|
||
hover_source_width: @mockup_source_width,
|
||
category: "Homewares",
|
||
in_stock: true,
|
||
on_sale: false
|
||
},
|
||
%{
|
||
id: "11",
|
||
name: "Ocean Waves Cushion",
|
||
description: "Soft polyester cushion featuring a stunning ocean sunset",
|
||
price: 2999,
|
||
compare_at_price: nil,
|
||
image_url: "/mockups/ocean-waves-cushion-1",
|
||
hover_image_url: "/mockups/ocean-waves-cushion-2",
|
||
source_width: @mockup_source_width,
|
||
hover_source_width: @mockup_source_width,
|
||
category: "Homewares",
|
||
in_stock: true,
|
||
on_sale: false
|
||
},
|
||
%{
|
||
id: "12",
|
||
name: "Night Sky Blanket",
|
||
description: "Cosy sherpa fleece blanket with mesmerising milky way print",
|
||
price: 5999,
|
||
compare_at_price: 6999,
|
||
image_url: "/mockups/night-sky-blanket-1",
|
||
hover_image_url: "/mockups/night-sky-blanket-2",
|
||
source_width: @mockup_source_width,
|
||
hover_source_width: @mockup_source_width,
|
||
category: "Homewares",
|
||
in_stock: true,
|
||
on_sale: true
|
||
},
|
||
# Stationery
|
||
%{
|
||
id: "13",
|
||
name: "Autumn Leaves Notebook",
|
||
description: "Hardcover journal with beautiful autumn foliage design",
|
||
price: 1999,
|
||
compare_at_price: nil,
|
||
image_url: "/mockups/autumn-leaves-notebook-1",
|
||
hover_image_url: "/mockups/autumn-leaves-notebook-2",
|
||
source_width: @mockup_source_width,
|
||
hover_source_width: @mockup_source_width,
|
||
category: "Stationery",
|
||
in_stock: true,
|
||
on_sale: false
|
||
},
|
||
%{
|
||
id: "14",
|
||
name: "Monstera Leaf Notebook",
|
||
description: "Tropical-inspired hardcover journal for your thoughts",
|
||
price: 1999,
|
||
compare_at_price: nil,
|
||
image_url: "/mockups/monstera-leaf-notebook-1",
|
||
hover_image_url: "/mockups/monstera-leaf-notebook-2",
|
||
source_width: @mockup_source_width,
|
||
hover_source_width: @mockup_source_width,
|
||
category: "Stationery",
|
||
in_stock: true,
|
||
on_sale: false
|
||
},
|
||
# Accessories
|
||
%{
|
||
id: "15",
|
||
name: "Monstera Leaf Phone Case",
|
||
description: "Tough phone case with stunning monstera leaf photography",
|
||
price: 2499,
|
||
compare_at_price: nil,
|
||
image_url: "/mockups/monstera-leaf-phone-case-1",
|
||
hover_image_url: "/mockups/monstera-leaf-phone-case-2",
|
||
source_width: @mockup_source_width,
|
||
hover_source_width: @mockup_source_width,
|
||
category: "Accessories",
|
||
in_stock: true,
|
||
on_sale: false
|
||
},
|
||
%{
|
||
id: "16",
|
||
name: "Blue Waves Laptop Sleeve",
|
||
description: "Protective laptop sleeve with abstract blue gradient design",
|
||
price: 3499,
|
||
compare_at_price: nil,
|
||
image_url: "/mockups/blue-waves-laptop-sleeve-1",
|
||
hover_image_url: "/mockups/blue-waves-laptop-sleeve-2",
|
||
source_width: @mockup_source_width,
|
||
hover_source_width: @mockup_source_width,
|
||
category: "Accessories",
|
||
in_stock: true,
|
||
on_sale: false
|
||
}
|
||
]
|
||
end
|
||
|
||
defp mock_cart_items do
|
||
products = mock_products()
|
||
|
||
[
|
||
%{
|
||
product: Enum.at(products, 0),
|
||
quantity: 1,
|
||
variant: "12″ x 18″ / Matte"
|
||
},
|
||
%{
|
||
product: Enum.at(products, 9),
|
||
quantity: 2,
|
||
variant: "11oz / White"
|
||
},
|
||
%{
|
||
product: Enum.at(products, 5),
|
||
quantity: 1,
|
||
variant: "Large / Black"
|
||
}
|
||
]
|
||
end
|
||
|
||
defp mock_testimonials do
|
||
[
|
||
%{
|
||
id: "1",
|
||
author: "Sample Customer A",
|
||
content:
|
||
"This is sample testimonial text for the demo store. Your real customer quotes will appear here – try to include specific details about what they loved.",
|
||
rating: 5,
|
||
date: "2025-01-15"
|
||
},
|
||
%{
|
||
id: "2",
|
||
author: "Sample Customer B",
|
||
content:
|
||
"Another example testimonial. A mix of short and longer quotes works well for the layout. This one's a bit longer to show how the card handles more text.",
|
||
rating: 5,
|
||
date: "2025-01-10"
|
||
},
|
||
%{
|
||
id: "3",
|
||
author: "Sample Customer C",
|
||
content:
|
||
"Short and sweet works too. Customers love seeing real feedback from other buyers.",
|
||
rating: 5,
|
||
date: "2025-01-05"
|
||
},
|
||
%{
|
||
id: "4",
|
||
author: "Sample Customer D",
|
||
content:
|
||
"Sample feedback showing how the grid handles four or more testimonials. Replace these with real quotes once you have them.",
|
||
rating: 5,
|
||
date: "2024-12-28"
|
||
},
|
||
%{
|
||
id: "5",
|
||
author: "Sample Customer E",
|
||
content:
|
||
"One more example to fill out the testimonials section. You can add as many as you like – the layout adapts to fit.",
|
||
rating: 5,
|
||
date: "2024-12-20"
|
||
},
|
||
%{
|
||
id: "6",
|
||
author: "Sample Customer F",
|
||
content:
|
||
"Final sample testimonial. When you connect your store, real reviews and feedback from customers will replace these placeholder entries.",
|
||
rating: 5,
|
||
date: "2024-12-15"
|
||
}
|
||
]
|
||
end
|
||
|
||
defp mock_categories do
|
||
[
|
||
%{
|
||
id: "1",
|
||
name: "Art Prints",
|
||
slug: "art-prints",
|
||
product_count: 5,
|
||
image_url: "/mockups/mountain-sunrise-print-2-thumb.jpg"
|
||
},
|
||
%{
|
||
id: "2",
|
||
name: "Apparel",
|
||
slug: "apparel",
|
||
product_count: 4,
|
||
image_url: "/mockups/forest-silhouette-tshirt-1-thumb.jpg"
|
||
},
|
||
%{
|
||
id: "3",
|
||
name: "Homewares",
|
||
slug: "homewares",
|
||
product_count: 3,
|
||
image_url: "/mockups/fern-leaf-mug-1-thumb.jpg"
|
||
},
|
||
%{
|
||
id: "4",
|
||
name: "Stationery",
|
||
slug: "stationery",
|
||
product_count: 2,
|
||
image_url: "/mockups/autumn-leaves-notebook-1-thumb.jpg"
|
||
},
|
||
%{
|
||
id: "5",
|
||
name: "Accessories",
|
||
slug: "accessories",
|
||
product_count: 2,
|
||
image_url: "/mockups/monstera-leaf-phone-case-1-thumb.jpg"
|
||
}
|
||
]
|
||
end
|
||
end
|