diff --git a/src/actions/communications/import-contacts.ts b/src/actions/communications/import-contacts.ts index 9a8e3ee..c22e0cc 100644 --- a/src/actions/communications/import-contacts.ts +++ b/src/actions/communications/import-contacts.ts @@ -171,4 +171,4 @@ export type ImportHistoryItem = { url: string; }; -export { previewContactImport, type ImportPreviewResult }; +export { type ImportPreviewResult }; diff --git a/src/actions/water-log/admin.ts b/src/actions/water-log/admin.ts index 8b83f22..4828d06 100644 --- a/src/actions/water-log/admin.ts +++ b/src/actions/water-log/admin.ts @@ -1025,6 +1025,4 @@ function cryptoRandomHex(bytes: number): string { return Array.from(arr, (b) => b.toString(16).padStart(2, "0")).join(""); } -// Re-export for the field action to do an admin-PIN check. -export { verifyPin }; -export { logAlert }; +// Re-exports removed — callers import from @/lib/water-log-pin and @/lib/water-log-audit directly. diff --git a/src/components/admin/ContactImportForm.tsx b/src/components/admin/ContactImportForm.tsx index 39650d2..a0b7f38 100644 --- a/src/components/admin/ContactImportForm.tsx +++ b/src/components/admin/ContactImportForm.tsx @@ -91,6 +91,107 @@ function getStatValueColor(highlight: boolean, warn: boolean) { return "text-[var(--admin-text-muted)]"; } +function applyMappings( + mappings: ColumnMapping[], + overrides: Record, + headers: string[], + rows: string[][] +): ContactImportEntry[] { + const effective = mappings.map((m) => ({ + ...m, + field: overrides[m.csvColumn] ?? m.field, + })); + + const seenEmails = new Set(); + const seenPhones = new Set(); + const entries: ContactImportEntry[] = []; + const headerIndex = new Map(); + headers.forEach((h, i) => headerIndex.set(h, i)); + + for (const row of rows) { + const entry: ContactImportEntry = {}; + const ignored: Record = {}; + + for (const mapping of effective) { + const colIdx = headerIndex.get(mapping.csvColumn); + if (colIdx === undefined) continue; + const raw = row[colIdx] ?? ""; + + if (mapping.field === null) { + if (raw) ignored[mapping.csvColumn] = raw; + continue; + } + + switch (mapping.field) { + case "email": { + entry.email = raw.trim().toLowerCase() || undefined; + break; + } + case "phone": { + entry.phone = raw.trim() || undefined; + break; + } + case "first_name": + entry.first_name = raw || undefined; + break; + case "last_name": + entry.last_name = raw || undefined; + break; + case "full_name": + entry.full_name = raw || undefined; + break; + case "tags": + entry.tags = raw + ? raw.split(/[;:,]/).flatMap((t) => { + const trimmed = t.trim(); + return trimmed ? [trimmed] : []; + }) + : undefined; + break; + case "email_opt_in": + entry.email_opt_in = + raw === "true" || + raw === "1" || + raw === "yes" || + raw === "y" || + raw === "on"; + break; + case "sms_opt_in": + entry.sms_opt_in = + raw === "true" || + raw === "1" || + raw === "yes" || + raw === "y" || + raw === "on"; + break; + case "external_id": + entry.external_id = raw || undefined; + break; + } + } + + if (!entry.email && !entry.phone) continue; + + let isDup = false; + if (entry.email) { + if (seenEmails.has(entry.email)) isDup = true; + seenEmails.add(entry.email); + } + if (entry.phone && !isDup) { + if (seenPhones.has(entry.phone)) isDup = true; + seenPhones.add(entry.phone); + } + if (isDup) continue; + + if (Object.keys(ignored).length > 0) { + entry._metadata = ignored; + } + entries.push(entry); + } + + return entries; +} + export default function ContactImportForm({ brandId }: { brandId: string }) { const [step, setStep] = useState("idle"); const [file, setFile] = useState(null); @@ -208,118 +309,6 @@ export default function ContactImportForm({ brandId }: { brandId: string }) { [handleFile] ); - // ── Mapping override ─────────────────────────────────────────────────────── - - function applyMappings( - mappings: ColumnMapping[], - overrides: Record, - headers: string[], - rows: string[][] - ): ContactImportEntry[] { - const effective = mappings.map((m) => ({ - ...m, - field: overrides[m.csvColumn] ?? m.field, - })); - - const emailColIdx = effective.findIndex((m) => m.field === "email"); - const phoneColIdx = effective.findIndex((m) => m.field === "phone"); - - const seenEmails = new Set(); - const seenPhones = new Set(); - const entries: ContactImportEntry[] = []; - const headerIndex = new Map(); - headers.forEach((h, i) => headerIndex.set(h, i)); - - for (let i = 0; i < rows.length; i++) { - const row = rows[i]; - const entry: ContactImportEntry = {}; - const ignored: Record = {}; - - for (const mapping of effective) { - const colIdx = headerIndex.get(mapping.csvColumn); - if (colIdx === undefined) continue; - const raw = row[colIdx] ?? ""; - - if (mapping.field === null) { - if (raw) ignored[mapping.csvColumn] = raw; - continue; - } - - switch (mapping.field) { - case "email": { - const cleaned = raw.trim().toLowerCase(); - entry.email = cleaned || undefined; - break; - } - case "phone": { - const stripped = raw.replace(/[\s\-().[\]]/g, ""); - entry.phone = raw.trim() || undefined; - break; - } - case "first_name": - entry.first_name = raw || undefined; - break; - case "last_name": - entry.last_name = raw || undefined; - break; - case "full_name": - entry.full_name = raw || undefined; - break; - case "tags": - entry.tags = raw - ? raw.split(/[;:,]/).flatMap((t) => { - const trimmed = t.trim(); - return trimmed ? [trimmed] : []; - }) - : undefined; - break; - case "email_opt_in": - entry.email_opt_in = - raw === "true" || - raw === "1" || - raw === "yes" || - raw === "y" || - raw === "on"; - break; - case "sms_opt_in": - entry.sms_opt_in = - raw === "true" || - raw === "1" || - raw === "yes" || - raw === "y" || - raw === "on"; - break; - case "external_id": - entry.external_id = raw || undefined; - break; - } - } - - const hasEmail = !!entry.email; - const hasPhone = !!entry.phone; - if (!hasEmail && !hasPhone) continue; - - // Dedupe within file - let isDup = false; - if (entry.email) { - if (seenEmails.has(entry.email!)) isDup = true; - seenEmails.add(entry.email!); - } - if (entry.phone && !isDup) { - if (seenPhones.has(entry.phone)) isDup = true; - seenPhones.add(entry.phone); - } - if (isDup) continue; - - if (Object.keys(ignored).length > 0) { - entry._metadata = ignored; - } - entries.push(entry); - } - - return entries; - } - // ── Confirm and import ───────────────────────────────────────────────────── const handleConfirm = useCallback( diff --git a/src/components/admin/ReportsDashboard.tsx b/src/components/admin/ReportsDashboard.tsx index 8d1575e..45ce9a6 100644 --- a/src/components/admin/ReportsDashboard.tsx +++ b/src/components/admin/ReportsDashboard.tsx @@ -68,6 +68,17 @@ function quarterLabel(startISO: string): string { return `Q${q} ${s.getFullYear()}`; } +function handleExportCSV(exportFn: () => string, filename: string) { + const csv = exportFn(); + const blob = new Blob([csv], { type: "text/csv" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); +} + function monthLabel(startISO: string): string { const s = new Date(startISO + "T00:00:00"); return s.toLocaleDateString("en-US", { month: "long", year: "numeric" }); @@ -307,17 +318,6 @@ export default function ReportsDashboard({ setExplainError(null); } - function handleExportCSV(exportFn: () => string, filename: string) { - const csv = exportFn(); - const blob = new Blob([csv], { type: "text/csv" }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = filename; - a.click(); - URL.revokeObjectURL(url); - } - // Auto-fetch when range or brand changes. The fetchAll call is wrapped in // an async IIFE so the effect body does not call setState synchronously // (avoids the cascading-renders ESLint rule). diff --git a/src/components/admin/TaxDashboard.tsx b/src/components/admin/TaxDashboard.tsx index 6f5fdcf..7517d63 100644 --- a/src/components/admin/TaxDashboard.tsx +++ b/src/components/admin/TaxDashboard.tsx @@ -43,6 +43,16 @@ function buildRange(preset: DatePreset, customStart?: string, customEnd?: string } } +function downloadCSV(csv: string, filename: string) { + const blob = new Blob([csv], { type: "text/csv" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); +} + function quarterLabel(start: string, end: string): string { const s = new Date(start + "T00:00:00"); const q = Math.floor(s.getMonth() / 3) + 1; @@ -261,16 +271,6 @@ export default function TaxDashboard({ } } - function downloadCSV(csv: string, filename: string) { - const blob = new Blob([csv], { type: "text/csv" }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = filename; - a.click(); - URL.revokeObjectURL(url); - } - const effectiveRate = summary && summary.total_gross_sales > 0 ? (summary.total_tax_collected / summary.total_gross_sales) * 100 : 0; diff --git a/src/components/admin/TimeTrackingSettingsClient.tsx b/src/components/admin/TimeTrackingSettingsClient.tsx index 3fee2f5..0fcda4f 100644 --- a/src/components/admin/TimeTrackingSettingsClient.tsx +++ b/src/components/admin/TimeTrackingSettingsClient.tsx @@ -3,6 +3,15 @@ import { useState, useEffect, useCallback } from "react"; +function triggerDownload(url: string) { + const a = document.createElement("a"); + a.href = url; + a.download = ""; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); +} + const triggerLabel: Record = { daily_approaching: { en: "Daily OT Approaching", color: "bg-amber-100 text-amber-700" }, daily_reached: { en: "Daily OT Reached", color: "bg-red-100 text-red-700" }, @@ -236,15 +245,6 @@ export default function TimeTrackingSettingsClient({ brandId }: TimeTrackingSett return `/api/time-tracking/export?${params}`; } - function triggerDownload(url: string) { - const a = document.createElement("a"); - a.href = url; - a.download = ""; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - } - const openAddWorker = () => { setEditingWorker(null); setWorkerName(""); setWorkerRole("worker"); setWorkerLang("en"); diff --git a/src/components/landing/HeroSection.tsx b/src/components/landing/HeroSection.tsx index cfd1948..ae9e609 100644 --- a/src/components/landing/HeroSection.tsx +++ b/src/components/landing/HeroSection.tsx @@ -13,6 +13,13 @@ if (typeof window !== "undefined") { // ───────────────────────────────────────────────────────────────────────────── // CINEMATIC HERO SECTION - APPLE-STYLE SCROLL-DRIVEN ANIMATIONS // ───────────────────────────────────────────────────────────────────────────── +function scrollToContent() { + const nextSection = document.getElementById("story"); + if (nextSection) { + nextSection.scrollIntoView({ behavior: "smooth" }); + } +} + export default function HeroSection() { const containerRef = useRef(null); const heroRef = useRef(null); @@ -227,13 +234,6 @@ export default function HeroSection() { return () => ctx.revert(); }, [mounted]); - const scrollToContent = () => { - const nextSection = document.getElementById("story"); - if (nextSection) { - nextSection.scrollIntoView({ behavior: "smooth" }); - } - }; - return ( <> {/* ─── SCROLL PROGRESS BAR ──────────────────────────────────────────── */} diff --git a/src/components/landing/LandingPageWrapper.tsx b/src/components/landing/LandingPageWrapper.tsx index 3cdacef..e0a0843 100644 --- a/src/components/landing/LandingPageWrapper.tsx +++ b/src/components/landing/LandingPageWrapper.tsx @@ -386,15 +386,3 @@ export function LandingPageWrapper({ children, className = "" }: WrapperProps) { ); } -// ============================================ -// DEFAULT EXPORT: Complete Landing Page -// ============================================ -export default function LandingPage({ - children, -}: { - children?: React.ReactNode; -}) { - return ( - {children} - ); -} \ No newline at end of file diff --git a/src/components/notifications/ToastNotification.tsx b/src/components/notifications/ToastNotification.tsx index 2d016e9..972dcb3 100644 --- a/src/components/notifications/ToastNotification.tsx +++ b/src/components/notifications/ToastNotification.tsx @@ -9,8 +9,6 @@ import { type Toast, } from "./toast-store"; -export { toast } from "./toast-store"; - interface ToastNotificationProps { toast: Toast; onDismiss: (id: string) => void; diff --git a/src/components/notifications/toast-store.ts b/src/components/notifications/toast-store.ts index 2eca1b5..c3434f7 100644 --- a/src/components/notifications/toast-store.ts +++ b/src/components/notifications/toast-store.ts @@ -19,13 +19,6 @@ function emit(type: ToastType, title: string, message?: string) { toastListeners.forEach((listener) => listener(newToast)); } -export const toast = { - success: (title: string, message?: string) => emit("success", title, message), - error: (title: string, message?: string) => emit("error", title, message), - warning: (title: string, message?: string) => emit("warning", title, message), - info: (title: string, message?: string) => emit("info", title, message), -}; - /** Internal subscription helper used by `ToastNotification` to receive * newly-fired toasts. Returns an unsubscribe function. */ export function subscribeToToasts(listener: (toast: Toast) => void): () => void { diff --git a/src/components/transitions/LoadingFade.tsx b/src/components/transitions/LoadingFade.tsx index ee92a81..53f4369 100644 --- a/src/components/transitions/LoadingFade.tsx +++ b/src/components/transitions/LoadingFade.tsx @@ -35,5 +35,3 @@ export function LoadingFade() { ); } - -export default LoadingFade; diff --git a/src/components/ui/ScrollAnimations.tsx b/src/components/ui/ScrollAnimations.tsx index 5e31fe4..e889894 100644 --- a/src/components/ui/ScrollAnimations.tsx +++ b/src/components/ui/ScrollAnimations.tsx @@ -19,65 +19,6 @@ function prefersReducedMotion(): boolean { return window.matchMedia("(prefers-reduced-motion: reduce)").matches === true; } -interface StickyScrollSectionProps { - children: React.ReactNode; - height?: string; - className?: string; -} - -export default function StickyScrollSection({ - children, - height = "300vh", - className = "", -}: StickyScrollSectionProps) { - const containerRef = useRef(null); - // Initial value is the prefers-reduced-motion check — users with - // reduced motion see the content "stuck" immediately, so we don't - // need a useEffect to flip the flag on mount. - const [isStuck, setIsStuck] = useState(() => prefersReducedMotion()); - - useEffect(() => { - if (typeof window === "undefined" || !containerRef.current) return; - if (prefersReducedMotion()) { - // With reduced motion, just show the content normally — no pinning. - return; - } - - const ctx = gsap.context(() => { - const trigger = ScrollTrigger.create({ - trigger: containerRef.current, - start: "top top", - end: "bottom bottom", - pin: true, - pinSpacing: false, - onToggle: (self) => { - setIsStuck(self.isActive); - }, - }); - - return () => trigger.kill(); - }, containerRef); - - return () => ctx.revert(); - }, []); - - return ( -
-
- {children} -
-
- ); -} - // ───────────────────────────────────────────────────────────────────────────── // SCROLL-REVEAL WRAPPER - Layers content as user scrolls // Motion-reduced: max 12px translation (was 60px), 320ms (was 1s), diff --git a/src/components/water/WaterAdminClient.tsx b/src/components/water/WaterAdminClient.tsx index 10c82ea..c98c7b1 100644 --- a/src/components/water/WaterAdminClient.tsx +++ b/src/components/water/WaterAdminClient.tsx @@ -260,6 +260,11 @@ function SectionHeader({ ); } +async function handleLogout() { + await logoutWaterAdmin(); + window.location.href = "/water"; +} + export default function WaterAdminClient() { const router = useRouter(); const [lang, setLang] = useState<"en" | "es">(() => { @@ -351,11 +356,6 @@ export default function WaterAdminClient() { return () => clearInterval(interval); }, [loadDisplaySummary]); - async function handleLogout() { - await logoutWaterAdmin(); - window.location.href = "/water"; - } - // Add user async function handleAddUser(e: React.FormEvent) { e.preventDefault();