chore(web): improve error message ui

This commit is contained in:
isra el
2026-01-20 20:32:24 +03:00
parent 86060456fa
commit 4ef65b08ff
5 changed files with 210 additions and 17 deletions

View File

@@ -27,6 +27,8 @@ import { ApiEndpoints } from '@/config/api'
import { useMutation, useQuery } from '@tanstack/react-query'
import { Spinner } from '@/components/ui/spinner'
import httpBrowserClient from '@/lib/httpBrowserClient'
import { formatError } from '@/lib/utils/errorHandler'
import { RateLimitError } from '@/components/shared/rate-limit-error'
const DEFAULT_MAX_FILE_SIZE = 1024 * 1024 // 1 MB
const DEFAULT_MAX_ROWS = 50
@@ -321,15 +323,27 @@ export default function BulkSMSSend() {
</div>
</section>
{sendingBulkSMSError && (
<Alert variant='destructive' className='mt-4'>
<AlertCircle className='h-4 w-4' />
<AlertTitle>Error</AlertTitle>
<AlertDescription>
{sendingBulkSMSError?.message}
</AlertDescription>
</Alert>
)}
{sendingBulkSMSError && (() => {
const formattedError = formatError(sendingBulkSMSError)
if (formattedError.isRateLimit) {
return (
<RateLimitError
errorData={formattedError.rateLimitData}
variant="alert"
className="mt-4"
/>
)
}
return (
<Alert variant='destructive' className='mt-4'>
<AlertCircle className='h-4 w-4' />
<AlertTitle>Error</AlertTitle>
<AlertDescription>
{formattedError.message}
</AlertDescription>
</Alert>
)
})()}
{isSendingBulkSMSuccess && (
<Alert variant='default' className='mt-4'>

View File

@@ -44,6 +44,8 @@ import { Textarea } from '@/components/ui/textarea'
import { Badge } from '@/components/ui/badge'
import { Spinner } from '@/components/ui/spinner'
import { toast } from '@/hooks/use-toast'
import { formatError } from '@/lib/utils/errorHandler'
import { formatRateLimitMessageForToast } from '@/components/shared/rate-limit-error'
// Helper function to format timestamps
@@ -115,9 +117,14 @@ function ReplyDialog({ sms, onClose, open, onOpenChange }: { sms: any; onClose?:
}, 1500)
},
onError: (error: any) => {
const formattedError = formatError(error)
const description = formattedError.isRateLimit
? formatRateLimitMessageForToast(formattedError.rateLimitData)
: formattedError.message || 'Please try again.'
toast({
title: 'Failed to send SMS.',
description: error.response?.data?.message || 'Please try again.',
description,
variant: 'destructive',
})
},
})
@@ -264,9 +271,14 @@ function FollowUpDialog({ message, onClose, open, onOpenChange }: { message: any
}, 1500)
},
onError: (error: any) => {
const formattedError = formatError(error)
const description = formattedError.isRateLimit
? formatRateLimitMessageForToast(formattedError.rateLimitData)
: formattedError.message || 'Please try again.'
toast({
title: 'Failed to send follow-up SMS.',
description: error.response?.data?.message || 'Please try again.',
description,
variant: 'destructive',
})
},
})

View File

@@ -27,6 +27,8 @@ import httpBrowserClient from '@/lib/httpBrowserClient'
import { ApiEndpoints } from '@/config/api'
import { useMutation, useQuery } from '@tanstack/react-query'
import { Spinner } from '@/components/ui/spinner'
import { formatError } from '@/lib/utils/errorHandler'
import { RateLimitError } from '@/components/shared/rate-limit-error'
export default function SendSms() {
const { data: devices, isLoading: isLoadingDevices } = useQuery({
@@ -180,12 +182,23 @@ export default function SendSms() {
)}
</div>
</div>
{sendSmsError && (
<div className='flex items-center gap-2 text-destructive'>
<p>Error sending SMS: {sendSmsError.message}</p>
<X className='h-5 w-5' />
</div>
)}
{sendSmsError && (() => {
const formattedError = formatError(sendSmsError)
if (formattedError.isRateLimit) {
return (
<RateLimitError
errorData={formattedError.rateLimitData}
variant="inline"
/>
)
}
return (
<div className='flex items-center gap-2 text-destructive'>
<p>Error sending SMS: {formattedError.message}</p>
<X className='h-5 w-5' />
</div>
)
})()}
{isSendSmsSuccess && (
<div className='flex items-center gap-2'>

View File

@@ -0,0 +1,69 @@
'use client'
import Link from 'next/link'
import { Button } from '@/components/ui/button'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { AlertCircle } from 'lucide-react'
import type { RateLimitErrorData } from '@/lib/utils/errorHandler'
interface RateLimitErrorProps {
errorData?: RateLimitErrorData
variant?: 'alert' | 'inline'
className?: string
}
/**
* Component for displaying rate limit (429) errors with upgrade option
*/
export function RateLimitError({
errorData,
variant = 'alert',
className,
}: RateLimitErrorProps) {
const message = errorData?.message || 'You have reached your usage limit.'
if (variant === 'inline') {
return (
<div className={`flex flex-col gap-2 ${className || ''}`}>
<p className="text-sm text-destructive">{message}</p>
<div className="flex gap-2 flex-wrap">
<Button asChild variant="default" size="sm">
<Link href="/checkout/pro">Upgrade Plan</Link>
</Button>
<p className="text-xs text-muted-foreground flex items-center">
or wait for your limit to reset
</p>
</div>
</div>
)
}
return (
<Alert variant="destructive" className={className}>
<AlertCircle className="h-4 w-4" />
<AlertTitle>Limit Reached</AlertTitle>
<AlertDescription className="flex flex-col gap-3 mt-2">
<p>{message}</p>
<div className="flex gap-2 flex-wrap items-center">
<Button asChild variant="outline" size="sm">
<Link href="/checkout/pro">Upgrade Plan</Link>
</Button>
<span className="text-xs text-muted-foreground">
or wait for your limit to reset
</span>
</div>
</AlertDescription>
</Alert>
)
}
/**
* Formats a rate limit error message for use in toast notifications
* Since toasts can't contain interactive components, this returns a plain message
*/
export function formatRateLimitMessageForToast(
errorData?: RateLimitErrorData
): string {
const baseMessage = errorData?.message || 'You have reached your usage limit.'
return `${baseMessage} Please upgrade your plan or wait for your limit to reset.`
}

View File

@@ -0,0 +1,85 @@
import { AxiosError } from 'axios'
export interface RateLimitErrorData {
message: string
hasReachedLimit: boolean
dailyLimit?: number
dailyRemaining?: number
monthlyRemaining?: number
bulkSendLimit?: number
monthlyLimit?: number
}
export interface FormattedError {
message: string
isRateLimit: boolean
rateLimitData?: RateLimitErrorData
}
/**
* Formats axios errors into user-friendly messages
* Special handling for 429 (rate limit) errors
*/
export function formatError(error: unknown): FormattedError {
if (!error) {
return {
message: 'An unexpected error occurred. Please try again.',
isRateLimit: false,
}
}
// Check if it's an axios error
const axiosError = error as AxiosError
if (axiosError.response) {
const status = axiosError.response.status
const data = axiosError.response.data as any
// Handle 429 rate limit errors
if (status === 429) {
const rateLimitData: RateLimitErrorData = {
message: data?.message || 'Rate limit reached',
hasReachedLimit: data?.hasReachedLimit ?? true,
dailyLimit: data?.dailyLimit,
dailyRemaining: data?.dailyRemaining,
monthlyRemaining: data?.monthlyRemaining,
bulkSendLimit: data?.bulkSendLimit,
monthlyLimit: data?.monthlyLimit,
}
return {
message: rateLimitData.message,
isRateLimit: true,
rateLimitData,
}
}
// For other HTTP errors, use the message from the response
if (data?.message) {
return {
message: data.message,
isRateLimit: false,
}
}
}
// For non-axios errors or errors without a response
if (error instanceof Error) {
return {
message: error.message || 'An unexpected error occurred. Please try again.',
isRateLimit: false,
}
}
return {
message: 'An unexpected error occurred. Please try again.',
isRateLimit: false,
}
}
/**
* Checks if an error is a rate limit (429) error
*/
export function isRateLimitError(error: unknown): boolean {
const formatted = formatError(error)
return formatted.isRateLimit
}