Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Meta, StoryObj } from '@storybook/react-native';
import { Box, Button, HStack, Text, VStack } from 'react-native-ficus-ui';

import { Card, CardBody, CardHeader, CardTitle } from './card';
import { Card, CardBody, CardHeader, CardTitle } from '.';

const meta: Meta<typeof Card> = {
title: 'UI/Card',
Expand Down
File renamed without changes.
33 changes: 33 additions & 0 deletions src/components/ui/skeleton/docs.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { Meta, StoryObj } from '@storybook/react-native';
import { Box } from 'react-native-ficus-ui';

import { Skeleton } from '.';

const meta: Meta<typeof Skeleton> = {
title: 'UI/Skeleton',
component: Skeleton,
parameters: {
notes: 'Basic skeleton component using react-native-ficus-ui.',
},
decorators: [
(Story) => (
<Box
p={16}
bg="white"
flex={1}
justifyContent="center"
alignItems="center"
>
<Story />
</Box>
),
],
};
export default meta;

type Story = StoryObj<typeof Skeleton>;

export const Basic: Story = {
name: 'Basic',
render: (args) => <Skeleton {...args} />,
};
43 changes: 43 additions & 0 deletions src/components/ui/skeleton/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useEffect } from 'react';
import { StyleProp, ViewProps, ViewStyle } from 'react-native';
import Animated, {
useAnimatedStyle,
useSharedValue,
withRepeat,
withTiming,
} from 'react-native-reanimated';

import theme from '@/lib/ficus-ui/theme';

export type SkeletonProps = Omit<ViewProps, 'style'> & {
style?: StyleProp<ViewStyle>;
};

export const Skeleton = (props: SkeletonProps) => {
const opacity = useSharedValue(0.6);

useEffect(() => {
opacity.value = withRepeat(withTiming(0.3, { duration: 800 }), -1, true);
}, [opacity]);

const animatedStyles = useAnimatedStyle(() => ({
opacity: opacity.value,
}));

return (
<Animated.View
{...props}
style={[
animatedStyles,
{
minHeight: 24,
minWidth: 24,
backgroundColor: theme.colors.neutral[500],
borderRadius: 8,
flex: 1,
},
props.style,
]}
/>
);
};
41 changes: 17 additions & 24 deletions src/features/books/book-cover.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,26 @@
import { Box, BoxProps, Text } from 'react-native-ficus-ui';
import { Box, BoxProps, Flex, Text } from 'react-native-ficus-ui';

import { BookGetByIdResponse } from '@/lib/hey-api/generated';

export type BookCoverProps = BoxProps & { book: BookGetByIdResponse };

const COVER_HEIGHT = 240;

export const BookCover = ({
book,
h = COVER_HEIGHT,
...props
}: BookCoverProps) => {
export const BookCover = ({ book, ...props }: BookCoverProps) => {
return (
<Box
justifyContent="space-between"
p={16}
bg={book.genre?.color}
h={h}
w="100%"
aspectRatio={2 / 3}
borderRadius="lg"
{...props}
>
<Text fontSize="md" fontWeight="bold" color="white">
{book.title}
</Text>
<Text fontSize="xs" fontWeight="medium" color="white">
{book.author}
</Text>
<Box flex={1} aspectRatio={2 / 3} {...props}>
<Flex
aspectRatio={2 / 3}
p={16}
bg={book.genre?.color}
borderRadius="lg"
justifyContent="space-between"
>
<Text fontSize="md" fontWeight="bold" color="white">
{book.title}
</Text>
<Text fontSize="xs" fontWeight="medium" color="white">
{book.author}
</Text>
</Flex>
</Box>
);
};
12 changes: 6 additions & 6 deletions src/features/books/view-book.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Center, Divider, HStack, Stack, Text } from 'react-native-ficus-ui';
import { api } from '@/lib/hey-api/api';

import { Card, CardBody } from '@/components/ui/card';
import { FullLoader } from '@/components/ui/full-loader';
import { Skeleton } from '@/components/ui/skeleton';

import { BookCover } from '@/features/books/book-cover';
import { ViewTabContent } from '@/layout/view-tab-content';
Expand All @@ -26,13 +26,13 @@ export const ViewBook = (props: { bookId: string }) => {
});

return (
<ViewTabContent withHeader>
<ViewTabContent withHeader fixed>
{ui
.match('pending', () => <FullLoader />)
.match('pending', () => <Skeleton style={{ maxHeight: 170 }} />)
.match('error', () => <></>)
.match('default', ({ data }) => (
<Stack gap={16} flex={1}>
<Card>
<Card maxW={500}>
<CardBody py={4}>
<HStack gap={8} py={12}>
<Text
Expand Down Expand Up @@ -91,8 +91,8 @@ export const ViewBook = (props: { bookId: string }) => {
</HStack>
</CardBody>
</Card>
<Center flex={1}>
<BookCover book={data} alignSelf="center" h="80%" />
<Center h="60%" p={16}>
<BookCover book={data} />
</Center>
</Stack>
))
Expand Down
41 changes: 35 additions & 6 deletions src/features/books/view-books.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@ import { getUiState } from '@bearstudio/ui-state';
import { useInfiniteQuery } from '@tanstack/react-query';
import { Link } from 'expo-router';
import { ActivityIndicator } from 'react-native';
import { FlashList, Text } from 'react-native-ficus-ui';
import {
Box,
FlashList,
HStack,
Text,
useMediaQuery,
} from 'react-native-ficus-ui';

import { api } from '@/lib/hey-api/api';

import { FullLoader } from '@/components/ui/full-loader';
import { Skeleton } from '@/components/ui/skeleton';

import { BookCover } from '@/features/books/book-cover';
import { ViewTabContent } from '@/layout/view-tab-content';
Expand All @@ -29,28 +35,51 @@ export const ViewBooks = () => {
});
});

const [isTablet] = useMediaQuery({
minWidth: 480,
});

return (
<ViewTabContent withHeader>
{ui
.match('pending', () => <FullLoader />)
.match('pending', () => (
<HStack>
<Box flex={1} p={8} aspectRatio={2 / 3}>
<Skeleton />
</Box>
<Box flex={1} p={8} aspectRatio={2 / 3}>
<Skeleton />
</Box>
{isTablet && (
<>
<Box flex={1} p={8} aspectRatio={2 / 3}>
<Skeleton />
</Box>
<Box flex={1} p={8} aspectRatio={2 / 3}>
<Skeleton />
</Box>
</>
)}
</HStack>
))
.match('error', () => <></>)
.match('empty', () => <Text>There is no books</Text>)
.match('default', ({ data }) => (
<FlashList
data={data}
keyExtractor={(item) => item.id}
numColumns={2}
numColumns={isTablet ? 4 : 2}
horizontal={false}
renderItem={({ item }) => (
<Link
href={{
pathname: '/books/[id]',
params: { id: item.id, title: item.title },
}}
style={{ padding: 8, flex: 1 }}
style={{ aspectRatio: 2 / 3 }}
>
<Link.Trigger>
<BookCover book={item} />
<BookCover book={item} p={8} />
</Link.Trigger>
<Link.Preview />
</Link>
Expand Down
12 changes: 11 additions & 1 deletion src/layout/view-tab-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,20 @@ import { isApple } from '@/constants/device';
export const ViewTabContent = ({
withHeader = isApple && WITH_NATIVE_TABS,
children,
fixed,
...props
}: BoxProps & { withHeader?: boolean }) => {
}: BoxProps & { withHeader?: boolean; fixed?: boolean }) => {
const insets = useSafeAreaInsets();

if (fixed) {
return (
<Box p={16} pt={(withHeader ? 0 : insets.top) + 16} flex={1} {...props}>
{children}
<Box h={insets.bottom} />
</Box>
);
}

return (
<ScrollBox
p={16}
Expand Down
Loading