feat: Add useReachedBottom hook, add post card skeleton, clean-up Post Card components

This commit is contained in:
MTG2000
2022-04-18 14:16:55 +03:00
parent 83652b09a6
commit c75777f5a8
19 changed files with 183 additions and 21 deletions

19
package-lock.json generated
View File

@@ -67,6 +67,7 @@
"@storybook/node-logger": "^6.3.12",
"@storybook/preset-create-react-app": "^3.2.0",
"@storybook/react": "^6.3.12",
"@types/lodash.debounce": "^4.0.6",
"@types/lodash.throttle": "^4.1.6",
"@types/react-copy-to-clipboard": "^5.0.2",
"autoprefixer": "^9.8.8",
@@ -9612,6 +9613,15 @@
"integrity": "sha512-0fDwydE2clKe9MNfvXHBHF9WEahRuj+msTuQqOmAApNORFvhMYZKNGGJdCzuhheVjMps/ti0Ak/iJPACMaevvw==",
"dev": true
},
"node_modules/@types/lodash.debounce": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.6.tgz",
"integrity": "sha512-4WTmnnhCfDvvuLMaF3KV4Qfki93KebocUF45msxhYyjMttZDQYzHkO639ohhk8+oco2cluAFL3t5+Jn4mleylQ==",
"dev": true,
"dependencies": {
"@types/lodash": "*"
}
},
"node_modules/@types/lodash.throttle": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/@types/lodash.throttle/-/lodash.throttle-4.1.6.tgz",
@@ -68051,6 +68061,15 @@
"integrity": "sha512-0fDwydE2clKe9MNfvXHBHF9WEahRuj+msTuQqOmAApNORFvhMYZKNGGJdCzuhheVjMps/ti0Ak/iJPACMaevvw==",
"dev": true
},
"@types/lodash.debounce": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.6.tgz",
"integrity": "sha512-4WTmnnhCfDvvuLMaF3KV4Qfki93KebocUF45msxhYyjMttZDQYzHkO639ohhk8+oco2cluAFL3t5+Jn4mleylQ==",
"dev": true,
"requires": {
"@types/lodash": "*"
}
},
"@types/lodash.throttle": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/@types/lodash.throttle/-/lodash.throttle-4.1.6.tgz",

View File

@@ -117,6 +117,7 @@
"@storybook/node-logger": "^6.3.12",
"@storybook/preset-create-react-app": "^3.2.0",
"@storybook/react": "^6.3.12",
"@types/lodash.debounce": "^4.0.6",
"@types/lodash.throttle": "^4.1.6",
"@types/react-copy-to-clipboard": "^5.0.2",
"autoprefixer": "^9.8.8",

View File

@@ -1,6 +1,6 @@
import VotesCount from "src/Components/VotesCount/VotesCount"
import { Bounty } from "src/features/Posts/types"
import Header from "./Header"
import Header from "../Header/Header"
import { FiUsers } from "react-icons/fi"
import Badge from "src/Components/Badge/Badge"
import Button from "src/Components/Button/Button"

View File

@@ -0,0 +1,24 @@
import Avatar from 'src/features/Profiles/Components/Avatar/Avatar';
import dayjs from 'dayjs'
import Skeleton from 'react-loading-skeleton';
interface Props {
size?: 'sm' | 'md'
}
export default function HeaderSkeleton({ size = 'md', }: Props) {
return (
<div className='flex gap-8'>
<Skeleton circle width={size === 'md' ? 40 : 32} height={size === 'md' ? 40 : 32} />
<div>
<p className={`${size === 'md' ? 'text-body4' : "text-body5"} text-black font-medium`}>
<Skeleton width={'12ch'} />
</p>
<p className={`text-body6 text-gray-600`}>
<Skeleton width={'7ch'} />
</p>
</div>
</div>
)
}

View File

@@ -0,0 +1,29 @@
import Skeleton from "react-loading-skeleton"
import HeaderSkeleton from "../Header/Header.Skeleton"
import Badge from 'src/Components/Badge/Badge'
export default function PostCardSkeleton() {
return <div className="bg-white rounded-12 overflow-hidden border">
<div className="relative h-[200px]">
<Skeleton height='100%' className='!leading-inherit' />
</div>
<div className="p-24">
<HeaderSkeleton />
<h2 className="text-h4 font-bolder mt-16">
<Skeleton width={'70%'} />
</h2>
<p className="text-body4 text-gray-600 mt-8">
<Skeleton width={'100%'} />
<Skeleton width={'40%'} />
</p>
<hr className="my-16 bg-gray-200" />
<div className="flex gap-24 items-center">
<Badge size="sm" isLoading />
<div className="text-gray-600">
<span className="align-middle text-body5"><Skeleton width={'10ch'} /></span>
</div>
</div>
</div>
</div>
}

View File

@@ -0,0 +1,29 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { MOCK_DATA } from 'src/mocks/data';
import PostCard from './PostCard';
import PostCardSkeleton from './PostCard.Skeleton';
export default {
title: 'Posts/Components/PostCard',
component: PostCard,
argTypes: {
backgroundColor: { control: 'color' },
},
} as ComponentMeta<typeof PostCard>;
const Template: ComponentStory<typeof PostCard> = (args) => <div className="max-w-[70ch]"><PostCard {...args} ></PostCard></div>
export const Default = Template.bind({});
Default.args = {
post: MOCK_DATA['posts'].stories[0]
}
const LoadingTemplate: ComponentStory<typeof PostCardSkeleton> = (args) => <div className="max-w-[70ch]"><PostCardSkeleton ></PostCardSkeleton></div>
export const Loading = LoadingTemplate.bind({});
Loading.args = {
}

View File

@@ -1,7 +1,7 @@
import { Post, isStory, isBounty, isQuestion } from "src/features/Posts/types"
import BountyCard from "./BountyCard"
import QuestionCard from "./QuestionCard"
import StoryCard from "./StoryCard"
import BountyCard from "../BountyCard/BountyCard"
import QuestionCard from "../QuestionCard/QuestionCard"
import StoryCard from "../StoryCard/StoryCard"
type Props = {
post: Post

View File

@@ -1,6 +1,6 @@
import VotesCount from "src/Components/VotesCount/VotesCount"
import { Question } from "src/features/Posts/types"
import Header from "./Header"
import Header from "../Header/Header"
import { FiUsers } from "react-icons/fi"
import Badge from "src/Components/Badge/Badge"
import Avatar from "src/features/Profiles/Components/Avatar/Avatar"

View File

@@ -1,6 +1,6 @@
import VotesCount from "src/Components/VotesCount/VotesCount"
import { Story } from "src/features/Posts/types"
import Header from "./Header"
import Header from "../Header/Header"
import { BiComment } from 'react-icons/bi'
interface Props {

View File

@@ -1,2 +1,5 @@
import PostCard from "./PostCard/PostCard";
export { }
export { default as PostCardSkeleton } from './PostCard/PostCard.Skeleton'
export default PostCard;

View File

@@ -16,7 +16,7 @@ const Template: ComponentStory<typeof PostsList> = (args) => <div className="max
export const Default = Template.bind({});
Default.args = {
posts: MOCK_DATA['feed']
items: MOCK_DATA['feed']
}

View File

@@ -1,20 +1,36 @@
import { useCallback } from "react"
import { Post } from "src/features/Posts/types"
import { useFeedQuery } from "src/graphql"
import PostCard from "../PostCard/PostCard"
import { useReachedBottom } from "src/utils/hooks/useReachedBottom"
import { ListProps } from "src/utils/interfaces"
import PostCard, { PostCardSkeleton } from "../PostCard"
interface Props {
posts: Post[]
}
type Props = ListProps<Post>
export default function PostsList(props: Props) {
const { data, loading } = useFeedQuery()
if (loading) return <h2>Loading</h2>
return (
<div className="flex flex-col gap-24">
{
data?.getFeed.map(post => <PostCard key={post.id} post={post} />)
const reachedBottom = useCallback(() => {
console.log("NEW FETCH")
}, [])
const { ref } = useReachedBottom<HTMLDivElement>(reachedBottom)
if (props.isLoading)
return <div className="flex flex-col gap-24">
{<>
<PostCardSkeleton />
<PostCardSkeleton />
<PostCardSkeleton />
</>
}
</div>
return (
<div ref={ref} className="flex flex-col gap-24">
{
props.items?.map(post => <PostCard key={post.id} post={post} />)
}
{props.isFetching && <PostCardSkeleton />}
</div>
)
}

View File

@@ -1,4 +1,5 @@
import { useFeedQuery } from 'src/graphql'
import { MOCK_DATA } from 'src/mocks/data'
import PostsList from '../../Components/PostsList/PostsList'
import TrendingCard from '../../Components/TrendingCard/TrendingCard'
@@ -7,17 +8,19 @@ import SortBy from './SortBy/SortBy'
import styles from './styles.module.css'
export default function FeedPage() {
const feedQuery = useFeedQuery()
return (
<div
className={`page-container grid w-full gap-32 ${styles.grid}`}
>
<aside>
<SortBy />
<hr className="my-24 bg-gray-100" />
<PopularCategories />
</aside>
<PostsList posts={MOCK_DATA['feed']} />
<PostsList isLoading={feedQuery.loading} items={feedQuery.data?.getFeed} />
<aside>
<TrendingCard />
</aside>

View File

@@ -0,0 +1,30 @@
import _debounce from "lodash.debounce";
import { useEffect, useRef } from "react";
export const useReachedBottom = <T extends HTMLElement>(cb?: () => void, options: Partial<{ offset: number, throttle: number }> = {}) => {
const { offset = window.innerHeight, throttle = 600 } = options
const ref = useRef<T>(null);
useEffect(() => {
if (!cb) return;
const cbDebounced = _debounce(cb, throttle)
const listener = () => {
if (!ref.current) return;
const curWindowPosition = window.scrollY + window.innerHeight;
const elTriggerPosition = ref.current.offsetTop + ref.current.scrollHeight - offset;
if (curWindowPosition > elTriggerPosition) cbDebounced();
}
document.addEventListener('scroll', listener)
return () => {
document.removeEventListener('scroll', listener)
}
}, [cb, offset, throttle])
return { ref }
}

View File

@@ -4,4 +4,12 @@ export type Tag = {
title: string
}
export type ListProps<T> = {
items?: T[]
isLoading?: boolean;
isFetching?: boolean;
onReachedBottom?: () => void
}
export type Image = string;