feat: add automated Printify mockup generation & POD sample content
Printify Integration:
- Add Printify API client module with full HTTP wrapper
- Add mockup generator with dynamic "cover" scaling algorithm
- Create Mix task (mix generate_mockups) for automated mockup generation
- Support --cleanup flag to delete products after downloading mockups
- Calculate optimal scale factor based on artwork/placeholder aspect ratios
- Handle landscape artwork on portrait print areas without white space
Sample Content (16 products across 5 POD categories):
- Art Prints: Mountain Sunrise, Ocean Waves, Wildflower Meadow, Geometric Abstract, Botanical Illustration
- Apparel: Forest Silhouette T-Shirt, Forest Light Hoodie
- Tote Bags: Wildflower Meadow, Sunset Gradient
- Homewares: Fern Leaf Mug, Ocean Waves Cushion, Night Sky Blanket
- Stationery: Autumn Leaves Notebook, Monstera Leaf Notebook
- Accessories: Monstera Leaf Phone Case, Blue Waves Laptop Sleeve
Preview Data Updates:
- Replace generic e-commerce products with POD-focused items
- Update categories to POD-relevant: Art Prints, Apparel, Homewares, Stationery, Accessories
- Update cart drawer items to match new product range
- Refresh testimonials with POD-appropriate reviews
Theme Content Updates:
- Update hero section for "Wildprint Studio" brand identity
- Rewrite about page narrative with humble, British, personal tone
- Update search hints to nature/POD relevant terms
- Add mockups directory to static paths
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 23:35:18 +00:00
|
|
|
defmodule SimpleshopTheme.Printify.MockupGenerator do
|
|
|
|
|
@moduledoc """
|
|
|
|
|
Generates product mockups using the Printify API.
|
|
|
|
|
|
|
|
|
|
This module handles the end-to-end process of:
|
|
|
|
|
1. Looking up product blueprints and variants
|
|
|
|
|
2. Downloading artwork from Unsplash
|
|
|
|
|
3. Uploading artwork to Printify
|
|
|
|
|
4. Creating products with the artwork
|
|
|
|
|
5. Downloading generated mockup images
|
|
|
|
|
6. Optionally cleaning up created products
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
alias SimpleshopTheme.Printify.Client
|
|
|
|
|
|
|
|
|
|
@output_dir "priv/static/mockups"
|
|
|
|
|
|
|
|
|
|
@doc """
|
|
|
|
|
Product definitions with their artwork URLs and Printify product types.
|
|
|
|
|
"""
|
|
|
|
|
def product_definitions do
|
|
|
|
|
[
|
|
|
|
|
%{
|
|
|
|
|
name: "Mountain Sunrise Art Print",
|
|
|
|
|
slug: "mountain-sunrise-print",
|
|
|
|
|
category: "Art Prints",
|
|
|
|
|
artwork_url: unsplash_download_url("UweNcthlmDc"),
|
|
|
|
|
product_type: :poster,
|
|
|
|
|
price: 2400
|
|
|
|
|
},
|
|
|
|
|
%{
|
|
|
|
|
name: "Ocean Waves Art Print",
|
|
|
|
|
slug: "ocean-waves-print",
|
|
|
|
|
category: "Art Prints",
|
|
|
|
|
artwork_url: unsplash_download_url("XRhUTUVuXAE"),
|
|
|
|
|
product_type: :poster,
|
|
|
|
|
price: 2400
|
|
|
|
|
},
|
|
|
|
|
%{
|
|
|
|
|
name: "Wildflower Meadow Art Print",
|
|
|
|
|
slug: "wildflower-meadow-print",
|
|
|
|
|
category: "Art Prints",
|
|
|
|
|
artwork_url: unsplash_download_url("QvjL4y7SF9k"),
|
|
|
|
|
product_type: :poster,
|
|
|
|
|
price: 2400
|
|
|
|
|
},
|
|
|
|
|
%{
|
|
|
|
|
name: "Geometric Abstract Art Print",
|
|
|
|
|
slug: "geometric-abstract-print",
|
|
|
|
|
category: "Art Prints",
|
|
|
|
|
artwork_url: unsplash_download_url("-6GvTDpkkPU"),
|
|
|
|
|
product_type: :poster,
|
|
|
|
|
price: 2800
|
|
|
|
|
},
|
|
|
|
|
%{
|
|
|
|
|
name: "Botanical Illustration Print",
|
|
|
|
|
slug: "botanical-illustration-print",
|
|
|
|
|
category: "Art Prints",
|
|
|
|
|
artwork_url: unsplash_download_url("FNtNIDQWUZY"),
|
|
|
|
|
product_type: :poster,
|
|
|
|
|
price: 2400
|
|
|
|
|
},
|
|
|
|
|
%{
|
|
|
|
|
name: "Forest Silhouette T-Shirt",
|
|
|
|
|
slug: "forest-silhouette-tshirt",
|
|
|
|
|
category: "Apparel",
|
|
|
|
|
artwork_url: unsplash_download_url("EhvMzMRO4_o"),
|
|
|
|
|
product_type: :tshirt,
|
|
|
|
|
price: 2999
|
|
|
|
|
},
|
|
|
|
|
%{
|
|
|
|
|
name: "Forest Light Hoodie",
|
|
|
|
|
slug: "forest-light-hoodie",
|
|
|
|
|
category: "Apparel",
|
|
|
|
|
artwork_url: unsplash_download_url("FwVkxITt8Bg"),
|
|
|
|
|
product_type: :hoodie,
|
|
|
|
|
price: 4499
|
|
|
|
|
},
|
|
|
|
|
%{
|
|
|
|
|
name: "Wildflower Meadow Tote Bag",
|
|
|
|
|
slug: "wildflower-meadow-tote",
|
|
|
|
|
category: "Apparel",
|
|
|
|
|
artwork_url: unsplash_download_url("QvjL4y7SF9k"),
|
|
|
|
|
product_type: :tote,
|
|
|
|
|
price: 1999
|
|
|
|
|
},
|
|
|
|
|
%{
|
|
|
|
|
name: "Sunset Gradient Tote Bag",
|
|
|
|
|
slug: "sunset-gradient-tote",
|
|
|
|
|
category: "Apparel",
|
|
|
|
|
artwork_url: unsplash_download_url("XRhUTUVuXAE"),
|
|
|
|
|
product_type: :tote,
|
|
|
|
|
price: 1999
|
|
|
|
|
},
|
|
|
|
|
%{
|
|
|
|
|
name: "Fern Leaf Mug",
|
|
|
|
|
slug: "fern-leaf-mug",
|
|
|
|
|
category: "Homewares",
|
|
|
|
|
artwork_url: unsplash_download_url("bYiJojtkHnc"),
|
|
|
|
|
product_type: :mug,
|
|
|
|
|
price: 1499
|
|
|
|
|
},
|
|
|
|
|
%{
|
|
|
|
|
name: "Ocean Waves Cushion",
|
|
|
|
|
slug: "ocean-waves-cushion",
|
|
|
|
|
category: "Homewares",
|
|
|
|
|
artwork_url: unsplash_download_url("XRhUTUVuXAE"),
|
|
|
|
|
product_type: :cushion,
|
|
|
|
|
price: 2999
|
|
|
|
|
},
|
|
|
|
|
%{
|
|
|
|
|
name: "Night Sky Blanket",
|
|
|
|
|
slug: "night-sky-blanket",
|
|
|
|
|
category: "Homewares",
|
|
|
|
|
artwork_url: unsplash_download_url("oQR1B87HsNs"),
|
|
|
|
|
product_type: :blanket,
|
|
|
|
|
price: 5999
|
|
|
|
|
},
|
|
|
|
|
%{
|
|
|
|
|
name: "Autumn Leaves Notebook",
|
|
|
|
|
slug: "autumn-leaves-notebook",
|
|
|
|
|
category: "Stationery",
|
|
|
|
|
artwork_url: unsplash_download_url("Aa3ALtIxEGY"),
|
|
|
|
|
product_type: :notebook,
|
|
|
|
|
price: 1999
|
|
|
|
|
},
|
|
|
|
|
%{
|
|
|
|
|
name: "Monstera Leaf Notebook",
|
|
|
|
|
slug: "monstera-leaf-notebook",
|
|
|
|
|
category: "Stationery",
|
|
|
|
|
artwork_url: unsplash_download_url("hETU8_b2IM0"),
|
|
|
|
|
product_type: :notebook,
|
|
|
|
|
price: 1999
|
|
|
|
|
},
|
|
|
|
|
%{
|
|
|
|
|
name: "Monstera Leaf Phone Case",
|
|
|
|
|
slug: "monstera-leaf-phone-case",
|
|
|
|
|
category: "Accessories",
|
|
|
|
|
artwork_url: unsplash_download_url("hETU8_b2IM0"),
|
|
|
|
|
product_type: :phone_case,
|
|
|
|
|
price: 2499
|
|
|
|
|
},
|
|
|
|
|
%{
|
|
|
|
|
name: "Blue Waves Laptop Sleeve",
|
|
|
|
|
slug: "blue-waves-laptop-sleeve",
|
|
|
|
|
category: "Accessories",
|
|
|
|
|
artwork_url: unsplash_download_url("dYksH3vHorc"),
|
|
|
|
|
product_type: :laptop_sleeve,
|
|
|
|
|
price: 3499
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
@doc """
|
|
|
|
|
Blueprint configurations for each product type.
|
|
|
|
|
These IDs need to be looked up from the Printify catalog.
|
|
|
|
|
"""
|
|
|
|
|
def blueprint_config do
|
|
|
|
|
%{
|
|
|
|
|
# Search terms matched to Printify's actual blueprint titles (partial match)
|
|
|
|
|
poster: %{blueprint_id: nil, print_provider_id: nil, search_term: "Matte Posters"},
|
|
|
|
|
tshirt: %{blueprint_id: nil, print_provider_id: nil, search_term: "Softstyle T-Shirt"},
|
|
|
|
|
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"},
|
|
|
|
|
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"},
|
|
|
|
|
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"}
|
|
|
|
|
}
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
@doc """
|
|
|
|
|
Generate Unsplash download URL from photo ID.
|
|
|
|
|
Uses the Unsplash download API which provides high-quality images.
|
|
|
|
|
"""
|
|
|
|
|
def unsplash_download_url(photo_id) do
|
|
|
|
|
"https://unsplash.com/photos/#{photo_id}/download?force=true"
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
@doc """
|
|
|
|
|
Search for a blueprint by name/term.
|
|
|
|
|
"""
|
|
|
|
|
def find_blueprint(search_term) do
|
|
|
|
|
case Client.get_blueprints() do
|
|
|
|
|
{:ok, blueprints} ->
|
|
|
|
|
found =
|
|
|
|
|
Enum.find(blueprints, fn bp ->
|
|
|
|
|
String.contains?(String.downcase(bp["title"] || ""), String.downcase(search_term))
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
case found do
|
|
|
|
|
nil -> {:error, {:blueprint_not_found, search_term}}
|
|
|
|
|
bp -> {:ok, bp}
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
error ->
|
|
|
|
|
error
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
@doc """
|
|
|
|
|
Find a suitable print provider for a blueprint.
|
|
|
|
|
Prefers providers with good ratings and reasonable pricing.
|
|
|
|
|
"""
|
|
|
|
|
def find_print_provider(blueprint_id) do
|
|
|
|
|
case Client.get_print_providers(blueprint_id) do
|
|
|
|
|
{:ok, providers} when is_list(providers) and length(providers) > 0 ->
|
|
|
|
|
# Just pick the first provider for simplicity
|
|
|
|
|
{:ok, hd(providers)}
|
|
|
|
|
|
|
|
|
|
{:ok, []} ->
|
|
|
|
|
{:error, :no_providers}
|
|
|
|
|
|
|
|
|
|
error ->
|
|
|
|
|
error
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
@doc """
|
|
|
|
|
Get variant and placeholder information for a blueprint/provider combination.
|
|
|
|
|
"""
|
|
|
|
|
def get_variant_info(blueprint_id, print_provider_id) do
|
|
|
|
|
case Client.get_variants(blueprint_id, print_provider_id) do
|
|
|
|
|
{:ok, %{"variants" => variants}} when is_list(variants) ->
|
|
|
|
|
{:ok, variants}
|
|
|
|
|
|
|
|
|
|
{:ok, variants} when is_list(variants) ->
|
|
|
|
|
{:ok, variants}
|
|
|
|
|
|
|
|
|
|
{:ok, response} when is_map(response) ->
|
|
|
|
|
# Handle case where variants might be nested differently
|
|
|
|
|
{:ok, response["variants"] || []}
|
|
|
|
|
|
|
|
|
|
error ->
|
|
|
|
|
error
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
@doc """
|
|
|
|
|
Upload artwork to Printify from a URL.
|
|
|
|
|
"""
|
|
|
|
|
def upload_artwork(name, url) do
|
|
|
|
|
file_name = "#{name}.jpg"
|
|
|
|
|
Client.upload_image(file_name, url)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
@doc """
|
|
|
|
|
Calculate scale factor for "cover" behavior.
|
|
|
|
|
Image will fill entire placeholder, cropping edges if necessary.
|
|
|
|
|
Printify scale is relative to placeholder width (1.0 = artwork width matches placeholder width).
|
|
|
|
|
"""
|
|
|
|
|
def calculate_cover_scale(artwork_width, artwork_height, placeholder_width, placeholder_height)
|
|
|
|
|
when is_number(artwork_width) and is_number(artwork_height) and
|
|
|
|
|
is_number(placeholder_width) and is_number(placeholder_height) and
|
|
|
|
|
artwork_width > 0 and artwork_height > 0 and
|
|
|
|
|
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)
|
|
|
|
|
max(width_scale, height_scale)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def calculate_cover_scale(_, _, _, _), do: 1.0
|
|
|
|
|
|
|
|
|
|
@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
|
|
|
|
|
# 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)
|
|
|
|
|
|
|
|
|
|
# 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)}")
|
|
|
|
|
|
|
|
|
|
product_data = %{
|
|
|
|
|
title: product_def.name,
|
|
|
|
|
description: "#{product_def.name} - Nature-inspired design from Wildprint Studio",
|
|
|
|
|
blueprint_id: blueprint_id,
|
|
|
|
|
print_provider_id: print_provider_id,
|
|
|
|
|
variants: [
|
|
|
|
|
%{
|
|
|
|
|
id: variant_id,
|
|
|
|
|
price: product_def.price,
|
|
|
|
|
is_enabled: true
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
print_areas: [
|
|
|
|
|
%{
|
|
|
|
|
variant_ids: [variant_id],
|
|
|
|
|
placeholders: [
|
|
|
|
|
%{
|
|
|
|
|
position: front_placeholder["position"] || "front",
|
|
|
|
|
images: [
|
|
|
|
|
%{
|
|
|
|
|
id: image_id,
|
|
|
|
|
x: 0.5,
|
|
|
|
|
y: 0.5,
|
|
|
|
|
scale: scale,
|
|
|
|
|
angle: 0
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Client.create_product(shop_id, product_data)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
@doc """
|
|
|
|
|
Extract mockup image URLs from a created product.
|
|
|
|
|
"""
|
|
|
|
|
def extract_mockup_urls(product) do
|
|
|
|
|
images = product["images"] || []
|
|
|
|
|
Enum.map(images, fn img -> img["src"] end)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
@doc """
|
2026-01-25 00:30:42 +00:00
|
|
|
Download mockup images, save as WebP source, and generate variants.
|
|
|
|
|
Sources are saved for regeneration on startup via VariantCache.
|
feat: add automated Printify mockup generation & POD sample content
Printify Integration:
- Add Printify API client module with full HTTP wrapper
- Add mockup generator with dynamic "cover" scaling algorithm
- Create Mix task (mix generate_mockups) for automated mockup generation
- Support --cleanup flag to delete products after downloading mockups
- Calculate optimal scale factor based on artwork/placeholder aspect ratios
- Handle landscape artwork on portrait print areas without white space
Sample Content (16 products across 5 POD categories):
- Art Prints: Mountain Sunrise, Ocean Waves, Wildflower Meadow, Geometric Abstract, Botanical Illustration
- Apparel: Forest Silhouette T-Shirt, Forest Light Hoodie
- Tote Bags: Wildflower Meadow, Sunset Gradient
- Homewares: Fern Leaf Mug, Ocean Waves Cushion, Night Sky Blanket
- Stationery: Autumn Leaves Notebook, Monstera Leaf Notebook
- Accessories: Monstera Leaf Phone Case, Blue Waves Laptop Sleeve
Preview Data Updates:
- Replace generic e-commerce products with POD-focused items
- Update categories to POD-relevant: Art Prints, Apparel, Homewares, Stationery, Accessories
- Update cart drawer items to match new product range
- Refresh testimonials with POD-appropriate reviews
Theme Content Updates:
- Update hero section for "Wildprint Studio" brand identity
- Rewrite about page narrative with humble, British, personal tone
- Update search hints to nature/POD relevant terms
- Add mockups directory to static paths
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 23:35:18 +00:00
|
|
|
"""
|
|
|
|
|
def download_mockups(product_slug, mockup_urls) do
|
2026-01-25 00:30:42 +00:00
|
|
|
alias SimpleshopTheme.Images.Optimizer
|
|
|
|
|
|
feat: add automated Printify mockup generation & POD sample content
Printify Integration:
- Add Printify API client module with full HTTP wrapper
- Add mockup generator with dynamic "cover" scaling algorithm
- Create Mix task (mix generate_mockups) for automated mockup generation
- Support --cleanup flag to delete products after downloading mockups
- Calculate optimal scale factor based on artwork/placeholder aspect ratios
- Handle landscape artwork on portrait print areas without white space
Sample Content (16 products across 5 POD categories):
- Art Prints: Mountain Sunrise, Ocean Waves, Wildflower Meadow, Geometric Abstract, Botanical Illustration
- Apparel: Forest Silhouette T-Shirt, Forest Light Hoodie
- Tote Bags: Wildflower Meadow, Sunset Gradient
- Homewares: Fern Leaf Mug, Ocean Waves Cushion, Night Sky Blanket
- Stationery: Autumn Leaves Notebook, Monstera Leaf Notebook
- Accessories: Monstera Leaf Phone Case, Blue Waves Laptop Sleeve
Preview Data Updates:
- Replace generic e-commerce products with POD-focused items
- Update categories to POD-relevant: Art Prints, Apparel, Homewares, Stationery, Accessories
- Update cart drawer items to match new product range
- Refresh testimonials with POD-appropriate reviews
Theme Content Updates:
- Update hero section for "Wildprint Studio" brand identity
- Rewrite about page narrative with humble, British, personal tone
- Update search hints to nature/POD relevant terms
- Add mockups directory to static paths
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 23:35:18 +00:00
|
|
|
File.mkdir_p!(@output_dir)
|
|
|
|
|
|
|
|
|
|
mockup_urls
|
|
|
|
|
|> Enum.with_index(1)
|
|
|
|
|
|> Enum.map(fn {url, index} ->
|
2026-01-25 00:30:42 +00:00
|
|
|
basename = "#{product_slug}-#{index}"
|
|
|
|
|
source_path = Path.join(@output_dir, "#{basename}.webp")
|
|
|
|
|
IO.puts(" Processing mockup #{index}...")
|
|
|
|
|
|
|
|
|
|
temp_path = Path.join(System.tmp_dir!(), "#{basename}-temp.jpg")
|
|
|
|
|
|
|
|
|
|
with {:ok, _} <- Client.download_file(url, temp_path),
|
|
|
|
|
{:ok, image_data} <- File.read(temp_path),
|
|
|
|
|
{:ok, webp_data, source_width, _} <- Optimizer.to_optimized_webp(image_data),
|
|
|
|
|
:ok <- File.write(source_path, webp_data),
|
|
|
|
|
{:ok, _} <- Optimizer.process_file(webp_data, basename, @output_dir) do
|
|
|
|
|
File.rm(temp_path)
|
|
|
|
|
IO.puts(" Saved source + variants for #{basename} (#{source_width}px)")
|
|
|
|
|
{:ok, basename, source_width}
|
|
|
|
|
else
|
|
|
|
|
{:error, reason} ->
|
|
|
|
|
File.rm(temp_path)
|
|
|
|
|
{:error, {url, reason}}
|
feat: add automated Printify mockup generation & POD sample content
Printify Integration:
- Add Printify API client module with full HTTP wrapper
- Add mockup generator with dynamic "cover" scaling algorithm
- Create Mix task (mix generate_mockups) for automated mockup generation
- Support --cleanup flag to delete products after downloading mockups
- Calculate optimal scale factor based on artwork/placeholder aspect ratios
- Handle landscape artwork on portrait print areas without white space
Sample Content (16 products across 5 POD categories):
- Art Prints: Mountain Sunrise, Ocean Waves, Wildflower Meadow, Geometric Abstract, Botanical Illustration
- Apparel: Forest Silhouette T-Shirt, Forest Light Hoodie
- Tote Bags: Wildflower Meadow, Sunset Gradient
- Homewares: Fern Leaf Mug, Ocean Waves Cushion, Night Sky Blanket
- Stationery: Autumn Leaves Notebook, Monstera Leaf Notebook
- Accessories: Monstera Leaf Phone Case, Blue Waves Laptop Sleeve
Preview Data Updates:
- Replace generic e-commerce products with POD-focused items
- Update categories to POD-relevant: Art Prints, Apparel, Homewares, Stationery, Accessories
- Update cart drawer items to match new product range
- Refresh testimonials with POD-appropriate reviews
Theme Content Updates:
- Update hero section for "Wildprint Studio" brand identity
- Rewrite about page narrative with humble, British, personal tone
- Update search hints to nature/POD relevant terms
- Add mockups directory to static paths
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 23:35:18 +00:00
|
|
|
end
|
|
|
|
|
end)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
@doc """
|
|
|
|
|
Generate mockups for all products.
|
|
|
|
|
"""
|
|
|
|
|
def generate_all(opts \\ []) do
|
|
|
|
|
cleanup = Keyword.get(opts, :cleanup, false)
|
|
|
|
|
|
|
|
|
|
IO.puts("Starting mockup generation...")
|
|
|
|
|
IO.puts("")
|
|
|
|
|
|
|
|
|
|
# Get shop ID
|
|
|
|
|
IO.puts("Fetching shop ID...")
|
|
|
|
|
{:ok, shop_id} = Client.get_shop_id()
|
|
|
|
|
IO.puts("Using shop ID: #{shop_id}")
|
|
|
|
|
IO.puts("")
|
|
|
|
|
|
|
|
|
|
results =
|
|
|
|
|
product_definitions()
|
|
|
|
|
|> Enum.map(fn product_def ->
|
|
|
|
|
IO.puts("Processing: #{product_def.name}")
|
|
|
|
|
result = generate_single(shop_id, product_def)
|
|
|
|
|
|
|
|
|
|
case result do
|
|
|
|
|
{:ok, product_id, mockup_paths} ->
|
|
|
|
|
IO.puts(" ✓ Generated #{length(mockup_paths)} mockups")
|
|
|
|
|
{:ok, product_def.slug, product_id, mockup_paths}
|
|
|
|
|
|
|
|
|
|
{:error, reason} ->
|
|
|
|
|
IO.puts(" ✗ Error: #{inspect(reason)}")
|
|
|
|
|
{:error, product_def.slug, reason}
|
|
|
|
|
end
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
# Cleanup if requested
|
|
|
|
|
if cleanup do
|
|
|
|
|
IO.puts("")
|
|
|
|
|
IO.puts("Cleaning up created products...")
|
|
|
|
|
|
|
|
|
|
results
|
|
|
|
|
|> Enum.filter(fn
|
|
|
|
|
{:ok, _, _, _} -> true
|
|
|
|
|
_ -> false
|
|
|
|
|
end)
|
|
|
|
|
|> Enum.each(fn {:ok, slug, product_id, _} ->
|
|
|
|
|
IO.puts(" Deleting #{slug}...")
|
|
|
|
|
Client.delete_product(shop_id, product_id)
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
IO.puts("Cleanup complete.")
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
IO.puts("")
|
|
|
|
|
IO.puts("Mockup generation complete!")
|
|
|
|
|
IO.puts("Output directory: #{@output_dir}")
|
|
|
|
|
|
|
|
|
|
results
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
@doc """
|
|
|
|
|
Generate mockups for a single product.
|
|
|
|
|
"""
|
|
|
|
|
def generate_single(shop_id, product_def) do
|
|
|
|
|
config = blueprint_config()[product_def.product_type]
|
|
|
|
|
|
|
|
|
|
with {:ok, blueprint} <- find_blueprint(config.search_term),
|
|
|
|
|
blueprint_id = blueprint["id"],
|
|
|
|
|
_ = IO.puts(" Found blueprint: #{blueprint["title"]} (#{blueprint_id})"),
|
|
|
|
|
{:ok, provider} <- find_print_provider(blueprint_id),
|
|
|
|
|
provider_id = provider["id"],
|
|
|
|
|
_ = IO.puts(" Using provider: #{provider["title"]} (#{provider_id})"),
|
|
|
|
|
{:ok, variants} <- get_variant_info(blueprint_id, provider_id),
|
|
|
|
|
_ = IO.puts(" Found #{length(variants)} variants"),
|
|
|
|
|
_ = IO.puts(" Uploading artwork..."),
|
|
|
|
|
{:ok, upload} <- upload_artwork(product_def.slug, product_def.artwork_url),
|
|
|
|
|
image_id = upload["id"],
|
|
|
|
|
image_width = upload["width"],
|
|
|
|
|
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),
|
|
|
|
|
product_id = product["id"],
|
|
|
|
|
mockup_urls = extract_mockup_urls(product),
|
|
|
|
|
_ = IO.puts(" Product created (ID: #{product_id})"),
|
|
|
|
|
_ = IO.puts(" Downloading #{length(mockup_urls)} mockups..."),
|
|
|
|
|
download_results <- download_mockups(product_def.slug, mockup_urls) do
|
|
|
|
|
successful_downloads =
|
|
|
|
|
download_results
|
|
|
|
|
|> Enum.filter(&match?({:ok, _}, &1))
|
|
|
|
|
|> Enum.map(fn {:ok, path} -> path end)
|
|
|
|
|
|
|
|
|
|
{:ok, product_id, successful_downloads}
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
@doc """
|
|
|
|
|
List all available blueprints (for discovery).
|
|
|
|
|
"""
|
|
|
|
|
def list_blueprints do
|
|
|
|
|
case Client.get_blueprints() do
|
|
|
|
|
{:ok, blueprints} ->
|
|
|
|
|
blueprints
|
|
|
|
|
|> Enum.map(fn bp -> {bp["id"], bp["title"]} end)
|
|
|
|
|
|> Enum.sort_by(fn {_, title} -> title end)
|
|
|
|
|
|
|
|
|
|
error ->
|
|
|
|
|
error
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
@doc """
|
|
|
|
|
Search blueprints by keyword.
|
|
|
|
|
"""
|
|
|
|
|
def search_blueprints(keyword) do
|
|
|
|
|
case Client.get_blueprints() do
|
|
|
|
|
{:ok, blueprints} ->
|
|
|
|
|
blueprints
|
|
|
|
|
|> Enum.filter(fn bp ->
|
|
|
|
|
String.contains?(String.downcase(bp["title"] || ""), String.downcase(keyword))
|
|
|
|
|
end)
|
|
|
|
|
|> Enum.map(fn bp -> {bp["id"], bp["title"]} end)
|
|
|
|
|
|
|
|
|
|
error ->
|
|
|
|
|
error
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|