diff --git a/components/video-drag-drop-uploader.tsx b/components/video-drag-drop-uploader.tsx index a337551..adb8498 100644 --- a/components/video-drag-drop-uploader.tsx +++ b/components/video-drag-drop-uploader.tsx @@ -1,6 +1,7 @@ 'use client'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import Link from 'next/link'; import { useRouter } from 'next/navigation'; import { CheckCircle2, Loader2, UploadCloud, XCircle } from 'lucide-react'; import { toast } from 'sonner'; @@ -22,6 +23,7 @@ import { DialogTitle, } from '@/components/ui/dialog'; import { resolvePublicBunnyCdnHostname } from '@/lib/bunny-cdn'; +import { isTrialStorageError, toastApiError } from '@/lib/client/api-error'; import { cleanupPendingProjectUpload, getDefaultTitleFromFile, @@ -46,6 +48,8 @@ type QueueItem = { status: QueueItemStatus; progress: number; error?: string; + /** The failure was the trial ceiling, so the row shows the way out. */ + errorIsTrialLimit?: boolean; }; interface VideoDragDropUploaderProps { @@ -306,12 +310,17 @@ export function VideoDragDropUploader({ failCount += 1; const message = error instanceof Error ? error.message : 'Failed to upload video'; + const isTrialLimit = isTrialStorageError(error); setQueue((prev) => prev.map((entry) => - entry.id === item.id ? { ...entry, status: 'error', error: message } : entry + entry.id === item.id + ? { ...entry, status: 'error', error: message, errorIsTrialLimit: isTrialLimit } + : entry ) ); - toast.error(`${item.file.name}: ${message}`); + // Keeps the error code alive to the toast: a trial account that has run + // out of room is shown the plan rather than just told the upload failed. + toastApiError(error, 'Failed to upload video', { prefix: item.file.name }); } } @@ -500,7 +509,17 @@ export function VideoDragDropUploader({
{item.file.name}
{item.error ? ( -{item.error}
++ {item.error} + {item.errorIsTrialLimit && ( + + Upgrade + + )} +
) : ({getDefaultTitleFromFile(item.file)} diff --git a/lib/client/api-error.ts b/lib/client/api-error.ts index e659015..f8856c1 100644 --- a/lib/client/api-error.ts +++ b/lib/client/api-error.ts @@ -42,6 +42,14 @@ export function apiRequestError( return new ApiRequestError(payload?.error || fallback, payload?.code); } +/** + * Whether this failure is the trial ceiling, for callers that draw the way out + * themselves rather than handing it to `toastApiError`. + */ +export function isTrialStorageError(source: unknown): boolean { + return codeOf(source) === API_ERROR_CODES.TRIAL_STORAGE_LIMIT_EXCEEDED; +} + function codeOf(source: unknown): string | null { if (source instanceof ApiRequestError) return source.code; if (source && typeof source === 'object' && 'code' in source) { @@ -82,13 +90,15 @@ export function toastApiError( const message = messageOf(source, fallback); const text = options.prefix ? `${options.prefix}: ${message}` : message; - if (codeOf(source) === API_ERROR_CODES.TRIAL_STORAGE_LIMIT_EXCEEDED) { + if (isTrialStorageError(source)) { toast.error(text, { // Longer than a plain error: this one is asking for a decision rather than // just reporting, and it disappears under the cursor at the usual timing. duration: 12000, action: { - label: 'See plans', + // The message ends in "Upgrade to get 200 GB", so the button is the verb + // that sentence just used rather than a second name for the same thing. + label: 'Upgrade', onClick: () => { window.location.href = '/settings'; },