This commit is contained in:
Vula Builder
2026-10-11 14:20:24 +00:00
parent 19599a07fc
commit 11bc586978
+130
View File
@@ -0,0 +1,130 @@
'use client'
import { useEffect, useRef, useState } from 'react'
export interface Enquiry {
name?: string
phone?: string
email?: string
message?: string
}
export type EnquiryChannel = 'whatsapp' | 'email'
export interface EnquiryResult {
ok: boolean
message: string
}
/** The message as it will appear to her, whichever way it is sent. */
export function enquiryText(businessName: string | null, e: Enquiry): string {
const name = (e.name ?? '').trim()
const phone = (e.phone ?? '').trim()
const email = (e.email ?? '').trim()
const greeting = 'Hi' + (businessName ? ' ' + businessName : '') + (name ? ', this is ' + name + '.' : '.')
return [greeting, (e.message ?? '').trim(), phone ? 'My number: ' + phone : '', email ? 'My email: ' + email : '']
.filter(Boolean)
.join('\n\n')
}
/**
* What the visitor is told afterwards. Written here rather than in each form,
* so no form can say a message arrived when it has not.
*/
export function resultMessage(via: EnquiryChannel, ok: boolean, businessName: string | null): string {
if (!ok) return 'Your message could not be sent. Please try again, or contact us another way.'
if (via === 'whatsapp') return "WhatsApp has opened with your message. Press send and we'll reply there."
return businessName
? 'Your message has been sent to ' + businessName + '. They will reply to you directly.'
: 'Your message has been sent. We will reply to you directly.'
}
/** Her number in international digits, with the message typed in. */
export function whatsAppUrl(number: string, text: string): string {
return 'https://wa.me/' + number + '?text=' + encodeURIComponent(text)
}
/** Phones first offer WhatsApp; laptops first offer email. Only what is switched on. */
export function channelsFor(on: { whatsapp: boolean; email: boolean }, onPhone: boolean): EnquiryChannel[] {
const order: EnquiryChannel[] = onPhone ? ['whatsapp', 'email'] : ['email', 'whatsapp']
return order.filter((c) => on[c])
}
/**
* Sends an enquiry to the business, the ways this site can: WhatsApp opened on
* the visitor's device with the message typed in, or email sent by the server
* when the business has it switched on.
*
* `available` is false when neither is on. Do not show a form then — a form
* that cannot reach her is worse than none.
*
* `channels` is what to offer, in order. On a laptop both are offered: whether
* WhatsApp is linked to that computer cannot be known from here.
*
* `send` resolves to what happened, with the words to show. WhatsApp "ok"
* means it OPENED — the visitor still presses send, and the message says so.
*/
export function useEnquiry() {
const [whatsapp, setWhatsapp] = useState<string | null>(null)
const [email, setEmail] = useState(false)
const [businessName, setBusinessName] = useState<string | null>(null)
const [onPhone, setOnPhone] = useState(true)
const [loading, setLoading] = useState(true)
const opened = useRef(0)
useEffect(() => {
// A touch-first screen is a phone or a tablet, where WhatsApp lives.
setOnPhone(window.matchMedia('(pointer: coarse)').matches)
const base = process.env.NEXT_PUBLIC_VULA_COMMERCE_URL
const id = process.env.NEXT_PUBLIC_VULA_PROJECT_ID
if (!base || !id) { setLoading(false); return }
let live = true
fetch(base + '/api/public/profile/' + id)
.then((r) => (r.ok ? r.json() : null))
.then((d) => {
const p = d?.profile
if (!live || !p) return
setWhatsapp(p.enquiry?.whatsapp === false ? null : p.whatsapp || null)
setEmail(p.enquiry?.email === true)
setBusinessName(p.businessName || null)
})
.catch(() => { /* nothing to send through means no form, which is where we already were */ })
.finally(() => { if (live) setLoading(false) })
return () => { live = false }
}, [])
const channels = channelsFor({ whatsapp: !!whatsapp, email }, onPhone)
async function send(e: Enquiry, via: EnquiryChannel = channels[0]): Promise<EnquiryResult> {
if (via === 'whatsapp') {
if (!whatsapp) return { ok: false, message: resultMessage(via, false, businessName) }
// Opened before anything is awaited. A browser only lets a page open a
// window in direct response to a tap, and the first await ends that.
// A second tap within a few seconds is the same enquiry: WhatsApp adds a
// link's text to an unsent draft, so opening it twice doubles the message.
const now = Date.now()
if (now - opened.current >= 5000) {
opened.current = now
window.open(whatsAppUrl(whatsapp, enquiryText(businessName, e)), '_blank', 'noopener,noreferrer')
}
return { ok: true, message: resultMessage(via, true, businessName) }
}
if (!email) return { ok: false, message: resultMessage(via, false, businessName) }
try {
const base = process.env.NEXT_PUBLIC_VULA_COMMERCE_URL
const id = process.env.NEXT_PUBLIC_VULA_PROJECT_ID
const r = await fetch(base + '/api/public/enquiry/' + id, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(e),
})
const d = r.ok ? await r.json() : null
// Sent only when the server says it sent. A reply that is merely "ok"
// is not the same claim, and the visitor is about to be told.
const sent = d?.sent === true
return { ok: sent, message: resultMessage(via, sent, businessName) }
} catch {
return { ok: false, message: resultMessage(via, false, businessName) }
}
}
return { available: !loading && channels.length > 0, loading, channels, send }
}