Files
Shifted/app/admin/products/page.tsx
2026-02-10 01:14:19 +00:00

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