Skip to content
Merged

Dev #73

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions app/[slug]/wait/[ticketId]/WaitClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { BellRing, Smartphone, MessageSquare, AlertCircle, Download } from "luci
import { playHapticBuzz, playSound, unlockAudio, type SoundChoice } from "@/lib/utils/notifications"
import { getBusinessWording } from "@/lib/utils/business-wording"
import { buildRecoverUrl } from "@/lib/utils/ticket-download"
import { toPng } from "html-to-image"
import { toBlob } from "html-to-image"

type NotificationChannels = {
sound: boolean
Expand Down Expand Up @@ -95,11 +95,21 @@ function WaitClient({ merchant, ticketId }: WaitClientProps) {
setIsDownloading(true)
setDownloadError(null)
try {
const dataUrl = await toPng(ticketCardRef.current, { pixelRatio: 2 })
const blob = await toBlob(ticketCardRef.current, { pixelRatio: 2 })
if (!blob) throw new Error("toBlob returned null")
// A blob: object URL triggers a direct save reliably; a raw data:
// URL on the same <a download> does not — Safari treats an image
// data: URL as content to preview and offers "View or Download"
// instead of saving it, which is what surfaced this in testing.
const objectUrl = URL.createObjectURL(blob)
const link = document.createElement("a")
link.href = dataUrl
link.href = objectUrl
link.download = `ticket-${merchant.slug}.png`
link.click()
// Revoking immediately can race the browser actually reading the
// blob (Safari in particular can be slow to pick it up) — give it
// a moment before freeing the URL.
setTimeout(() => URL.revokeObjectURL(objectUrl), 1000)
} catch (err) {
console.error("[WaitClient] Ticket image export failed:", err)
setDownloadError(
Expand Down Expand Up @@ -445,7 +455,11 @@ function WaitClient({ merchant, ticketId }: WaitClientProps) {
)}

{canDownloadTicket && (
<Dialog open={ticketDialogOpen} onClose={() => setTicketDialogOpen(false)}>
<Dialog
open={ticketDialogOpen}
onClose={() => setTicketDialogOpen(false)}
closeOnBackdropClick
>
<DialogHeader onClose={() => setTicketDialogOpen(false)}>Votre ticket</DialogHeader>
<DialogContent>
<div className="flex flex-col items-center gap-4">
Expand Down
21 changes: 20 additions & 1 deletion components/ui/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,17 @@ type DialogProps = {
onClose: () => void
children: React.ReactNode
className?: string
/**
* Lets a click on the backdrop dismiss the dialog, in addition to Escape
* and any explicit close control. Off by default: several dialogs in this
* app (e.g. the "called" reminder, the moderation warning) deliberately
* force an explicit acknowledgment, and a stray backdrop click must not
* dismiss those. Opt in only for dialogs that are purely informational.
*/
closeOnBackdropClick?: boolean
}

function Dialog({ open, onClose, children, className }: DialogProps) {
function Dialog({ open, onClose, children, className, closeOnBackdropClick = false }: DialogProps) {
const dialogRef = useRef<HTMLDialogElement>(null)
const previousFocusRef = useRef<HTMLElement | null>(null)

Expand Down Expand Up @@ -48,6 +56,17 @@ function Dialog({ open, onClose, children, className }: DialogProps) {
return (
<dialog
ref={dialogRef}
onClick={
closeOnBackdropClick
? (e) => {
// A click on the ::backdrop registers as a click on the
// <dialog> element itself (target === currentTarget);
// a click on any actual content is a click on a
// descendant, so this only fires for the backdrop.
if (e.target === e.currentTarget) handleClose()
}
: undefined
}
className={cn(
"m-auto w-[calc(100%-2rem)] max-w-md rounded-lg border border-border-default bg-surface-card p-0 shadow-xl",
"backdrop:bg-surface-overlay",
Expand Down
Loading