| 159 | } |
| 160 | |
| 161 | export function UpsellPanel({ source, returnPath, className, licenseState = 'free', title, description, loadingVariant = 'skeleton' }: UpsellPanelProps) { |
| 162 | const { data: offers, isPending, isError, refetch } = useOffers(); |
| 163 | |
| 164 | if (isError) { |
| 165 | return <UpsellLoadError variant="inline" onRetry={() => { void refetch(); }} className={className} />; |
| 166 | } |
| 167 | |
| 168 | if (isPending || !offers) { |
| 169 | if (loadingVariant === 'spinner') { |
| 170 | return ( |
| 171 | <div className={cn("flex items-center justify-center py-12", className)} aria-busy="true"> |
| 172 | <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /> |
| 173 | </div> |
| 174 | ); |
| 175 | } |
| 176 | return ( |
| 177 | <div className={cn("flex flex-col gap-6", className)} aria-busy="true"> |
| 178 | <div className="flex flex-col gap-2"> |
| 179 | <Skeleton className="h-6 w-6 rounded-full" /> |
| 180 | <Skeleton className="h-6 w-48" /> |
| 181 | <Skeleton className="h-5 w-72" /> |
| 182 | </div> |
| 183 | <Skeleton className="h-64 w-full" /> |
| 184 | <div className="flex flex-col-reverse items-center gap-2 sm:flex-row sm:justify-end sm:gap-4"> |
| 185 | <Skeleton className="h-10 w-32" /> |
| 186 | <Skeleton className="h-10 w-32" /> |
| 187 | </div> |
| 188 | </div> |
| 189 | ); |
| 190 | } |
| 191 | |
| 192 | return ( |
| 193 | <div className={cn("flex flex-col gap-6", className)}> |
| 194 | <UpsellPanelContent offers={offers} source={source} returnPath={returnPath} variant="inline" licenseState={licenseState} titleOverride={title} descriptionOverride={description} /> |
| 195 | </div> |
| 196 | ); |
| 197 | } |
| 198 | |
| 199 | interface UpsellPanelContentProps { |
| 200 | offers: OffersResponse; |