// Common.jsx — Allen Web Ecommerce shared bits.
// Exports: AllenHeader, AllenFooter, AllenIcon, ProductCard, RatingStars,
// Breadcrumb, EmptyState, plus a useToast hook.
const { useState, useEffect, useRef, createContext, useContext } = React;
// ---------------------------------------------------------------
// Icon helper — material symbols outlined (CDN).
const AllenIcon = ({ name, size = 20, weight = 400, fill = 0, color = "currentColor", style }) => (
{name}
);
// ---------------------------------------------------------------
// Toast context (lightweight)
const ToastCtx = createContext(() => {});
const useToast = () => useContext(ToastCtx);
const ToastHost = ({ children }) => {
const [msg, setMsg] = useState(null);
const timer = useRef();
const show = (m) => {
clearTimeout(timer.current);
setMsg(m);
timer.current = setTimeout(() => setMsg(null), 2200);
};
return (
{children}
{msg}
);
};
// ---------------------------------------------------------------
// Rating stars
const RatingStars = ({ value = 4.6, count, size = 14 }) => {
const full = Math.floor(value);
return (
{[0,1,2,3,4].map(i => (
))}
{value.toFixed(1)}
{count != null && ({count.toLocaleString("es-MX")})}
);
};
// ---------------------------------------------------------------
// Product card (PLP / carousel)
const ProductCard = ({ p, onOpen, onAdd, onFav, fav }) => (
onOpen?.(p)}
style={{
background: "var(--bg-elev-1)", borderRadius: "var(--radius-card)", padding: 14,
display: "flex", flexDirection: "column", gap: 8,
boxShadow: "var(--elev-1)", cursor: "pointer",
transition: "transform var(--dur-base) var(--ease-out-quart), box-shadow var(--dur-base) var(--ease-out-quart)",
}}
onMouseEnter={e => { e.currentTarget.style.transform = "translateY(-2px)"; e.currentTarget.style.boxShadow = "var(--elev-2)"; }}
onMouseLeave={e => { e.currentTarget.style.transform = "translateY(0)"; e.currentTarget.style.boxShadow = "var(--elev-1)"; }}
>
{p.img ? (

) : (
{p.emoji}
)}
{p.discount && (
−{p.discount}%
)}
{p.brand}
{p.name}
${p.price.toLocaleString("es-MX")}
{p.was && ${p.was.toLocaleString("es-MX")}}
{p.shipping && (
)}
);
// ---------------------------------------------------------------
// Viewport hook — used across pages to swap grid configs at mobile/tablet/desktop.
// Buckets: 'mobile' < 768 | 'tablet' < 1100 | 'desktop' >= 1100
const useViewport = () => {
const [w, setW] = useState(() => typeof window === "undefined" ? 1280 : window.innerWidth);
useEffect(() => {
const onR = () => setW(window.innerWidth);
window.addEventListener("resize", onR);
return () => window.removeEventListener("resize", onR);
}, []);
return {
w,
isMobile: w < 768,
isTablet: w >= 768 && w < 1100,
isDesktop: w >= 1100,
};
};
// ---------------------------------------------------------------
// Theme toggle — light <-> dark, persisted in localStorage.
// PRODUCT.md mandates dark mode day-one; this is the web counterpart.
const useTheme = () => {
const [theme, setTheme] = useState(() => {
if (typeof window === "undefined") return "light";
return localStorage.getItem("allen-theme") || "light";
});
useEffect(() => {
document.documentElement.setAttribute("data-theme", theme);
localStorage.setItem("allen-theme", theme);
}, [theme]);
return [theme, () => setTheme(t => t === "light" ? "dark" : "light")];
};
const ThemeToggle = () => {
const [theme, toggle] = useTheme();
const isDark = theme === "dark";
return (
);
};
// ---------------------------------------------------------------
// Header (utility bar + main nav)
const AllenHeader = ({ cartCount, user, onNav, onLogout, currentPath = "/", onReview }) => {
const toast = useToast();
const [q, setQ] = useState("");
const [mobileMenu, setMobileMenu] = useState(false);
const [mobileSearch, setMobileSearch] = useState(false);
const { isMobile, isTablet } = useViewport();
const navItems = [
{ id: "home", label: "Inicio" },
{ id: "ofertas", label: "Ofertas" },
{ id: "novedades", label: "Lo más nuevo" },
{ id: "marcas", label: "Marcas" },
{ id: "vende", label: "Vende en Allen" },
];
const handleNav = (id) => {
setMobileMenu(false);
if (id === "home") return onNav("home");
if (id === "ofertas") return onNav("plp", { onlyDiscount: true });
if (id === "novedades")return onNav("plp", { sort: "newest" });
if (id === "marcas") return onNav("plp");
if (id === "vende") return onNav("vende");
};
const submitSearch = (e) => {
e?.preventDefault?.();
const v = q.trim();
if (!v) return;
setMobileSearch(false);
onNav("plp", { q: v });
};
return (
{/* Utility bar (desktop/tablet only) */}
{!isMobile && (
)}
{/* Main header */}
{isMobile && (
)}
onNav("home")} style={{ cursor: "pointer", display: "flex", alignItems: "center" }}>
{!isMobile && (
)}
{isMobile && (
)}
{!isMobile && !isTablet && (
<>
>
)}
{isTablet && (
)}
{!isMobile && typeof window.NotificationBell !== "undefined" && (
)}
{/* Sub-nav (desktop only) */}
{!isMobile && (
)}
{/* Mobile drawer */}
{isMobile && mobileMenu && (
setMobileMenu(false)} style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,.55)", zIndex: 100 }}>
e.stopPropagation()} style={{
position: "absolute", top: 0, left: 0, bottom: 0, width: 300, background: "var(--bg-elev-1)",
padding: "24px 20px", display: "flex", flexDirection: "column", gap: 6,
animation: "slideIn .2s var(--ease-out-quart)",
}}>
{navItems.map(n => (
))}
)}
{/* Mobile search drawer */}
{isMobile && mobileSearch && (
)}
);
};
const headerBtn = {
background: "transparent", border: 0, padding: "10px 14px", borderRadius: "9999px",
display: "flex", alignItems: "center", gap: 8, font: "600 13px var(--font-sans)",
color: "var(--fg-strong)", cursor: "pointer", transition: "background var(--dur-fast)",
};
const drawerBtn = {
background: "transparent", border: 0, padding: "12px 14px", borderRadius: "var(--radius-md)",
display: "flex", alignItems: "center", gap: 12, font: "600 14px var(--font-sans)",
color: "var(--fg-strong)", cursor: "pointer", textAlign: "left", width: "100%",
};
// ---------------------------------------------------------------
// Footer
const AllenFooter = () => (
);
// ---------------------------------------------------------------
// Breadcrumb
const Breadcrumb = ({ items = [] }) => (
);
Object.assign(window, { AllenIcon, ToastHost, useToast, useTheme, useViewport, ThemeToggle, RatingStars, ProductCard, AllenHeader, AllenFooter, Breadcrumb });