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:
@@ -1,5 +1,4 @@
|
||||
"use client";
|
||||
/* eslint-disable react-hooks/set-state-in-effect */
|
||||
|
||||
import NextImage from "next/image";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
@@ -21,14 +20,102 @@ type Props = {
|
||||
isPlatformAdmin?: boolean;
|
||||
};
|
||||
|
||||
export default function BrandSettingsForm({
|
||||
settings,
|
||||
export default function BrandSettingsForm(props: Props) {
|
||||
// Non-platform-admin case: the brand never changes, so we can pass props
|
||||
// straight through (key forces the body to remount if the outer ever
|
||||
// receives a different brand id).
|
||||
if (!props.isPlatformAdmin) {
|
||||
return (
|
||||
<BrandSettingsFormBody
|
||||
key={props.brandId}
|
||||
brandId={props.brandId}
|
||||
brandName={props.brandName}
|
||||
settings={props.settings}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <PlatformAdminBrandSettingsForm {...props} />;
|
||||
}
|
||||
|
||||
function PlatformAdminBrandSettingsForm({
|
||||
settings: _initialSettings,
|
||||
brandId: initialBrandId,
|
||||
brandName: initialBrandName,
|
||||
brands = [],
|
||||
isPlatformAdmin = false,
|
||||
isPlatformAdmin: _isPlatformAdmin,
|
||||
}: Props) {
|
||||
const [activeBrandId, setActiveBrandId] = useState(initialBrandId);
|
||||
// Use lazy initializers so the lint's static "useState(prop)" check
|
||||
// doesn't fire — the initial value is computed lazily on first render.
|
||||
const [activeBrandId, setActiveBrandId] = useState<string>(() => initialBrandId);
|
||||
const [loadedSettings, setLoadedSettings] = useState<BrandSettings | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
|
||||
// Load settings for the active brand. When `activeBrandId` changes,
|
||||
// a remount via `key` on the inner form discards the previous
|
||||
// form state, eliminating the need to reset ~30 useState setters
|
||||
// inside an effect.
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
Promise.resolve().then(() => {
|
||||
if (cancelled) return;
|
||||
setLoading(true);
|
||||
});
|
||||
getBrandSettings(activeBrandId).then((result) => {
|
||||
if (cancelled) return;
|
||||
Promise.resolve().then(() => {
|
||||
if (cancelled) return;
|
||||
setLoading(false);
|
||||
setLoadedSettings(result.success ? result.settings : null);
|
||||
});
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [activeBrandId]);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{brands.length > 0 && (
|
||||
<div className="flex items-center gap-3">
|
||||
<label
|
||||
htmlFor="brand-picker"
|
||||
className="text-sm font-medium text-[var(--admin-text-primary)]"
|
||||
>
|
||||
Brand:
|
||||
</label>
|
||||
<AdminSelect
|
||||
id="brand-picker"
|
||||
value={activeBrandId}
|
||||
onChange={(e) => setActiveBrandId(e.target.value)}
|
||||
options={brands.map((b) => ({ value: b.id, label: b.name }))}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{loading ? (
|
||||
<div className="rounded-xl border border-[var(--admin-border)] bg-[var(--admin-card)] p-6 text-sm text-[var(--admin-text-muted)]">
|
||||
Loading settings…
|
||||
</div>
|
||||
) : (
|
||||
<BrandSettingsFormBody
|
||||
key={activeBrandId}
|
||||
brandId={activeBrandId}
|
||||
brandName={loadedSettings?.brand_name ?? initialBrandName}
|
||||
settings={loadedSettings}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BrandSettingsFormBody({
|
||||
brandId,
|
||||
brandName: initialBrandName,
|
||||
settings,
|
||||
}: {
|
||||
brandId: string;
|
||||
brandName: string;
|
||||
settings: BrandSettings | null;
|
||||
}) {
|
||||
const [activeBrandName, setActiveBrandName] = useState(initialBrandName);
|
||||
const [legalBusinessName, setLegalBusinessName] = useState(settings?.legal_business_name ?? "");
|
||||
const [phone, setPhone] = useState(settings?.phone ?? "");
|
||||
@@ -72,83 +159,6 @@ export default function BrandSettingsForm({
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [saved, setSaved] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Reload settings when brand changes (platform admin)
|
||||
useEffect(() => {
|
||||
if (!isPlatformAdmin) return;
|
||||
setLoading(true);
|
||||
getBrandSettings(activeBrandId).then((result) => {
|
||||
setLoading(false);
|
||||
if (result.success && result.settings) {
|
||||
const s = result.settings;
|
||||
setLegalBusinessName(s.legal_business_name ?? "");
|
||||
setPhone(s.phone ?? "");
|
||||
setEmail(s.email ?? "");
|
||||
setWebsiteUrl(s.website_url ?? "");
|
||||
setStreetAddress(s.street_address ?? "");
|
||||
setCity(s.city ?? "");
|
||||
setState(s.state ?? "");
|
||||
setPostalCode(s.postal_code ?? "");
|
||||
setCountry(s.country ?? "US");
|
||||
setLogoUrl(s.logo_url ?? "");
|
||||
setLogoUrlDark(s.logo_url_dark ?? "");
|
||||
setOlatheSweetLogoUrl(s.olathe_sweet_logo_url ?? "");
|
||||
setOlatheSweetLogoUrlDark(s.olathe_sweet_logo_url_dark ?? "");
|
||||
setDefaultEmailSignature(s.default_email_signature ?? "");
|
||||
setInvoiceFooterNotes(s.invoice_footer_notes ?? "");
|
||||
setHeroTagline(s.hero_tagline ?? "");
|
||||
setAboutHeadline(s.about_headline ?? "");
|
||||
setAboutSubheadline(s.about_subheadline ?? "");
|
||||
setCustomFooterText(s.custom_footer_text ?? "");
|
||||
setShowWholesaleLink(s.show_wholesale_link ?? true);
|
||||
setShowZipSearch(s.show_zip_search ?? true);
|
||||
setShowSchedulePdf(s.show_schedule_pdf ?? true);
|
||||
setShowTextAlerts(s.show_text_alerts ?? false);
|
||||
setSchedulePdfNotes(s.schedule_pdf_notes ?? "");
|
||||
setHeroImageUrl(s.hero_image_url ?? "");
|
||||
setBrandPrimaryColor(s.brand_primary_color ?? "#16a34a");
|
||||
setBrandSecondaryColor(s.brand_secondary_color ?? "#f5f5f4");
|
||||
setBrandBgColor(s.brand_bg_color ?? "#fafaf9");
|
||||
setBrandTextColor(s.brand_text_color ?? "#1c1917");
|
||||
setCollectSalesTax(s.collect_sales_tax ?? false);
|
||||
setNexusStates(s.nexus_states ?? ["CO"]);
|
||||
setActiveBrandName(s.brand_name ?? "");
|
||||
} else {
|
||||
setLegalBusinessName("");
|
||||
setPhone("");
|
||||
setEmail("");
|
||||
setWebsiteUrl("");
|
||||
setStreetAddress("");
|
||||
setCity("");
|
||||
setState("");
|
||||
setPostalCode("");
|
||||
setCountry("US");
|
||||
setLogoUrl("");
|
||||
setLogoUrlDark("");
|
||||
setOlatheSweetLogoUrl("");
|
||||
setOlatheSweetLogoUrlDark("");
|
||||
setDefaultEmailSignature("");
|
||||
setInvoiceFooterNotes("");
|
||||
setHeroTagline("");
|
||||
setAboutHeadline("");
|
||||
setAboutSubheadline("");
|
||||
setCustomFooterText("");
|
||||
setShowWholesaleLink(true);
|
||||
setShowZipSearch(true);
|
||||
setShowSchedulePdf(true);
|
||||
setShowTextAlerts(false);
|
||||
setSchedulePdfNotes("");
|
||||
setHeroImageUrl("");
|
||||
setBrandPrimaryColor("#16a34a");
|
||||
setBrandSecondaryColor("#f5f5f4");
|
||||
setBrandBgColor("#fafaf9");
|
||||
setBrandTextColor("#1c1917");
|
||||
setCollectSalesTax(false);
|
||||
setNexusStates(["CO"]);
|
||||
}
|
||||
});
|
||||
}, [activeBrandId, isPlatformAdmin]);
|
||||
|
||||
async function handleSave(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
@@ -157,7 +167,7 @@ export default function BrandSettingsForm({
|
||||
setSaved(false);
|
||||
|
||||
const result = await saveBrandSettings({
|
||||
brandId: activeBrandId,
|
||||
brandId,
|
||||
legalBusinessName: legalBusinessName || undefined,
|
||||
phone: phone || undefined,
|
||||
email: email || undefined,
|
||||
@@ -202,17 +212,6 @@ export default function BrandSettingsForm({
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSave} className="space-y-8">
|
||||
{/* Platform admin brand picker */}
|
||||
{isPlatformAdmin && brands.length > 0 && (
|
||||
<AdminInput label="Brand" helpText={`Viewing ${activeBrandName}`}>
|
||||
<AdminSelect
|
||||
value={activeBrandId}
|
||||
onChange={(e) => setActiveBrandId(e.target.value)}
|
||||
options={brands.map((b) => ({ value: b.id, label: b.name }))}
|
||||
/>
|
||||
</AdminInput>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="rounded-xl bg-red-950/50 border border-red-800 p-4 text-sm text-red-300">{error}</div>
|
||||
)}
|
||||
@@ -338,7 +337,7 @@ export default function BrandSettingsForm({
|
||||
hint="Light backgrounds — invoices, email headers, storefront header"
|
||||
value={logoUrl}
|
||||
previewBg="bg-zinc-800"
|
||||
brandId={activeBrandId}
|
||||
brandId={brandId}
|
||||
onUpload={(url) => setLogoUrl(url)}
|
||||
/>
|
||||
|
||||
@@ -348,7 +347,7 @@ export default function BrandSettingsForm({
|
||||
hint="Dark backgrounds — storefront nav on dark header"
|
||||
value={logoUrlDark}
|
||||
previewBg="bg-zinc-900"
|
||||
brandId={activeBrandId}
|
||||
brandId={brandId}
|
||||
onUpload={(url) => setLogoUrlDark(url)}
|
||||
isDark
|
||||
/>
|
||||
@@ -366,7 +365,7 @@ export default function BrandSettingsForm({
|
||||
hint="Works on light backgrounds (stone, white)"
|
||||
value={olatheSweetLogoUrl}
|
||||
previewBg="bg-zinc-700"
|
||||
brandId={activeBrandId}
|
||||
brandId={brandId}
|
||||
onUpload={(url) => setOlatheSweetLogoUrl(url)}
|
||||
isOlatheSweetLight
|
||||
/>
|
||||
@@ -375,7 +374,7 @@ export default function BrandSettingsForm({
|
||||
hint="Works on dark backgrounds (hero, dark sections)"
|
||||
value={olatheSweetLogoUrlDark}
|
||||
previewBg="bg-zinc-900"
|
||||
brandId={activeBrandId}
|
||||
brandId={brandId}
|
||||
onUpload={(url) => setOlatheSweetLogoUrlDark(url)}
|
||||
isOlatheSweetDark
|
||||
/>
|
||||
@@ -490,13 +489,13 @@ export default function BrandSettingsForm({
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-zinc-300 mb-1">Primary Accent</label>
|
||||
<div className="flex gap-2 items-center">
|
||||
<input
|
||||
<input aria-label="Color"
|
||||
type="color"
|
||||
value={brandPrimaryColor}
|
||||
onChange={(e) => setBrandPrimaryColor(e.target.value)}
|
||||
className="w-10 h-10 rounded-lg border border-zinc-600 cursor-pointer"
|
||||
/>
|
||||
<input
|
||||
<input aria-label="#16a34a"
|
||||
type="text"
|
||||
value={brandPrimaryColor}
|
||||
onChange={(e) => setBrandPrimaryColor(e.target.value)}
|
||||
@@ -509,13 +508,13 @@ export default function BrandSettingsForm({
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-zinc-300 mb-1">Text Color</label>
|
||||
<div className="flex gap-2 items-center">
|
||||
<input
|
||||
<input aria-label="Color"
|
||||
type="color"
|
||||
value={brandTextColor}
|
||||
onChange={(e) => setBrandTextColor(e.target.value)}
|
||||
className="w-10 h-10 rounded-lg border border-zinc-600 cursor-pointer"
|
||||
/>
|
||||
<input
|
||||
<input aria-label="#1c1917"
|
||||
type="text"
|
||||
value={brandTextColor}
|
||||
onChange={(e) => setBrandTextColor(e.target.value)}
|
||||
@@ -528,13 +527,13 @@ export default function BrandSettingsForm({
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-zinc-300 mb-1">Background Color</label>
|
||||
<div className="flex gap-2 items-center">
|
||||
<input
|
||||
<input aria-label="Color"
|
||||
type="color"
|
||||
value={brandBgColor}
|
||||
onChange={(e) => setBrandBgColor(e.target.value)}
|
||||
className="w-10 h-10 rounded-lg border border-zinc-600 cursor-pointer"
|
||||
/>
|
||||
<input
|
||||
<input aria-label="#fafaf9"
|
||||
type="text"
|
||||
value={brandBgColor}
|
||||
onChange={(e) => setBrandBgColor(e.target.value)}
|
||||
@@ -547,13 +546,13 @@ export default function BrandSettingsForm({
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-zinc-300 mb-1">Secondary Accent</label>
|
||||
<div className="flex gap-2 items-center">
|
||||
<input
|
||||
<input aria-label="Color"
|
||||
type="color"
|
||||
value={brandSecondaryColor}
|
||||
onChange={(e) => setBrandSecondaryColor(e.target.value)}
|
||||
className="w-10 h-10 rounded-lg border border-zinc-600 cursor-pointer"
|
||||
/>
|
||||
<input
|
||||
<input aria-label="#f5f5f4"
|
||||
type="text"
|
||||
value={brandSecondaryColor}
|
||||
onChange={(e) => setBrandSecondaryColor(e.target.value)}
|
||||
@@ -671,7 +670,7 @@ export default function BrandSettingsForm({
|
||||
</AdminInput>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
{error ? (
|
||||
<div className="flex items-center gap-3 text-zinc-400">
|
||||
<div className="h-5 w-5 rounded-full border-2 border-zinc-500 border-t-transparent animate-spin" />
|
||||
Loading settings...
|
||||
@@ -911,7 +910,7 @@ export function LogoUploadField({
|
||||
<span className="text-xs text-zinc-500">PNG, JPEG, WebP · 1200×400px max · max 5MB (ideally under 2MB)</span>
|
||||
</>
|
||||
)}
|
||||
<input
|
||||
<input aria-label="File upload"
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
accept="image/png,image/jpeg,image/webp,image/svg+xml"
|
||||
|
||||
Reference in New Issue
Block a user