fix(buyer/billing/comms/a11y): Codex review pass round 2
Tuxedo buyer path (subagent 2): - src/app/tuxedo/page.tsx: remove duplicate CinematicShowcase render - src/components/storefront/CinematicShowcase.tsx: wire up useCart, Add to Cart button, brand-aware fulfillment - src/app/tuxedo/stops/TuxedoStopsList.tsx: improved empty state with calendar icon + CTAs - src/app/cart/CartClient.tsx: guard against empty cart checkout; 'Cart is Empty' state Billing reconciliation (subagent 3): - src/actions/billing/billing-overview.ts: NEW — single source of truth - src/app/admin/settings/billing/page.tsx: use getBillingOverview - src/app/admin/settings/billing/BillingClientPage.tsx: rewritten to consume BillingOverview (status pill, addons state, removable flags, derived invoice amounts, usage footer) - src/app/admin/page.tsx: use getBillingOverview (aligns dashboard with billing) - src/components/admin/DashboardClient.tsx: 'Active Products' now reads from getBillingOverview - supabase/migrations/203_plan_usage_active_products.sql: get_brand_plan_info counts products as active=true AND deleted_at IS NULL Harvest Reach dedup + audience preview (manual, subagent 4 didn't complete): - src/components/admin/CommunicationsPage.tsx: add initialTab prop - src/app/admin/communications/compose/page.tsx: now renders with initialTab='compose' (single compose experience, no duplicate edit panel) - src/components/admin/HarvestReach/CampaignComposerPage.tsx: always-visible audience preview panel (count + sample emails), loads via previewCampaignAudience action Layout/content consistency + a11y sweep (subagent 5): - src/components/layout/SiteHeader.tsx: Admin link only shows for authenticated admin users - src/components/Providers.tsx: suppress public SiteHeader/Footer for /admin, /cart, /checkout, /wholesale, /water (fixes duplicate headers) - src/app/contact/ContactClientPage.tsx: Phone/Email now use tel:/mailto: links; dynamic year - src/app/blog/page.tsx, changelog, privacy-policy, roadmap, security, terms-and-conditions, waitlist: dynamic year - src/app/admin/wholesale/WholesaleClient.tsx: proper htmlFor/id, type=email/tel, autoComplete - src/app/admin/settings/ai/AIClient.tsx: proper htmlFor/id, required + aria-required - src/app/admin/settings/integrations/IntegrationsClient.tsx: only mask secret fields; add required + aria-required + CredentialField.required type - src/app/admin/water-log/headgates/HeadgatesManager.tsx: htmlFor/id, aria-required - src/components/admin/CreateUserModal.tsx: htmlFor/id, required, aria-required, aria-describedby, autoComplete - src/app/admin/me/AdminMeClient.tsx, products/import, sales/import, water-log/settings, login, brands, tuxedo: a11y polish (ids/required/aria)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import { type Campaign, type CampaignType } from "@/actions/harvest-reach/campaigns";
|
||||
import { type Template } from "@/actions/communications/templates";
|
||||
import type { Segment, SegmentRuleV2 } from "@/actions/harvest-reach/segments";
|
||||
@@ -323,10 +323,54 @@ export default function CampaignComposerPage({ brandId, campaigns, templates, se
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [saved, setSaved] = useState(false);
|
||||
// Audience preview (visible count + sample contacts)
|
||||
const [previewCount, setPreviewCount] = useState<number | null>(null);
|
||||
const [previewSamples, setPreviewSamples] = useState<string[]>([]);
|
||||
const [previewLoading, setPreviewLoading] = useState(false);
|
||||
|
||||
const selectedTemplate = templates.find((t) => t.id === selectedTemplateId);
|
||||
const selectedSegment = segments.find((s) => s.id === selectedSegmentId);
|
||||
|
||||
// Load audience preview when entering the Audience step or changing the segment
|
||||
useEffect(() => {
|
||||
if (step !== 3) return;
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
setPreviewLoading(true);
|
||||
try {
|
||||
const { previewCampaignAudience } = await import("@/actions/communications/send");
|
||||
// For "All contacts" (no segment), pass a rules object that targets all_customers.
|
||||
// For a chosen segment, use the segment's rules. The action's rules type
|
||||
// accepts a permissive object, so we cast through unknown.
|
||||
const rules = (selectedSegment?.rules ?? { target: "all_customers" }) as unknown as Parameters<typeof previewCampaignAudience>[1];
|
||||
const result = await previewCampaignAudience(brandId, rules);
|
||||
if (cancelled) return;
|
||||
if (result) {
|
||||
setPreviewCount(result.count ?? 0);
|
||||
setPreviewSamples(
|
||||
(result.sample_customers ?? [])
|
||||
.map((c) => c.email)
|
||||
.filter((e): e is string => Boolean(e))
|
||||
.slice(0, 5),
|
||||
);
|
||||
} else {
|
||||
setPreviewCount(0);
|
||||
setPreviewSamples([]);
|
||||
}
|
||||
} catch (err) {
|
||||
if (!cancelled) {
|
||||
setPreviewCount(0);
|
||||
setPreviewSamples([]);
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) setPreviewLoading(false);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [step, selectedSegment, brandId]);
|
||||
|
||||
const handleTemplateSelect = useCallback((template: Template) => {
|
||||
setSelectedTemplateId(template.id);
|
||||
setSubject(template.subject ?? "");
|
||||
@@ -550,6 +594,44 @@ export default function CampaignComposerPage({ brandId, campaigns, templates, se
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Audience Preview — always-visible count + sample */}
|
||||
<div
|
||||
className="rounded-xl border border-emerald-200 bg-emerald-50 p-4"
|
||||
data-testid="audience-preview"
|
||||
aria-live="polite"
|
||||
aria-busy={previewLoading}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-wide text-emerald-700">
|
||||
Audience Preview
|
||||
</p>
|
||||
{previewLoading ? (
|
||||
<p className="text-sm text-emerald-700 mt-1">Counting recipients…</p>
|
||||
) : previewCount === null ? (
|
||||
<p className="text-sm text-emerald-700 mt-1">Calculating…</p>
|
||||
) : (
|
||||
<p className="text-sm text-emerald-900 mt-1">
|
||||
<span className="text-2xl font-bold text-emerald-700">
|
||||
{previewCount.toLocaleString()}
|
||||
</span>{" "}
|
||||
<span className="text-stone-700">
|
||||
{previewCount === 1 ? "contact" : "contacts"} will receive this campaign
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{!previewLoading && previewSamples.length > 0 && (
|
||||
<div className="mt-3 pt-3 border-t border-emerald-200">
|
||||
<p className="text-xs font-semibold text-emerald-700 mb-1">Sample recipients:</p>
|
||||
<p className="text-xs text-emerald-800 break-words">
|
||||
{previewSamples.join(", ")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content preview */}
|
||||
<div className="p-4 bg-stone-50 rounded-xl border border-stone-200">
|
||||
<p className="text-xs font-semibold text-stone-500 uppercase tracking-wide mb-2">Content Summary</p>
|
||||
|
||||
Reference in New Issue
Block a user