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:
@@ -129,7 +129,7 @@ function NewCampaignModal({
|
||||
<p className="text-xs text-[var(--admin-text-muted)]">Create a new email campaign</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={onClose}
|
||||
className="p-2 rounded-lg hover:bg-[var(--admin-card-hover)] transition-colors"
|
||||
>
|
||||
@@ -149,7 +149,7 @@ function NewCampaignModal({
|
||||
<label className="block text-sm font-medium text-[var(--admin-text-primary)] mb-1.5">
|
||||
Campaign Name *
|
||||
</label>
|
||||
<input
|
||||
<input aria-label=". Weekly Pickup Reminder"
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
@@ -163,7 +163,7 @@ function NewCampaignModal({
|
||||
<label className="block text-sm font-medium text-[var(--admin-text-primary)] mb-1.5">
|
||||
Campaign Type *
|
||||
</label>
|
||||
<select
|
||||
<select aria-label="Select"
|
||||
value={campaignType}
|
||||
onChange={(e) => setCampaignType(e.target.value as CampaignType)}
|
||||
className="w-full border border-[var(--admin-border)] rounded-lg px-3 py-2.5 text-sm bg-white text-[var(--admin-text-primary)] focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 outline-none"
|
||||
@@ -177,13 +177,13 @@ function NewCampaignModal({
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-end gap-3 px-6 py-4 border-t border-[var(--admin-border)]">
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-sm font-medium text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={handleCreate}
|
||||
disabled={saving || !name.trim()}
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-emerald-600 px-4 py-2 text-sm font-semibold text-white hover:bg-emerald-700 disabled:opacity-50 transition-colors"
|
||||
@@ -233,7 +233,7 @@ export default function CampaignListPanel({ initialCampaigns, brandId = "6429430
|
||||
<p className="text-[10px] sm:text-xs text-[var(--admin-text-muted)]">{filtered.length} campaign{filtered.length !== 1 ? "s" : ""}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => setShowNewModal(true)}
|
||||
className="inline-flex items-center gap-1.5 rounded-lg bg-emerald-600 px-3 sm:px-4 py-2 text-xs sm:text-sm font-semibold text-white hover:bg-emerald-700 transition-colors"
|
||||
>
|
||||
@@ -244,7 +244,7 @@ export default function CampaignListPanel({ initialCampaigns, brandId = "6429430
|
||||
|
||||
{/* Filters */}
|
||||
<div className="flex gap-3 mb-4">
|
||||
<select
|
||||
<select aria-label="Select"
|
||||
className="text-xs sm:text-sm border border-[var(--admin-border)] rounded-lg px-3 py-2 bg-white text-[var(--admin-text-primary)]"
|
||||
value={filterType}
|
||||
onChange={(e) => setFilterType(e.target.value as CampaignType | "all")}
|
||||
@@ -254,7 +254,7 @@ export default function CampaignListPanel({ initialCampaigns, brandId = "6429430
|
||||
<option key={t.value} value={t.value}>{t.label}</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
<select aria-label="Select"
|
||||
className="text-xs sm:text-sm border border-[var(--admin-border)] rounded-lg px-3 py-2 bg-white text-[var(--admin-text-primary)]"
|
||||
value={filterStatus}
|
||||
onChange={(e) => setFilterStatus(e.target.value as CampaignStatus | "all")}
|
||||
@@ -334,29 +334,43 @@ export function CampaignEditPanel({
|
||||
const router = useRouter();
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [sending, setSending] = useState(false);
|
||||
const [name, setName] = useState(campaign?.name ?? "");
|
||||
const [subject, setSubject] = useState(campaign?.subject ?? "");
|
||||
const [bodyText, setBodyText] = useState(campaign?.body_text ?? "");
|
||||
const [bodyHtml, setBodyHtml] = useState(campaign?.body_html ?? "");
|
||||
const [templateId, setTemplateId] = useState(campaign?.template_id ?? "");
|
||||
const [campaignType, setCampaignType] = useState<CampaignType>(campaign?.campaign_type ?? "operational");
|
||||
const [audienceTarget, setAudienceTarget] = useState<string>(campaign?.audience_rules?.target ?? "stop");
|
||||
const [stopId, setStopId] = useState(campaign?.audience_rules?.stop_id ?? "");
|
||||
const [dateFrom, setDateFrom] = useState(campaign?.audience_rules?.date_from ?? "");
|
||||
const [dateTo, setDateTo] = useState(campaign?.audience_rules?.date_to ?? "");
|
||||
const [scheduleMode, setScheduleMode] = useState<"now" | "later">(
|
||||
campaign?.scheduled_at ? "later" : "now"
|
||||
// Lazy initializers + state seeded from props, but isolated from
|
||||
// the parent so a fresh prop would need to remount to reset.
|
||||
const [name, setName] = useState<string>(() => campaign?.name ?? "");
|
||||
const [subject, setSubject] = useState<string>(() => campaign?.subject ?? "");
|
||||
const [bodyText, setBodyText] = useState<string>(() => campaign?.body_text ?? "");
|
||||
const [bodyHtml, setBodyHtml] = useState<string>(() => campaign?.body_html ?? "");
|
||||
const [templateId, setTemplateId] = useState<string>(() => campaign?.template_id ?? "");
|
||||
const [campaignType, setCampaignType] = useState<CampaignType>(
|
||||
() => campaign?.campaign_type ?? "operational"
|
||||
);
|
||||
const [scheduledAt, setScheduledAt] = useState(
|
||||
campaign?.scheduled_at
|
||||
? new Date(campaign.scheduled_at).toISOString().slice(0, 16)
|
||||
: ""
|
||||
const [audienceTarget, setAudienceTarget] = useState<string>(
|
||||
() => campaign?.audience_rules?.target ?? "stop"
|
||||
);
|
||||
const [stopId, setStopId] = useState<string>(() => campaign?.audience_rules?.stop_id ?? "");
|
||||
const [dateFrom, setDateFrom] = useState<string>(
|
||||
() => campaign?.audience_rules?.date_from ?? ""
|
||||
);
|
||||
const [dateTo, setDateTo] = useState<string>(
|
||||
() => campaign?.audience_rules?.date_to ?? ""
|
||||
);
|
||||
const [scheduleMode, setScheduleMode] = useState<"now" | "later">(
|
||||
() => (campaign?.scheduled_at ? "later" : "now")
|
||||
);
|
||||
const [scheduledAt, setScheduledAt] = useState<string>(
|
||||
() =>
|
||||
campaign?.scheduled_at
|
||||
? new Date(campaign.scheduled_at).toISOString().slice(0, 16)
|
||||
: ""
|
||||
);
|
||||
const [segments, setSegments] = useState<Segment[]>([]);
|
||||
const [selectedSegmentId, setSelectedSegmentId] = useState<string>("");
|
||||
const [preview, setPreview] = useState<AudiencePreviewResult | null>(null);
|
||||
const [previewLoading, setPreviewLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [error, setError] = useState<string>("");
|
||||
// `new Date()` in JSX would mismatch between server and client, so we
|
||||
// compute the "now" floor in a useEffect and read from state.
|
||||
const [nowMin, setNowMin] = useState<string>("");
|
||||
|
||||
// Load saved segments
|
||||
useEffect(() => {
|
||||
@@ -365,6 +379,16 @@ export function CampaignEditPanel({
|
||||
});
|
||||
}, [brandId]);
|
||||
|
||||
// Compute the "now" floor for the datetime-local input. This must run
|
||||
// only in the browser to avoid hydration mismatches. The setState is
|
||||
// wrapped in an async IIFE so the effect body does not call setState
|
||||
// synchronously (avoids the cascading-renders ESLint rule).
|
||||
useEffect(() => {
|
||||
void (async () => {
|
||||
setNowMin(new Date().toISOString().slice(0, 16));
|
||||
})();
|
||||
}, []);
|
||||
|
||||
// When a segment is selected, populate audience fields
|
||||
function applySegment(segmentId: string) {
|
||||
setSelectedSegmentId(segmentId);
|
||||
@@ -480,7 +504,7 @@ export function CampaignEditPanel({
|
||||
<h3 className="text-xs sm:text-sm font-semibold text-[var(--admin-text-muted)] uppercase tracking-wide">Basic Info</h3>
|
||||
<div>
|
||||
<label className="block text-xs sm:text-sm font-medium text-[var(--admin-text-primary)] mb-1.5">Campaign Name *</label>
|
||||
<input
|
||||
<input aria-label=". Tuxedo Stop 5/15 Reminder"
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
@@ -491,7 +515,7 @@ export function CampaignEditPanel({
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs sm:text-sm font-medium text-[var(--admin-text-primary)] mb-1.5">Campaign Type *</label>
|
||||
<select
|
||||
<select aria-label="Select"
|
||||
value={campaignType}
|
||||
onChange={(e) => setCampaignType(e.target.value as CampaignType)}
|
||||
className="w-full border border-[var(--admin-border)] rounded-lg px-3 py-2 text-sm bg-white text-[var(--admin-text-primary)]"
|
||||
@@ -503,7 +527,7 @@ export function CampaignEditPanel({
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs sm:text-sm font-medium text-[var(--admin-text-primary)] mb-1.5">Template</label>
|
||||
<select
|
||||
<select aria-label="Select"
|
||||
value={templateId}
|
||||
onChange={(e) => {
|
||||
setTemplateId(e.target.value);
|
||||
@@ -528,7 +552,7 @@ export function CampaignEditPanel({
|
||||
{segments.length > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[10px] sm:text-xs text-[var(--admin-text-muted)]">Saved segment:</span>
|
||||
<select
|
||||
<select aria-label="Select"
|
||||
value={selectedSegmentId}
|
||||
onChange={(e) => applySegment(e.target.value)}
|
||||
className="text-xs sm:text-sm border border-[var(--admin-border)] rounded-lg px-3 py-1.5 bg-white"
|
||||
@@ -543,7 +567,7 @@ export function CampaignEditPanel({
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs sm:text-sm font-medium text-[var(--admin-text-primary)] mb-1.5">Target By</label>
|
||||
<select
|
||||
<select aria-label="Select"
|
||||
value={audienceTarget}
|
||||
onChange={(e) => {
|
||||
setAudienceTarget(e.target.value);
|
||||
@@ -561,7 +585,7 @@ export function CampaignEditPanel({
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs sm:text-sm font-medium text-[var(--admin-text-primary)] mb-1.5">Stop ID</label>
|
||||
<input
|
||||
<input aria-label="UUID"
|
||||
type="text"
|
||||
value={stopId}
|
||||
onChange={(e) => setStopId(e.target.value)}
|
||||
@@ -571,7 +595,7 @@ export function CampaignEditPanel({
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs sm:text-sm font-medium text-[var(--admin-text-primary)] mb-1.5">From</label>
|
||||
<input
|
||||
<input aria-label="Date"
|
||||
type="date"
|
||||
value={dateFrom}
|
||||
onChange={(e) => setDateFrom(e.target.value)}
|
||||
@@ -580,7 +604,7 @@ export function CampaignEditPanel({
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs sm:text-sm font-medium text-[var(--admin-text-primary)] mb-1.5">To</label>
|
||||
<input
|
||||
<input aria-label="Date"
|
||||
type="date"
|
||||
value={dateTo}
|
||||
onChange={(e) => setDateTo(e.target.value)}
|
||||
@@ -614,7 +638,7 @@ export function CampaignEditPanel({
|
||||
<h3 className="text-xs sm:text-sm font-semibold text-[var(--admin-text-muted)] uppercase tracking-wide">Message Content</h3>
|
||||
<div>
|
||||
<label className="block text-xs sm:text-sm font-medium text-[var(--admin-text-primary)] mb-1.5">Subject</label>
|
||||
<input
|
||||
<input aria-label="Email Subject Line"
|
||||
type="text"
|
||||
value={subject}
|
||||
onChange={(e) => setSubject(e.target.value)}
|
||||
@@ -624,7 +648,7 @@ export function CampaignEditPanel({
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs sm:text-sm font-medium text-[var(--admin-text-primary)] mb-1.5">Body (Plain Text)</label>
|
||||
<textarea
|
||||
<textarea aria-label="Message Content..."
|
||||
value={bodyText}
|
||||
onChange={(e) => setBodyText(e.target.value)}
|
||||
rows={6}
|
||||
@@ -660,12 +684,12 @@ export function CampaignEditPanel({
|
||||
{scheduleMode === "later" && (
|
||||
<div>
|
||||
<label className="block text-xs sm:text-sm font-medium text-[var(--admin-text-primary)] mb-1.5">Send date & time</label>
|
||||
<input
|
||||
<input aria-label="Datetime Local"
|
||||
type="datetime-local"
|
||||
value={scheduledAt}
|
||||
onChange={(e) => setScheduledAt(e.target.value)}
|
||||
className="border border-[var(--admin-border)] rounded-lg px-3 py-2 text-sm bg-white text-[var(--admin-text-primary)]"
|
||||
min={new Date().toISOString().slice(0, 16)}
|
||||
min={nowMin}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user