ultisuite-client/lib/pending-send-toast.tsx
2026-05-15 17:40:17 +02:00

178 lines
5.3 KiB
TypeScript

"use client"
import { useEffect, useRef, useState } from "react"
import { Ban, Loader2, Send } from "lucide-react"
import { toast } from "sonner"
const DEFAULT_DURATION_MS = 3000
type RefBox<T> = { current: T }
export function showPendingSendToast(options: {
durationMs?: number
onCommit: () => void | Promise<void>
onCancel: () => void
}) {
const durationMs = options.durationMs ?? DEFAULT_DURATION_MS
const committedRef: RefBox<boolean> = { current: false }
const timerRef: RefBox<number | null> = {
current: null,
}
toast.custom(
(toastId) => (
<PendingSendToastBody
toastId={toastId}
durationMs={durationMs}
committedRef={committedRef}
timerRef={timerRef}
onCommit={options.onCommit}
onCancel={options.onCancel}
/>
),
{
duration: Infinity,
onDismiss: () => {
if (timerRef.current != null) {
clearTimeout(timerRef.current)
timerRef.current = null
}
if (!committedRef.current) {
committedRef.current = true
options.onCancel()
}
},
}
)
}
function PendingSendToastBody({
toastId,
durationMs,
committedRef,
timerRef,
onCommit,
onCancel,
}: {
toastId: string | number
durationMs: number
committedRef: RefBox<boolean>
timerRef: RefBox<number | null>
onCommit: () => void | Promise<void>
onCancel: () => void
}) {
const onCommitRef = useRef(onCommit)
const onCancelRef = useRef(onCancel)
onCommitRef.current = onCommit
onCancelRef.current = onCancel
const [barImmediate, setBarImmediate] = useState(false)
const commitNowRef = useRef<(opts?: { manual?: boolean }) => void>(() => {})
commitNowRef.current = (opts?: { manual?: boolean }) => {
if (committedRef.current) return
if (opts?.manual) setBarImmediate(true)
committedRef.current = true
if (timerRef.current != null) {
clearTimeout(timerRef.current)
timerRef.current = null
}
void (async () => {
try {
await onCommitRef.current()
toast.success("Message envoyé")
} catch {
toast.error("L'envoi a échoué")
onCancelRef.current()
} finally {
toast.dismiss(toastId)
}
})()
}
useEffect(() => {
timerRef.current = window.setTimeout(() => {
commitNowRef.current()
}, durationMs)
return () => {
if (timerRef.current != null) {
clearTimeout(timerRef.current)
timerRef.current = null
}
}
}, [durationMs, timerRef])
const handleSendNow = () => {
commitNowRef.current({ manual: true })
}
const handleCancel = () => {
if (committedRef.current) return
committedRef.current = true
if (timerRef.current != null) {
clearTimeout(timerRef.current)
timerRef.current = null
}
onCancelRef.current()
toast.dismiss(toastId)
}
return (
<div className="relative box-border w-full max-w-full overflow-hidden rounded-xl border border-[#dadce0] bg-linear-to-b from-[#f8fbff] to-white text-[#202124] shadow-md ring-1 ring-[#1a73e8]/8 backdrop-blur-sm">
<div className="px-3.5 pb-2.5 pt-3">
<div className="flex items-center gap-3">
<span
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[#e8f0fe] text-[#1a73e8]"
aria-hidden
>
<Loader2 className="h-4 w-4 animate-spin" strokeWidth={2} />
</span>
<div className="min-w-0">
<p className="text-[13px] font-semibold leading-snug tracking-tight text-[#3c4043]">
Envoi en cours
</p>
</div>
</div>
<div className="mt-2.5 grid grid-cols-[minmax(0,1.2fr)_minmax(0,1fr)] gap-2">
<button
type="button"
onClick={handleSendNow}
className="inline-flex min-h-9 min-w-0 items-center justify-center gap-1.5 whitespace-nowrap rounded-lg bg-[#1a73e8] px-2.5 py-2 text-xs font-semibold leading-tight text-white shadow-sm transition-colors hover:bg-[#1765cc] active:bg-[#1557b0]"
>
<Send className="h-3.5 w-3.5 shrink-0 opacity-95" strokeWidth={2} aria-hidden />
<span>Envoyer maintenant</span>
</button>
<button
type="button"
onClick={handleCancel}
className="inline-flex min-h-9 min-w-0 items-center justify-center gap-1.5 whitespace-nowrap rounded-lg border border-[#dadce0] bg-white px-2.5 py-2 text-xs font-semibold leading-tight text-[#5f6368] shadow-sm transition-colors hover:border-[#bdc1c6] hover:bg-[#f8f9fa] hover:text-[#3c4043]"
>
<Ban className="h-3.5 w-3.5 shrink-0" strokeWidth={2} aria-hidden />
<span>Annuler l&apos;envoi</span>
</button>
</div>
</div>
<div className="relative h-1 w-full shrink-0 bg-[#e8eaed]">
<div
className="absolute inset-y-0 left-0 bg-linear-to-r from-[#1a73e8] to-[#4285f4]"
style={
barImmediate
? { width: "100%" }
: {
width: "0%",
animation: `ultimail-pending-send-progress ${durationMs}ms linear forwards`,
}
}
/>
</div>
<style>{`
@keyframes ultimail-pending-send-progress {
from { width: 0%; }
to { width: 100%; }
}
`}</style>
</div>
)
}