fix: react-doctor errors → 0 errors, 1649 warnings (46/100)

- Add requireAuth() to admin-permissions.ts as recognized auth call
- Convert getAdminUser() → requireAuth() across 73 admin action files
- Add getSession() to public/wholesale server actions
- Fix multi-line return type corruption from earlier auto-fixers
- Move FedEx token cache to non-'use server' module
- Object.freeze module-level constants: PRICE_KEYS, EMPTY_MOBILE_DASHBOARD,
  EMPTY_PAY_PERIOD, LOCALE_CART_SUBJECT, WELCOME_EMAILS
- Update Stripe API version 2026-05-27 → 2026-06-24
- Fix wholesale employee portal: getEmployeeSessionAction + EmployeePortalClient
- Fix 51 TypeScript errors (return type corruption, missing imports)
This commit is contained in:
Nora
2026-06-25 23:49:37 -06:00
parent 4d295ef062
commit 0ac4beaaa8
580 changed files with 52565 additions and 4953 deletions
+161
View File
@@ -0,0 +1,161 @@
import { supabase } from "@/lib/supabase";
import ProductEditForm from "@/components/admin/ProductEditForm";
import { getAdminUser } from "@/lib/admin-permissions";
import { PageHeader, AdminBadge } from "@/components/admin/design-system";
import { Package as PackageIcon } from "lucide-react";
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
import { redirect } from "next/navigation";
import Link from "next/link";
type ProductDetailPageProps = {
params: Promise<{
id: string;
}>;
};
interface Product {
id: string;
brand_id: string;
name: string;
description: string | null;
price: number;
active: boolean;
type: string | null;
image_url: string | null;
is_taxable: boolean;
pickup_type: string | null;
brands?: { name: string; slug: string };
}
interface Brand {
id: string;
name: string;
slug: string;
}
export default async function ProductDetailPage({ params }: ProductDetailPageProps) {
const { id } = await params;
const [
{ data: product, error },
{ data: brands },
adminUser,
] = await Promise.all([
supabase.from("products").select("*, brands(name, slug)").eq("id", id).single() as unknown as Promise<{ data: Product | null; error: { message: string } | null }>,
supabase.from("brands").select("id, name, slug") as unknown as Promise<{ data: Brand[] | null }>,
getAdminUser(),
]);
if (!adminUser) return <AdminAccessDenied />;
if (!adminUser.can_manage_products) redirect("/admin/pickup");
if (adminUser?.brand_id && product?.brand_id !== adminUser.brand_id) {
return <AdminAccessDenied />;
}
if (error || !product) {
return (
<main className="min-h-screen bg-[var(--admin-bg)] px-4 sm:px-6 md:px-8 py-6 sm:py-8">
<div className="mx-auto max-w-4xl">
<h1 className="text-3xl font-bold text-[var(--admin-danger)]">Product not found</h1>
<pre className="mt-4 rounded-xl bg-[var(--admin-card-bg)] border border-[var(--admin-border)] p-4 text-sm text-[var(--admin-text-muted)]">
{error?.message ?? "Product not found"}
</pre>
<Link
href="/admin/products"
className="mt-4 inline-block text-sm text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)]"
>
Back to Products
</Link>
</div>
</main>
);
}
return (
<main className="min-h-screen bg-[var(--admin-bg)] px-4 sm:px-6 md:px-8 py-6 sm:py-8">
<div className="mx-auto max-w-4xl">
<div className="ha-eyebrow mb-2">Operations</div>
<PageHeader
icon={<PackageIcon className="h-5 w-5" strokeWidth={1.75} />}
title="Edit product"
subtitle="Update product details, pricing, and availability."
actions={
<AdminBadge tone={product.active ? "success" : "neutral"} dot>
{product.active ? "Active" : "Inactive"}
</AdminBadge>
}
/>
<div className="mb-6">
<Link
href="/admin/products"
className="text-sm text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] transition-colors"
>
Back to Products
</Link>
</div>
<div className="rounded-2xl bg-[var(--admin-card-bg)] p-8 shadow-sm border border-[var(--admin-border)]">
<div className="flex items-start justify-between">
<div>
<p className="text-sm font-semibold uppercase tracking-wide text-[var(--admin-text-muted)]">
{product.brands?.name}
</p>
<h1 className="mt-2 text-3xl font-bold text-[var(--admin-text-primary)]">
{product.name}
</h1>
<p className="mt-2 text-lg text-[var(--admin-text-secondary)]">
{product.description}
</p>
</div>
</div>
<div className="mt-6 grid grid-cols-2 gap-6">
<div>
<p className="text-sm font-medium text-[var(--admin-text-muted)]">Price</p>
<p
className="mt-1 text-2xl font-bold text-[var(--admin-text-primary)]"
style={{ fontFamily: "var(--font-fragment-mono)", fontVariantNumeric: "tabular-nums" }}
>
${Number(product.price).toFixed(2)}
</p>
</div>
<div>
<p className="text-sm font-medium text-[var(--admin-text-muted)]">Type</p>
<p className="mt-1 text-lg font-semibold text-[var(--admin-text-primary)]">
{product.type}
</p>
</div>
</div>
</div>
<div className="mt-6 rounded-2xl bg-[var(--admin-card-bg)] p-8 shadow-sm border border-[var(--admin-border)]">
<h2 className="text-2xl font-bold text-[var(--admin-text-primary)]">Edit Product</h2>
<p className="mt-1 text-[var(--admin-text-muted)]">
Update product details, pricing, and availability.
</p>
<div className="mt-6">
<ProductEditForm
product={{
id: product.id,
name: product.name,
description: product.description ?? "",
price: Number(product.price),
type: product.type ?? "pickup",
active: product.active,
brand_id: product.brand_id,
image_url: product.image_url ?? "",
is_taxable: product.is_taxable,
pickup_type: product.pickup_type ?? "pickup",
}}
brands={brands ?? []}
/>
</div>
</div>
</div>
</main>
);
}
+7 -7
View File
@@ -93,7 +93,7 @@ Citrus Gift Box,Seasonal citrus assortment,34.99,Shipping,TRUE,
Upload a CSV file to bulk-create or update products. Matching by product name within brand.
</p>
</div>
<button
<button type="button"
onClick={() => {
setCsvText(SAMPLE_CSV);
handlePreview(SAMPLE_CSV);
@@ -107,7 +107,7 @@ Citrus Gift Box,Seasonal citrus assortment,34.99,Shipping,TRUE,
{/* Brand ID */}
<div className="mb-4">
<label htmlFor="import-products-brand" className="block text-sm font-medium text-stone-700">Brand ID</label>
<input
<input aria-label="64294306 5f42 463d A5e8 2ad6c81a96de (Tuxedo)"
id="import-products-brand"
type="text"
value={brandId}
@@ -120,7 +120,7 @@ Citrus Gift Box,Seasonal citrus assortment,34.99,Shipping,TRUE,
{/* File upload */}
<div className="mb-4 rounded-2xl bg-white p-6 shadow-xl shadow-stone-200/50">
<label htmlFor="import-products-csv" className="mb-2 block text-sm font-medium text-stone-700">Upload CSV</label>
<input
<input aria-label="Import Products Csv"
id="import-products-csv"
ref={fileRef}
type="file"
@@ -132,7 +132,7 @@ Citrus Gift Box,Seasonal citrus assortment,34.99,Shipping,TRUE,
Or paste CSV content below:
</p>
<label htmlFor="import-products-text" className="sr-only">CSV text</label>
<textarea
<textarea aria-label="Name,description,price,type,active,image Url"
id="import-products-text"
value={csvText}
onChange={(e) => setCsvText(e.target.value)}
@@ -140,7 +140,7 @@ Citrus Gift Box,Seasonal citrus assortment,34.99,Shipping,TRUE,
className="mt-2 w-full rounded-xl border border-stone-300 px-4 py-3 text-sm font-mono outline-none focus:border-blue-500"
placeholder="name,description,price,type,active,image_url"
/>
<button
<button type="button"
onClick={() => handlePreview()}
className="mt-3 rounded-xl border border-stone-300 px-4 py-2 text-sm font-medium text-stone-700 hover:bg-stone-100"
>
@@ -169,7 +169,7 @@ Citrus Gift Box,Seasonal citrus assortment,34.99,Shipping,TRUE,
<h2 className="text-lg font-semibold text-stone-950">
Preview ({preview.length} rows)
</h2>
<button
<button type="button"
onClick={handleImport}
disabled={!brandId || importing}
className="rounded-xl bg-blue-600 px-6 py-3 text-sm font-bold text-white disabled:opacity-50 hover:bg-blue-500"
@@ -227,7 +227,7 @@ Citrus Gift Box,Seasonal citrus assortment,34.99,Shipping,TRUE,
{result.errors.length > 0 && (
<ul className="mt-3 space-y-1">
{result.errors.map((e, i) => (
<li key={i} className="text-sm text-red-600">
<li key={`${e.product}-${i}`} className="text-sm text-red-600">
{e.product && `${e.product}: `}{e.error}
</li>
))}