From a0f052847799ba36e8ac5d7a50195fd46c767fc5 Mon Sep 17 00:00:00 2001 From: dvirlabs Date: Wed, 25 Feb 2026 03:24:00 +0200 Subject: [PATCH] Set the dynamic fileds for each template --- backend/custom_templates.json | 33 +- backend/main.py | 38 +- backend/schemas.py | 3 +- backend/whatsapp_templates.py | 2 + frontend/src/api/api.js | 8 +- frontend/src/components/EventForm.css | 50 ++- frontend/src/components/GuestList.jsx | 3 +- frontend/src/components/TemplateEditor.jsx | 47 ++- .../src/components/WhatsAppInviteModal.css | 31 ++ .../src/components/WhatsAppInviteModal.jsx | 378 ++++++++---------- 10 files changed, 332 insertions(+), 261 deletions(-) diff --git a/backend/custom_templates.json b/backend/custom_templates.json index 9e26dfe..6afe969 100644 --- a/backend/custom_templates.json +++ b/backend/custom_templates.json @@ -1 +1,32 @@ -{} \ No newline at end of file +{ + "wedding_invitation_by_vered": { + "meta_name": "wedding_invitation_by_vered", + "language_code": "he", + "friendly_name": "wedding_invitation_by_vered", + "description": "This template design be Vered", + "header_text": "", + "body_text": "היי {{1}},\nאנחנו שמחים ומתרגשים להזמין אותך לחתונה שלנו 🤍🥂\n\nנשמח מאוד לראותכם ביום {{2}} ה-{{3}} ב\"{{4}}\", {{5}}.\n\n{{6}} קבלת פנים 🍸\n{{7}} חופה 🤵🏻💍👰🏻‍♀️\n{{8}} ארוחה וריקודים 🕺🏻💃🏻\n\nמתרגשים לחגוג איתכם,\n{{9}} ו{{10}}\n👰🏻‍♀️🤍🤵🏻‍♂", + "header_params": [], + "body_params": [ + "אורח", + "שני", + "15/06", + "הרמוניה בגן", + "בכנות", + "18:15", + "19:15", + "20:00", + "ורד", + "דביר" + ], + "fallbacks": { + "contact_name": "דוד", + "groom_name": "דוד", + "bride_name": "ורד", + "venue": "אולם הגן", + "event_date": "15/06", + "event_time": "18:30", + "guest_link": "https://invy.dvirlabs.com/guest" + } + } +} \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index 1fcbe3b..24c0285 100644 --- a/backend/main.py +++ b/backend/main.py @@ -796,14 +796,16 @@ async def send_wedding_invitation_bulk( )) continue - # Format event details — form overrides take priority over DB values + # Build params — contact_name always comes from the guest record guest_name = f"{guest.first_name} {guest.last_name}".strip() or guest.first_name or "חבר" + + # Standard named params (built-in template keys) with DB fallbacks partner1 = (request_body.partner1_name or event.partner1_name or "").strip() partner2 = (request_body.partner2_name or event.partner2_name or "").strip() - venue = (request_body.venue or event.venue or event.location or "").strip() + venue = (request_body.venue or event.venue or event.location or "").strip() event_time = (request_body.event_time or event.event_time or "").strip() - # Convert event_date: YYYY-MM-DD (from form input) → DD/MM, or use DB date + # Convert event_date YYYY-MM-DD → DD/MM if still in ISO format (backend fallback) if request_body.event_date: try: from datetime import datetime as _dt @@ -814,23 +816,31 @@ async def send_wedding_invitation_bulk( else: event_date = event.date.strftime("%d/%m") if event.date else "" - # Build guest link guest_link = ( request_body.guest_link or event.guest_link or f"https://invy.dvirlabs.com/guest?event={event_id}" ).strip() - - result = await service.send_wedding_invitation( + + params = { + "contact_name": guest_name, # always auto from guest + "groom_name": partner1, + "bride_name": partner2, + "venue": venue, + "event_date": event_date, + "event_time": event_time, + "guest_link": guest_link, + } + + # Merge extra_params last so they fully override standard params + # (used by custom templates whose param keys differ from the built-in names) + if request_body.extra_params: + params.update(request_body.extra_params) + + result = await service.send_by_template_key( + key=request_body.template_key or "wedding_invitation", to_phone=to_phone, - guest_name=guest_name, - partner1_name=partner1, - partner2_name=partner2, - venue=venue, - event_date=event_date, - event_time=event_time, - guest_link=guest_link, - template_key=request_body.template_key, + params=params, ) results.append(schemas.WhatsAppSendResult( diff --git a/backend/schemas.py b/backend/schemas.py index fa2bf0f..b4cf410 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -1,5 +1,5 @@ from pydantic import BaseModel, Field -from typing import Optional, List +from typing import Optional, List, Dict from datetime import datetime from uuid import UUID @@ -190,6 +190,7 @@ class WhatsAppWeddingInviteRequest(BaseModel): event_date: Optional[str] = None # YYYY-MM-DD or DD/MM event_time: Optional[str] = None # HH:mm guest_link: Optional[str] = None # RSVP link + extra_params: Optional[Dict[str, str]] = None # Custom/extra param values keyed by param name class Config: from_attributes = True diff --git a/backend/whatsapp_templates.py b/backend/whatsapp_templates.py index 473ae2b..00ade83 100644 --- a/backend/whatsapp_templates.py +++ b/backend/whatsapp_templates.py @@ -215,6 +215,8 @@ def list_templates_for_frontend() -> list: "header_param_count": len(tpl["header_params"]), "body_param_count": len(tpl["body_params"]), "is_custom": key in custom_keys, + "body_params": tpl["body_params"], + "header_params": tpl["header_params"], "body_text": tpl.get("body_text", ""), "header_text": tpl.get("header_text", ""), } diff --git a/frontend/src/api/api.js b/frontend/src/api/api.js index 843535a..e04616a 100644 --- a/frontend/src/api/api.js +++ b/frontend/src/api/api.js @@ -241,17 +241,19 @@ export const deleteWhatsAppTemplate = async (key) => { return response.data } -export const sendWhatsAppInvitationToGuests = async (eventId, guestIds, formData, templateKey = 'wedding_invitation') => { +export const sendWhatsAppInvitationToGuests = async (eventId, guestIds, formData, templateKey = 'wedding_invitation', extraParams = null) => { const response = await api.post(`/events/${eventId}/whatsapp/invite`, { guest_ids: guestIds, template_key: templateKey, - // Form field overrides — take priority over DB values on the backend + // Standard named params — used by built-in templates (backend applies fallbacks) partner1_name: formData?.partner1 || null, partner2_name: formData?.partner2 || null, venue: formData?.venue || null, - event_date: formData?.eventDate || null, // YYYY-MM-DD, backend converts to DD/MM + event_date: formData?.eventDate || null, event_time: formData?.eventTime || null, guest_link: formData?.guestLink || null, + // Custom / extra params — used by custom templates; overrides standard params + extra_params: extraParams || null, }) return response.data } diff --git a/frontend/src/components/EventForm.css b/frontend/src/components/EventForm.css index ae81831..f9f8758 100644 --- a/frontend/src/components/EventForm.css +++ b/frontend/src/components/EventForm.css @@ -22,18 +22,19 @@ .event-form { position: relative; - background: white; - border-radius: 8px; + background: var(--color-background-secondary); + border: 1px solid var(--color-border); + border-radius: 12px; padding: 2rem; max-width: 500px; width: 90%; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + box-shadow: var(--shadow-heavy); } .event-form h2 { margin-top: 0; margin-bottom: 1.5rem; - color: #2c3e50; + color: var(--color-text); font-size: 1.5rem; } @@ -44,30 +45,39 @@ .form-group label { display: block; margin-bottom: 0.5rem; - color: #2c3e50; + color: var(--color-text-secondary); font-weight: 500; + font-size: 0.88rem; } .form-group input { width: 100%; padding: 0.75rem; - border: 1px solid #ddd; - border-radius: 4px; + border: 1.5px solid var(--color-border); + border-radius: 6px; font-size: 1rem; font-family: inherit; + background: var(--color-background); + color: var(--color-text); + transition: border-color 0.15s, box-shadow 0.15s; +} + +.form-group input::placeholder { + color: var(--color-text-light); } .form-group input:focus { outline: none; - border-color: #3498db; - box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.1); + border-color: var(--color-primary); + box-shadow: 0 0 0 3px rgba(82, 148, 255, 0.15); } .error-message { - background: #fee; - color: #c33; + background: var(--color-error-bg); + color: var(--color-danger); + border: 1px solid var(--color-danger); padding: 0.75rem; - border-radius: 4px; + border-radius: 6px; margin-bottom: 1rem; font-size: 0.9rem; } @@ -77,6 +87,8 @@ gap: 1rem; justify-content: flex-end; margin-top: 2rem; + padding-top: 1.25rem; + border-top: 1px solid var(--color-border); } .btn-cancel, @@ -91,21 +103,25 @@ } .btn-cancel { - background: #ecf0f1; - color: #2c3e50; + background: var(--color-background-tertiary); + color: var(--color-text-secondary); + border: 1.5px solid var(--color-border); } .btn-cancel:hover:not(:disabled) { - background: #d5dbdb; + background: var(--color-border); + color: var(--color-text); } .btn-submit { - background: #3498db; + background: var(--color-primary); color: white; } .btn-submit:hover:not(:disabled) { - background: #2980b9; + background: var(--color-primary-hover); + transform: translateY(-1px); + box-shadow: var(--shadow-medium); } .btn-submit:disabled { diff --git a/frontend/src/components/GuestList.jsx b/frontend/src/components/GuestList.jsx index 7e65cb0..374051b 100644 --- a/frontend/src/components/GuestList.jsx +++ b/frontend/src/components/GuestList.jsx @@ -281,7 +281,8 @@ function GuestList({ eventId, onBack, onShowMembers }) { eventId, Array.from(selectedGuestIds), data.formData, - data.templateKey || 'wedding_invitation' + data.templateKey || 'wedding_invitation', + data.extraParams || null ) // Clear selection after successful send diff --git a/frontend/src/components/TemplateEditor.jsx b/frontend/src/components/TemplateEditor.jsx index 3b3fe23..9405678 100644 --- a/frontend/src/components/TemplateEditor.jsx +++ b/frontend/src/components/TemplateEditor.jsx @@ -61,7 +61,9 @@ function renderPreview(text, paramKeys) { if (!text) return '' return text.replace(/\{\{(\d+)\}\}/g, (_m, n) => { const key = paramKeys[parseInt(n, 10) - 1] - return key ? (SAMPLE_MAP[key] || `[${key}]`) : `{{${n}}}` + if (!key) return `{{${n}}}` + // Known built-in key → use sample value; custom key → show the key name itself + return SAMPLE_MAP[key] || key }) } @@ -273,18 +275,31 @@ export default function TemplateEditor({ onBack }) {

{he.paramMapping}

+ {/* Shared datalist for suggestions */} + + {PARAM_OPTIONS.map(o => ( + + {hNums.map((n, i) => (
{he.headerParam} {`{{${n}}}`} - + className="te-param-select" + dir="ltr" + /> - {headerParamKeys[i] ? SAMPLE_MAP[headerParamKeys[i]] : ''} + {headerParamKeys[i] + ? (SAMPLE_MAP[headerParamKeys[i]] || headerParamKeys[i]) + : ''}
))} @@ -292,14 +307,20 @@ export default function TemplateEditor({ onBack }) {
{he.bodyParam} {`{{${n}}}`} - + className="te-param-select" + dir="ltr" + /> - {bodyParamKeys[i] ? SAMPLE_MAP[bodyParamKeys[i]] : ''} + {bodyParamKeys[i] + ? (SAMPLE_MAP[bodyParamKeys[i]] || bodyParamKeys[i]) + : ''}
))} diff --git a/frontend/src/components/WhatsAppInviteModal.css b/frontend/src/components/WhatsAppInviteModal.css index e8ae102..acb0304 100644 --- a/frontend/src/components/WhatsAppInviteModal.css +++ b/frontend/src/components/WhatsAppInviteModal.css @@ -137,6 +137,37 @@ opacity: 0.6; } +/* Dynamic params grid — 2 cols for wider fields, 1 col on mobile */ +.dynamic-params-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; + margin-top: 8px; +} + +/* Date / time / URL inputs span full width */ +.dynamic-params-grid .form-group:has(input[type="date"]), +.dynamic-params-grid .form-group:has(input[type="time"]), +.dynamic-params-grid .form-group:has(input[type="url"]) { + grid-column: span 1; +} + +.auto-param-note { + font-size: 0.82rem; + color: var(--color-text-secondary); + margin-bottom: 10px; + padding: 6px 10px; + background: var(--color-background-tertiary); + border-radius: 6px; + border-right: 3px solid var(--color-primary); +} + +@media (max-width: 520px) { + .dynamic-params-grid { + grid-template-columns: 1fr; + } +} + /* Message Preview */ .message-preview { background: var(--color-background-secondary); diff --git a/frontend/src/components/WhatsAppInviteModal.jsx b/frontend/src/components/WhatsAppInviteModal.jsx index b6a5bdf..7045edc 100644 --- a/frontend/src/components/WhatsAppInviteModal.jsx +++ b/frontend/src/components/WhatsAppInviteModal.jsx @@ -1,74 +1,74 @@ -import { useState, useEffect } from 'react' +import { useState, useEffect, useMemo } from 'react' import { getWhatsAppTemplates, deleteWhatsAppTemplate } from '../api/api' import './WhatsAppInviteModal.css' +// ── Known system parameter keys → field definitions ───────────────────────── +// contact_name is always resolved per-guest on the backend; never shown as a field. +const SYSTEM_FIELDS = { + contact_name: null, // skip — auto-filled from guest record + groom_name: { label: 'שם החתן', type: 'text', placeholder: 'דוד', required: true }, + bride_name: { label: 'שם הכלה', type: 'text', placeholder: 'ורד', required: true }, + venue: { label: 'שם האולם / מקום', type: 'text', placeholder: 'אולם הגן', required: true }, + event_date: { label: 'תאריך האירוע', type: 'date', required: true }, + event_time: { label: 'שעת ההתחלה (HH:mm)', type: 'time', required: true }, + guest_link: { label: 'קישור RSVP', type: 'url', placeholder: 'https://...', required: false }, +} + +// Map system key → eventData field to pre-fill from +const EVENT_PREFILL = { + groom_name: d => d?.partner1_name || '', + bride_name: d => d?.partner2_name || '', + venue: d => d?.venue || d?.location || '', + event_date: d => { + if (!d?.date) return '' + try { return new Date(d.date).toISOString().split('T')[0] } catch { return '' } + }, + event_time: d => d?.event_time || '', + guest_link: d => d?.guest_link || '', +} + +// Render a template's body_text replacing {{N}} with param values +function renderTemplatePreview(bodyText, bodyParams, params) { + if (!bodyText) return null + return bodyText.replace(/\{\{(\d+)\}\}/g, (_m, n) => { + const key = bodyParams?.[parseInt(n, 10) - 1] + if (!key || key === 'contact_name') return '[שם האורח]' + return params[key] || `[${key}]` + }) +} + const he = { title: 'שלח הזמנה בוואטסאפ', templateLabel: 'סוג הודעה', templateLoading: '...טוען תבניות', - partners: 'שמות בני הזוג', - partner1Name: 'שם החתן', - partner2Name: 'שם הכלה', - venue: 'שם האולם/מקום', - eventDate: 'תאריך האירוע', - eventTime: 'שעת ההתחלה (HH:mm)', - guestLink: 'קישור RSVP', selectedGuests: 'אורחים שנבחרו', - guestCount: '{count} אורחים', - allFields: 'יש למלא את כל השדות החובה', noPhone: 'אין טלפון', noPhones: 'לא נבחר אורח עם טלפון', + allFields: 'יש למלא את כל השדות החובה', sending: 'שולח הזמנות...', send: 'שלח הזמנות', cancel: 'ביטול', close: 'סגור', results: 'תוצאות שליחה', - succeeded: 'התוצאות הצליחו', + succeeded: 'הצליחו', failed: 'נכשלו', success: 'הצליח', error: 'שגיאה', preview: 'תצוגה מקדימה של ההודעה', - guestFirstName: 'שם האורח', - backToList: 'חזור לרשימה' + autoGuest: '(שם האורח ממולא אוטומטית)', + paramsSection: 'פרמטרי ההודעה', } function WhatsAppInviteModal({ isOpen, onClose, selectedGuests = [], eventData = {}, onSend }) { - const [formData, setFormData] = useState({ - partner1: '', - partner2: '', - venue: '', - eventDate: '', - eventTime: '', - guestLink: '' - }) - - const [templates, setTemplates] = useState([]) + const [params, setParams] = useState({}) + const [templates, setTemplates] = useState([]) const [selectedTemplateKey, setSelectedTemplateKey] = useState('wedding_invitation') const [templatesLoading, setTemplatesLoading] = useState(false) - const [sending, setSending] = useState(false) - const [results, setResults] = useState(null) - const [showResults, setShowResults] = useState(false) + const [sending, setSending] = useState(false) + const [results, setResults] = useState(null) + const [showResults, setShowResults] = useState(false) - // Initialize form with event data - useEffect(() => { - if (eventData) { - let eventDate = '' - if (eventData.date) { - const dateObj = new Date(eventData.date) - eventDate = dateObj.toISOString().split('T')[0] - } - setFormData({ - partner1: eventData.partner1_name || '', - partner2: eventData.partner2_name || '', - venue: eventData.venue || eventData.location || '', - eventDate: eventDate, - eventTime: eventData.event_time || '', - guestLink: eventData.guest_link || '' - }) - } - }, [eventData, isOpen]) - - // Fetch available templates when modal opens (or after saving a new template) + // Fetch templates when modal opens const fetchTemplates = () => { setTemplatesLoading(true) getWhatsAppTemplates() @@ -78,14 +78,41 @@ function WhatsAppInviteModal({ isOpen, onClose, selectedGuests = [], eventData = setSelectedTemplateKey(data.templates[0].key) } }) - .catch(err => console.error('Failed to load templates:', err)) + .catch(console.error) .finally(() => setTemplatesLoading(false)) } + useEffect(() => { if (isOpen) fetchTemplates() }, [isOpen]) + + // Derive selected template object + const selectedTemplate = useMemo( + () => templates.find(t => t.key === selectedTemplateKey) || null, + [templates, selectedTemplateKey] + ) + + // Unique param keys for this template (header + body, deduplicated, skip contact_name) + const paramKeys = useMemo(() => { + if (!selectedTemplate) return [] + const all = [ + ...(selectedTemplate.header_params || []), + ...(selectedTemplate.body_params || []), + ] + const seen = new Set() + return all.filter(k => { + if (k === 'contact_name' || seen.has(k)) return false + seen.add(k); return true + }) + }, [selectedTemplate]) + + // Re-init params whenever template or eventData changes useEffect(() => { - if (!isOpen) return - fetchTemplates() - }, [isOpen]) + const initial = {} + for (const key of paramKeys) { + const prefill = EVENT_PREFILL[key] + initial[key] = prefill ? prefill(eventData) : '' + } + setParams(initial) + }, [selectedTemplateKey, paramKeys.join(','), isOpen]) const handleDeleteTemplate = async (key) => { if (!window.confirm(`האם למחוק את התבנית "${key}"? פעולה זו אינה הפיכה.`)) return @@ -98,43 +125,51 @@ function WhatsAppInviteModal({ isOpen, onClose, selectedGuests = [], eventData = } } - const handleInputChange = (e) => { - const { name, value } = e.target - setFormData(prev => ({ - ...prev, - [name]: value - })) - } - const validateForm = () => { - // Check required fields - if (!formData.partner1 || !formData.partner2 || !formData.venue || !formData.eventDate || !formData.eventTime) { - alert(he.allFields) - return false - } + const hasPhones = selectedGuests.some(g => g.phone_number || g.phone) + if (!hasPhones) { alert(he.noPhones); return false } - // Check if any selected guest has a phone - const hasPhones = selectedGuests.some(guest => guest.phone_number || guest.phone) - if (!hasPhones) { - alert(he.noPhones) - return false + for (const key of paramKeys) { + const sysDef = SYSTEM_FIELDS[key] + const isRequired = sysDef ? sysDef.required : true // custom keys are required + if (isRequired && !params[key]?.trim()) { + const label = sysDef ? sysDef.label : key + alert(`יש למלא: ${label}`) + return false + } } - return true } const handleSend = async () => { if (!validateForm()) return - - setSending(true) - setResults(null) - + setSending(true); setResults(null) try { if (onSend) { + // Build extra_params: convert event_date to DD/MM if in YYYY-MM-DD format + const extraParams = { ...params } + if (extraParams.event_date) { + try { + const [y, m, d] = extraParams.event_date.split('-') + if (y && m && d) extraParams.event_date = `${d}/${m}` + } catch {} + } + + // Also provide legacy formData for backward compat + const formData = { + partner1: params.groom_name || '', + partner2: params.bride_name || '', + venue: params.venue || '', + eventDate: params.event_date || '', + eventTime: params.event_time || '', + guestLink: params.guest_link || '', + } + const result = await onSend({ formData, guestIds: selectedGuests.map(g => g.id), templateKey: selectedTemplateKey, + extraParams, }) setResults(result) setShowResults(true) @@ -158,21 +193,16 @@ function WhatsAppInviteModal({ isOpen, onClose, selectedGuests = [], eventData = } } - const handleClose = () => { - setResults(null) - setShowResults(false) - onClose() - } + const handleClose = () => { setResults(null); setShowResults(false); onClose() } if (!isOpen) return null - // Show results screen + // ── Results screen ──────────────────────────────────────────────────────── if (showResults && results) { return (
e.stopPropagation()}>

{he.results}

-
{results.succeeded}
@@ -183,44 +213,39 @@ function WhatsAppInviteModal({ isOpen, onClose, selectedGuests = [], eventData =
{he.failed}
-
- {results.results.map((result, idx) => ( -
+ {results.results.map((r, idx) => ( +
- {result.guest_name} - - {result.status === 'sent' ? he.success : he.error} - + {r.guest_name} + {r.status === 'sent' ? he.success : he.error}
-
{result.phone}
- {result.error && ( -
{result.error}
- )} +
{r.phone}
+ {r.error &&
{r.error}
}
))}
-
- +
) } - // Show form screen + // ── Form screen ─────────────────────────────────────────────────────────── + const previewText = renderTemplatePreview( + selectedTemplate?.body_text, + selectedTemplate?.body_params, + params + ) + return (
e.stopPropagation()}>

{he.title}

- {/* Template selector */} + {/* ── Template selector ── */}
@@ -245,30 +270,25 @@ function WhatsAppInviteModal({ isOpen, onClose, selectedGuests = [], eventData = ))} - {templates.find(t => t.key === selectedTemplateKey)?.is_custom && ( + {selectedTemplate?.is_custom && ( + >🗑️ )}
)} - {templates.find(t => t.key === selectedTemplateKey)?.description && ( - - {templates.find(t => t.key === selectedTemplateKey).description} - + {selectedTemplate?.description && ( + {selectedTemplate.description} )}
+ {/* ── Guests list ── */}
-
- {he.selectedGuests} ({selectedGuests.length}) -
+
{he.selectedGuests} ({selectedGuests.length})
{selectedGuests.map((guest, idx) => (
@@ -281,124 +301,60 @@ function WhatsAppInviteModal({ isOpen, onClose, selectedGuests = [], eventData =
- {/* Form */} + {/* ── Dynamic param form ── */}
-

{he.partners}

-
-
- - -
-
- - -
-
-
+

{he.paramsSection}

-
-
- - -
-
+ {/* contact_name note */} + {(selectedTemplate?.header_params?.includes('contact_name') || + selectedTemplate?.body_params?.includes('contact_name')) && ( +

👤 {he.autoGuest}

+ )} -
-
-
- - -
-
- - -
-
-
+
+ {paramKeys.map(key => { + const sysDef = SYSTEM_FIELDS[key] + if (sysDef === null) return null // explicitly skip (contact_name) + const label = sysDef?.label || key + const inputType = sysDef?.type || 'text' + const placeholder = sysDef?.placeholder || '' + const required = sysDef ? sysDef.required : true -
-
- - + return ( +
+ + setParams(p => ({ ...p, [key]: e.target.value }))} + placeholder={placeholder} + disabled={sending} + dir={inputType === 'text' || inputType === 'url' ? 'rtl' : undefined} + /> +
+ ) + })}
- {/* Message Preview */} + {/* ── Message preview ── */}
{he.preview}
- {`היי ${formData.partner1 ? formData.partner1 : he.guestFirstName} 🤍 - -זה קורה! 🎉 -${formData.partner1} ו-${formData.partner2} מתחתנים ונשמח שתהיה/י איתנו ברגע המיוחד הזה ✨ - -📍 האולם: "${formData.venue}" -📅 התאריך: ${formData.eventDate ? (() => { try { const [y,m,d] = formData.eventDate.split('-'); return `${d}/${m}` } catch { return formData.eventDate } })() : ''} -🕒 השעה: ${formData.eventTime || '—'} - -לאישור הגעה ופרטים נוספים: -${formData.guestLink || '[קישור RSVP]'} - -מתרגשים ומצפים לראותך 💞`} + {previewText + ? previewText + : (selectedTemplate?.body_text || '— בחר תבנית —')}
- {/* Buttons */} + {/* ── Buttons ── */}
- -