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 { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import type { FormEvent } from 'react'
|
import type { FormEvent } from 'react'
|
||||||
import { API_BASE_URL } from '../../lib/apiBase'
|
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}`
|
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() {
|
export function AdminWhatsAppPage() {
|
||||||
const [config, setConfig] = useState<AdminWhatsAppConfig | null>(null)
|
const [config, setConfig] = useState<AdminWhatsAppConfig | null>(null)
|
||||||
const [messages, setMessages] = useState<AdminWhatsAppMessage[]>([])
|
const [messages, setMessages] = useState<AdminWhatsAppMessage[]>([])
|
||||||
|
|
@ -57,8 +93,10 @@ export function AdminWhatsAppPage() {
|
||||||
const [buttonThree, setButtonThree] = useState('')
|
const [buttonThree, setButtonThree] = useState('')
|
||||||
const [reactionEmoji, setReactionEmoji] = useState('👍')
|
const [reactionEmoji, setReactionEmoji] = useState('👍')
|
||||||
const [reactionTarget, setReactionTarget] = useState('')
|
const [reactionTarget, setReactionTarget] = useState('')
|
||||||
|
const [threadQuery, setThreadQuery] = useState('')
|
||||||
|
|
||||||
const chatEndRef = useRef<HTMLDivElement | null>(null)
|
const chatEndRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
const replyRef = useRef<HTMLTextAreaElement | null>(null)
|
||||||
|
|
||||||
async function loadMessages(peer?: string | null) {
|
async function loadMessages(peer?: string | null) {
|
||||||
const items = await adminApi.listWhatsAppMessages(300, peer || undefined)
|
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 ?? ''))
|
.sort((a, b) => (b.latest?.created_at ?? '').localeCompare(a.latest?.created_at ?? ''))
|
||||||
}, [messages])
|
}, [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(() => {
|
const activeThread = useMemo(() => {
|
||||||
if (!selectedPeer) return []
|
if (!selectedPeer) return []
|
||||||
const found = threads.find((item) => item.peer === selectedPeer)
|
const found = threads.find((item) => item.peer === selectedPeer)
|
||||||
return found?.thread ?? []
|
return found?.thread ?? []
|
||||||
}, [threads, selectedPeer])
|
}, [threads, selectedPeer])
|
||||||
|
|
||||||
|
const activeLatest = activeThread[activeThread.length - 1]
|
||||||
|
const activeFirst = activeThread[0]
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||||
}, [activeThread.length, selectedPeer])
|
}, [activeThread.length, selectedPeer])
|
||||||
|
|
@ -271,18 +321,28 @@ export function AdminWhatsAppPage() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="admin-whatsapp-messenger">
|
<div className="admin-whatsapp-messenger">
|
||||||
<aside className="panel admin-whatsapp-sidebar">
|
<aside className="admin-whatsapp-sidebar">
|
||||||
<div className="panel-heading">
|
<div className="admin-whatsapp-sidebar-head">
|
||||||
<div>
|
<div>
|
||||||
<h2>Conversations</h2>
|
<h2>Conversations</h2>
|
||||||
<p>{threads.length} active threads</p>
|
<p>{threads.length} active {threads.length === 1 ? 'thread' : 'threads'}</p>
|
||||||
</div>
|
</div>
|
||||||
</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 ? (
|
{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">
|
<div className="admin-whatsapp-thread-list compact">
|
||||||
{threads.map(({ peer, latest, thread }) => (
|
{visibleThreads.map(({ peer, latest, thread }) => (
|
||||||
<button
|
<button
|
||||||
key={peer}
|
key={peer}
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -293,32 +353,50 @@ export function AdminWhatsAppPage() {
|
||||||
if (lastInbound) markRead(lastInbound)
|
if (lastInbound) markRead(lastInbound)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<span className="admin-whatsapp-avatar">{peerInitials(peer)}</span>
|
||||||
|
<span className="admin-whatsapp-thread-copy">
|
||||||
<strong>{formatWaNumber(peer)}</strong>
|
<strong>{formatWaNumber(peer)}</strong>
|
||||||
<span>{latest.body || `[${latest.message_type}]`}</span>
|
<span>{latest.body || `[${latest.message_type}]`}</span>
|
||||||
<small>{new Date(latest.created_at).toLocaleString()}</small>
|
</span>
|
||||||
|
<small>{formatChatTime(latest.created_at)}</small>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<section className="panel admin-whatsapp-chat">
|
<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>
|
||||||
|
|
||||||
|
<div className="admin-whatsapp-chat-body">
|
||||||
{!selectedPeer ? (
|
{!selectedPeer ? (
|
||||||
<div className="admin-whatsapp-chat-empty">
|
<div className="admin-whatsapp-chat-empty">
|
||||||
<p>Select a conversation to view messages and reply.</p>
|
<MessageSquareText size={28} />
|
||||||
|
<p>Choose a thread on the left to read it and reply from the box below.</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
activeThread.map((item, index) => {
|
||||||
<div className="admin-whatsapp-chat-head">
|
const previous = activeThread[index - 1]
|
||||||
<strong>{formatWaNumber(selectedPeer)}</strong>
|
const showDay = !previous || new Date(previous.created_at).toDateString() !== new Date(item.created_at).toDateString()
|
||||||
<small>{activeThread.length} messages</small>
|
return (
|
||||||
</div>
|
<div key={item.id} className="admin-whatsapp-message-row">
|
||||||
<div className="admin-whatsapp-chat-body">
|
{showDay && <div className="admin-whatsapp-day">{formatDayLabel(item.created_at)}</div>}
|
||||||
{activeThread.map((item) => (
|
<div className={`admin-whatsapp-bubble ${item.direction === 'inbound' ? 'inbound' : 'outbound'}`}>
|
||||||
<div
|
|
||||||
key={item.id}
|
|
||||||
className={`admin-whatsapp-bubble ${item.direction === 'inbound' ? 'inbound' : 'outbound'}`}
|
|
||||||
>
|
|
||||||
{item.media?.id && ['image', 'sticker'].includes(item.message_type) && (
|
{item.media?.id && ['image', 'sticker'].includes(item.message_type) && (
|
||||||
<img src={mediaUrl(item.media.id)} alt={item.message_type} className="admin-whatsapp-media" />
|
<img src={mediaUrl(item.media.id)} alt={item.message_type} className="admin-whatsapp-media" />
|
||||||
)}
|
)}
|
||||||
|
|
@ -333,11 +411,23 @@ export function AdminWhatsAppPage() {
|
||||||
)}
|
)}
|
||||||
<span>{item.body || `[${item.message_type} · ${item.status}]`}</span>
|
<span>{item.body || `[${item.message_type} · ${item.status}]`}</span>
|
||||||
<small>
|
<small>
|
||||||
{new Date(item.created_at).toLocaleTimeString()}
|
{new Date(item.created_at).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}
|
||||||
{item.status ? ` · ${item.status}` : ''}
|
{item.status ? ` · ${item.status}` : ''}
|
||||||
{item.wa_message_id ? ` · ${item.wa_message_id.slice(0, 12)}…` : ''}
|
{item.wa_message_id ? ` · ${item.wa_message_id.slice(0, 12)}…` : ''}
|
||||||
</small>
|
</small>
|
||||||
{item.direction === 'inbound' && item.wa_message_id && (
|
{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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="link-button"
|
className="link-button"
|
||||||
|
|
@ -348,35 +438,30 @@ export function AdminWhatsAppPage() {
|
||||||
>
|
>
|
||||||
React
|
React
|
||||||
</button>
|
</button>
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)}
|
||||||
<div ref={chatEndRef} />
|
<div ref={chatEndRef} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="panel admin-whatsapp-compose-panel">
|
<form className="admin-whatsapp-composer" onSubmit={sendMessage}>
|
||||||
<div className="panel-heading">
|
|
||||||
<div>
|
|
||||||
<h2>Send</h2>
|
|
||||||
<p>Supported: text, template, media, buttons, reaction, read receipts.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-whatsapp-mode-tabs">
|
<div className="admin-whatsapp-mode-tabs">
|
||||||
{(['text', 'template', 'media', 'buttons', 'reaction'] as SendMode[]).map((mode) => (
|
{SEND_MODES.map((mode) => (
|
||||||
<button
|
<button
|
||||||
key={mode}
|
key={mode.id}
|
||||||
type="button"
|
type="button"
|
||||||
className={sendMode === mode ? 'active' : ''}
|
className={sendMode === mode.id ? 'active' : ''}
|
||||||
onClick={() => setSendMode(mode)}
|
onClick={() => setSendMode(mode.id)}
|
||||||
>
|
>
|
||||||
{mode}
|
{mode.label}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<form className="admin-whatsapp-form" onSubmit={sendMessage}>
|
<div className="admin-whatsapp-form">
|
||||||
{!selectedPeer && (
|
{!selectedPeer && (
|
||||||
<label>
|
<label>
|
||||||
<span>New recipient</span>
|
<span>New recipient</span>
|
||||||
|
|
@ -390,9 +475,24 @@ export function AdminWhatsAppPage() {
|
||||||
|
|
||||||
{sendMode === 'text' && (
|
{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>
|
<label>
|
||||||
<span>Message</span>
|
<span>Message</span>
|
||||||
<textarea value={textBody} onChange={(e) => setTextBody(e.target.value)} rows={5} required />
|
<textarea
|
||||||
|
ref={replyRef}
|
||||||
|
value={textBody}
|
||||||
|
onChange={(e) => setTextBody(e.target.value)}
|
||||||
|
rows={3}
|
||||||
|
placeholder="Type a message…"
|
||||||
|
required
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<span>Reply to message ID (optional)</span>
|
<span>Reply to message ID (optional)</span>
|
||||||
|
|
@ -447,6 +547,10 @@ export function AdminWhatsAppPage() {
|
||||||
<input value={mediaFilename} onChange={(e) => setMediaFilename(e.target.value)} />
|
<input value={mediaFilename} onChange={(e) => setMediaFilename(e.target.value)} />
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
<label>
|
||||||
|
<span>Reply to message ID (optional)</span>
|
||||||
|
<input value={replyTo} onChange={(e) => setReplyTo(e.target.value)} placeholder="wamid…" />
|
||||||
|
</label>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -454,11 +558,15 @@ export function AdminWhatsAppPage() {
|
||||||
<>
|
<>
|
||||||
<label>
|
<label>
|
||||||
<span>Body</span>
|
<span>Body</span>
|
||||||
<textarea value={buttonBody} onChange={(e) => setButtonBody(e.target.value)} rows={4} required />
|
<textarea value={buttonBody} onChange={(e) => setButtonBody(e.target.value)} rows={3} required />
|
||||||
</label>
|
</label>
|
||||||
<label><span>Button 1</span><input value={buttonOne} onChange={(e) => setButtonOne(e.target.value)} 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 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>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>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -476,12 +584,62 @@ export function AdminWhatsAppPage() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{error && <p className="auth-error">{error}</p>}
|
{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())}>
|
<button className="primary-button" type="submit" disabled={sending || !readyToSend || (!selectedPeer && !newPeer.trim())}>
|
||||||
|
<Send size={14} />
|
||||||
{sending ? 'Sending…' : 'Send'}
|
{sending ? 'Sending…' : 'Send'}
|
||||||
</button>
|
</button>
|
||||||
<small>Within 24h of customer message: free-form text/media/buttons. Outside window: use template.</small>
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -857,6 +857,7 @@ main { padding: 30px clamp(20px, 3vw, 44px) 50px; }
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
.admin-content, .admin-login-page { padding: 28px; }
|
.admin-content, .admin-login-page { padding: 28px; }
|
||||||
|
.admin-content.admin-whatsapp-page { max-width: none; padding: 18px 20px 24px; }
|
||||||
.admin-login-page { min-height: calc(100vh - 72px); display: grid; place-items: center; }
|
.admin-login-page { min-height: calc(100vh - 72px); display: grid; place-items: center; }
|
||||||
.admin-login-card { width: min(420px, 100%); }
|
.admin-login-card { width: min(420px, 100%); }
|
||||||
.admin-kyc-card { margin-bottom: 18px; }
|
.admin-kyc-card { margin-bottom: 18px; }
|
||||||
|
|
@ -954,21 +955,22 @@ main { padding: 30px clamp(20px, 3vw, 44px) 50px; }
|
||||||
.admin-whatsapp-bubble {
|
.admin-whatsapp-bubble {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
max-width: 88%;
|
max-width: min(78%, 460px);
|
||||||
padding: 8px 10px;
|
padding: 10px 12px;
|
||||||
border-radius: 10px;
|
border-radius: 14px;
|
||||||
font-size: 11px;
|
font-size: 13px;
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
|
box-shadow: 0 1px 1px rgba(18, 31, 54, .04);
|
||||||
}
|
}
|
||||||
.admin-whatsapp-bubble.inbound {
|
.admin-whatsapp-bubble.inbound {
|
||||||
justify-self: start;
|
justify-self: start;
|
||||||
background: white;
|
background: white;
|
||||||
border: 1px solid #dfe5ed;
|
border-bottom-left-radius: 4px;
|
||||||
}
|
}
|
||||||
.admin-whatsapp-bubble.outbound {
|
.admin-whatsapp-bubble.outbound {
|
||||||
justify-self: end;
|
justify-self: end;
|
||||||
background: #e8f2ff;
|
background: #d9fdd3;
|
||||||
border: 1px solid #c8ddff;
|
border-bottom-right-radius: 4px;
|
||||||
}
|
}
|
||||||
.admin-whatsapp-bubble small { color: #8a95a7; font-size: 9px; }
|
.admin-whatsapp-bubble small { color: #8a95a7; font-size: 9px; }
|
||||||
.admin-whatsapp-form {
|
.admin-whatsapp-form {
|
||||||
|
|
@ -995,7 +997,7 @@ main { padding: 30px clamp(20px, 3vw, 44px) 50px; }
|
||||||
color: #1f2a3d;
|
color: #1f2a3d;
|
||||||
background: white;
|
background: white;
|
||||||
}
|
}
|
||||||
.admin-whatsapp-form textarea { resize: vertical; min-height: 120px; }
|
.admin-whatsapp-form textarea { resize: vertical; min-height: 72px; }
|
||||||
.admin-whatsapp-form small {
|
.admin-whatsapp-form small {
|
||||||
color: #8a95a7;
|
color: #8a95a7;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
|
@ -1004,12 +1006,13 @@ main { padding: 30px clamp(20px, 3vw, 44px) 50px; }
|
||||||
@media (max-width: 960px) {
|
@media (max-width: 960px) {
|
||||||
.admin-whatsapp-grid { grid-template-columns: 1fr; }
|
.admin-whatsapp-grid { grid-template-columns: 1fr; }
|
||||||
}
|
}
|
||||||
.admin-whatsapp-page { max-width: 1400px; }
|
.admin-whatsapp-page { max-width: none; }
|
||||||
.admin-whatsapp-heading {
|
.admin-whatsapp-heading {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
margin-bottom: 14px;
|
||||||
}
|
}
|
||||||
.admin-whatsapp-heading-actions {
|
.admin-whatsapp-heading-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -1019,99 +1022,257 @@ main { padding: 30px clamp(20px, 3vw, 44px) 50px; }
|
||||||
}
|
}
|
||||||
.admin-whatsapp-messenger {
|
.admin-whatsapp-messenger {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 260px minmax(0, 1fr) 320px;
|
grid-template-columns: 300px minmax(0, 1fr) 280px;
|
||||||
gap: 16px;
|
min-height: calc(100vh - 210px);
|
||||||
align-items: start;
|
background: white;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 10px 30px rgba(18, 31, 54, .06);
|
||||||
}
|
}
|
||||||
.admin-whatsapp-sidebar,
|
.admin-whatsapp-sidebar,
|
||||||
.admin-whatsapp-chat,
|
.admin-whatsapp-chat,
|
||||||
.admin-whatsapp-compose-panel {
|
.admin-whatsapp-details {
|
||||||
min-height: 640px;
|
min-height: 0;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
.admin-whatsapp-sidebar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border-right: 1px solid #edf1f6;
|
||||||
|
background: #fbfcfe;
|
||||||
|
}
|
||||||
|
.admin-whatsapp-sidebar-head {
|
||||||
|
padding: 18px 16px 8px;
|
||||||
|
}
|
||||||
|
.admin-whatsapp-sidebar-head h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 15px;
|
||||||
|
letter-spacing: -.02em;
|
||||||
|
color: #17253e;
|
||||||
|
}
|
||||||
|
.admin-whatsapp-sidebar-head p {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
color: #8a95a7;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.admin-whatsapp-search {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 8px 14px 12px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border: 1px solid #e4eaf2;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: white;
|
||||||
|
color: #8a95a7;
|
||||||
|
}
|
||||||
|
.admin-whatsapp-search input {
|
||||||
|
width: 100%;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
padding: 10px 0;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #1f2a3d;
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
.admin-whatsapp-thread-list.compact {
|
.admin-whatsapp-thread-list.compact {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
align-content: start;
|
||||||
max-height: 560px;
|
gap: 2px;
|
||||||
|
padding: 0 8px 12px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
.admin-whatsapp-thread-item {
|
.admin-whatsapp-thread-item {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 4px;
|
grid-template-columns: 38px minmax(0, 1fr) auto;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border: 1px solid #e6ebf2;
|
border: 0;
|
||||||
border-radius: 10px;
|
border-radius: 12px;
|
||||||
padding: 10px 12px;
|
padding: 10px;
|
||||||
background: white;
|
background: transparent;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.admin-whatsapp-thread-item.active {
|
.admin-whatsapp-thread-item:hover { background: #f3f6fb; }
|
||||||
border-color: #1475ff;
|
.admin-whatsapp-thread-item.active { background: #eaf2ff; }
|
||||||
background: #f3f8ff;
|
.admin-whatsapp-thread-copy { display: grid; min-width: 0; gap: 2px; }
|
||||||
}
|
|
||||||
.admin-whatsapp-thread-item strong { font-size: 12px; color: #1f2a3d; }
|
.admin-whatsapp-thread-item strong { font-size: 12px; color: #1f2a3d; }
|
||||||
.admin-whatsapp-thread-item span {
|
.admin-whatsapp-thread-copy > span {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #607089;
|
color: #607089;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
.admin-whatsapp-thread-item small { font-size: 10px; color: #8a95a7; }
|
.admin-whatsapp-thread-item small { font-size: 10px; color: #8a95a7; align-self: start; margin-top: 2px; }
|
||||||
|
.admin-whatsapp-avatar {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(160deg, #2f6bff, #163a8a);
|
||||||
|
color: white;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: .02em;
|
||||||
|
}
|
||||||
|
.admin-whatsapp-avatar.large { width: 64px; height: 64px; font-size: 16px; }
|
||||||
.admin-whatsapp-chat {
|
.admin-whatsapp-chat {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto 1fr;
|
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||||
|
min-height: 680px;
|
||||||
|
background: #f7f4ef;
|
||||||
}
|
}
|
||||||
.admin-whatsapp-chat-empty {
|
.admin-whatsapp-chat-empty {
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
justify-items: center;
|
||||||
min-height: 520px;
|
align-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 240px;
|
||||||
color: #8a95a7;
|
color: #8a95a7;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 24px;
|
||||||
}
|
}
|
||||||
|
.admin-whatsapp-chat-empty p { margin: 0; max-width: 280px; line-height: 1.5; }
|
||||||
.admin-whatsapp-chat-head {
|
.admin-whatsapp-chat-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 12px;
|
||||||
padding-bottom: 12px;
|
padding: 14px 16px;
|
||||||
border-bottom: 1px solid #e6ebf2;
|
background: white;
|
||||||
margin-bottom: 12px;
|
border-bottom: 1px solid #edf1f6;
|
||||||
}
|
}
|
||||||
.admin-whatsapp-chat-head strong { font-size: 13px; }
|
.admin-whatsapp-chat-head strong { display: block; font-size: 14px; color: #17253e; }
|
||||||
.admin-whatsapp-chat-head small { color: #8a95a7; font-size: 10px; }
|
.admin-whatsapp-chat-head small { display: block; margin-top: 2px; color: #8a95a7; font-size: 11px; }
|
||||||
.admin-whatsapp-chat-body {
|
.admin-whatsapp-chat-body {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
gap: 8px;
|
||||||
align-content: start;
|
align-content: start;
|
||||||
max-height: 560px;
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding-right: 4px;
|
padding: 18px 18px 8px;
|
||||||
|
}
|
||||||
|
.admin-whatsapp-message-row { display: grid; gap: 8px; }
|
||||||
|
.admin-whatsapp-day {
|
||||||
|
justify-self: center;
|
||||||
|
margin: 6px 0;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, .85);
|
||||||
|
color: #6d7b90;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: .04em;
|
||||||
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
.admin-whatsapp-media {
|
.admin-whatsapp-media {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
.admin-whatsapp-bubble-actions { display: flex; gap: 10px; }
|
||||||
|
.admin-whatsapp-composer {
|
||||||
|
padding: 10px 12px 12px;
|
||||||
|
background: white;
|
||||||
|
border-top: 1px solid #edf1f6;
|
||||||
|
}
|
||||||
.admin-whatsapp-mode-tabs {
|
.admin-whatsapp-mode-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
.admin-whatsapp-mode-tabs button {
|
.admin-whatsapp-mode-tabs button {
|
||||||
border: 1px solid #dfe5ed;
|
border: 1px solid #e4eaf2;
|
||||||
background: white;
|
background: #f7f9fc;
|
||||||
color: #607089;
|
color: #607089;
|
||||||
border-radius: 8px;
|
border-radius: 999px;
|
||||||
padding: 7px 10px;
|
padding: 6px 12px;
|
||||||
font-size: 10px;
|
font-size: 11px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
}
|
||||||
.admin-whatsapp-mode-tabs button.active {
|
.admin-whatsapp-mode-tabs button.active {
|
||||||
background: #1475ff;
|
background: #1475ff;
|
||||||
border-color: #1475ff;
|
border-color: #1475ff;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
.admin-whatsapp-reply-chip {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-left: 3px solid #1475ff;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f3f8ff;
|
||||||
|
color: #35527a;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.admin-whatsapp-reply-chip button {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #607089;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
.admin-whatsapp-composer-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.admin-whatsapp-composer-footer .primary-button { flex: 0 0 auto; }
|
||||||
|
.admin-whatsapp-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 20px 16px;
|
||||||
|
border-left: 1px solid #edf1f6;
|
||||||
|
background: #fbfcfe;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.admin-whatsapp-details-hero {
|
||||||
|
display: grid;
|
||||||
|
justify-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
text-align: center;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
.admin-whatsapp-details-hero strong { font-size: 14px; color: #17253e; }
|
||||||
|
.admin-whatsapp-details-hero small { color: #8a95a7; font-size: 11px; }
|
||||||
|
.admin-whatsapp-details-list { margin: 0; display: grid; gap: 10px; }
|
||||||
|
.admin-whatsapp-details-list div {
|
||||||
|
display: grid;
|
||||||
|
gap: 2px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid #edf1f6;
|
||||||
|
}
|
||||||
|
.admin-whatsapp-details-list dt {
|
||||||
|
color: #8a95a7;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: .06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.admin-whatsapp-details-list dd { margin: 0; color: #1f2a3d; font-size: 12px; }
|
||||||
|
.admin-whatsapp-quick { display: grid; gap: 8px; }
|
||||||
|
.admin-whatsapp-quick h3 { margin: 0 0 2px; font-size: 12px; color: #17253e; }
|
||||||
|
.admin-whatsapp-quick button {
|
||||||
|
text-align: left;
|
||||||
|
border: 1px solid #e4eaf2;
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 9px 12px;
|
||||||
|
color: #35435a;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.admin-whatsapp-quick button:hover { border-color: #1475ff; color: #1475ff; }
|
||||||
.admin-whatsapp-setup-form {
|
.admin-whatsapp-setup-form {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
|
@ -1158,10 +1319,12 @@ main { padding: 30px clamp(20px, 3vw, 44px) 50px; }
|
||||||
justify-self: start;
|
justify-self: start;
|
||||||
}
|
}
|
||||||
@media (max-width: 1100px) {
|
@media (max-width: 1100px) {
|
||||||
.admin-whatsapp-messenger { grid-template-columns: 1fr; }
|
.admin-whatsapp-messenger { grid-template-columns: 1fr; min-height: auto; }
|
||||||
.admin-whatsapp-sidebar,
|
.admin-whatsapp-sidebar { border-right: 0; border-bottom: 1px solid #edf1f6; }
|
||||||
.admin-whatsapp-chat,
|
.admin-whatsapp-details { border-left: 0; border-top: 1px solid #edf1f6; }
|
||||||
.admin-whatsapp-compose-panel { min-height: auto; }
|
.admin-whatsapp-chat { min-height: 560px; }
|
||||||
|
.admin-whatsapp-thread-list.compact { max-height: 280px; }
|
||||||
|
.admin-whatsapp-composer-footer { align-items: stretch; flex-direction: column; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* UrmiOTP */
|
/* UrmiOTP */
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue