diff --git a/.changeset/many-lamps-watch.md b/.changeset/many-lamps-watch.md new file mode 100644 index 00000000..0d518a07 --- /dev/null +++ b/.changeset/many-lamps-watch.md @@ -0,0 +1,7 @@ +--- +'@youversion/platform-react-ui': minor +'@youversion/platform-core': minor +'@youversion/platform-react-hooks': minor +--- + +Added loading skeleton to bible card diff --git a/packages/ui/src/components/bible-card-skeleton.tsx b/packages/ui/src/components/bible-card-skeleton.tsx new file mode 100644 index 00000000..105f76d3 --- /dev/null +++ b/packages/ui/src/components/bible-card-skeleton.tsx @@ -0,0 +1,45 @@ +import { BibleAppLogoLockup } from './bible-app-logo-lockup'; +import { Skeleton } from './ui/skeleton'; + +type BibleCardSkeletonProps = { + showVersionPicker?: boolean; + background?: 'light' | 'dark'; +}; + +export function BibleCardSkeleton({ + showVersionPicker = false, + background = 'light', +}: BibleCardSkeletonProps): React.ReactElement { + return ( +
+ Loading Bible verse + +
+ + {showVersionPicker ? : null} +
+ +
+ +
+ +
+
+ + +
+
+ +
+
+
+ ); +} diff --git a/packages/ui/src/components/bible-card.stories.tsx b/packages/ui/src/components/bible-card.stories.tsx index 5bcdecea..6a53af20 100644 --- a/packages/ui/src/components/bible-card.stories.tsx +++ b/packages/ui/src/components/bible-card.stories.tsx @@ -1,6 +1,39 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import { within, expect, userEvent, screen, waitFor } from 'storybook/test'; +import { delay, http, HttpResponse } from 'msw'; import { BibleCard } from './bible-card'; +import mockPassages from '../test/mock-data/passages.json'; +import mockBibles from '../test/mock-data/bibles.json'; + +const LOADING_DELAY = 1000; + +const delayedHandlers = [ + http.get('*/v1/bibles/:id/passages/*', async ({ params }) => { + await delay(LOADING_DELAY); + + const id = String(params.id); + if (id === '111') { + return HttpResponse.json(mockPassages['LUK.1.39-45.NIV']); + } + + if (id === '1588') { + return HttpResponse.json(mockPassages['LUK.1.39-45.AMP']); + } + + return new HttpResponse(null, { status: 404 }); + }), + http.get('*/v1/bibles/:id', async ({ params }) => { + await delay(LOADING_DELAY); + + const id = String(params.id); + const bible = mockBibles.individual[id as keyof typeof mockBibles.individual]; + if (bible) { + return HttpResponse.json(bible); + } + + return new HttpResponse(null, { status: 404 }); + }), +]; const meta = { title: 'Components/BibleCard', @@ -117,6 +150,74 @@ export const WithVersionPicker: Story = { }, }; +export const Loading: Story = { + args: { + reference: 'LUK.1.39-45', + versionId: 111, + }, + tags: ['integration'], + parameters: { + msw: { + handlers: delayedHandlers, + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const loadingSkeleton = await canvas.findByRole('status', { + name: /loading bible verse/i, + }); + await expect(loadingSkeleton).toHaveAttribute('aria-busy', 'true'); + }, +}; + +export const LoadingWithVersionPicker: Story = { + args: { + reference: 'LUK.1.39-45', + versionId: 111, + showVersionPicker: true, + }, + tags: ['integration'], + parameters: { + msw: { + handlers: delayedHandlers, + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const loadingSkeleton = await canvas.findByRole('status', { + name: /loading bible verse/i, + }); + await expect(loadingSkeleton).toHaveAttribute('aria-busy', 'true'); + }, +}; + +export const LoadingDarkMode: Story = { + args: { + reference: 'LUK.1.39-45', + versionId: 111, + showVersionPicker: true, + }, + globals: { + theme: 'dark', + }, + tags: ['integration'], + parameters: { + msw: { + handlers: delayedHandlers, + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const loadingSkeleton = await canvas.findByRole('status', { + name: /loading bible verse/i, + }); + await expect(loadingSkeleton).toHaveAttribute('aria-busy', 'true'); + }, +}; + export const RealAPI: Story = { args: { reference: 'LUK.1.39-45', diff --git a/packages/ui/src/components/bible-card.tsx b/packages/ui/src/components/bible-card.tsx index 85491422..ca6ef517 100644 --- a/packages/ui/src/components/bible-card.tsx +++ b/packages/ui/src/components/bible-card.tsx @@ -2,6 +2,7 @@ import { usePassage, useVersion, useTheme } from '@youversion/platform-react-hoo import { BibleTextView } from './verse'; import { BibleAppLogoLockup } from './bible-app-logo-lockup'; import { BibleVersionPicker } from './bible-version-picker'; +import { BibleCardSkeleton } from './bible-card-skeleton'; import { Button } from './ui/button'; import { useState } from 'react'; import { SOURCE_SERIF_FONT } from '@/lib/verse-html-utils'; @@ -19,8 +20,12 @@ export function BibleCard({ showVersionPicker = false, }: BibleCardProps): React.ReactNode { const [versionNum, setVersionNum] = useState(versionId); - const { version } = useVersion(versionNum); - const { passage } = usePassage({ + const { version, loading: versionLoading } = useVersion(versionNum); + const { + passage, + loading: passageLoading, + error, + } = usePassage({ versionId: versionNum, usfm: reference, include_headings: true, @@ -29,6 +34,10 @@ export function BibleCard({ const providerTheme = useTheme(); const theme = background || providerTheme; + if (versionLoading || passageLoading) { + return ; + } + return (
diff --git a/packages/ui/src/components/ui/skeleton.tsx b/packages/ui/src/components/ui/skeleton.tsx new file mode 100644 index 00000000..f4b223b8 --- /dev/null +++ b/packages/ui/src/components/ui/skeleton.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { cn } from '@/lib/utils'; + +function Skeleton({ className, ...props }: React.ComponentProps<'div'>): React.ReactElement { + return ( +