Deploy
This commit is contained in:
@@ -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 }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user