basic printify integration

This commit is contained in:
James Greenwood
2025-11-30 11:05:49 +00:00
parent dab1ffc91f
commit 39e9744eb7
16 changed files with 1181 additions and 199 deletions

View File

@@ -0,0 +1,194 @@
defmodule SimpleshopWeb.DebugLive do
use SimpleshopWeb, :live_view
require Logger
@impl true
def mount(_params, _session, socket) do
send(self(), :fetch_shops)
{:ok,
socket
|> assign(:loading, true)
|> assign(:shops, [])
|> assign(:error, nil)
|> assign(:response_details, nil)}
end
@impl true
def handle_info(:fetch_shops, socket) do
config = Application.get_env(:simpleshop, :printify)
token = Keyword.get(config, :access_token)
if token && token != "your_personal_access_token_here" do
url = "https://api.printify.com/v1/shops.json"
# Try without the Content-Type header - some APIs don't like it for GET requests
headers = [
{"Authorization", "Bearer #{token}"},
{"User-Agent", "SimpleshopMVP"}
]
Logger.info("Making request to: #{url}")
Logger.info("Token (first 20 chars): #{String.slice(token, 0, 20)}...")
case Req.get(url, headers: headers) do
{:ok, %{status: 200, body: shops}} ->
Logger.info("Successfully fetched #{length(shops)} shops")
{:noreply,
socket
|> assign(:loading, false)
|> assign(:shops, shops)
|> assign(:error, nil)
|> assign(:response_details, nil)}
{:ok, %{status: status, body: body, headers: resp_headers}} ->
Logger.error("Failed to fetch shops: #{status}")
Logger.error("Response body: #{inspect(body)}")
Logger.error("Response headers: #{inspect(resp_headers)}")
error_msg = case body do
%{"errors" => errors} -> "API Error: #{inspect(errors)}"
%{"message" => message} -> "API Error: #{message}"
_ -> inspect(body)
end
{:noreply,
socket
|> assign(:loading, false)
|> assign(:error, "HTTP #{status}: Authentication failed")
|> assign(:response_details, %{
status: status,
body: body,
error_msg: error_msg
})}
{:error, error} ->
Logger.error("HTTP error fetching shops: #{inspect(error)}")
{:noreply,
socket
|> assign(:loading, false)
|> assign(:error, "Network error: #{inspect(error)}")
|> assign(:response_details, nil)}
end
else
{:noreply,
socket
|> assign(:loading, false)
|> assign(:error, "Access token not configured. Please add it to config/dev.exs")
|> assign(:response_details, nil)}
end
end
@impl true
def render(assigns) do
~H"""
<div class="mx-auto max-w-4xl py-12 px-4">
<h1 class="text-3xl font-bold mb-8">Printify Shop Information</h1>
<%= if @loading do %>
<div class="text-center py-12">
<div class="animate-spin rounded-full h-16 w-16 border-b-2 border-blue-600 mx-auto"></div>
<p class="text-gray-600 mt-4">Fetching your shop information...</p>
</div>
<% else %>
<%= if @error do %>
<div class="bg-red-50 border border-red-200 rounded-lg p-6 mb-6">
<h2 class="text-lg font-semibold text-red-800 mb-2">Error</h2>
<p class="text-red-700 mb-4"><%= @error %></p>
<%= if @response_details do %>
<div class="mt-4 p-4 bg-white rounded border border-red-300">
<p class="text-sm font-semibold text-gray-700 mb-2">Debug Information:</p>
<div class="text-xs text-gray-600 space-y-2">
<p><strong>Status Code:</strong> <%= @response_details.status %></p>
<p><strong>Error Message:</strong> <%= @response_details.error_msg %></p>
<details class="mt-2">
<summary class="cursor-pointer text-gray-700 hover:text-gray-900">Full Response Body</summary>
<pre class="mt-2 bg-gray-100 p-2 rounded overflow-auto text-xs"><%= Jason.encode!(@response_details.body, pretty: true) %></pre>
</details>
</div>
</div>
<div class="mt-4 p-4 bg-yellow-50 rounded border border-yellow-300">
<p class="text-sm font-semibold text-yellow-900 mb-2">Troubleshooting Tips:</p>
<ul class="text-xs text-yellow-800 space-y-1 list-disc list-inside">
<li>Make sure you copied the FULL token (it's very long)</li>
<li>Check that there are no extra spaces before or after the token</li>
<li>The token should start with "eyJ"</li>
<li>Make sure the token hasn't expired</li>
<li>Try creating a new Personal Access Token in Printify</li>
</ul>
</div>
<% end %>
</div>
<% else %>
<%= if @shops == [] do %>
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-6">
<p class="text-yellow-800">No shops found for this account.</p>
</div>
<% else %>
<div class="space-y-6">
<%= for shop <- @shops do %>
<div class="bg-white border border-gray-200 rounded-lg shadow-sm p-6">
<div class="mb-4 pb-4 border-b">
<h2 class="text-2xl font-bold text-gray-900"><%= shop["title"] %></h2>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="bg-green-50 border border-green-200 rounded-lg p-4">
<p class="text-sm font-semibold text-green-900 mb-2">Shop ID (Use this!)</p>
<p class="text-2xl font-mono font-bold text-green-700">
<%= shop["id"] %>
</p>
</div>
<div class="space-y-2">
<div>
<p class="text-sm font-medium text-gray-600">Sales Channel</p>
<p class="text-gray-900"><%= shop["sales_channel"] || "N/A" %></p>
</div>
</div>
</div>
<div class="mt-6 p-4 bg-blue-50 rounded-lg">
<p class="text-sm text-blue-800">
<strong>Next step:</strong> Copy the Shop ID above and paste it into
<code class="bg-blue-100 px-2 py-1 rounded">config/dev.exs</code>
</p>
<pre class="mt-2 text-xs bg-blue-100 p-2 rounded overflow-x-auto">shop_id: "<%= shop["id"] %>",</pre>
</div>
<details class="mt-4">
<summary class="cursor-pointer text-sm text-gray-600 hover:text-gray-800">
View full shop details (JSON)
</summary>
<pre class="mt-2 bg-gray-100 p-4 rounded-lg overflow-auto text-xs"><%= Jason.encode!(shop, pretty: true) %></pre>
</details>
</div>
<% end %>
</div>
<% end %>
<% end %>
<div class="mt-8 flex gap-4">
<.link
navigate={~p"/"}
class="inline-block bg-gray-600 text-white px-6 py-2 rounded-lg hover:bg-gray-700"
>
← Back to Home
</.link>
<.link
navigate={~p"/products"}
class="inline-block bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700"
>
View Products →
</.link>
</div>
<% end %>
</div>
"""
end
end

View File

@@ -0,0 +1,78 @@
defmodule SimpleshopWeb.OAuthCallbackLive do
use SimpleshopWeb, :live_view
require Logger
alias Simpleshop.Printify.OAuth
@impl true
def mount(params, _session, socket) do
cond do
Map.has_key?(params, "code") ->
code = params["code"]
Logger.info("Received OAuth callback with code")
case OAuth.exchange_code(code) do
{:ok, _token} ->
{:ok,
socket
|> put_flash(:info, "Successfully connected to Printify!")
|> push_navigate(to: ~p"/products")}
{:error, reason} ->
Logger.error("OAuth token exchange failed: #{inspect(reason)}")
{:ok,
socket
|> put_flash(:error, "Failed to connect to Printify. Please try again.")
|> assign(:error, inspect(reason))}
end
Map.has_key?(params, "error") ->
{:ok,
socket
|> put_flash(:error, "Authorization declined. Please try again.")
|> assign(:error, "User declined authorization")}
true ->
{:ok,
socket
|> put_flash(:error, "Invalid callback. Missing authorization code.")
|> assign(:error, "No code parameter")}
end
end
@impl true
def render(assigns) do
~H"""
<div class="mx-auto max-w-2xl py-12 px-4">
<div class="text-center">
<h1 class="text-2xl font-bold mb-4">Processing Printify Authorization...</h1>
<%= if assigns[:error] do %>
<div class="bg-red-50 border border-red-200 rounded-lg p-6 mb-4">
<p class="text-red-800 font-semibold mb-2">Authentication Failed</p>
<p class="text-red-600 text-sm mb-4">
We couldn't complete the connection to Printify.
</p>
<.link
navigate={~p"/"}
class="inline-block bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700"
>
Return Home
</.link>
</div>
<details class="text-left bg-gray-50 border border-gray-200 rounded-lg p-4 mt-4">
<summary class="cursor-pointer font-medium text-gray-700">
Technical Details (for debugging)
</summary>
<pre class="mt-2 text-xs text-gray-600 overflow-auto"><%= @error %></pre>
</details>
<% else %>
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<p class="text-gray-600 mt-4">Redirecting to products...</p>
<% end %>
</div>
</div>
"""
end
end

View File

@@ -0,0 +1,158 @@
defmodule SimpleshopWeb.OrderConfirmationLive do
use SimpleshopWeb, :live_view
require Logger
@impl true
def mount(%{"id" => order_id} = params, _session, socket) do
order_data =
case params["data"] do
nil ->
%{"id" => order_id}
encoded_data ->
case Base.url_decode64(encoded_data) do
{:ok, json} ->
case Jason.decode(json) do
{:ok, data} -> data
{:error, _} -> %{"id" => order_id}
end
{:error, _} ->
%{"id" => order_id}
end
end
{:ok,
socket
|> assign(:order_id, order_id)
|> assign(:order_data, order_data)}
end
@impl true
def render(assigns) do
~H"""
<div class="mx-auto max-w-4xl py-12 px-4">
<div class="text-center mb-8">
<div class="inline-block bg-green-100 dark:bg-green-900 rounded-full p-3 mb-4">
<svg
class="w-12 h-12 text-green-600 dark:text-green-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
/>
</svg>
</div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100 mb-2">Order Created Successfully!</h1>
<p class="text-gray-600 dark:text-gray-300">Your test order has been submitted to Printify</p>
</div>
<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm p-6 mb-6">
<h2 class="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-4">Order Summary</h2>
<div class="space-y-3">
<div class="flex justify-between border-b border-gray-200 dark:border-gray-700 pb-2">
<span class="font-medium text-gray-700 dark:text-gray-300">Order ID:</span>
<span class="text-gray-900 dark:text-gray-100"><%= @order_id %></span>
</div>
<%= if @order_data["status"] do %>
<div class="flex justify-between border-b border-gray-200 dark:border-gray-700 pb-2">
<span class="font-medium text-gray-700 dark:text-gray-300">Status:</span>
<span class="px-3 py-1 bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 rounded-full text-sm font-medium">
<%= @order_data["status"] %>
</span>
</div>
<% end %>
<%= if @order_data["created_at"] do %>
<div class="flex justify-between border-b border-gray-200 dark:border-gray-700 pb-2">
<span class="font-medium text-gray-700 dark:text-gray-300">Created:</span>
<span class="text-gray-900 dark:text-gray-100"><%= @order_data["created_at"] %></span>
</div>
<% end %>
<%= if line_items = @order_data["line_items"] do %>
<div class="border-b border-gray-200 dark:border-gray-700 pb-2">
<span class="font-medium text-gray-700 dark:text-gray-300 block mb-2">Line Items:</span>
<%= for item <- line_items do %>
<div class="ml-4 text-sm text-gray-600 dark:text-gray-400">
<span>Product: <%= item["product_id"] %></span>
<span class="ml-4">Variant: <%= item["variant_id"] %></span>
<span class="ml-4">Qty: <%= item["quantity"] %></span>
</div>
<% end %>
</div>
<% end %>
<%= if address = @order_data["address_to"] do %>
<div class="border-b border-gray-200 dark:border-gray-700 pb-2">
<span class="font-medium text-gray-700 dark:text-gray-300 block mb-2">Shipping Address:</span>
<div class="ml-4 text-sm text-gray-600 dark:text-gray-400">
<p><%= address["first_name"] %> <%= address["last_name"] %></p>
<p><%= address["address1"] %></p>
<%= if address["address2"] do %>
<p><%= address["address2"] %></p>
<% end %>
<p>
<%= address["city"] %>, <%= address["region"] %> <%= address["zip"] %>
</p>
<p><%= address["country"] %></p>
<%= if address["email"] do %>
<p class="mt-1">Email: <%= address["email"] %></p>
<% end %>
<%= if address["phone"] do %>
<p>Phone: <%= address["phone"] %></p>
<% end %>
</div>
</div>
<% end %>
</div>
</div>
<div class="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-6 mb-6">
<details>
<summary class="cursor-pointer font-semibold text-gray-900 dark:text-gray-100 mb-2">
Full API Response (for debugging)
</summary>
<pre class="bg-gray-900 dark:bg-gray-950 text-green-400 dark:text-green-300 p-4 rounded-lg overflow-auto text-xs mt-4"><%= Jason.encode!(@order_data, pretty: true) %></pre>
</details>
</div>
<div class="bg-blue-50 dark:bg-blue-900 border border-blue-200 dark:border-blue-700 rounded-lg p-4 mb-6">
<p class="text-blue-800 dark:text-blue-200">
<span class="font-semibold">Next steps:</span>
Check your Printify dashboard to see this order and manage its fulfillment.
</p>
</div>
<div class="flex gap-4 justify-center">
<.link
navigate={~p"/products"}
class="bg-blue-600 text-white px-6 py-3 rounded-lg font-semibold hover:bg-blue-700 transition-colors"
>
Order Another Product
</.link>
<a
href="https://printify.com/app/orders"
target="_blank"
class="bg-gray-600 text-white px-6 py-3 rounded-lg font-semibold hover:bg-gray-700 transition-colors"
>
View in Printify Dashboard →
</a>
</div>
<p class="text-center text-sm text-gray-500 dark:text-gray-400 mt-6">
This was a test order with a hardcoded shipping address.
</p>
</div>
"""
end
end

View File

@@ -0,0 +1,423 @@
defmodule SimpleshopWeb.ProductLive do
use SimpleshopWeb, :live_view
require Logger
alias Simpleshop.Printify.{Client, TokenStore}
@impl true
def mount(_params, _session, socket) do
if TokenStore.has_tokens?() do
send(self(), :load_products)
{:ok,
socket
|> assign(:loading, true)
|> assign(:products, [])
|> assign(:selected_product, nil)
|> assign(:selected_variants, %{})
|> assign(:error, nil)}
else
{:ok,
socket
|> assign(:loading, false)
|> assign(:products, [])
|> assign(:selected_product, nil)
|> assign(:selected_variants, %{})
|> assign(:error, :not_configured)}
end
end
@impl true
def handle_info(:load_products, socket) do
case Client.get_products() do
{:ok, products} ->
{:noreply,
socket
|> assign(:loading, false)
|> assign(:products, products)
|> assign(:error, nil)}
{:error, :not_configured} ->
{:noreply,
socket
|> assign(:loading, false)
|> assign(:error, :not_configured)}
{:error, :authentication_required} ->
{:noreply,
socket
|> assign(:loading, false)
|> assign(:error, :not_configured)}
{:error, reason} ->
Logger.error("Failed to load products: #{inspect(reason)}")
{:noreply,
socket
|> assign(:loading, false)
|> assign(:error, "Failed to load products. Please check your configuration and try again.")}
end
end
@impl true
def handle_event("select_product", %{"id" => product_id}, socket) do
case Client.get_product(product_id) do
{:ok, product} ->
variant_options = parse_variant_options(product)
initial_selections = initialize_variant_selections(variant_options)
{:noreply,
socket
|> assign(:selected_product, product)
|> assign(:variant_options, variant_options)
|> assign(:selected_variants, initial_selections)
|> assign(:error, nil)}
{:error, reason} ->
Logger.error("Failed to load product: #{inspect(reason)}")
{:noreply,
socket
|> assign(:error, "Failed to load product details. Please try again.")}
end
end
@impl true
def handle_event("back_to_list", _params, socket) do
{:noreply,
socket
|> assign(:selected_product, nil)
|> assign(:selected_variants, %{})
|> assign(:variant_options, nil)}
end
@impl true
def handle_event("update_variant", %{"option" => option_name, "value" => value}, socket) do
selected_variants = Map.put(socket.assigns.selected_variants, option_name, value)
{:noreply, assign(socket, :selected_variants, selected_variants)}
end
@impl true
def handle_event("checkout", _params, socket) do
product = socket.assigns.selected_product
selected_variant = find_matching_variant(product, socket.assigns.selected_variants)
case selected_variant do
nil ->
{:noreply,
socket
|> put_flash(:error, "Please select all variant options")
|> assign(:error, "Please select all variant options")}
variant ->
product_id = product["id"]
variant_id = variant["id"]
case Client.create_order(product_id, variant_id, 1) do
{:ok, order_response} ->
order_id = order_response["id"] || "unknown"
{:noreply,
socket
|> push_navigate(to: ~p"/orders/#{order_id}/confirmation?data=#{encode_order_data(order_response)}")}
{:error, reason} ->
Logger.error("Failed to create order: #{inspect(reason)}")
{:noreply,
socket
|> put_flash(:error, "Failed to create order. Please try again.")
|> assign(:error, "Failed to create order: #{inspect(reason)}")}
end
end
end
defp parse_variant_options(product) do
option_value_lookup = build_option_value_lookup(product)
variants = product["variants"] || []
Enum.reduce(variants, %{}, fn variant, acc ->
option_ids = variant["options"] || []
Enum.reduce(option_ids, acc, fn option_id, acc2 ->
case Map.get(option_value_lookup, option_id) do
nil ->
acc2
{option_name, option_value} ->
Map.update(acc2, option_name, [option_value], fn existing ->
if option_value in existing, do: existing, else: [option_value | existing]
end)
end
end)
end)
|> Enum.map(fn {name, values} -> {name, Enum.reverse(values)} end)
|> Enum.into(%{})
end
defp build_option_value_lookup(product) do
product_options = product["options"] || []
Enum.reduce(product_options, %{}, fn option, acc ->
option_name = option["name"]
values = option["values"] || []
Enum.reduce(values, acc, fn value, acc2 ->
value_id = value["id"]
value_title = value["title"]
Map.put(acc2, value_id, {option_name, value_title})
end)
end)
end
defp initialize_variant_selections(variant_options) do
Enum.map(variant_options, fn {name, values} -> {name, List.first(values)} end)
|> Enum.into(%{})
end
defp find_matching_variant(product, selected_variants) do
option_value_lookup = build_option_value_lookup(product)
variants = product["variants"] || []
Enum.find(variants, fn variant ->
option_ids = variant["options"] || []
Enum.all?(option_ids, fn option_id ->
case Map.get(option_value_lookup, option_id) do
nil ->
false
{option_name, option_value} ->
selected_variants[option_name] == option_value
end
end)
end)
end
defp encode_order_data(order_response) do
Jason.encode!(order_response) |> Base.url_encode64()
end
defp format_price(price) when is_integer(price) do
dollars = div(price, 100)
cents = rem(price, 100)
"$#{dollars}.#{String.pad_leading(Integer.to_string(cents), 2, "0")}"
end
defp format_price(_), do: "N/A"
@impl true
def render(assigns) do
~H"""
<div class="mx-auto max-w-6xl py-12 px-4">
<%= if @loading do %>
<div class="text-center py-12">
<div class="animate-spin rounded-full h-16 w-16 border-b-2 border-blue-600 mx-auto"></div>
<p class="text-gray-600 mt-4">Loading products...</p>
</div>
<% else %>
<%= if @error == :not_configured do %>
<div class="max-w-2xl mx-auto">
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-8 mb-6">
<h2 class="text-2xl font-bold text-yellow-900 mb-4">Configuration Required</h2>
<p class="text-yellow-800 mb-4">
You need to add your Printify Personal Access Token to the configuration file.
</p>
<div class="bg-white rounded p-4 mb-4">
<p class="text-sm font-semibold text-gray-700 mb-2">Steps:</p>
<ol class="list-decimal list-inside space-y-2 text-sm text-gray-600">
<li>
Visit
<a
href="https://printify.com/app/account/api"
target="_blank"
class="text-blue-600 underline"
>
printify.com/app/account/api
</a>
</li>
<li>Create a new Personal Access Token</li>
<li>
Open <code class="bg-gray-100 px-2 py-1 rounded">config/dev.exs</code>
</li>
<li>Replace <code class="bg-gray-100 px-2 py-1 rounded">your_personal_access_token_here</code> with your actual token</li>
<li>Also add your <code class="bg-gray-100 px-2 py-1 rounded">shop_id</code></li>
<li>Restart the Phoenix server</li>
</ol>
</div>
<.link
navigate={~p"/"}
class="inline-block bg-yellow-600 text-white px-6 py-2 rounded-lg hover:bg-yellow-700"
>
← Back to Home
</.link>
</div>
</div>
<% else %>
<%= if @error do %>
<div class="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
<p class="text-red-800"><%= @error %></p>
</div>
<% end %>
<%= if @selected_product do %>
<.product_detail
product={@selected_product}
variant_options={@variant_options}
selected_variants={@selected_variants}
/>
<% else %>
<.product_list products={@products} />
<% end %>
<% end %>
<% end %>
</div>
"""
end
defp product_list(assigns) do
~H"""
<div>
<h1 class="text-3xl font-bold mb-8">Your Printify Products</h1>
<%= if @products == [] do %>
<div class="text-center py-12 bg-gray-50 rounded-lg">
<p class="text-gray-600 text-lg">No products found in your Printify shop.</p>
<p class="text-gray-500 mt-2">Add products in your Printify dashboard first.</p>
</div>
<% else %>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<%= for product <- @products do %>
<div class="border rounded-lg overflow-hidden hover:shadow-lg transition-shadow cursor-pointer">
<div phx-click="select_product" phx-value-id={product["id"]}>
<%= if product["images"] && length(product["images"]) > 0 do %>
<img
src={hd(product["images"])["src"]}
alt={product["title"]}
class="w-full h-64 object-cover"
/>
<% else %>
<div class="w-full h-64 bg-gray-200 flex items-center justify-center">
<span class="text-gray-400">No image</span>
</div>
<% end %>
<div class="p-4">
<h3 class="font-semibold text-lg mb-2"><%= product["title"] %></h3>
<p class="text-gray-600 dark:text-gray-300 text-sm line-clamp-2">
<%= product["description"] || "No description" %>
</p>
<button class="mt-4 w-full bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
View Details
</button>
</div>
</div>
</div>
<% end %>
</div>
<% end %>
</div>
"""
end
defp product_detail(assigns) do
~H"""
<div>
<button
phx-click="back_to_list"
class="mb-6 text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 flex items-center underline"
>
← Back to Products
</button>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div>
<%= if @product["images"] && length(@product["images"]) > 0 do %>
<img
src={hd(@product["images"])["src"]}
alt={@product["title"]}
class="w-full rounded-lg shadow-lg"
/>
<% else %>
<div class="w-full h-96 bg-gray-200 rounded-lg flex items-center justify-center">
<span class="text-gray-400">No image available</span>
</div>
<% end %>
</div>
<div>
<h1 class="text-3xl font-bold mb-4"><%= @product["title"] %></h1>
<p class="text-gray-900 dark:text-gray-100 mb-6"><%= @product["description"] || "No description available" %></p>
<%= if @variant_options && map_size(@variant_options) > 0 do %>
<div class="space-y-4 mb-6">
<%= for {option_name, option_values} <- @variant_options do %>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-2">
<%= option_name %>
</label>
<select
phx-change="update_variant"
name="variant_option"
phx-value-option={option_name}
class="w-full border border-gray-300 rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<%= for value <- option_values do %>
<option value={value} selected={@selected_variants[option_name] == value}>
<%= value %>
</option>
<% end %>
</select>
</div>
<% end %>
</div>
<%= if variant = find_matching_variant(@product, @selected_variants) do %>
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
<p class="text-2xl font-bold text-blue-900">
<%= format_price(variant["price"]) %>
</p>
<p class="text-sm text-blue-700 mt-1">
Variant ID: <%= variant["id"] %>
</p>
</div>
<% end %>
<% end %>
<button
phx-click="checkout"
class="w-full bg-green-600 text-white px-6 py-3 rounded-lg font-semibold hover:bg-green-700 transition-colors"
>
Checkout (Test Order)
</button>
<p class="text-sm text-gray-500 mt-3 text-center">
This will create a test order with hardcoded shipping address
</p>
</div>
</div>
</div>
"""
end
defp find_matching_variant(product, selected_variants) do
option_value_lookup = build_option_value_lookup(product)
variants = product["variants"] || []
Enum.find(variants, fn variant ->
option_ids = variant["options"] || []
Enum.all?(option_ids, fn option_id ->
case Map.get(option_value_lookup, option_id) do
nil ->
false
{option_name, option_value} ->
selected_variants[option_name] == option_value
end
end)
end)
end
end