basic printify integration
This commit is contained in:
194
lib/simpleshop_web/live/debug_live.ex
Normal file
194
lib/simpleshop_web/live/debug_live.ex
Normal 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
|
||||
78
lib/simpleshop_web/live/oauth_callback_live.ex
Normal file
78
lib/simpleshop_web/live/oauth_callback_live.ex
Normal 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
|
||||
158
lib/simpleshop_web/live/order_confirmation_live.ex
Normal file
158
lib/simpleshop_web/live/order_confirmation_live.ex
Normal 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
|
||||
423
lib/simpleshop_web/live/product_live.ex
Normal file
423
lib/simpleshop_web/live/product_live.ex
Normal 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
|
||||
Reference in New Issue
Block a user