mirror of
https://git.coastit.co.za/caitlin/CRM_ENVR.git
synced 2026-07-10 11:15:43 +02:00
351 lines
14 KiB
TypeScript
351 lines
14 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect, useCallback } from "react"
|
|
import { Phone, X, Copy, MessageSquare } from "lucide-react"
|
|
import { getSupabase } from "@/lib/supabase"
|
|
import { useWebRTCCall } from "@/hooks/useWebRTCCall"
|
|
|
|
interface Contact {
|
|
id: string
|
|
name: string
|
|
phone: string
|
|
avatar_url: string | null
|
|
}
|
|
|
|
interface VoiceCallModalProps {
|
|
open: boolean
|
|
onClose: () => void
|
|
}
|
|
|
|
function formatPhoneForWhatsApp(phone: string): string {
|
|
const digits = phone.replace(/[^0-9]/g, "")
|
|
if (!digits) return ""
|
|
if (digits.startsWith("27")) return digits
|
|
if (digits.startsWith("0")) return "27" + digits.slice(1)
|
|
return digits
|
|
}
|
|
|
|
export default function VoiceCallModal({ open, onClose }: VoiceCallModalProps) {
|
|
const [contacts, setContacts] = useState<Contact[]>([])
|
|
const [contactsLoading, setContactsLoading] = useState(false)
|
|
const [contactsError, setContactsError] = useState(false)
|
|
const [searchQuery, setSearchQuery] = useState("")
|
|
const [phoneNumber, setPhoneNumber] = useState("")
|
|
const [phoneError, setPhoneError] = useState(false)
|
|
|
|
const [callLink, setCallLink] = useState<string | null>(null)
|
|
const [shareSent, setShareSent] = useState(false)
|
|
const [shareError, setShareError] = useState<string | null>(null)
|
|
const [targetPhone, setTargetPhone] = useState<string>("")
|
|
const [showWaUrl, setShowWaUrl] = useState<string | null>(null)
|
|
|
|
const { createRoom } = useWebRTCCall()
|
|
|
|
const handleCall = useCallback((phone?: string) => {
|
|
setShareSent(false)
|
|
setShareError(null)
|
|
const { roomId, link } = createRoom()
|
|
setCallLink(link)
|
|
setTargetPhone(phone || "")
|
|
console.log("[call] room created, roomId:", roomId)
|
|
console.log("[call] join link:", link)
|
|
console.log("[call] target phone:", phone || "none")
|
|
}, [createRoom])
|
|
|
|
const copyLink = useCallback(async () => {
|
|
if (!callLink) return
|
|
try {
|
|
await navigator.clipboard.writeText(callLink)
|
|
setShareSent(true)
|
|
setShareError(null)
|
|
console.log("[share] link copied")
|
|
} catch {
|
|
setShareError("Failed to copy URL")
|
|
console.error("[share] copy failed")
|
|
}
|
|
}, [callLink])
|
|
|
|
const sendWhatsApp = useCallback(() => {
|
|
if (!callLink) return
|
|
|
|
const originalPhone = targetPhone
|
|
const formattedPhone = formatPhoneForWhatsApp(originalPhone)
|
|
const generatedCallLink = callLink
|
|
|
|
console.log("[wa] original phone:", originalPhone)
|
|
console.log("[wa] formatted phone:", formattedPhone)
|
|
console.log("[wa] call link:", generatedCallLink)
|
|
|
|
if (!formattedPhone) {
|
|
setShareError("Invalid phone number.")
|
|
console.error("[wa] invalid phone number")
|
|
return
|
|
}
|
|
|
|
const msg = encodeURIComponent(`Hi! Join me on this link so we can start our call:\n\n${generatedCallLink}`)
|
|
const waUrl = `https://wa.me/${formattedPhone}?text=${msg}`
|
|
|
|
console.log("[wa] WhatsApp URL:", waUrl)
|
|
setShowWaUrl(waUrl)
|
|
|
|
const w = window.open(waUrl, "_blank")
|
|
if (!w || w.closed) {
|
|
setShareError("Could not open WhatsApp. Please check your browser allows popups.")
|
|
console.error("[wa] failed: window.open returned null or closed")
|
|
} else {
|
|
setShareSent(true)
|
|
setShareError(null)
|
|
console.log("[wa] success: WhatsApp opened")
|
|
}
|
|
}, [callLink, targetPhone])
|
|
|
|
useEffect(() => {
|
|
if (!open) {
|
|
setSearchQuery("")
|
|
setPhoneNumber("")
|
|
setPhoneError(false)
|
|
setCallLink(null)
|
|
setShareSent(false)
|
|
setShareError(null)
|
|
setTargetPhone("")
|
|
setShowWaUrl(null)
|
|
}
|
|
}, [open])
|
|
|
|
const FALLBACK_CONTACTS: Contact[] = [
|
|
{ id: "fallback-dillen", name: "Dillen", phone: "0799158142", avatar_url: null },
|
|
{ id: "fallback-ewan", name: "Ewan", phone: "0845172665", avatar_url: null },
|
|
{ id: "fallback-caitlin", name: "Caitlin", phone: "0612729281", avatar_url: null },
|
|
]
|
|
|
|
useEffect(() => {
|
|
if (!open) return
|
|
const fetchContacts = async () => {
|
|
setContactsLoading(true)
|
|
setContactsError(false)
|
|
try {
|
|
const sb = getSupabase()
|
|
const { data, error } = await sb
|
|
.from("contacts")
|
|
.select("id, name, phone, avatar_url")
|
|
.order("name", { ascending: true })
|
|
if (error) throw error
|
|
setContacts([...(data || []), ...FALLBACK_CONTACTS])
|
|
} catch {
|
|
setContacts(FALLBACK_CONTACTS)
|
|
} finally {
|
|
setContactsLoading(false)
|
|
}
|
|
}
|
|
fetchContacts()
|
|
}, [open])
|
|
|
|
useEffect(() => {
|
|
if (!open) return
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
if (e.key === "Escape") onClose()
|
|
}
|
|
document.addEventListener("keydown", handleKeyDown)
|
|
return () => document.removeEventListener("keydown", handleKeyDown)
|
|
}, [open, onClose])
|
|
|
|
const filteredContacts = contacts.filter(
|
|
(c) =>
|
|
c.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
c.phone.toLowerCase().includes(searchQuery.toLowerCase()),
|
|
)
|
|
|
|
if (!open) return null
|
|
|
|
return (
|
|
<div
|
|
className="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm flex items-center justify-center"
|
|
onClick={(e) => { if (e.target === e.currentTarget) onClose() }}
|
|
>
|
|
<div className="bg-white dark:bg-[#141414] rounded-2xl p-6 w-full max-w-md border border-[#E0E0E0] dark:border-[#CC0000]/20 shadow-[0_8px_40px_rgba(0,0,0,0.18)] relative">
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
className="absolute top-4 right-4 text-[#888888] hover:text-[#111111] dark:hover:text-white transition-colors"
|
|
>
|
|
<X className="h-5 w-5" />
|
|
</button>
|
|
|
|
{callLink ? (
|
|
shareSent ? (
|
|
<>
|
|
<p className="text-sm text-[#00AA00] font-semibold text-center mb-3">Call link copied.</p>
|
|
<h2 className="font-bold text-lg text-[#111111] dark:text-white">Waiting for participant</h2>
|
|
<p className="text-[#888888] text-sm mt-1 mb-4">
|
|
The recipient must receive and open the call link.
|
|
</p>
|
|
|
|
<div className="bg-[#F5F5F5] dark:bg-[#1A1A1A] rounded-xl p-3 mb-4 break-all text-xs text-[#111111] dark:text-white">
|
|
{callLink}
|
|
</div>
|
|
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={copyLink}
|
|
className="flex-1 flex items-center justify-center gap-2 bg-[#444444] hover:bg-[#555555] dark:bg-[#333333] dark:hover:bg-[#444444] text-white font-semibold text-sm rounded-xl py-2.5 transition-all duration-200"
|
|
>
|
|
<Copy className="h-4 w-4" />
|
|
Copy URL
|
|
</button>
|
|
<button
|
|
onClick={() => { setShareSent(false); setShareError(null) }}
|
|
className="flex-1 flex items-center justify-center gap-2 bg-[#CC0000] hover:bg-[#990000] dark:bg-[#FF1111] dark:hover:bg-[#CC0000] text-white font-semibold text-sm rounded-xl py-2.5 transition-all duration-200"
|
|
>
|
|
<MessageSquare className="h-4 w-4" />
|
|
WhatsApp
|
|
</button>
|
|
</div>
|
|
|
|
<a
|
|
href={callLink}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="block w-full mt-4 bg-[#444444] hover:bg-[#555555] dark:bg-[#333333] dark:hover:bg-[#444444] text-white font-semibold text-sm rounded-xl py-2.5 text-center transition-all duration-200"
|
|
>
|
|
Open Call
|
|
</a>
|
|
</>
|
|
) : (
|
|
<>
|
|
<h2 className="font-bold text-lg text-[#111111] dark:text-white">Choose how to notify this person</h2>
|
|
<p className="text-[#888888] text-sm mt-1 mb-4">
|
|
Select a method to send the call link
|
|
</p>
|
|
|
|
<div className="bg-[#F5F5F5] dark:bg-[#1A1A1A] rounded-xl p-3 mb-4 break-all text-xs text-[#111111] dark:text-white">
|
|
{callLink}
|
|
</div>
|
|
|
|
{showWaUrl && (
|
|
<div className="bg-[#1A1A1A] dark:bg-[#000000] rounded-xl p-2 mb-3 break-all text-[10px] text-[#00AA00] font-mono">
|
|
{showWaUrl}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex flex-col gap-2 mb-4">
|
|
<button
|
|
onClick={sendWhatsApp}
|
|
className="flex items-center justify-center gap-2 bg-[#25D366] hover:bg-[#1da851] text-white font-semibold text-sm rounded-xl py-2.5 transition-all duration-200"
|
|
>
|
|
<MessageSquare className="h-4 w-4" />
|
|
WhatsApp
|
|
</button>
|
|
<button
|
|
onClick={copyLink}
|
|
className="flex items-center justify-center gap-2 bg-[#444444] hover:bg-[#555555] dark:bg-[#333333] dark:hover:bg-[#444444] text-white font-semibold text-sm rounded-xl py-2.5 transition-all duration-200"
|
|
>
|
|
<Copy className="h-4 w-4" />
|
|
Copy URL
|
|
</button>
|
|
</div>
|
|
|
|
{shareError && (
|
|
<p className="text-[#CC0000] text-xs text-center">{shareError}</p>
|
|
)}
|
|
</>
|
|
)
|
|
) : (
|
|
<>
|
|
<h2 className="font-bold text-lg text-[#111111] dark:text-white">Start a Call</h2>
|
|
<p className="text-[#888888] text-sm mt-1 mb-5">Search contacts or dial a number</p>
|
|
|
|
<p className="text-[10px] font-bold uppercase tracking-widest text-[#888888] dark:text-[#666666] mb-2">
|
|
CONTACTS
|
|
</p>
|
|
|
|
<input
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
placeholder="Search contacts..."
|
|
className="bg-[#F5F5F5] dark:bg-[#1A1A1A] border border-[#E0E0E0] dark:border-[#333333] text-[#111111] dark:text-white placeholder-[#AAAAAA] dark:placeholder-[#555555] rounded-xl px-4 py-2.5 text-sm w-full mb-3 outline-none transition-colors focus:border-[#CC0000] dark:focus:border-[#FF4444]"
|
|
/>
|
|
|
|
<div className="max-h-[200px] overflow-y-auto space-y-1 mb-4">
|
|
{contactsLoading && (
|
|
<p className="text-[#AAAAAA] text-sm text-center py-4 animate-pulse">Loading contacts...</p>
|
|
)}
|
|
{contactsError && (
|
|
<p className="text-[#CC0000] text-sm text-center py-4">Could not load contacts</p>
|
|
)}
|
|
{!contactsLoading && !contactsError && filteredContacts.length === 0 && (
|
|
<p className="text-[#AAAAAA] text-sm text-center py-4">No contacts found</p>
|
|
)}
|
|
{!contactsLoading && !contactsError && filteredContacts.map((contact) => (
|
|
<div
|
|
key={contact.id}
|
|
className="flex items-center gap-3 px-3 py-2.5 rounded-xl cursor-pointer transition-all duration-150 hover:bg-[#F5F5F5] dark:hover:bg-[#1A1A1A]"
|
|
>
|
|
<div className="w-9 h-9 rounded-full flex items-center justify-center text-sm font-bold shrink-0 bg-[#FFF0F0] dark:bg-[#CC0000]/15 text-[#CC0000] dark:text-[#FF4444]">
|
|
{contact.avatar_url ? (
|
|
<img
|
|
src={contact.avatar_url}
|
|
alt={contact.name}
|
|
className="w-full h-full rounded-full object-cover"
|
|
/>
|
|
) : (
|
|
contact.name.charAt(0).toUpperCase()
|
|
)}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm font-medium text-[#111111] dark:text-white truncate">{contact.name}</p>
|
|
<p className="text-xs text-[#888888] dark:[#666666] truncate">{contact.phone}</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => handleCall(contact.phone)}
|
|
className="shrink-0 text-[#CC0000] dark:text-[#FF4444] hover:opacity-80 transition-opacity"
|
|
>
|
|
<Phone className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3 my-4">
|
|
<hr className="flex-1 border-t border-[#E0E0E0] dark:border-[#222222]" />
|
|
<span className="text-xs text-[#AAAAAA] dark:text-[#555555]">OR</span>
|
|
<hr className="flex-1 border-t border-[#E0E0E0] dark:border-[#222222]" />
|
|
</div>
|
|
|
|
<p className="text-[10px] font-bold uppercase tracking-widest text-[#888888] dark:text-[#666666] mb-2">
|
|
DIAL A NUMBER
|
|
</p>
|
|
|
|
<input
|
|
type="tel"
|
|
value={phoneNumber}
|
|
onChange={(e) => { setPhoneNumber(e.target.value); setPhoneError(false) }}
|
|
placeholder="+27 000 000 0000"
|
|
className="bg-[#F5F5F5] dark:bg-[#1A1A1A] border border-[#E0E0E0] dark:border-[#333333] text-[#111111] dark:text-white placeholder-[#AAAAAA] dark:placeholder-[#555555] rounded-xl px-4 py-2.5 text-sm w-full outline-none transition-colors focus:border-[#CC0000] dark:focus:border-[#FF4444]"
|
|
/>
|
|
{phoneError && (
|
|
<p className="text-[#CC0000] text-xs mt-1">Please enter a phone number</p>
|
|
)}
|
|
|
|
<button
|
|
onClick={() => {
|
|
const trimmed = phoneNumber.trim()
|
|
if (!trimmed) {
|
|
setPhoneError(true)
|
|
return
|
|
}
|
|
setPhoneError(false)
|
|
handleCall(trimmed)
|
|
}}
|
|
className="w-full mt-3 bg-[#CC0000] hover:bg-[#990000] dark:bg-[#FF1111] dark:hover:bg-[#CC0000] text-white font-semibold text-sm rounded-xl py-2.5 flex items-center justify-center gap-2 transition-all duration-200"
|
|
>
|
|
<Phone className="h-4 w-4" />
|
|
Call Now
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|