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>
This commit is contained in:
182
lib/simpleshop_theme/printify/client.ex
Normal file
182
lib/simpleshop_theme/printify/client.ex
Normal file
@@ -0,0 +1,182 @@
|
||||
defmodule SimpleshopTheme.Printify.Client do
|
||||
@moduledoc """
|
||||
HTTP client for the Printify API.
|
||||
|
||||
Handles authentication and provides low-level API access.
|
||||
Requires PRINTIFY_API_TOKEN environment variable to be set.
|
||||
"""
|
||||
|
||||
@base_url "https://api.printify.com/v1"
|
||||
|
||||
@doc """
|
||||
Get the API token from environment.
|
||||
"""
|
||||
def api_token do
|
||||
System.get_env("PRINTIFY_API_TOKEN") ||
|
||||
raise "PRINTIFY_API_TOKEN environment variable is not set"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Make a GET request to the Printify API.
|
||||
"""
|
||||
def get(path, opts \\ []) do
|
||||
url = @base_url <> path
|
||||
|
||||
case Req.get(url, headers: auth_headers(), receive_timeout: 30_000) do
|
||||
{:ok, %Req.Response{status: status, body: body}} when status in 200..299 ->
|
||||
{:ok, body}
|
||||
|
||||
{:ok, %Req.Response{status: status, body: body}} ->
|
||||
{:error, {status, body}}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Make a POST request to the Printify API.
|
||||
"""
|
||||
def post(path, body, opts \\ []) do
|
||||
url = @base_url <> path
|
||||
|
||||
case Req.post(url, json: body, headers: auth_headers(), receive_timeout: 60_000) do
|
||||
{:ok, %Req.Response{status: status, body: body}} when status in 200..299 ->
|
||||
{:ok, body}
|
||||
|
||||
{:ok, %Req.Response{status: status, body: body}} ->
|
||||
{:error, {status, body}}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Make a DELETE request to the Printify API.
|
||||
"""
|
||||
def delete(path, opts \\ []) do
|
||||
url = @base_url <> path
|
||||
|
||||
case Req.delete(url, headers: auth_headers(), receive_timeout: 30_000) do
|
||||
{:ok, %Req.Response{status: status, body: body}} when status in 200..299 ->
|
||||
{:ok, body}
|
||||
|
||||
{:ok, %Req.Response{status: status}} when status == 204 ->
|
||||
{:ok, nil}
|
||||
|
||||
{:ok, %Req.Response{status: status, body: body}} ->
|
||||
{:error, {status, body}}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get all shops for the authenticated account.
|
||||
"""
|
||||
def get_shops do
|
||||
get("/shops.json")
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get the first shop ID for the account.
|
||||
"""
|
||||
def get_shop_id do
|
||||
case get_shops() do
|
||||
{:ok, shops} when is_list(shops) and length(shops) > 0 ->
|
||||
{:ok, hd(shops)["id"]}
|
||||
|
||||
{:ok, []} ->
|
||||
{:error, :no_shops}
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get all blueprints (product types) from the catalog.
|
||||
"""
|
||||
def get_blueprints do
|
||||
get("/catalog/blueprints.json")
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get print providers for a specific blueprint.
|
||||
"""
|
||||
def get_print_providers(blueprint_id) do
|
||||
get("/catalog/blueprints/#{blueprint_id}/print_providers.json")
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get variants for a specific blueprint and print provider.
|
||||
"""
|
||||
def get_variants(blueprint_id, print_provider_id) do
|
||||
get("/catalog/blueprints/#{blueprint_id}/print_providers/#{print_provider_id}/variants.json")
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get shipping information for a blueprint/provider combination.
|
||||
"""
|
||||
def get_shipping(blueprint_id, print_provider_id) do
|
||||
get("/catalog/blueprints/#{blueprint_id}/print_providers/#{print_provider_id}/shipping.json")
|
||||
end
|
||||
|
||||
@doc """
|
||||
Upload an image to Printify via URL.
|
||||
"""
|
||||
def upload_image(file_name, url) do
|
||||
post("/uploads/images.json", %{
|
||||
file_name: file_name,
|
||||
url: url
|
||||
})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Create a product in a shop.
|
||||
"""
|
||||
def create_product(shop_id, product_data) do
|
||||
post("/shops/#{shop_id}/products.json", product_data)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get a product by ID.
|
||||
"""
|
||||
def get_product(shop_id, product_id) do
|
||||
get("/shops/#{shop_id}/products/#{product_id}.json")
|
||||
end
|
||||
|
||||
@doc """
|
||||
Delete a product from a shop.
|
||||
"""
|
||||
def delete_product(shop_id, product_id) do
|
||||
delete("/shops/#{shop_id}/products/#{product_id}.json")
|
||||
end
|
||||
|
||||
@doc """
|
||||
Download a file from a URL to a local path.
|
||||
"""
|
||||
def download_file(url, output_path) do
|
||||
case Req.get(url, into: File.stream!(output_path), receive_timeout: 60_000) do
|
||||
{:ok, %Req.Response{status: status}} when status in 200..299 ->
|
||||
{:ok, output_path}
|
||||
|
||||
{:ok, %Req.Response{status: status}} ->
|
||||
File.rm(output_path)
|
||||
{:error, {:http_error, status}}
|
||||
|
||||
{:error, reason} ->
|
||||
File.rm(output_path)
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp auth_headers do
|
||||
[
|
||||
{"Authorization", "Bearer #{api_token()}"},
|
||||
{"Content-Type", "application/json"}
|
||||
]
|
||||
end
|
||||
end
|
||||
476
lib/simpleshop_theme/printify/mockup_generator.ex
Normal file
476
lib/simpleshop_theme/printify/mockup_generator.ex
Normal file
@@ -0,0 +1,476 @@
|
||||
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 """
|
||||
Download mockup images to the output directory.
|
||||
"""
|
||||
def download_mockups(product_slug, mockup_urls) do
|
||||
File.mkdir_p!(@output_dir)
|
||||
|
||||
mockup_urls
|
||||
|> Enum.with_index(1)
|
||||
|> Enum.map(fn {url, index} ->
|
||||
output_path = Path.join(@output_dir, "#{product_slug}-#{index}.jpg")
|
||||
IO.puts(" Downloading mockup #{index} to #{output_path}...")
|
||||
|
||||
case Client.download_file(url, output_path) do
|
||||
{:ok, path} -> {:ok, path}
|
||||
{:error, reason} -> {:error, {url, reason}}
|
||||
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("")
|
||||
|
||||
# Track created products for cleanup
|
||||
created_products = []
|
||||
|
||||
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
|
||||
@@ -34,16 +34,16 @@ defmodule SimpleshopTheme.Theme.PreviewData do
|
||||
def cart_drawer_items do
|
||||
[
|
||||
%{
|
||||
name: "Autumn Fern",
|
||||
variant: "A4 / Unframed",
|
||||
name: "Mountain Sunrise Art Print",
|
||||
variant: "12″ x 18″ / Matte",
|
||||
price: "£24.00",
|
||||
image: "https://picsum.photos/seed/fern/120/120"
|
||||
image: "/mockups/mountain-sunrise-print-1.jpg"
|
||||
},
|
||||
%{
|
||||
name: "Wild Roses",
|
||||
variant: "A3 / Oak frame",
|
||||
price: "£48.00",
|
||||
image: "https://picsum.photos/seed/roses/120/120"
|
||||
name: "Fern Leaf Mug",
|
||||
variant: "11oz / White",
|
||||
price: "£14.99",
|
||||
image: "/mockups/fern-leaf-mug-1.jpg"
|
||||
}
|
||||
]
|
||||
end
|
||||
@@ -93,147 +93,200 @@ defmodule SimpleshopTheme.Theme.PreviewData do
|
||||
|
||||
defp mock_products do
|
||||
[
|
||||
# Art Prints
|
||||
%{
|
||||
id: "1",
|
||||
name: "Classic Cotton T-Shirt",
|
||||
description: "Soft, breathable cotton tee perfect for everyday wear",
|
||||
price: 2999,
|
||||
name: "Mountain Sunrise Art Print",
|
||||
description: "Capture the magic of dawn with this stunning mountain landscape print",
|
||||
price: 2400,
|
||||
compare_at_price: nil,
|
||||
image_url: "https://images.unsplash.com/photo-1521572163474-6864f9cf17ab?w=600&h=800&fit=crop&q=80",
|
||||
hover_image_url: "https://images.unsplash.com/photo-1622445275576-721325763afe?w=600&h=800&fit=crop&q=80",
|
||||
category: "Clothing",
|
||||
image_url: "/mockups/mountain-sunrise-print-1.jpg",
|
||||
hover_image_url: "/mockups/mountain-sunrise-print-2.jpg",
|
||||
category: "Art Prints",
|
||||
in_stock: true,
|
||||
on_sale: false
|
||||
},
|
||||
%{
|
||||
id: "2",
|
||||
name: "Leather Crossbody Bag",
|
||||
description: "Handcrafted genuine leather bag with adjustable strap",
|
||||
price: 8999,
|
||||
compare_at_price: 11999,
|
||||
image_url: "https://images.unsplash.com/photo-1548036328-c9fa89d128fa?w=600&h=800&fit=crop&q=80",
|
||||
hover_image_url: "https://images.unsplash.com/photo-1590874103328-eac38a683ce7?w=600&h=800&fit=crop&q=80",
|
||||
category: "Accessories",
|
||||
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.jpg",
|
||||
hover_image_url: "/mockups/ocean-waves-print-2.jpg",
|
||||
category: "Art Prints",
|
||||
in_stock: true,
|
||||
on_sale: true
|
||||
on_sale: false
|
||||
},
|
||||
%{
|
||||
id: "3",
|
||||
name: "Ceramic Coffee Mug",
|
||||
description: "Handmade ceramic mug with unique glaze finish",
|
||||
price: 2499,
|
||||
name: "Wildflower Meadow Art Print",
|
||||
description: "Beautiful wildflower meadow captured in the summer sunshine",
|
||||
price: 2400,
|
||||
compare_at_price: nil,
|
||||
image_url: "https://images.unsplash.com/photo-1514228742587-6b1558fcca3d?w=600&h=800&fit=crop&q=80",
|
||||
hover_image_url: "https://images.unsplash.com/photo-1481833761820-0509d3217039?w=600&h=800&fit=crop&q=80",
|
||||
category: "Home",
|
||||
image_url: "/mockups/wildflower-meadow-print-1.jpg",
|
||||
hover_image_url: "/mockups/wildflower-meadow-print-2.jpg",
|
||||
category: "Art Prints",
|
||||
in_stock: true,
|
||||
on_sale: false
|
||||
},
|
||||
%{
|
||||
id: "4",
|
||||
name: "Minimalist Watch",
|
||||
description: "Sleek design with Japanese quartz movement",
|
||||
price: 12999,
|
||||
compare_at_price: nil,
|
||||
image_url: "https://images.unsplash.com/photo-1523275335684-37898b6baf30?w=600&h=800&fit=crop&q=80",
|
||||
hover_image_url: "https://images.unsplash.com/photo-1524592094714-0f0654e20314?w=600&h=800&fit=crop&q=80",
|
||||
category: "Accessories",
|
||||
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.jpg",
|
||||
hover_image_url: "/mockups/geometric-abstract-print-2.jpg",
|
||||
category: "Art Prints",
|
||||
in_stock: true,
|
||||
on_sale: false
|
||||
},
|
||||
%{
|
||||
id: "5",
|
||||
name: "Wool Throw Blanket",
|
||||
description: "Cozy merino wool blanket in herringbone pattern",
|
||||
price: 7999,
|
||||
compare_at_price: 9999,
|
||||
image_url: "https://images.unsplash.com/photo-1580301762395-21ce84d00bc6?w=600&h=800&fit=crop&q=80",
|
||||
hover_image_url: "https://images.unsplash.com/photo-1600369671854-2d9f5db4a5e0?w=600&h=800&fit=crop&q=80",
|
||||
category: "Home",
|
||||
in_stock: false,
|
||||
on_sale: true
|
||||
},
|
||||
%{
|
||||
id: "6",
|
||||
name: "Artisan Soap Set",
|
||||
description: "Natural handmade soaps with essential oils",
|
||||
price: 3499,
|
||||
id: "5",
|
||||
name: "Botanical Illustration Print",
|
||||
description: "Vintage-inspired botanical drawing with intricate detail",
|
||||
price: 2400,
|
||||
compare_at_price: nil,
|
||||
image_url: "https://images.unsplash.com/photo-1607006344380-b6775a0824a7?w=600&h=800&fit=crop&q=80",
|
||||
hover_image_url: "https://images.unsplash.com/photo-1600857544200-b2f666a9a2ec?w=600&h=800&fit=crop&q=80",
|
||||
category: "Beauty",
|
||||
image_url: "/mockups/botanical-illustration-print-1.jpg",
|
||||
hover_image_url: "/mockups/botanical-illustration-print-2.jpg",
|
||||
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.jpg",
|
||||
hover_image_url: "/mockups/forest-silhouette-tshirt-2.jpg",
|
||||
category: "Apparel",
|
||||
in_stock: true,
|
||||
on_sale: false
|
||||
},
|
||||
%{
|
||||
id: "7",
|
||||
name: "Denim Jacket",
|
||||
description: "Classic cut denim jacket with vintage wash",
|
||||
price: 8499,
|
||||
compare_at_price: nil,
|
||||
image_url: "https://images.unsplash.com/photo-1576995853123-5a10305d93c0?w=600&h=800&fit=crop&q=80",
|
||||
hover_image_url: "https://images.unsplash.com/photo-1601333144130-8cbb312386b6?w=600&h=800&fit=crop&q=80",
|
||||
category: "Clothing",
|
||||
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.jpg",
|
||||
hover_image_url: "/mockups/forest-light-hoodie-2.jpg",
|
||||
category: "Apparel",
|
||||
in_stock: true,
|
||||
on_sale: false
|
||||
on_sale: true
|
||||
},
|
||||
%{
|
||||
id: "8",
|
||||
name: "Canvas Tote Bag",
|
||||
description: "Durable organic cotton canvas with reinforced handles",
|
||||
price: 2999,
|
||||
name: "Wildflower Meadow Tote Bag",
|
||||
description: "Sturdy cotton tote bag with vibrant wildflower design",
|
||||
price: 1999,
|
||||
compare_at_price: nil,
|
||||
image_url: "https://images.unsplash.com/photo-1622560480605-d83c853bc5c3?w=600&h=800&fit=crop&q=80",
|
||||
hover_image_url: "https://images.unsplash.com/photo-1597633125097-5a9ae3cb8a8f?w=600&h=800&fit=crop&q=80",
|
||||
category: "Accessories",
|
||||
image_url: "/mockups/wildflower-meadow-tote-1.jpg",
|
||||
hover_image_url: "/mockups/wildflower-meadow-tote-2.jpg",
|
||||
category: "Apparel",
|
||||
in_stock: true,
|
||||
on_sale: false
|
||||
},
|
||||
%{
|
||||
id: "9",
|
||||
name: "Scented Candle",
|
||||
description: "Soy wax candle with cedar and vanilla notes",
|
||||
price: 3299,
|
||||
compare_at_price: 3999,
|
||||
image_url: "https://images.unsplash.com/photo-1602028915047-37269d1a73f7?w=600&h=800&fit=crop&q=80",
|
||||
hover_image_url: "https://images.unsplash.com/photo-1603006905003-be475563bc59?w=600&h=800&fit=crop&q=80",
|
||||
category: "Home",
|
||||
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.jpg",
|
||||
hover_image_url: "/mockups/sunset-gradient-tote-2.jpg",
|
||||
category: "Apparel",
|
||||
in_stock: true,
|
||||
on_sale: true
|
||||
on_sale: false
|
||||
},
|
||||
# Homewares
|
||||
%{
|
||||
id: "10",
|
||||
name: "Stainless Steel Water Bottle",
|
||||
description: "Insulated bottle keeps drinks cold for 24 hours",
|
||||
price: 3999,
|
||||
name: "Fern Leaf Mug",
|
||||
description: "Start your morning right with this nature-inspired ceramic mug",
|
||||
price: 1499,
|
||||
compare_at_price: nil,
|
||||
image_url: "https://images.unsplash.com/photo-1602143407151-7111542de6e8?w=600&h=800&fit=crop&q=80",
|
||||
hover_image_url: "https://images.unsplash.com/photo-1570831739435-6601aa3fa4fb?w=600&h=800&fit=crop&q=80",
|
||||
category: "Accessories",
|
||||
image_url: "/mockups/fern-leaf-mug-1.jpg",
|
||||
hover_image_url: "/mockups/fern-leaf-mug-2.jpg",
|
||||
category: "Homewares",
|
||||
in_stock: true,
|
||||
on_sale: false
|
||||
},
|
||||
%{
|
||||
id: "11",
|
||||
name: "Organic Cotton Socks",
|
||||
description: "Comfortable crew socks in solid colors",
|
||||
price: 1499,
|
||||
name: "Ocean Waves Cushion",
|
||||
description: "Soft polyester cushion featuring a stunning ocean sunset",
|
||||
price: 2999,
|
||||
compare_at_price: nil,
|
||||
image_url: "https://images.unsplash.com/photo-1586350977771-b3b0abd50c82?w=600&h=800&fit=crop&q=80",
|
||||
hover_image_url: "https://images.unsplash.com/photo-1582966772680-860e372bb558?w=600&h=800&fit=crop&q=80",
|
||||
category: "Clothing",
|
||||
image_url: "/mockups/ocean-waves-cushion-1.jpg",
|
||||
hover_image_url: "/mockups/ocean-waves-cushion-2.jpg",
|
||||
category: "Homewares",
|
||||
in_stock: true,
|
||||
on_sale: false
|
||||
},
|
||||
%{
|
||||
id: "12",
|
||||
name: "Bamboo Cutting Board",
|
||||
description: "Sustainable bamboo with juice groove",
|
||||
price: 4499,
|
||||
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.jpg",
|
||||
hover_image_url: "/mockups/night-sky-blanket-2.jpg",
|
||||
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: "https://images.unsplash.com/photo-1594226801341-41427b4e5c22?w=600&h=800&fit=crop&q=80",
|
||||
hover_image_url: "https://images.unsplash.com/photo-1606760227091-3dd870d97f1d?w=600&h=800&fit=crop&q=80",
|
||||
category: "Kitchen",
|
||||
image_url: "/mockups/autumn-leaves-notebook-1.jpg",
|
||||
hover_image_url: "/mockups/autumn-leaves-notebook-2.jpg",
|
||||
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.jpg",
|
||||
hover_image_url: "/mockups/monstera-leaf-notebook-2.jpg",
|
||||
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.jpg",
|
||||
hover_image_url: "/mockups/monstera-leaf-phone-case-2.jpg",
|
||||
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.jpg",
|
||||
hover_image_url: "/mockups/blue-waves-laptop-sleeve-2.jpg",
|
||||
category: "Accessories",
|
||||
in_stock: true,
|
||||
on_sale: false
|
||||
}
|
||||
@@ -246,16 +299,16 @@ defmodule SimpleshopTheme.Theme.PreviewData do
|
||||
[
|
||||
%{
|
||||
product: Enum.at(products, 0),
|
||||
quantity: 2,
|
||||
variant: "Medium / Navy"
|
||||
},
|
||||
%{
|
||||
product: Enum.at(products, 2),
|
||||
quantity: 1,
|
||||
variant: "One Size"
|
||||
variant: "12″ x 18″ / Matte"
|
||||
},
|
||||
%{
|
||||
product: Enum.at(products, 6),
|
||||
product: Enum.at(products, 9),
|
||||
quantity: 2,
|
||||
variant: "11oz / White"
|
||||
},
|
||||
%{
|
||||
product: Enum.at(products, 5),
|
||||
quantity: 1,
|
||||
variant: "Large / Black"
|
||||
}
|
||||
@@ -267,44 +320,44 @@ defmodule SimpleshopTheme.Theme.PreviewData do
|
||||
%{
|
||||
id: "1",
|
||||
author: "Sarah M.",
|
||||
content: "Absolutely love the quality! The attention to detail is incredible.",
|
||||
content: "The print quality is absolutely stunning - colours are exactly as shown online. My living room looks so much better now!",
|
||||
rating: 5,
|
||||
date: "2024-01-15"
|
||||
date: "2025-01-15"
|
||||
},
|
||||
%{
|
||||
id: "2",
|
||||
author: "James L.",
|
||||
content: "Fast shipping and beautiful packaging. Will definitely 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: "2024-01-10"
|
||||
date: "2025-01-10"
|
||||
},
|
||||
%{
|
||||
id: "3",
|
||||
author: "Emily R.",
|
||||
content: "These products have become my everyday favorites. Highly recommend!",
|
||||
content: "My new favourite mug! I love sipping my morning tea while looking at that beautiful fern design.",
|
||||
rating: 5,
|
||||
date: "2024-01-05"
|
||||
date: "2025-01-05"
|
||||
},
|
||||
%{
|
||||
id: "4",
|
||||
author: "Michael T.",
|
||||
content: "Great customer service and even better products. Worth every penny.",
|
||||
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: "2023-12-28"
|
||||
date: "2024-12-28"
|
||||
},
|
||||
%{
|
||||
id: "5",
|
||||
author: "Lisa K.",
|
||||
content: "The craftsmanship is outstanding. You can tell these are made with care.",
|
||||
content: "The night sky blanket is gorgeous and so cosy. Perfect for film nights on the sofa. Highly recommend!",
|
||||
rating: 5,
|
||||
date: "2023-12-20"
|
||||
date: "2024-12-20"
|
||||
},
|
||||
%{
|
||||
id: "6",
|
||||
author: "David P.",
|
||||
content: "Perfect gift idea! My friends loved what I got them from here.",
|
||||
content: "Ordered several prints for my new flat. They arrived well packaged and look amazing on the wall.",
|
||||
rating: 5,
|
||||
date: "2023-12-15"
|
||||
date: "2024-12-15"
|
||||
}
|
||||
]
|
||||
end
|
||||
@@ -313,38 +366,38 @@ defmodule SimpleshopTheme.Theme.PreviewData do
|
||||
[
|
||||
%{
|
||||
id: "1",
|
||||
name: "Clothing",
|
||||
slug: "clothing",
|
||||
product_count: 3,
|
||||
image_url: "https://images.unsplash.com/photo-1489987707025-afc232f7ea0f?w=400&h=300&fit=crop&q=80"
|
||||
name: "Art Prints",
|
||||
slug: "art-prints",
|
||||
product_count: 5,
|
||||
image_url: "/mockups/mountain-sunrise-print-2.jpg"
|
||||
},
|
||||
%{
|
||||
id: "2",
|
||||
name: "Accessories",
|
||||
slug: "accessories",
|
||||
name: "Apparel",
|
||||
slug: "apparel",
|
||||
product_count: 4,
|
||||
image_url: "https://images.unsplash.com/photo-1606760227091-3dd870d97f1d?w=400&h=300&fit=crop&q=80"
|
||||
image_url: "/mockups/forest-silhouette-tshirt-1.jpg"
|
||||
},
|
||||
%{
|
||||
id: "3",
|
||||
name: "Home",
|
||||
slug: "home",
|
||||
name: "Homewares",
|
||||
slug: "homewares",
|
||||
product_count: 3,
|
||||
image_url: "https://images.unsplash.com/photo-1616046229478-9901c5536a45?w=400&h=300&fit=crop&q=80"
|
||||
image_url: "/mockups/fern-leaf-mug-1.jpg"
|
||||
},
|
||||
%{
|
||||
id: "4",
|
||||
name: "Kitchen",
|
||||
slug: "kitchen",
|
||||
product_count: 1,
|
||||
image_url: "https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?w=400&h=300&fit=crop&q=80"
|
||||
name: "Stationery",
|
||||
slug: "stationery",
|
||||
product_count: 2,
|
||||
image_url: "/mockups/autumn-leaves-notebook-1.jpg"
|
||||
},
|
||||
%{
|
||||
id: "5",
|
||||
name: "Beauty",
|
||||
slug: "beauty",
|
||||
product_count: 1,
|
||||
image_url: "https://images.unsplash.com/photo-1596462502278-27bfdc403348?w=400&h=300&fit=crop&q=80"
|
||||
name: "Accessories",
|
||||
slug: "accessories",
|
||||
product_count: 2,
|
||||
image_url: "/mockups/monstera-leaf-phone-case-1.jpg"
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user