| 1 | import type { ReactNode } from 'react' |
| 2 | import type { ToastId } from '../../../store/toastStore' |
| 3 | |
| 4 | type ExportProgressToastOptions = { |
| 5 | id?: ToastId |
| 6 | description?: ReactNode |
| 7 | duration?: number |
| 8 | } |
| 9 | |
| 10 | type ExportProgressToastApi = { |
| 11 | loading: (message: ReactNode, options?: ExportProgressToastOptions) => ToastId |
| 12 | success: (message: ReactNode, options?: ExportProgressToastOptions) => ToastId |
| 13 | info: (message: ReactNode, options?: ExportProgressToastOptions) => ToastId |
| 14 | error: (message: ReactNode, options?: ExportProgressToastOptions) => ToastId |
| 15 | } |
| 16 | |
| 17 | export const EXPORT_PROGRESS_TOAST_DURATION_MS = 60 * 60 * 1000 |
| 18 | |
| 19 | export function clampExportProgress(progress: number): number { |
| 20 | return Math.max(0, Math.min(100, Math.round(progress))) |
| 21 | } |
| 22 | |
| 23 | function ExportProgressDescription({ |
| 24 | description, |
| 25 | progress |
| 26 | }: { |
| 27 | description: ReactNode |
| 28 | progress: number |
| 29 | }): React.JSX.Element { |
| 30 | return ( |
| 31 | <div className="mt-1 flex w-[260px] max-w-full flex-col gap-2"> |
| 32 | <div className="text-[12px] leading-snug text-muted-foreground">{description}</div> |
| 33 | <div className="flex items-center gap-2"> |
| 34 | <div className="soft-inset h-2 flex-1 overflow-hidden rounded-full"> |
| 35 | <div |
| 36 | className="h-full rounded-full bg-primary transition-all" |
| 37 | style={{ width: `${Math.min(100, Math.max(0, progress))}%` }} |
| 38 | /> |
| 39 | </div> |
| 40 | <span className="w-9 text-right text-[11px] tabular-nums text-primary">{progress}%</span> |
| 41 | </div> |
| 42 | </div> |
| 43 | ) |
| 44 | } |
| 45 | |
| 46 | export function startExportProgressToast({ |
| 47 | toast, |
| 48 | title, |
| 49 | description, |
| 50 | initialProgress = 8 |
| 51 | }: { |
| 52 | toast: ExportProgressToastApi |
| 53 | title: string |
| 54 | description: ReactNode |
| 55 | initialProgress?: number |
| 56 | }): { |
| 57 | success: (message: string, options?: ExportProgressToastOptions) => void |
| 58 | cancel: (message: string) => void |
| 59 | error: (message: string) => void |
| 60 | update: (payload: { progress: number; description?: ReactNode }) => void |
| 61 | dispose: () => void |
| 62 | } { |
| 63 | let finished = false |
| 64 | let progress = clampExportProgress(initialProgress) |
| 65 | let currentDescription = description |
| 66 | |
| 67 | const renderDescription = (): React.JSX.Element => ( |
| 68 | <ExportProgressDescription description={currentDescription} progress={progress} /> |
| 69 | ) |
| 70 | |
| 71 | const toastId = toast.loading(title, { |
| 72 | description: renderDescription(), |
| 73 | duration: EXPORT_PROGRESS_TOAST_DURATION_MS |
| 74 | }) |
| 75 | |
| 76 | const stop = (): void => { |
| 77 | if (finished) return |
| 78 | finished = true |
| 79 | } |
| 80 | |
| 81 | const terminalOptions = (options?: ExportProgressToastOptions): ExportProgressToastOptions => ({ |
| 82 | ...options, |
| 83 | id: toastId, |
| 84 | description: options?.description ?? null |
| 85 | }) |
| 86 | |
| 87 | return { |
| 88 | success: (message, options) => { |
| 89 | stop() |
| 90 | toast.success(message, terminalOptions(options)) |
| 91 | }, |
| 92 | cancel: (message) => { |
| 93 | stop() |
| 94 | toast.info(message, terminalOptions({ duration: 3000 })) |
| 95 | }, |
| 96 | error: (message) => { |
| 97 | stop() |
| 98 | toast.error(message, terminalOptions({ duration: 6000 })) |
| 99 | }, |
| 100 | update: (payload) => { |
| 101 | if (finished) return |
| 102 | progress = clampExportProgress(payload.progress) |
| 103 | currentDescription = payload.description ?? description |
| 104 | toast.loading(title, { |
| 105 | id: toastId, |
| 106 | description: renderDescription(), |
| 107 | duration: EXPORT_PROGRESS_TOAST_DURATION_MS |
| 108 | }) |
| 109 | }, |
| 110 | dispose: stop |
| 111 | } |
| 112 | } |
| 113 |