170 lines
5.5 KiB
TypeScript
170 lines
5.5 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
type Product = {
|
|
id: string;
|
|
name: string;
|
|
description?: string | null;
|
|
unitAmount?: number | null;
|
|
currency?: string | null;
|
|
stripePriceId?: string | null;
|
|
};
|
|
|
|
export default function StorefrontPage({ params }: { params: { accountId: string } }) {
|
|
const { accountId: slug } = params;
|
|
const [products, setProducts] = useState<Product[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [message, setMessage] = useState("");
|
|
const [accountId, setAccountId] = useState("");
|
|
|
|
useEffect(() => {
|
|
if (slug) {
|
|
window.localStorage.setItem("connectStoreSlug", slug);
|
|
}
|
|
async function load() {
|
|
setLoading(true);
|
|
setMessage("");
|
|
try {
|
|
const lookup = await fetch(`/api/connect/account/lookup?slug=${slug}`);
|
|
const lookupData = await lookup.json();
|
|
if (!lookup.ok) throw new Error(lookupData.error || "Store not found");
|
|
setAccountId(lookupData.accountId);
|
|
|
|
const res = await fetch(`/api/storefront/products?slug=${slug}`);
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.error || "Failed to load products");
|
|
const normalized =
|
|
data.products?.map((p: any) => ({
|
|
id: p.stripeProductId ?? p.id,
|
|
name: p.name,
|
|
description: p.description,
|
|
unitAmount: p.unitAmount ?? p.default_price?.unit_amount ?? null,
|
|
currency: p.currency ?? p.default_price?.currency ?? null,
|
|
stripePriceId: p.stripePriceId ?? p.default_price?.id ?? null,
|
|
})) || [];
|
|
setProducts(normalized);
|
|
} catch (err: any) {
|
|
setMessage(err.message || "Failed to load products.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
load();
|
|
}, [slug]);
|
|
|
|
async function buyNow(product: Product) {
|
|
if (!product.unitAmount || !product.currency || !accountId) {
|
|
setMessage("Product is missing a price.");
|
|
return;
|
|
}
|
|
setLoading(true);
|
|
setMessage("");
|
|
try {
|
|
const res = await fetch("/api/connect/checkout", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
accountId,
|
|
name: product.name,
|
|
description: product.description,
|
|
unitAmount: product.unitAmount,
|
|
currency: product.currency,
|
|
quantity: 1,
|
|
}),
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.error || "Failed to start checkout");
|
|
window.location.href = data.url;
|
|
} catch (err: any) {
|
|
setMessage(err.message || "Checkout failed.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
async function addToCart(product: Product) {
|
|
if (!product.unitAmount || !product.currency || !product.stripePriceId) {
|
|
setMessage("Product is missing a price.");
|
|
return;
|
|
}
|
|
setLoading(true);
|
|
setMessage("");
|
|
try {
|
|
const res = await fetch("/api/cart/add", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
slug,
|
|
productId: product.id,
|
|
priceId: product.stripePriceId,
|
|
name: product.name,
|
|
unitAmount: product.unitAmount,
|
|
currency: product.currency,
|
|
quantity: 1,
|
|
}),
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.error || "Failed to add to cart");
|
|
setMessage("Added to cart.");
|
|
} catch (err: any) {
|
|
setMessage(err.message || "Failed to add to cart.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<main className="storefront-page">
|
|
<section className="section">
|
|
<div className="container storefront-page__inner">
|
|
<h1 className="page-title">Storefront</h1>
|
|
<p className="storefront-muted">
|
|
Store: {slug} (mapped to a connected account)
|
|
</p>
|
|
|
|
{message ? <div className="connect-message">{message}</div> : null}
|
|
|
|
<div className="storefront-grid">
|
|
{products.map((product) => (
|
|
<div key={product.id} className="storefront-card">
|
|
<h3>{product.name}</h3>
|
|
<p>{product.description || "No description provided."}</p>
|
|
<div className="storefront-price">
|
|
{product.unitAmount
|
|
? `${(product.unitAmount / 100).toFixed(2)} ${product.currency?.toUpperCase() || "USD"}`
|
|
: "No price"}
|
|
</div>
|
|
<div className="storefront-actions">
|
|
<button className="btn" onClick={() => buyNow(product)} disabled={loading}>
|
|
Buy now (Checkout)
|
|
</button>
|
|
<a
|
|
className="btn btn--ghost"
|
|
href={`/storefront/${slug}/pay?productId=${product.id}`}
|
|
>
|
|
Pay with card
|
|
</a>
|
|
<button
|
|
className="btn btn--ghost"
|
|
onClick={() => addToCart(product)}
|
|
disabled={loading}
|
|
>
|
|
Add to cart
|
|
</button>
|
|
<a className="btn btn--ghost" href="/cart">
|
|
View cart
|
|
</a>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{!products.length && !loading ? (
|
|
<div className="storefront-muted">No products yet.</div>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
);
|
|
}
|