// Public invitation view + other sub-screens const PublishedInvitation = ({ palette, onBack }) => { const [copied, setCopied] = React.useState(false); const [orderData, setOrderData] = React.useState(null); // Read published data from localStorage (set during publish flow) const publishedData = React.useMemo(() => { try { return JSON.parse(localStorage.getItem('inv23_last_published') || 'null'); } catch { return null; } }, []); const previewUrl = publishedData?.previewUrl || null; const subdomain = publishedData?.subdomain || null; const displayUrl = subdomain ? `invitacion23.com/${subdomain}` : (previewUrl ? `localhost:4000${previewUrl}` : 'invitacion23.com/mi-evento'); const shareUrl = previewUrl ? `${window.location.origin}${previewUrl}` : `https://invitacion23.com/${subdomain || 'mi-evento'}`; const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=160x160&data=${encodeURIComponent(shareUrl)}`; // Load real order data to display event details React.useEffect(() => { const orderId = localStorage.getItem('inv23_active_order'); if (!orderId || !window.clientFetch) return; window.clientFetch(window.BACKEND + '/client/orders/' + orderId) .then(r => r.json()) .then(d => { if (d.order) setOrderData(d.order); }) .catch(() => {}); }, []); // Extract real event fields from order const eventTitle = orderData?.title || publishedData?.title || null; const eventDate = orderData?.date || orderData?.brief?.date || null; const eventVenue = orderData?.ceremony?.location || orderData?.ceremony?.name || orderData?.brief?.venue || null; const eventTime = orderData?.time || orderData?.ceremony?.time || null; const copyLink = async () => { try { await navigator.clipboard.writeText(shareUrl); setCopied(true); setTimeout(() => setCopied(false), 2500); } catch { // Fallback: select text const el = document.createElement('textarea'); el.value = shareUrl; document.body.appendChild(el); el.select(); document.execCommand('copy'); document.body.removeChild(el); setCopied(true); setTimeout(() => setCopied(false), 2500); } }; const waUrl = `https://wa.me/?text=${encodeURIComponent(`¡Te invito a ${eventTitle || 'mi evento'}! Confirma tu asistencia aquí: ${shareUrl}`)}`; return (
{/* Header */}
Publicada
{/* Success card */}
{/* Hero */}
¡INVITACIÓN PUBLICADA!
20 ? 36 : 52, fontWeight: palette.displayWeight, letterSpacing: palette.trackingHero, lineHeight: 0.95, background: `linear-gradient(135deg, ${palette.accentDeep}, ${palette.accent})`, WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text', marginBottom: 8 }}> {eventTitle || 'Tu Evento'}
{(eventDate || eventTime) && (
{[eventDate, eventTime].filter(Boolean).join(' · ')}
)} {eventVenue && (
{eventVenue}
)}
{/* URL strip */}
{displayUrl}
{/* Share actions */}
Compartir por WhatsApp Ver invitación
{/* QR code */}
Código QR para compartir
QR code
Escanea para abrir la invitación
creado con invitacion23 · hecho con ia
); }; // Calendar const MONTH_NAMES_ES = ['enero','febrero','marzo','abril','mayo','junio','julio','agosto','septiembre','octubre','noviembre','diciembre']; const MONTH_SHORT_ES = ['ene','feb','mar','abr','may','jun','jul','ago','sep','oct','nov','dic']; const CalendarScreen = ({ palette, onSectionChange, onLogout }) => { const now = new Date(); const [year, setYear] = React.useState(now.getFullYear()); const [month, setMonth] = React.useState(now.getMonth()); // 0-based const [orders, setOrders] = React.useState([]); const [loading, setLoading] = React.useState(true); React.useEffect(() => { const fetchOrders = async () => { try { const res = await window.clientFetch(window.BACKEND + '/client/orders'); if (res.ok) { const data = await res.json(); // API returns { ok: true, orders: [...] } const arr = Array.isArray(data.orders) ? data.orders : Array.isArray(data) ? data : []; setOrders(arr); } } catch {} setLoading(false); }; fetchOrders(); }, []); // Build event map: day-of-month → order title (for current month/year) const eventMap = {}; orders.forEach(o => { if (!o.eventDate) return; const d = new Date(o.eventDate); if (isNaN(d.getTime())) return; if (d.getFullYear() === year && d.getMonth() === month) { eventMap[d.getDate()] = o.eventName || o.title || 'Evento'; } }); // Calendar grid: find weekday of first day (Monday=0) const firstDay = new Date(year, month, 1).getDay(); // 0=Sun const startOffset = (firstDay === 0 ? 6 : firstDay - 1); // shift so Mon=0 const daysInMonth = new Date(year, month + 1, 0).getDate(); const totalCells = Math.ceil((startOffset + daysInMonth) / 7) * 7; const prevMonth = () => { if (month === 0) { setMonth(11); setYear(y => y - 1); } else setMonth(m => m - 1); }; const nextMonth = () => { if (month === 11) { setMonth(0); setYear(y => y + 1); } else setMonth(m => m + 1); }; // Upcoming events: all orders with future eventDate, sorted const upcoming = orders .filter(o => o.eventDate && !isNaN(new Date(o.eventDate).getTime())) .sort((a, b) => new Date(a.eventDate) - new Date(b.eventDate)) .slice(0, 5) .map(o => { const d = new Date(o.eventDate); const diffDays = Math.ceil((d - Date.now()) / 86400000); const badge = diffDays < 0 ? 'pasado' : diffDays === 0 ? 'hoy' : `en ${diffDays} día${diffDays !== 1 ? 's' : ''}`; return { date: `${d.getDate()} ${MONTH_SHORT_ES[d.getMonth()]}`, title: o.eventName || o.title || 'Evento', sub: o.published ? 'Publicada' : 'Borrador', badge }; }); return (
Calendario

{MONTH_NAMES_ES[month]} {year}

{['Lu', 'Ma', 'Mi', 'Ju', 'Vi', 'Sa', 'Do'].map((d, i) => (
{d}
))}
{Array.from({ length: totalCells }, (_, i) => { const dayNum = i - startOffset + 1; const valid = dayNum >= 1 && dayNum <= daysInMonth; const event = valid ? eventMap[dayNum] : null; const isToday = valid && year === now.getFullYear() && month === now.getMonth() && dayNum === now.getDate(); return (
{valid ? dayNum : ''}
{event &&
{event}
}
); })}
Próximos eventos
{loading ? (
Cargando eventos...
) : upcoming.length === 0 ? (
Aún no tienes eventos programados. Crea tu primera invitación y aparecerá aquí.
) : (
{upcoming.map((e, i) => (
{e.date.split(' ')[0]}
{e.date.split(' ')[1]}
{e.title}
{e.sub}
{e.badge}
))}
)}
); }; // Guests — shows real RSVPs from /client/orders/:id/rsvps const GuestsScreen = ({ palette, onSectionChange, onLogout }) => { const [orders, setOrders] = React.useState([]); const [selectedOrderId, setSelectedOrderId] = React.useState(null); const [rsvps, setRsvps] = React.useState([]); const [loadingOrders, setLoadingOrders] = React.useState(true); const [loadingRsvps, setLoadingRsvps] = React.useState(false); const [search, setSearch] = React.useState(''); // Load orders on mount React.useEffect(() => { const fetchOrders = async () => { try { const res = await window.clientFetch(window.BACKEND + '/client/orders'); if (res.ok) { const data = await res.json(); // API returns { ok: true, orders: [...] } const arr = Array.isArray(data.orders) ? data.orders : Array.isArray(data) ? data : []; setOrders(arr); if (arr.length > 0) setSelectedOrderId(arr[0].id || arr[0]._id || null); } } catch {} setLoadingOrders(false); }; fetchOrders(); }, []); // Load RSVPs when selected order changes React.useEffect(() => { if (!selectedOrderId) { setRsvps([]); return; } setLoadingRsvps(true); window.clientFetch(window.BACKEND + '/client/orders/' + selectedOrderId + '/rsvps') .then(r => r.json()) .then(data => { setRsvps(Array.isArray(data.rsvps) ? data.rsvps : []); }) .catch(() => setRsvps([])) .finally(() => setLoadingRsvps(false)); }, [selectedOrderId]); const selectedOrder = orders.find(o => (o.id || o._id) === selectedOrderId) || null; const filtered = search ? rsvps.filter(r => (r.name || '').toLowerCase().includes(search.toLowerCase())) : rsvps; const confirmed = rsvps.filter(r => r.response === 'yes').length; const declined = rsvps.filter(r => r.response === 'no').length; const total = rsvps.length; const statusBadge = (response) => response === 'yes' ? { label: 'Confirmado', bg: '#dcfce7', fg: '#16a34a' } : { label: 'No asiste', bg: '#fee2e2', fg: '#dc2626' }; const formatDate = (iso) => { if (!iso) return '—'; try { return new Date(iso).toLocaleDateString('es-MX', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' }); } catch { return iso; } }; const loading = loadingOrders || loadingRsvps; return (
{selectedOrder ? (selectedOrder.eventName || selectedOrder.title || 'Tu evento') : 'Evento'}

Tus invitados

{orders.length > 1 && ( )}
{[{ n: total, l: 'Respuestas' }, { n: confirmed, l: 'Confirmados', color: '#16a34a' }, { n: total - confirmed - declined, l: 'Pendientes' }, { n: declined, l: 'No asisten', color: '#dc2626' }].map((s, i) => (
{s.n}
{s.l}
))}
setSearch(e.target.value)} placeholder="Buscar por nombre..." style={{ flex: 1, background: 'transparent', border: 'none', outline: 'none', fontSize: 13, color: palette.text, fontFamily: palette.fontBody }}/>
{loading ? (
Cargando respuestas...
) : filtered.length === 0 ? (
👥
{search ? 'Nadie coincide con esa búsqueda' : 'Aún no hay respuestas de RSVP'}
{search ? 'Prueba con otro nombre.' : 'Cuando tus invitados confirmen desde la invitación, aparecerán aquí.'}
) : (
Invitado
Estado
Fecha
{filtered.map((r, i) => { const badge = statusBadge(r.response); return (
{r.name || '—'}
{badge.label}
{formatDate(r.submittedAt)}
); })}
)}
); }; // Plans — pay-per-invitation model + planner subscription const PlansScreen = ({ palette, onSectionChange, onLogout }) => { const [currency, setCurrency] = React.useState('mxn'); // mxn | usd const P = (mxn, usd) => currency === 'mxn' ? `$${mxn}` : `$${usd}`; const unit = currency === 'mxn' ? 'MXN' : 'USD'; const payPer = [ { name: 'Prueba', price: P(0, 0), sub: 'gratis', tag: 'Para conocer Mariana', features: [ '1 invitación incluida', 'Todas las plantillas y diseño con IA', 'Hasta 20 invitados', 'Marca de agua invitacion23', 'Link para compartir', ], cta: 'Plan actual', current: true, }, { name: 'Una invitación', price: P(380, 20), sub: `${unit} · pago único`, tag: 'Un evento, sin suscripción', features: [ 'Invitación completa sin marca de agua', 'Invitados ilimitados', 'Mariana IA ilimitada', 'RSVP + lista de invitados', 'Activa hasta 6 meses después del evento', 'Exportar a PDF', ], cta: 'Comprar invitación', highlight: true, }, { name: 'Paquete 3', price: P(990, 52), sub: `${unit} · pago único`, tag: 'Ahorra ~13%', features: [ 'Todo lo de "Una invitación"', '3 invitaciones para usar cuando quieras', 'Créditos sin caducidad', 'Comparte invitaciones entre eventos', ], cta: 'Comprar paquete', badge: 'Ahorra $150', }, ]; const planner = { name: 'Wedding Planner', price: P(1490, 79), sub: `${unit} / mes`, tag: 'Para quienes crean más de 10 invitaciones al mes', features: [ 'Invitaciones ilimitadas', 'Mariana IA + voz', 'Panel multi-evento con filtros', 'Hasta 3 colaboradores', 'Dominio propio (tubodafeliz.com)', 'Marca blanca opcional', 'Soporte prioritario en WhatsApp', ], }; return (
PRECIOS

Paga sólo por la invitación que celebras

Sin suscripciones. Sin contratos. Tu invitación, tu evento, tu ritmo.

{/* Currency toggle */}
{[{ id: 'mxn', l: 'MXN $' }, { id: 'usd', l: 'USD $' }].map(c => ( ))}
{/* Pay-per-invitation tier */}
{payPer.map((p, i) => (
{p.highlight &&
MÁS POPULAR
} {p.badge && !p.highlight &&
{p.badge}
}
{p.name}
{p.tag}
{p.price}
{p.sub}
{p.features.map((f, j) => (
{f}
))}
))}
{/* Planner subscription — wide card */}
¿CREAS INVITACIONES PARA OTROS?
{planner.name}
{planner.tag}
{planner.features.map((f, j) => (
{f}
))}
{planner.price}
{planner.sub}
14 días de prueba · sin tarjeta
{/* FAQ strip */}
{[ { q: '¿Qué incluye una invitación?', a: 'Diseño completo con Mariana, invitados ilimitados, RSVP, recordatorios y link público.' }, { q: '¿Los créditos caducan?', a: 'Nunca. Compra un paquete y úsalo cuando tengas el evento.' }, { q: '¿Puedo cambiar después?', a: 'Sí. Empieza con una invitación y si creas más, te hacemos descuento al paquete.' }, ].map((f, i) => (
{f.q}
{f.a}
))}
); }; // Profile const ProfileScreen = ({ palette, onSectionChange, onLogout }) => { const [user, setUser] = React.useState(null); const [name, setName] = React.useState(''); const [phone, setPhone] = React.useState(''); const [city, setCity] = React.useState(''); const [saving, setSaving] = React.useState(false); const [saveMsg, setSaveMsg] = React.useState(''); const [prefs, setPrefs] = React.useState({ emailReminders: true, marianaIdeas: true, newsletter: false }); React.useEffect(() => { const fetchMe = async () => { try { const res = await window.clientFetch(window.BACKEND + '/auth/me'); if (res.ok) { const data = await res.json(); const u = data.user || data; setUser(u); setName(u.name || ''); setPhone(u.phone || ''); setCity(u.city || ''); } } catch {} }; fetchMe(); }, []); const saveProfile = async () => { setSaving(true); setSaveMsg(''); try { const res = await window.clientFetch(window.BACKEND + '/auth/profile', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, phone, city }), }); if (res.ok) { setSaveMsg('✓ Guardado'); setTimeout(() => setSaveMsg(''), 2500); } else { setSaveMsg('Error al guardar'); } } catch { setSaveMsg('Error de conexión'); } setSaving(false); }; const initial = (name || user?.name || '?')[0].toUpperCase(); const joinDate = user?.createdAt ? new Date(user.createdAt).toLocaleDateString('es-MX', { month: 'long', year: 'numeric' }) : ''; return (
Configuración

Tu perfil

{initial}
{name || user?.name || '...'}
{user?.email || ''} {joinDate ? ` · desde ${joinDate}` : ''}
Información personal
{saveMsg &&
{saveMsg}
}
{}}/>
Preferencias
{[ { key: 'emailReminders', label: 'Recordatorios por correo', sub: 'Cuando alguien confirma o tu evento se acerca' }, { key: 'marianaIdeas', label: 'Sugerencias de Mariana', sub: 'Ideas automáticas basadas en tus eventos' }, { key: 'newsletter', label: 'Newsletter mensual', sub: 'Plantillas nuevas y tendencias' }, ].map((p, i) => (
0 ? `1px solid ${palette.border}` : 'none' }}>
{p.label}
{p.sub}
setPrefs(prev => ({ ...prev, [p.key]: !prev[p.key] }))} style={{ width: 38, height: 22, borderRadius: 999, background: prefs[p.key] ? palette.accent : palette.border, position: 'relative', cursor: 'pointer', transition: 'background 0.15s' }}>
))}
Cerrar sesión
Saldrás de este dispositivo
); }; // ── Public marketing screens (no auth required) ────────────────────────────── const PublicNav = ({ palette, onBack, onGoLogin }) => ( ); const HowItWorksPublic = ({ palette, onBack, onGoLogin }) => { const steps = [ { n: '01', title: 'Describe tu evento', body: 'Cuéntale a Mariana qué celebras: boda, XV años, baby shower, cumpleaños. Ella te guía con preguntas simples.' }, { n: '02', title: 'Mariana diseña tu invitación', body: 'La IA elige colores, tipografías y layout según el tono de tu evento. Tú ajustas lo que quieras en tiempo real.' }, { n: '03', title: 'Comparte el link', body: 'Tu invitación queda en invitacion23.com/tu-evento. Compártela por WhatsApp, Instagram o QR. Los invitados confirman ahí mismo.' }, ]; return (
CÓMO FUNCIONA

De idea a invitación
en minutos

{steps.map((s, i) => (
{s.n}
{s.title}
{s.body}
))}
Sin tarjeta · primera invitación gratis
); }; const TemplatesPublic = ({ palette, onBack, onGoLogin }) => { const categories = [ { icon: 'heart', label: 'Boda', examples: ['Clásica', 'Íntima', 'Bohemia', 'Minimalista'] }, { icon: 'sparkle', label: 'XV Años', examples: ['Elegante', 'Rosa', 'Real', 'Moderna'] }, { icon: 'baby', label: 'Baby Shower', examples: ['Pastel', 'Revelación', 'Niña', 'Niño'] }, { icon: 'cake', label: 'Cumpleaños', examples: ['Infantil', 'Adulto', 'Sorpresa', 'Tropical'] }, { icon: 'graduation', label: 'Graduación', examples: ['Formal', 'Casual', 'Fiesta', 'Reunión'] }, { icon: 'calendar', label: 'Corporativo', examples: ['Cocktail', 'Gala', 'Conferencia', 'Retiro'] }, ]; return (
PLANTILLAS

Plantillas para cada
celebración

Mariana las personaliza con tus colores, textos y fotos.

{categories.map((c, i) => (
e.currentTarget.style.borderColor = palette.accent} onMouseLeave={e => e.currentTarget.style.borderColor = palette.borderHero}>
{c.label}
{c.examples.map((ex, j) => ( {ex} ))}
))}
Primera invitación gratis · sin tarjeta
); }; const PricingPublic = ({ palette, onBack, onGoLogin }) => { const [currency, setCurrency] = React.useState('mxn'); const P = (mxn, usd) => currency === 'mxn' ? `$${mxn}` : `$${usd}`; const unit = currency === 'mxn' ? 'MXN' : 'USD'; const plans = [ { name: 'Prueba', price: P(0, 0), sub: 'gratis', tag: 'Para conocer Mariana', features: ['1 invitación incluida', 'Todas las plantillas y diseño con IA', 'Hasta 20 invitados', 'Marca de agua invitacion23', 'Link para compartir'], cta: 'Crear gratis' }, { name: 'Una invitación', price: P(380, 20), sub: `${unit} · pago único`, tag: 'Un evento, sin suscripción', highlight: true, features: ['Sin marca de agua', 'Invitados ilimitados', 'Mariana IA ilimitada', 'RSVP + lista de invitados', 'Activa hasta 6 meses', 'Exportar a PDF'], cta: 'Comprar', badge: 'MÁS POPULAR' }, { name: 'Paquete 3', price: P(990, 52), sub: `${unit} · pago único`, tag: 'Ahorra ~13%', features: ['Todo lo de "Una invitación"', '3 invitaciones para usar cuando quieras', 'Créditos sin caducidad'], cta: 'Comprar paquete', badge: 'Ahorra $150' }, ]; return (
PRECIOS

Paga sólo por la invitación
que celebras

Sin suscripciones. Sin contratos.

{[{ id: 'mxn', l: 'MXN $' }, { id: 'usd', l: 'USD $' }].map(c => ( ))}
{plans.map((p, i) => (
{p.badge && p.highlight &&
{p.badge}
} {p.badge && !p.highlight &&
{p.badge}
}
{p.name}
{p.tag}
{p.price}
{p.sub}
{p.features.map((f, j) => (
{f}
))}
))}
); }; window.PublishedInvitation = PublishedInvitation; window.CalendarScreen = CalendarScreen; window.GuestsScreen = GuestsScreen; window.PlansScreen = PlansScreen; window.ProfileScreen = ProfileScreen; window.HowItWorksPublic = HowItWorksPublic; window.TemplatesPublic = TemplatesPublic; window.PricingPublic = PricingPublic;