This commit is contained in:
2026-06-25 15:15:35 +02:00
parent 906e37e845
commit 1465016b56
21 changed files with 1979 additions and 666 deletions
+243 -367
View File
@@ -1,7 +1,7 @@
"use client"
import { useState, useEffect, useCallback } from "react"
import { Phone, PhoneOff, Mic, MicOff, Volume2, VolumeX, X } from "lucide-react"
import { Phone, X, Copy, MessageSquare } from "lucide-react"
import { getSupabase } from "@/lib/supabase"
import { useWebRTCCall } from "@/hooks/useWebRTCCall"
@@ -17,6 +17,14 @@ interface VoiceCallModalProps {
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)
@@ -24,69 +32,94 @@ export default function VoiceCallModal({ open, onClose }: VoiceCallModalProps) {
const [searchQuery, setSearchQuery] = useState("")
const [phoneNumber, setPhoneNumber] = useState("")
const [phoneError, setPhoneError] = useState(false)
const [lookupMessage, setLookupMessage] = useState<string | null>(null)
const setLookupMessageWithReset = useCallback((msg: string | null) => {
setLookupMessage(msg)
if (msg !== "This phone number is not registered on the platform.") {
setInviteUrl(null)
}
}, [])
const [inviteUrl, setInviteUrl] = useState<string | null>(null)
const [inviteLoading, setInviteLoading] = useState(false)
const generateInvite = useCallback(async (phone: string) => {
setInviteLoading(true)
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 {
const res = await fetch("/api/invite/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ phone }),
})
const data = await res.json()
if (data.inviteUrl) {
setInviteUrl(data.inviteUrl)
}
await navigator.clipboard.writeText(callLink)
setShareSent(true)
setShareError(null)
console.log("[share] link copied")
} catch {
// fallback: generate client-side
const token = crypto.randomUUID?.() || Math.random().toString(36).slice(2)
setInviteUrl(`${window.location.origin}/join/${token}`)
} finally {
setInviteLoading(false)
setShareError("Failed to copy URL")
console.error("[share] copy failed")
}
}, [])
}, [callLink])
const {
isReady,
isCallActive,
isIncoming,
isMuted,
isSpeakerOn,
callState,
callDuration,
error: callError,
callerInfo,
makeCall,
acceptCall,
rejectCall,
endCall,
toggleMute,
toggleSpeaker,
formatDuration,
} = useWebRTCCall()
const sendWhatsApp = useCallback(() => {
if (!callLink) return
useEffect(() => {
if (!open && callState !== "idle") {
endCall()
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
}
}, [open, callState, endCall])
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)
return
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)
@@ -97,9 +130,9 @@ export default function VoiceCallModal({ open, onClose }: VoiceCallModalProps) {
.select("id, name, phone, avatar_url")
.order("name", { ascending: true })
if (error) throw error
setContacts(data || [])
setContacts([...(data || []), ...FALLBACK_CONTACTS])
} catch {
setContactsError(true)
setContacts(FALLBACK_CONTACTS)
} finally {
setContactsLoading(false)
}
@@ -138,334 +171,177 @@ export default function VoiceCallModal({ open, onClose }: VoiceCallModalProps) {
<X className="h-5 w-5" />
</button>
<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>
{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>
<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 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-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:text-[#666666] truncate">{contact.phone}</p>
<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>
<button
type="button"
onClick={() => makeCall(contact.phone)}
className="shrink-0 text-[#CC0000] dark:text-[#FF4444] hover:opacity-80 transition-opacity"
<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"
>
<Phone className="h-4 w-4" />
</button>
</div>
))}
</div>
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="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>
)}
{isIncoming ? (
<div className="text-center py-4">
<div className="text-2xl font-bold dark:text-white text-[#111111] mb-1">
Incoming Call
</div>
<div className="flex items-center justify-center gap-3 mb-6">
<div className="w-12 h-12 rounded-full flex items-center justify-center text-lg font-bold bg-[#FFF0F0] dark:bg-[#CC0000]/15 text-[#CC0000] dark:text-[#FF4444]">
{callerInfo?.firstName?.charAt(0).toUpperCase()}
<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="text-left">
<p className="text-sm font-medium text-[#111111] dark:text-white">
{callerInfo?.firstName} {callerInfo?.lastName}
</p>
<p className="text-xs text-[#888888]">{callerInfo?.username}</p>
{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>
</div>
<p className="text-[#888888] text-sm mb-6 animate-pulse">Ringing...</p>
<div className="flex justify-center gap-4">
<button
onClick={rejectCall}
className="w-14 h-14 rounded-full bg-[#CC0000] hover:bg-[#990000] dark:bg-[#FF1111] flex items-center justify-center text-white font-bold transition-all duration-200"
>
<PhoneOff className="h-5 w-5" />
</button>
<button
onClick={acceptCall}
className="w-14 h-14 rounded-full bg-[#00AA00] hover:bg-[#008800] dark:bg-[#00CC00] flex items-center justify-center text-white font-bold transition-all duration-200"
>
<Phone className="h-5 w-5" />
</button>
</div>
</div>
) : isCallActive ? (
<div className="text-center py-4">
{callState === "calling" && (
<>
<div className="text-xl font-bold dark:text-white text-[#111111] mb-4">
{phoneNumber}
</div>
<p className="text-[#888888] text-sm mb-4 animate-pulse">Calling...</p>
<div className="flex justify-center">
<button
onClick={endCall}
className="w-14 h-14 rounded-full bg-[#CC0000] hover:bg-[#990000] dark:bg-[#FF1111] flex items-center justify-center text-white font-bold transition-all duration-200"
>
<PhoneOff className="h-5 w-5" />
</button>
</div>
</>
)}
{callState === "ringing" && (
<>
<div className="text-xl font-bold dark:text-white text-[#111111] mb-4">
{phoneNumber}
</div>
<p className="text-[#888888] text-sm mb-4 animate-pulse">Ringing...</p>
<div className="flex justify-center">
<button
onClick={endCall}
className="w-14 h-14 rounded-full bg-[#CC0000] hover:bg-[#990000] dark:bg-[#FF1111] flex items-center justify-center text-white font-bold transition-all duration-200"
>
<PhoneOff className="h-5 w-5" />
</button>
</div>
</>
)}
{callState === "connecting" && (
<>
<div className="text-xl font-bold dark:text-white text-[#111111] mb-4">
{phoneNumber}
</div>
<p className="text-[#888888] text-sm mb-4 animate-pulse">Connecting...</p>
<div className="flex justify-center">
<button
onClick={endCall}
className="w-14 h-14 rounded-full bg-[#CC0000] hover:bg-[#990000] dark:bg-[#FF1111] flex items-center justify-center text-white font-bold transition-all duration-200"
>
<PhoneOff className="h-5 w-5" />
</button>
</div>
</>
)}
{callState === "connected" && (
<>
<div className="text-2xl font-bold dark:text-white text-[#111111] mb-1">
{phoneNumber}
</div>
<div className="text-[#CC0000] font-mono text-lg mb-6">
{formatDuration(callDuration)}
</div>
<div className="flex justify-center gap-4">
<button
onClick={toggleSpeaker}
className={`w-14 h-14 rounded-full flex items-center justify-center text-white font-bold transition-all duration-200 ${isSpeakerOn ? 'bg-[#0033CC] dark:bg-[#1144FF]' : 'bg-[#444444] dark:bg-[#333333]'}`}
>
{isSpeakerOn ? <Volume2 className="h-5 w-5" /> : <VolumeX className="h-5 w-5" />}
</button>
<button
onClick={toggleMute}
className={`w-14 h-14 rounded-full flex items-center justify-center text-white font-bold transition-all duration-200 ${isMuted ? 'bg-[#0033CC] dark:bg-[#1144FF]' : 'bg-[#444444] dark:bg-[#333333]'}`}
>
{isMuted ? <MicOff className="h-5 w-5" /> : <Mic className="h-5 w-5" />}
</button>
<button
onClick={endCall}
className="w-14 h-14 rounded-full bg-[#CC0000] hover:bg-[#990000] dark:bg-[#FF1111] flex items-center justify-center text-white font-bold transition-all duration-200"
>
<PhoneOff className="h-5 w-5" />
</button>
</div>
</>
)}
{(callState === "ended" || callState === "missed" || callState === "rejected" || callState === "offline" || callState === "busy") && (
<>
<div className="text-xl font-bold dark:text-white text-[#111111] mb-4">
{phoneNumber}
</div>
<p className="text-[#888888] text-sm mb-4">
{callState === "missed" && "This phone number is not registered on the platform."}
{callState === "offline" && "User is offline"}
{callState === "rejected" && "Call rejected"}
{callState === "busy" && "User is busy"}
{callState === "ended" && "Call ended"}
</p>
{callState === "missed" && (
<div className="flex flex-col gap-2">
{!inviteUrl ? (
<button
onClick={() => generateInvite(phoneNumber)}
disabled={inviteLoading}
className="w-full bg-[#CC0000] hover:bg-[#990000] disabled:bg-[#CCCCCC] disabled:cursor-not-allowed 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"
>
{inviteLoading ? "Generating..." : "Invite User"}
</button>
) : (
<div className="flex flex-col gap-2">
<p className="text-xs text-[#888888] break-all bg-[#F5F5F5] dark:bg-[#1A1A1A] rounded-xl p-3">
{inviteUrl}
</p>
<div className="flex gap-2">
<button
onClick={() => navigator.clipboard.writeText(inviteUrl)}
className="flex-1 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 Link
</button>
<button
onClick={() => {
if (navigator.share) {
navigator.share({ url: inviteUrl, title: "Join me on this platform!" })
} else {
navigator.clipboard.writeText(inviteUrl)
}
}}
className="flex-1 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"
>
Share
</button>
</div>
</div>
)}
</div>
)}
</>
)}
</div>
{shareError && (
<p className="text-[#CC0000] text-xs text-center">{shareError}</p>
)}
</>
)
) : (
<>
{lookupMessage ? (
<div className="text-center">
<p className="text-[#888888] text-sm mb-4">{lookupMessage}</p>
{lookupMessage === "This phone number is not registered on the platform." && (
<div className="flex flex-col gap-2">
{!inviteUrl ? (
<button
onClick={() => generateInvite(phoneNumber)}
disabled={inviteLoading}
className="w-full bg-[#CC0000] hover:bg-[#990000] disabled:bg-[#CCCCCC] disabled:cursor-not-allowed 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"
>
{inviteLoading ? "Generating..." : "Invite User"}
</button>
<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"
/>
) : (
<div className="flex flex-col gap-2">
<p className="text-xs text-[#888888] break-all bg-[#F5F5F5] dark:bg-[#1A1A1A] rounded-xl p-3">
{inviteUrl}
</p>
<div className="flex gap-2">
<button
onClick={() => {
navigator.clipboard.writeText(inviteUrl)
setLookupMessageWithReset(null)
}}
className="flex-1 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 Link
</button>
<button
onClick={() => {
if (navigator.share) {
navigator.share({ url: inviteUrl, title: "Join me on this platform!" })
} else {
navigator.clipboard.writeText(inviteUrl)
}
setLookupMessageWithReset(null)
}}
className="flex-1 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"
>
Share
</button>
</div>
</div>
contact.name.charAt(0).toUpperCase()
)}
</div>
)}
</div>
) : (
<>
<button
onClick={async () => {
const trimmed = phoneNumber.trim()
if (!trimmed) {
setPhoneError(true)
return
}
setPhoneError(false)
setLookupMessageWithReset(null)
try {
const res = await fetch(`/api/users/lookup?phone=${encodeURIComponent(trimmed)}`)
const data = await res.json()
if (!data.found) {
setLookupMessageWithReset("This phone number is not registered on the platform.")
return
}
makeCall(trimmed)
} catch {
if (isReady) {
makeCall(trimmed)
} else {
setLookupMessageWithReset("Could not verify phone number. Please try again.")
}
}
}}
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>
{callError && (
<p className="text-[#CC0000] text-xs mt-2 text-center">{callError}</p>
)}
</>
<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>