Files
2026-02-10 01:14:19 +00:00

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>
);
}