172 lines
5.4 KiB
TypeScript
172 lines
5.4 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
type Product = {
|
|
stripeProductId: string;
|
|
stripePriceId: string;
|
|
name: string;
|
|
description?: string | null;
|
|
unitAmount: number;
|
|
currency: string;
|
|
active: boolean;
|
|
};
|
|
|
|
export default function AdminProductsPage() {
|
|
const [products, setProducts] = useState<Product[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [message, setMessage] = useState("");
|
|
|
|
async function loadProducts() {
|
|
setLoading(true);
|
|
setMessage("");
|
|
try {
|
|
const res = await fetch("/api/admin/products/list");
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.error || "Failed to load products");
|
|
setProducts(data.products || []);
|
|
} catch (err: any) {
|
|
setMessage(err.message || "Failed to load products.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
loadProducts();
|
|
}, []);
|
|
|
|
async function refreshFromStripe() {
|
|
setLoading(true);
|
|
setMessage("");
|
|
try {
|
|
const res = await fetch("/api/admin/products/refresh", { method: "POST" });
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.error || "Failed to refresh products");
|
|
await loadProducts();
|
|
} catch (err: any) {
|
|
setMessage(err.message || "Failed to refresh products.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
async function updateProduct(p: Product) {
|
|
setLoading(true);
|
|
setMessage("");
|
|
try {
|
|
const res = await fetch("/api/admin/products/update", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
productId: p.stripeProductId,
|
|
name: p.name,
|
|
description: p.description,
|
|
unitAmount: p.unitAmount,
|
|
active: p.active,
|
|
}),
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.error || "Failed to update product");
|
|
setMessage("Saved.");
|
|
} catch (err: any) {
|
|
setMessage(err.message || "Failed to update product.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<main className="admin-page">
|
|
<section className="section">
|
|
<div className="container admin-page__inner">
|
|
<h1 className="page-title">Admin: Product Cache</h1>
|
|
<p className="connect-muted">
|
|
This dashboard manages cached products shown on the storefront.
|
|
</p>
|
|
<div className="connect-actions">
|
|
<button className="btn" onClick={refreshFromStripe} disabled={loading}>
|
|
Refresh from Stripe
|
|
</button>
|
|
<button className="btn btn--ghost" onClick={loadProducts} disabled={loading}>
|
|
Reload
|
|
</button>
|
|
</div>
|
|
|
|
{message ? <div className="connect-message">{message}</div> : null}
|
|
|
|
<div className="admin-grid">
|
|
{products.map((p) => (
|
|
<div key={p.stripeProductId} className="admin-card">
|
|
<input
|
|
value={p.name}
|
|
onChange={(e) =>
|
|
setProducts((prev) =>
|
|
prev.map((item) =>
|
|
item.stripeProductId === p.stripeProductId
|
|
? { ...item, name: e.target.value }
|
|
: item
|
|
)
|
|
)
|
|
}
|
|
/>
|
|
<textarea
|
|
value={p.description || ""}
|
|
onChange={(e) =>
|
|
setProducts((prev) =>
|
|
prev.map((item) =>
|
|
item.stripeProductId === p.stripeProductId
|
|
? { ...item, description: e.target.value }
|
|
: item
|
|
)
|
|
)
|
|
}
|
|
/>
|
|
<div className="admin-row">
|
|
<input
|
|
type="number"
|
|
value={p.unitAmount}
|
|
onChange={(e) =>
|
|
setProducts((prev) =>
|
|
prev.map((item) =>
|
|
item.stripeProductId === p.stripeProductId
|
|
? { ...item, unitAmount: Number(e.target.value) }
|
|
: item
|
|
)
|
|
)
|
|
}
|
|
/>
|
|
<div className="admin-currency">{p.currency.toUpperCase()}</div>
|
|
</div>
|
|
<label className="admin-toggle">
|
|
<input
|
|
type="checkbox"
|
|
checked={p.active}
|
|
onChange={(e) =>
|
|
setProducts((prev) =>
|
|
prev.map((item) =>
|
|
item.stripeProductId === p.stripeProductId
|
|
? { ...item, active: e.target.checked }
|
|
: item
|
|
)
|
|
)
|
|
}
|
|
/>
|
|
Active on storefront
|
|
</label>
|
|
<button className="btn" onClick={() => updateProduct(p)} disabled={loading}>
|
|
Save
|
|
</button>
|
|
</div>
|
|
))}
|
|
{!products.length && !loading ? (
|
|
<div className="connect-muted">No cached products.</div>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
);
|
|
}
|
|
|