mirror of
https://github.com/langgenius/dify.git
synced 2026-05-29 21:27:54 +08:00
fix: banner and template card skeleton style
This commit is contained in:
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -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[]
|
||||
|
||||
Reference in New Issue
Block a user