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:
@@ -11,6 +11,7 @@ type CredentialField = {
|
||||
label: string;
|
||||
placeholder: string;
|
||||
isSecret?: boolean;
|
||||
required?: boolean;
|
||||
};
|
||||
|
||||
type SyncOption = {
|
||||
@@ -346,22 +347,38 @@ function IntegrationCard({
|
||||
{/* Credentials */}
|
||||
<div className="space-y-3 mb-5">
|
||||
<p className="text-xs font-semibold text-zinc-500 uppercase tracking-wider">Credentials</p>
|
||||
{integration.credentials.map((field) => (
|
||||
<div key={field.key}>
|
||||
<label className="block text-xs font-medium text-zinc-400 mb-1">{field.label}</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showSecrets[field.key] ? "text" : "password"}
|
||||
value={credentials[field.key] ?? ""}
|
||||
onChange={(e) => setCredentials((p) => ({ ...p, [field.key]: e.target.value }))}
|
||||
placeholder={field.placeholder}
|
||||
className="w-full rounded-lg border border-zinc-600 bg-zinc-800 px-3 py-2 text-sm pr-16 text-zinc-100 outline-none focus:border-violet-500"
|
||||
/>
|
||||
{integration.credentials.map((field) => {
|
||||
// Only mask the field as password when explicitly marked as a secret
|
||||
// (e.g. API keys, tokens). Plain identifiers (emails, phone numbers,
|
||||
// IDs) stay readable so admins can verify what they entered.
|
||||
const inputId = `integration-${integration.id}-${field.key}`;
|
||||
const inputType = field.isSecret
|
||||
? showSecrets[field.key]
|
||||
? "text"
|
||||
: "password"
|
||||
: (field as { type?: string }).type ?? "text";
|
||||
return (
|
||||
<div key={field.key}>
|
||||
<label htmlFor={inputId} className="block text-xs font-medium text-zinc-400 mb-1">
|
||||
{field.label}
|
||||
{field.required && <span className="text-red-400 ml-1" aria-hidden="true">*</span>}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
id={inputId}
|
||||
type={inputType}
|
||||
value={credentials[field.key] ?? ""}
|
||||
onChange={(e) => setCredentials((p) => ({ ...p, [field.key]: e.target.value }))}
|
||||
placeholder={field.placeholder}
|
||||
aria-required={field.required ? "true" : undefined}
|
||||
className="w-full rounded-lg border border-zinc-600 bg-zinc-800 px-3 py-2 text-sm pr-16 text-zinc-100 outline-none focus:border-violet-500"
|
||||
/>
|
||||
<div className="absolute right-2 top-1/2 -translate-y-1/2 flex gap-1">
|
||||
{field.isSecret && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleSecret(field.key)}
|
||||
aria-label={showSecrets[field.key] ? `Hide ${field.label}` : `Show ${field.label}`}
|
||||
className="text-xs text-zinc-500 hover:text-zinc-300 px-1"
|
||||
>
|
||||
{showSecrets[field.key] ? "Hide" : "Show"}
|
||||
@@ -370,7 +387,8 @@ function IntegrationCard({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Environment toggle */}
|
||||
|
||||
Reference in New Issue
Block a user