UI finish
This commit is contained in:
parent
614cf1e7f5
commit
b469a09cde
2 changed files with 552 additions and 231 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { MessageSquareText, RefreshCw, Settings2 } from 'lucide-react'
|
||||
import { MessageSquareText, RefreshCw, Search, Send, Settings2, X } from 'lucide-react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import type { FormEvent } from 'react'
|
||||
import { API_BASE_URL } from '../../lib/apiBase'
|
||||
|
|
@ -26,6 +26,42 @@ function mediaUrl(mediaId: string) {
|
|||
return `${API_BASE_URL}/api/v1/admin/whatsapp/media/${mediaId}`
|
||||
}
|
||||
|
||||
function peerInitials(value: string) {
|
||||
const digits = value.replace(/\D/g, '')
|
||||
return digits.slice(-2) || 'WA'
|
||||
}
|
||||
|
||||
function formatChatTime(value: string) {
|
||||
const date = new Date(value)
|
||||
const now = new Date()
|
||||
const sameDay = date.toDateString() === now.toDateString()
|
||||
if (sameDay) {
|
||||
return date.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })
|
||||
}
|
||||
const yesterday = new Date(now)
|
||||
yesterday.setDate(now.getDate() - 1)
|
||||
if (date.toDateString() === yesterday.toDateString()) return 'Yesterday'
|
||||
return date.toLocaleDateString([], { month: 'short', day: 'numeric' })
|
||||
}
|
||||
|
||||
function formatDayLabel(value: string) {
|
||||
const date = new Date(value)
|
||||
const now = new Date()
|
||||
if (date.toDateString() === now.toDateString()) return 'Today'
|
||||
const yesterday = new Date(now)
|
||||
yesterday.setDate(now.getDate() - 1)
|
||||
if (date.toDateString() === yesterday.toDateString()) return 'Yesterday'
|
||||
return date.toLocaleDateString([], { weekday: 'short', month: 'short', day: 'numeric' })
|
||||
}
|
||||
|
||||
const SEND_MODES: { id: SendMode; label: string }[] = [
|
||||
{ id: 'text', label: 'Text' },
|
||||
{ id: 'template', label: 'Template' },
|
||||
{ id: 'media', label: 'Media' },
|
||||
{ id: 'buttons', label: 'Buttons' },
|
||||
{ id: 'reaction', label: 'Reaction' },
|
||||
]
|
||||
|
||||
export function AdminWhatsAppPage() {
|
||||
const [config, setConfig] = useState<AdminWhatsAppConfig | null>(null)
|
||||
const [messages, setMessages] = useState<AdminWhatsAppMessage[]>([])
|
||||
|
|
@ -57,8 +93,10 @@ export function AdminWhatsAppPage() {
|
|||
const [buttonThree, setButtonThree] = useState('')
|
||||
const [reactionEmoji, setReactionEmoji] = useState('👍')
|
||||
const [reactionTarget, setReactionTarget] = useState('')
|
||||
const [threadQuery, setThreadQuery] = useState('')
|
||||
|
||||
const chatEndRef = useRef<HTMLDivElement | null>(null)
|
||||
const replyRef = useRef<HTMLTextAreaElement | null>(null)
|
||||
|
||||
async function loadMessages(peer?: string | null) {
|
||||
const items = await adminApi.listWhatsAppMessages(300, peer || undefined)
|
||||
|
|
@ -104,12 +142,24 @@ export function AdminWhatsAppPage() {
|
|||
.sort((a, b) => (b.latest?.created_at ?? '').localeCompare(a.latest?.created_at ?? ''))
|
||||
}, [messages])
|
||||
|
||||
const visibleThreads = useMemo(() => {
|
||||
const query = threadQuery.trim().toLowerCase()
|
||||
if (!query) return threads
|
||||
return threads.filter(({ peer, latest }) => {
|
||||
const haystack = `${formatWaNumber(peer)} ${peer} ${latest.body ?? ''} ${latest.message_type}`.toLowerCase()
|
||||
return haystack.includes(query)
|
||||
})
|
||||
}, [threads, threadQuery])
|
||||
|
||||
const activeThread = useMemo(() => {
|
||||
if (!selectedPeer) return []
|
||||
const found = threads.find((item) => item.peer === selectedPeer)
|
||||
return found?.thread ?? []
|
||||
}, [threads, selectedPeer])
|
||||
|
||||
const activeLatest = activeThread[activeThread.length - 1]
|
||||
const activeFirst = activeThread[0]
|
||||
|
||||
useEffect(() => {
|
||||
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||
}, [activeThread.length, selectedPeer])
|
||||
|
|
@ -271,18 +321,28 @@ export function AdminWhatsAppPage() {
|
|||
)}
|
||||
|
||||
<div className="admin-whatsapp-messenger">
|
||||
<aside className="panel admin-whatsapp-sidebar">
|
||||
<div className="panel-heading">
|
||||
<aside className="admin-whatsapp-sidebar">
|
||||
<div className="admin-whatsapp-sidebar-head">
|
||||
<div>
|
||||
<h2>Conversations</h2>
|
||||
<p>{threads.length} active threads</p>
|
||||
<p>{threads.length} active {threads.length === 1 ? 'thread' : 'threads'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<label className="admin-whatsapp-search">
|
||||
<Search size={14} />
|
||||
<input
|
||||
value={threadQuery}
|
||||
onChange={(event) => setThreadQuery(event.target.value)}
|
||||
placeholder="Search by name, phone or message…"
|
||||
/>
|
||||
</label>
|
||||
{threads.length === 0 ? (
|
||||
<p className="admin-whatsapp-empty">No messages yet. Ask someone to message +91 94112 57916 or scan your Meta QR.</p>
|
||||
<p className="admin-whatsapp-empty">No messages yet. Ask someone to message {config ? formatWaNumber(config.business_phone) : '+91 94112 57916'} or scan your Meta QR.</p>
|
||||
) : visibleThreads.length === 0 ? (
|
||||
<p className="admin-whatsapp-empty">No conversations match that search.</p>
|
||||
) : (
|
||||
<div className="admin-whatsapp-thread-list compact">
|
||||
{threads.map(({ peer, latest, thread }) => (
|
||||
{visibleThreads.map(({ peer, latest, thread }) => (
|
||||
<button
|
||||
key={peer}
|
||||
type="button"
|
||||
|
|
@ -293,195 +353,293 @@ export function AdminWhatsAppPage() {
|
|||
if (lastInbound) markRead(lastInbound)
|
||||
}}
|
||||
>
|
||||
<strong>{formatWaNumber(peer)}</strong>
|
||||
<span>{latest.body || `[${latest.message_type}]`}</span>
|
||||
<small>{new Date(latest.created_at).toLocaleString()}</small>
|
||||
<span className="admin-whatsapp-avatar">{peerInitials(peer)}</span>
|
||||
<span className="admin-whatsapp-thread-copy">
|
||||
<strong>{formatWaNumber(peer)}</strong>
|
||||
<span>{latest.body || `[${latest.message_type}]`}</span>
|
||||
</span>
|
||||
<small>{formatChatTime(latest.created_at)}</small>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
|
||||
<section className="panel admin-whatsapp-chat">
|
||||
{!selectedPeer ? (
|
||||
<div className="admin-whatsapp-chat-empty">
|
||||
<p>Select a conversation to view messages and reply.</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="admin-whatsapp-chat-head">
|
||||
<strong>{formatWaNumber(selectedPeer)}</strong>
|
||||
<small>{activeThread.length} messages</small>
|
||||
<section className="admin-whatsapp-chat">
|
||||
<div className="admin-whatsapp-chat-head">
|
||||
{selectedPeer ? (
|
||||
<>
|
||||
<span className="admin-whatsapp-avatar">{peerInitials(selectedPeer)}</span>
|
||||
<div>
|
||||
<strong>{formatWaNumber(selectedPeer)}</strong>
|
||||
<small>{activeThread.length} messages · {activeLatest ? formatChatTime(activeLatest.created_at) : 'No activity'}</small>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div>
|
||||
<strong>Select a conversation</strong>
|
||||
<small>Or start one with a new recipient below.</small>
|
||||
</div>
|
||||
<div className="admin-whatsapp-chat-body">
|
||||
{activeThread.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={`admin-whatsapp-bubble ${item.direction === 'inbound' ? 'inbound' : 'outbound'}`}
|
||||
>
|
||||
{item.media?.id && ['image', 'sticker'].includes(item.message_type) && (
|
||||
<img src={mediaUrl(item.media.id)} alt={item.message_type} className="admin-whatsapp-media" />
|
||||
)}
|
||||
{item.media?.id && item.message_type === 'video' && (
|
||||
<video src={mediaUrl(item.media.id)} controls className="admin-whatsapp-media" />
|
||||
)}
|
||||
{item.media?.id && item.message_type === 'audio' && (
|
||||
<audio src={mediaUrl(item.media.id)} controls className="admin-whatsapp-media" />
|
||||
)}
|
||||
{item.media?.id && item.message_type === 'document' && (
|
||||
<a href={mediaUrl(item.media.id)} target="_blank" rel="noreferrer">Download document</a>
|
||||
)}
|
||||
<span>{item.body || `[${item.message_type} · ${item.status}]`}</span>
|
||||
<small>
|
||||
{new Date(item.created_at).toLocaleTimeString()}
|
||||
{item.status ? ` · ${item.status}` : ''}
|
||||
{item.wa_message_id ? ` · ${item.wa_message_id.slice(0, 12)}…` : ''}
|
||||
</small>
|
||||
{item.direction === 'inbound' && item.wa_message_id && (
|
||||
<button
|
||||
type="button"
|
||||
className="link-button"
|
||||
onClick={() => {
|
||||
setSendMode('reaction')
|
||||
setReactionTarget(item.wa_message_id || '')
|
||||
}}
|
||||
>
|
||||
React
|
||||
</button>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="admin-whatsapp-chat-body">
|
||||
{!selectedPeer ? (
|
||||
<div className="admin-whatsapp-chat-empty">
|
||||
<MessageSquareText size={28} />
|
||||
<p>Choose a thread on the left to read it and reply from the box below.</p>
|
||||
</div>
|
||||
) : (
|
||||
activeThread.map((item, index) => {
|
||||
const previous = activeThread[index - 1]
|
||||
const showDay = !previous || new Date(previous.created_at).toDateString() !== new Date(item.created_at).toDateString()
|
||||
return (
|
||||
<div key={item.id} className="admin-whatsapp-message-row">
|
||||
{showDay && <div className="admin-whatsapp-day">{formatDayLabel(item.created_at)}</div>}
|
||||
<div className={`admin-whatsapp-bubble ${item.direction === 'inbound' ? 'inbound' : 'outbound'}`}>
|
||||
{item.media?.id && ['image', 'sticker'].includes(item.message_type) && (
|
||||
<img src={mediaUrl(item.media.id)} alt={item.message_type} className="admin-whatsapp-media" />
|
||||
)}
|
||||
{item.media?.id && item.message_type === 'video' && (
|
||||
<video src={mediaUrl(item.media.id)} controls className="admin-whatsapp-media" />
|
||||
)}
|
||||
{item.media?.id && item.message_type === 'audio' && (
|
||||
<audio src={mediaUrl(item.media.id)} controls className="admin-whatsapp-media" />
|
||||
)}
|
||||
{item.media?.id && item.message_type === 'document' && (
|
||||
<a href={mediaUrl(item.media.id)} target="_blank" rel="noreferrer">Download document</a>
|
||||
)}
|
||||
<span>{item.body || `[${item.message_type} · ${item.status}]`}</span>
|
||||
<small>
|
||||
{new Date(item.created_at).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}
|
||||
{item.status ? ` · ${item.status}` : ''}
|
||||
{item.wa_message_id ? ` · ${item.wa_message_id.slice(0, 12)}…` : ''}
|
||||
</small>
|
||||
{item.direction === 'inbound' && item.wa_message_id && (
|
||||
<span className="admin-whatsapp-bubble-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="link-button"
|
||||
onClick={() => {
|
||||
setSendMode('text')
|
||||
setReplyTo(item.wa_message_id || '')
|
||||
replyRef.current?.focus()
|
||||
}}
|
||||
>
|
||||
Reply
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="link-button"
|
||||
onClick={() => {
|
||||
setSendMode('reaction')
|
||||
setReactionTarget(item.wa_message_id || '')
|
||||
}}
|
||||
>
|
||||
React
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div ref={chatEndRef} />
|
||||
)
|
||||
})
|
||||
)}
|
||||
<div ref={chatEndRef} />
|
||||
</div>
|
||||
|
||||
<form className="admin-whatsapp-composer" onSubmit={sendMessage}>
|
||||
<div className="admin-whatsapp-mode-tabs">
|
||||
{SEND_MODES.map((mode) => (
|
||||
<button
|
||||
key={mode.id}
|
||||
type="button"
|
||||
className={sendMode === mode.id ? 'active' : ''}
|
||||
onClick={() => setSendMode(mode.id)}
|
||||
>
|
||||
{mode.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="admin-whatsapp-form">
|
||||
{!selectedPeer && (
|
||||
<label>
|
||||
<span>New recipient</span>
|
||||
<input
|
||||
value={newPeer}
|
||||
onChange={(e) => setNewPeer(e.target.value)}
|
||||
placeholder="9411257916 or +91 94112 57916"
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
|
||||
{sendMode === 'text' && (
|
||||
<>
|
||||
{replyTo && (
|
||||
<div className="admin-whatsapp-reply-chip">
|
||||
<span>Replying to {replyTo.slice(0, 18)}…</span>
|
||||
<button type="button" onClick={() => setReplyTo('')} aria-label="Clear reply">
|
||||
<X size={12} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<label>
|
||||
<span>Message</span>
|
||||
<textarea
|
||||
ref={replyRef}
|
||||
value={textBody}
|
||||
onChange={(e) => setTextBody(e.target.value)}
|
||||
rows={3}
|
||||
placeholder="Type a message…"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>Reply to message ID (optional)</span>
|
||||
<input value={replyTo} onChange={(e) => setReplyTo(e.target.value)} placeholder="wamid…" />
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
|
||||
{sendMode === 'template' && (
|
||||
<>
|
||||
<label>
|
||||
<span>Approved template</span>
|
||||
<select value={templateName} onChange={(e) => setTemplateName(e.target.value)} required>
|
||||
<option value="">Select template</option>
|
||||
{templates.map((template) => (
|
||||
<option key={template.id ?? template.name} value={template.name}>
|
||||
{template.name} ({template.language})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Language code</span>
|
||||
<input value={templateLanguage} onChange={(e) => setTemplateLanguage(e.target.value)} />
|
||||
</label>
|
||||
<small>Templates work outside the 24-hour customer service window.</small>
|
||||
</>
|
||||
)}
|
||||
|
||||
{sendMode === 'media' && (
|
||||
<>
|
||||
<label>
|
||||
<span>Media type</span>
|
||||
<select value={mediaType} onChange={(e) => setMediaType(e.target.value as typeof mediaType)}>
|
||||
<option value="image">Image</option>
|
||||
<option value="document">Document</option>
|
||||
<option value="audio">Audio</option>
|
||||
<option value="video">Video</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Public HTTPS URL</span>
|
||||
<input value={mediaLink} onChange={(e) => setMediaLink(e.target.value)} placeholder="https://…" required />
|
||||
</label>
|
||||
<label>
|
||||
<span>Caption</span>
|
||||
<input value={mediaCaption} onChange={(e) => setMediaCaption(e.target.value)} />
|
||||
</label>
|
||||
{mediaType === 'document' && (
|
||||
<label>
|
||||
<span>Filename</span>
|
||||
<input value={mediaFilename} onChange={(e) => setMediaFilename(e.target.value)} />
|
||||
</label>
|
||||
)}
|
||||
<label>
|
||||
<span>Reply to message ID (optional)</span>
|
||||
<input value={replyTo} onChange={(e) => setReplyTo(e.target.value)} placeholder="wamid…" />
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
|
||||
{sendMode === 'buttons' && (
|
||||
<>
|
||||
<label>
|
||||
<span>Body</span>
|
||||
<textarea value={buttonBody} onChange={(e) => setButtonBody(e.target.value)} rows={3} required />
|
||||
</label>
|
||||
<label><span>Button 1</span><input value={buttonOne} onChange={(e) => setButtonOne(e.target.value)} required /></label>
|
||||
<label><span>Button 2</span><input value={buttonTwo} onChange={(e) => setButtonTwo(e.target.value)} /></label>
|
||||
<label><span>Button 3</span><input value={buttonThree} onChange={(e) => setButtonThree(e.target.value)} /></label>
|
||||
<label>
|
||||
<span>Reply to message ID (optional)</span>
|
||||
<input value={replyTo} onChange={(e) => setReplyTo(e.target.value)} placeholder="wamid…" />
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
|
||||
{sendMode === 'reaction' && (
|
||||
<>
|
||||
<label>
|
||||
<span>Message ID</span>
|
||||
<input value={reactionTarget} onChange={(e) => setReactionTarget(e.target.value)} required />
|
||||
</label>
|
||||
<label>
|
||||
<span>Emoji</span>
|
||||
<input value={reactionEmoji} onChange={(e) => setReactionEmoji(e.target.value)} required />
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
|
||||
{error && <p className="auth-error">{error}</p>}
|
||||
<div className="admin-whatsapp-composer-footer">
|
||||
<small>Within 24h of a customer message: free-form text, media, and buttons. Outside that window, use a template.</small>
|
||||
<button className="primary-button" type="submit" disabled={sending || !readyToSend || (!selectedPeer && !newPeer.trim())}>
|
||||
<Send size={14} />
|
||||
{sending ? 'Sending…' : 'Send'}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section className="panel admin-whatsapp-compose-panel">
|
||||
<div className="panel-heading">
|
||||
<div>
|
||||
<h2>Send</h2>
|
||||
<p>Supported: text, template, media, buttons, reaction, read receipts.</p>
|
||||
</div>
|
||||
<aside className="admin-whatsapp-details">
|
||||
<div className="admin-whatsapp-details-hero">
|
||||
<span className="admin-whatsapp-avatar large">{selectedPeer ? peerInitials(selectedPeer) : 'WA'}</span>
|
||||
<strong>{selectedPeer ? formatWaNumber(selectedPeer) : 'No contact selected'}</strong>
|
||||
<small>{selectedPeer ? 'WhatsApp conversation' : 'Pick a thread to see details'}</small>
|
||||
</div>
|
||||
<div className="admin-whatsapp-mode-tabs">
|
||||
{(['text', 'template', 'media', 'buttons', 'reaction'] as SendMode[]).map((mode) => (
|
||||
<button
|
||||
key={mode}
|
||||
type="button"
|
||||
className={sendMode === mode ? 'active' : ''}
|
||||
onClick={() => setSendMode(mode)}
|
||||
>
|
||||
{mode}
|
||||
<dl className="admin-whatsapp-details-list">
|
||||
<div>
|
||||
<dt>Phone</dt>
|
||||
<dd>{selectedPeer ? formatWaNumber(selectedPeer) : '—'}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Messages</dt>
|
||||
<dd>{selectedPeer ? activeThread.length : threads.length}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>First seen</dt>
|
||||
<dd>{activeFirst ? new Date(activeFirst.created_at).toLocaleString() : '—'}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Last message</dt>
|
||||
<dd>{activeLatest ? new Date(activeLatest.created_at).toLocaleString() : '—'}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Last status</dt>
|
||||
<dd>{activeLatest?.status || '—'}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Source</dt>
|
||||
<dd>{activeLatest ? (activeLatest.direction === 'inbound' ? 'Customer' : 'Business') : '—'}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Business line</dt>
|
||||
<dd>{config ? formatWaNumber(config.business_phone) : '+91 94112 57916'}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div className="admin-whatsapp-quick">
|
||||
<h3>Quick actions</h3>
|
||||
{SEND_MODES.map((mode) => (
|
||||
<button key={mode.id} type="button" onClick={() => setSendMode(mode.id)}>
|
||||
{mode.label === 'Text' ? 'Reply with text' : `Send ${mode.label.toLowerCase()}`}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<form className="admin-whatsapp-form" onSubmit={sendMessage}>
|
||||
{!selectedPeer && (
|
||||
<label>
|
||||
<span>New recipient</span>
|
||||
<input
|
||||
value={newPeer}
|
||||
onChange={(e) => setNewPeer(e.target.value)}
|
||||
placeholder="9411257916 or +91 94112 57916"
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
|
||||
{sendMode === 'text' && (
|
||||
<>
|
||||
<label>
|
||||
<span>Message</span>
|
||||
<textarea value={textBody} onChange={(e) => setTextBody(e.target.value)} rows={5} required />
|
||||
</label>
|
||||
<label>
|
||||
<span>Reply to message ID (optional)</span>
|
||||
<input value={replyTo} onChange={(e) => setReplyTo(e.target.value)} placeholder="wamid…" />
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
|
||||
{sendMode === 'template' && (
|
||||
<>
|
||||
<label>
|
||||
<span>Approved template</span>
|
||||
<select value={templateName} onChange={(e) => setTemplateName(e.target.value)} required>
|
||||
<option value="">Select template</option>
|
||||
{templates.map((template) => (
|
||||
<option key={template.id ?? template.name} value={template.name}>
|
||||
{template.name} ({template.language})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Language code</span>
|
||||
<input value={templateLanguage} onChange={(e) => setTemplateLanguage(e.target.value)} />
|
||||
</label>
|
||||
<small>Templates work outside the 24-hour customer service window.</small>
|
||||
</>
|
||||
)}
|
||||
|
||||
{sendMode === 'media' && (
|
||||
<>
|
||||
<label>
|
||||
<span>Media type</span>
|
||||
<select value={mediaType} onChange={(e) => setMediaType(e.target.value as typeof mediaType)}>
|
||||
<option value="image">Image</option>
|
||||
<option value="document">Document</option>
|
||||
<option value="audio">Audio</option>
|
||||
<option value="video">Video</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Public HTTPS URL</span>
|
||||
<input value={mediaLink} onChange={(e) => setMediaLink(e.target.value)} placeholder="https://…" required />
|
||||
</label>
|
||||
<label>
|
||||
<span>Caption</span>
|
||||
<input value={mediaCaption} onChange={(e) => setMediaCaption(e.target.value)} />
|
||||
</label>
|
||||
{mediaType === 'document' && (
|
||||
<label>
|
||||
<span>Filename</span>
|
||||
<input value={mediaFilename} onChange={(e) => setMediaFilename(e.target.value)} />
|
||||
</label>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{sendMode === 'buttons' && (
|
||||
<>
|
||||
<label>
|
||||
<span>Body</span>
|
||||
<textarea value={buttonBody} onChange={(e) => setButtonBody(e.target.value)} rows={4} required />
|
||||
</label>
|
||||
<label><span>Button 1</span><input value={buttonOne} onChange={(e) => setButtonOne(e.target.value)} required /></label>
|
||||
<label><span>Button 2</span><input value={buttonTwo} onChange={(e) => setButtonTwo(e.target.value)} /></label>
|
||||
<label><span>Button 3</span><input value={buttonThree} onChange={(e) => setButtonThree(e.target.value)} /></label>
|
||||
</>
|
||||
)}
|
||||
|
||||
{sendMode === 'reaction' && (
|
||||
<>
|
||||
<label>
|
||||
<span>Message ID</span>
|
||||
<input value={reactionTarget} onChange={(e) => setReactionTarget(e.target.value)} required />
|
||||
</label>
|
||||
<label>
|
||||
<span>Emoji</span>
|
||||
<input value={reactionEmoji} onChange={(e) => setReactionEmoji(e.target.value)} required />
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
|
||||
{error && <p className="auth-error">{error}</p>}
|
||||
<button className="primary-button" type="submit" disabled={sending || !readyToSend || (!selectedPeer && !newPeer.trim())}>
|
||||
{sending ? 'Sending…' : 'Send'}
|
||||
</button>
|
||||
<small>Within 24h of customer message: free-form text/media/buttons. Outside window: use template.</small>
|
||||
</form>
|
||||
</section>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue