mirror of
https://github.com/aljazceru/landscape-template.git
synced 2026-01-31 12:14:30 +01:00
feat: Add useReachedBottom hook, add post card skeleton, clean-up Post Card components
This commit is contained in:
19
package-lock.json
generated
19
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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 = {
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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 {
|
||||
@@ -1,2 +1,5 @@
|
||||
import PostCard from "./PostCard/PostCard";
|
||||
|
||||
export { }
|
||||
export { default as PostCardSkeleton } from './PostCard/PostCard.Skeleton'
|
||||
|
||||
export default PostCard;
|
||||
@@ -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']
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
30
src/utils/hooks/useReachedBottom.ts
Normal file
30
src/utils/hooks/useReachedBottom.ts
Normal 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 }
|
||||
}
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user