fix: banner and template card skeleton style

This commit is contained in:
yyh
2026-05-29 21:15:03 +08:00
parent 44831839d1
commit 71ba903d4c
3 changed files with 55 additions and 41 deletions

View File

@ -73,23 +73,26 @@ export function ExploreHeaderSkeleton() {
function ExploreAppCardSkeleton() {
return (
<div className="col-span-1 flex h-[142px] flex-col overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg pb-3 shadow-xs shadow-shadow-shadow-3">
<SkeletonContainer className="h-full px-4 pt-4">
<SkeletonRow>
<div className="flex shrink-0 items-center gap-3 px-4 pt-4 pb-2">
<div className="relative shrink-0">
<SkeletonRectangle className="size-10 shrink-0 animate-pulse rounded-lg" />
<div className="flex min-w-0 flex-1 flex-col gap-1">
<SkeletonRectangle className="h-4 w-2/3 animate-pulse" />
<SkeletonRectangle className="h-3 w-1/3 animate-pulse" />
</div>
</SkeletonRow>
<div className="mt-3 flex flex-col gap-1">
<SkeletonRectangle className="h-3 w-full animate-pulse" />
<SkeletonRectangle className="h-3 w-4/5 animate-pulse" />
</div>
<div className="mt-auto flex gap-1 pb-1">
<SkeletonRectangle className="h-5 w-20 animate-pulse rounded-[5px]" />
<SkeletonRectangle className="h-5 w-16 animate-pulse rounded-[5px]" />
<div className="flex w-0 grow flex-col gap-1 py-px">
<SkeletonRectangle className="my-0 h-4 w-3/5 animate-pulse" />
<SkeletonRectangle className="my-0 h-3 w-16 animate-pulse" />
</div>
</SkeletonContainer>
</div>
<div className="flex shrink-0 items-start px-4 py-1">
<div className="flex flex-1 flex-col gap-1">
<SkeletonRectangle className="my-0 h-3 w-full animate-pulse" />
<SkeletonRectangle className="my-0 h-3 w-4/5 animate-pulse" />
</div>
</div>
<div className="relative flex h-[26px] w-full shrink-0 flex-col gap-2 overflow-hidden px-3">
<div className="flex w-full shrink-0 items-center gap-1 rounded-lg p-1">
<SkeletonRectangle className="my-0 h-5 w-20 animate-pulse rounded-[5px]" />
</div>
</div>
</div>
)
}

View File

@ -155,11 +155,10 @@ describe('Banner', () => {
render(<Banner />)
const loadingWrapper = document.querySelector('[style*="min-height"]')
expect(loadingWrapper).toBeInTheDocument()
expect(screen.getByRole('status', { name: 'loading' })).toBeInTheDocument()
})
it('shows loading indicator with correct minimum height', () => {
it('matches the loaded greeting card shell while loading', () => {
mockUseGetBanners.mockReturnValue({
data: null,
isLoading: true,
@ -168,39 +167,42 @@ describe('Banner', () => {
render(<Banner />)
const loadingWrapper = document.querySelector('[style*="min-height: 168px"]')
expect(loadingWrapper).toBeInTheDocument()
const loadingWrapper = screen.getByRole('status', { name: 'loading' })
expect(loadingWrapper).toHaveClass('rounded-[24px]', 'bg-background-default-dodge')
expect(loadingWrapper.querySelector('.px-8.pt-8.pb-8')).toBeInTheDocument()
})
})
describe('error state', () => {
it('returns null when isError is true', () => {
it('renders the greeting shell without slider when isError is true', () => {
mockUseGetBanners.mockReturnValue({
data: null,
isLoading: false,
isError: true,
})
const { container } = render(<Banner />)
render(<Banner />)
expect(container.firstChild).toBeNull()
expect(screen.getByText('Welcome back, Evan 👋')).toBeInTheDocument()
expect(screen.queryByTestId('carousel')).not.toBeInTheDocument()
})
})
describe('empty state', () => {
it('returns null when banners array is empty', () => {
it('renders the greeting shell without slider when banners array is empty', () => {
mockUseGetBanners.mockReturnValue({
data: [],
isLoading: false,
isError: false,
})
const { container } = render(<Banner />)
render(<Banner />)
expect(container.firstChild).toBeNull()
expect(screen.getByText('Welcome back, Evan 👋')).toBeInTheDocument()
expect(screen.queryByTestId('carousel')).not.toBeInTheDocument()
})
it('returns null when all banners are disabled', () => {
it('renders the greeting shell without slider when all banners are disabled', () => {
mockUseGetBanners.mockReturnValue({
data: [
createMockBanner('1', 'disabled'),
@ -210,21 +212,23 @@ describe('Banner', () => {
isError: false,
})
const { container } = render(<Banner />)
render(<Banner />)
expect(container.firstChild).toBeNull()
expect(screen.getByText('Welcome back, Evan 👋')).toBeInTheDocument()
expect(screen.queryByTestId('carousel')).not.toBeInTheDocument()
})
it('returns null when data is undefined', () => {
it('renders the greeting shell without slider when data is undefined', () => {
mockUseGetBanners.mockReturnValue({
data: undefined,
isLoading: false,
isError: false,
})
const { container } = render(<Banner />)
render(<Banner />)
expect(container.firstChild).toBeNull()
expect(screen.getByText('Welcome back, Evan 👋')).toBeInTheDocument()
expect(screen.queryByTestId('carousel')).not.toBeInTheDocument()
})
})

View File

@ -6,24 +6,31 @@ import { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { trackEvent } from '@/app/components/base/amplitude'
import { Carousel, useCarousel } from '@/app/components/base/carousel'
import { SkeletonRectangle } from '@/app/components/base/skeleton'
import { useSelector } from '@/context/app-context'
import { useLocale } from '@/context/i18n'
import { useGetBanners } from '@/service/use-explore'
import Loading from '../../base/loading'
import { BannerItem } from './banner-item'
const AUTOPLAY_DELAY = 5000
const MIN_LOADING_HEIGHT = 168
const RESIZE_DEBOUNCE_DELAY = 50
const LoadingState: FC = () => (
<div
className="flex items-center justify-center rounded-[24px] bg-background-default-dodge shadow-xs"
style={{ minHeight: MIN_LOADING_HEIGHT }}
>
<Loading />
</div>
)
const LoadingState: FC = () => {
const { t } = useTranslation()
return (
<div
role="status"
aria-label={t('loading', { ns: 'common' })}
className="relative flex w-full flex-col items-start overflow-hidden rounded-[24px] bg-background-default-dodge shadow-xs"
>
<div className="flex w-full flex-col gap-1 px-8 pt-8 pb-8">
<SkeletonRectangle className="my-0 h-8 w-[360px] max-w-full animate-pulse" />
<SkeletonRectangle className="my-0 h-4 w-72 max-w-full animate-pulse" />
</div>
</div>
)
}
type BannerImpressionTrackerProps = {
banners: BannerType[]