feat: update story card & page to accept tagged project

This commit is contained in:
MTG2000
2022-10-03 23:10:06 +03:00
parent 383a59a8e5
commit bd44334f69
16 changed files with 201 additions and 48 deletions

View File

@@ -660,6 +660,7 @@ export interface NexusGenFieldTypes {
excerpt: string; // String!
id: number; // Int!
is_published: boolean | null; // Boolean
project: NexusGenRootTypes['Project'] | null; // Project
tags: NexusGenRootTypes['Tag'][]; // [Tag!]!
title: string; // String!
type: string; // String!
@@ -1051,6 +1052,7 @@ export interface NexusGenFieldTypeNames {
excerpt: 'String'
id: 'Int'
is_published: 'Boolean'
project: 'Project'
tags: 'Tag'
title: 'String'
type: 'String'

View File

@@ -399,6 +399,7 @@ type Story implements PostBase {
excerpt: String!
id: Int!
is_published: Boolean
project: Project
tags: [Tag!]!
title: String!
type: String!

View File

@@ -114,6 +114,13 @@ const Story = objectType({
});
t.field('project', {
type: "Project",
resolve(parent) {
return null
}
})
},
})

View File

@@ -8,7 +8,7 @@ interface Props {
export default function HeaderSkeleton({ size = 'md', }: Props) {
return (
<div className='flex gap-8'>
<div className='flex gap-8 items-center'>
<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`}>

View File

@@ -1,14 +1,20 @@
import Skeleton from "react-loading-skeleton"
import HeaderSkeleton from "../Header/Header.Skeleton"
import Badge from 'src/Components/Badge/Badge'
import Card from "src/Components/Card/Card"
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' />
return <div>
<div className="flex gap-8 items-center mb-8">
<Skeleton circle width={24} height={24} />
<span className='flex gap-4 mt-4'>
<p className="text-gray-900 text-body5 font-medium"><Skeleton width="12ch" /></p>
</span>
</div>
<div className="p-24">
<HeaderSkeleton />
<Card>
<div className="relative h-[200px]">
<Skeleton height='100%' className='!leading-inherit rounded-8' />
</div>
<h2 className="text-h4 font-bolder mt-16">
<Skeleton width={'70%'} />
</h2>
@@ -24,6 +30,8 @@ export default function PostCardSkeleton() {
<span className="align-middle text-body5"><Skeleton width={'10ch'} /></span>
</div>
</div>
</div>
</Card>
</div>
}

View File

@@ -0,0 +1,52 @@
import Avatar from 'src/features/Profiles/Components/Avatar/Avatar';
import dayjs from 'dayjs'
import { UnionToObjectKeys } from 'src/utils/types/utils';
import { trimText } from 'src/utils/helperFunctions';
import { Link } from 'react-router-dom';
import { createRoute } from 'src/utils/routing';
import { Project, User } from 'src/graphql';
interface Props {
author?: Pick<User, 'id' | 'name' | 'avatar'>
project?: Pick<Project, 'id' | 'title' | "thumbnail_image" | 'hashtag'> | null
date: string;
}
export default function PostCardHeader(props: Props) {
const dateToShow = () => {
const passedTime = dayjs().diff(props.date, 'hour');
if (passedTime === 0) return 'now';
if (passedTime < 24) return `${dayjs().diff(props.date, 'hour')}h ago`
return dayjs(props.date).format('MMMM DD');
}
if (!props.author) return null
return (
<div className="flex gap-8 items-center mb-8">
<span className='flex'>
<Link to={createRoute({ type: 'profile', id: props.author.id, username: props.author.name })}>
<Avatar width={24} src={props.author.avatar} />
</Link>
{props.project && <Link className='-ml-12' to={createRoute({ type: 'projects-page', })}>
<Avatar src={props.project.thumbnail_image} width={24} />
</Link>}
</span>
<span className='flex gap-4'>
<Link className='hover:underline' to={createRoute({ type: 'profile', id: props.author.id, username: props.author.name })}>
<p className="text-gray-900 text-body5 font-medium">{trimText(props.author.name, 30)}</p>
</Link>
{props.project && <>
<span className="text-body5 text-gray-500 font-medium">for</span>
<Link className='hover:underline' to={createRoute({ type: 'profile', id: props.author.id, username: props.author.name })}>
<p className="text-gray-900 text-body5 font-medium">{trimText(props.project.title, 30)}</p>
</Link>
</>}
</span>
<p className="text-body6 text-gray-500 font-medium">{dateToShow()}</p>
</div>
)
}

View File

@@ -1,5 +1,4 @@
import { Story } from "src/features/Posts/types"
import Header from "../Header/Header"
import { Link } from "react-router-dom"
import VoteButton from "src/Components/VoteButton/VoteButton"
import { useVote } from "src/utils/hooks"
@@ -8,6 +7,7 @@ import Badge from "src/Components/Badge/Badge"
import { createRoute } from "src/utils/routing"
import { BiComment } from "react-icons/bi"
import Card from "src/Components/Card/Card"
import PostCardHeader from "../PostCardHeader/PostCardHeader"
export type StoryCardType = Pick<Story,
@@ -19,6 +19,7 @@ export type StoryCardType = Pick<Story,
| 'excerpt'
| 'votes_count'
| 'comments_count'
| 'project'
> & {
tags: Array<Pick<Tag, 'id' | "title">>,
author: Pick<Author, 'id' | 'name' | 'avatar' | 'join_date'>
@@ -34,30 +35,37 @@ export default function StoryCard({ story }: Props) {
itemType: Vote_Item_Type.Story
});
return (
<Card className="overflow-hidden" defaultPadding={false}>
{story.cover_image && <img src={story.cover_image} className='h-[200px] w-full object-cover' alt="" />}
<div className="p-24">
<Header author={story.author} date={story.createdAt} />
<Link to={createRoute({ type: 'story', id: story.id, title: story.title, username: story.author.name })}>
<h2 className="text-h5 font-bolder mt-16">{story.title}</h2>
</Link>
<p className="text-body4 text-gray-600 mt-8">{story.excerpt}...</p>
<div className="flex gap-8 mt-8">
{story.tags.map(tag => <Badge key={tag.id} size='sm'>
{tag.title}
</Badge>)}
</div>
<hr className="my-16 bg-gray-200" />
<div className="flex gap-24 items-center">
<VoteButton votes={story.votes_count} dense onVote={vote} />
<div className="text-gray-600">
<BiComment /> <span className="align-middle text-body5">{story.comments_count} Comments</span>
return (
<div>
<PostCardHeader author={story.author} project={story.project} date={story.createdAt} />
<Card className="overflow-hidden mt-8" >
{story.cover_image &&
<Link className="mb-16 block" to={createRoute({ type: 'story', id: story.id, title: story.title, username: story.author.name })}>
<img src={story.cover_image} className='h-[200px] w-full object-cover rounded-8' alt="" />
</Link>
}
<div >
<Link to={createRoute({ type: 'story', id: story.id, title: story.title, username: story.author.name })}>
<h2 className="text-h5 font-bolder">{story.title}</h2>
</Link>
<p className="text-body4 text-gray-600 mt-8">{story.excerpt}...</p>
<div className="flex gap-8 mt-8">
{story.tags.map(tag => <Badge key={tag.id} size='sm'>
{tag.title}
</Badge>)}
</div>
<hr className="my-16 bg-gray-200" />
<div className="flex gap-24 items-center">
<VoteButton votes={story.votes_count} dense onVote={vote} />
<div className="text-gray-600">
<BiComment /> <span className="align-middle text-body5">{story.comments_count} Comments</span>
</div>
</div>
</div>
</div>
</Card>
</Card>
</div>
)
}

View File

@@ -19,6 +19,12 @@ query Feed($take: Int, $skip: Int, $sortBy: String, $tag: Int) {
type
cover_image
comments_count
project {
id
title
thumbnail_image
hashtag
}
}
... on Bounty {
id

View File

@@ -8,10 +8,13 @@ import HeaderSkeleton from "src/features/Posts/Components/PostCard/Header/Header
export default function PageContentSkeleton() {
return <div id="content" className="bg-white md:p-32 md:border-2 border-gray-200 rounded-16 relative">
<div className="flex flex-col gap-24 relative">
<HeaderSkeleton />
<div className="relative w-full h-[120px] md:h-[240px] object-cover rounded-12 mb-16">
<Skeleton height='100%' className='!leading-inherit rounded-8' />
</div>
<h1 className="text-[42px] leading-[58px] font-bolder">
<Skeleton width={'min(80%,16ch)'} />
</h1>
<HeaderSkeleton />
<div className="flex flex-wrap gap-8">
{Array(3).fill(0).map((_, idx) => <Badge key={idx} size='sm'>
<div className="opacity-0">hidden</div>

View File

@@ -1,4 +1,4 @@
import { isBounty, isQuestion, isStory, Post } from "src/features/Posts/types"
import { isBounty, isQuestion, isStory } from "src/features/Posts/types"
import StoryPageContent from "../StoryPageContent/StoryPageContent";
import BountyPageContent from "../BountyPageContent/BountyPageContent";
import QuestionPageContent from "../QuestionPageContent/QuestionPageContent";

View File

@@ -0,0 +1,54 @@
import Avatar from 'src/features/Profiles/Components/Avatar/Avatar';
import dayjs from 'dayjs'
import { trimText } from 'src/utils/helperFunctions';
import { Link } from 'react-router-dom';
import { createRoute } from 'src/utils/routing';
import { Project, User } from 'src/graphql';
interface Props {
author?: Pick<User, 'id' | 'name' | 'avatar'>
project?: Pick<Project, 'id' | 'title' | "thumbnail_image" | 'hashtag'> | null
date: string;
className?: string
}
export default function PostPageHeader(props: Props) {
const dateToShow = () => {
const passedTime = dayjs().diff(props.date, 'hour');
if (passedTime === 0) return 'now';
if (passedTime < 24) return `${dayjs().diff(props.date, 'hour')}h ago`
return dayjs(props.date).format('MMMM DD');
}
if (!props.author) return null
return (
<div className={`flex gap-16 items-center ${props.className}`}>
<div className='relative'>
<Link to={createRoute({ type: 'profile', id: props.author.id, username: props.author.name })}>
<Avatar width={48} src={props.author.avatar} />
</Link>
{props.project && <Link className='absolute bottom-0 right-0 translate-x-8' to={createRoute({ type: 'projects-page', })}>
<Avatar src={props.project.thumbnail_image} width={24} />
</Link>}
</div>
<div className="flex flex-col gap-4">
<span className='flex gap-4'>
<Link className='hover:underline' to={createRoute({ type: 'profile', id: props.author.id, username: props.author.name })}>
<p className="text-gray-900 text-body4 font-medium">{trimText(props.author.name, 30)}</p>
</Link>
{props.project && <>
<span className="text-body4 text-gray-500 font-medium">for</span>
<Link className='hover:underline' to={createRoute({ type: 'profile', id: props.author.id, username: props.author.name })}>
<p className="text-gray-900 text-body4 font-medium">{trimText(props.project.title, 30)}</p>
</Link>
</>}
</span>
<p className="text-body5 text-gray-500 font-medium">Published {dateToShow()}</p>
</div>
</div>
)
}

View File

@@ -1,4 +1,4 @@
import Header from "src/features/Posts/Components/PostCard/Header/Header"
import { Story } from "src/features/Posts/types"
import { marked } from 'marked';
import styles from '../PageContent/styles.module.scss'
@@ -10,6 +10,7 @@ import { useUpdateStory } from './useUpdateStory'
import { FaPen } from "react-icons/fa";
import DOMPurify from 'dompurify';
import Card from "src/Components/Card/Card";
import PostPageHeader from "../PostPageHeader/PostPageHeader";
interface Props {
@@ -29,6 +30,11 @@ export default function StoryPageContent({ story }: Props) {
<>
<div id="content" className="bg-white md:p-32 md:border-2 border-gray-200 rounded-16 relative"> </div>
<Card id="content" onlyMd className="relative max">
<PostPageHeader
className="mb-16"
author={story.author}
project={story.project}
date={story.createdAt} />
{story.cover_image &&
<img src={story.cover_image}
className='w-full h-[120px] md:h-[240px] object-cover rounded-12 mb-16'
@@ -52,7 +58,6 @@ export default function StoryPageContent({ story }: Props) {
</MenuItem>
</Menu>}
<h1 className="text-[42px] leading-[58px] font-bolder">{story.title}</h1>
<Header size="lg" showTimeAgo={false} author={story.author} date={story.createdAt} />
{story.tags.length > 0 && <div className="flex flex-wrap gap-8">
{story.tags.map(tag => <Badge key={tag.id} size='sm'>
{tag.title}

View File

@@ -30,8 +30,6 @@ export const useUpdateStory = (story: Story) => {
dispatch(stageStory({
...story,
cover_image: story.cover_image ? { id: null, name: null, url: story.cover_image } : null,
project: null,
// TODO: UPDATE WHEN API READY
}))
navigate(createRoute({ type: "write-story" }))

View File

@@ -1,18 +1,8 @@
import { Helmet } from 'react-helmet'
import { useParams } from 'react-router-dom'
import LoadingPage from 'src/Components/LoadingPage/LoadingPage'
import NotFoundPage from 'src/features/Shared/pages/NotFoundPage/NotFoundPage'
import { usePostDetailsQuery } from 'src/graphql'
import { capitalize } from 'src/utils/helperFunctions'
import { useAppSelector, } from 'src/utils/hooks'
import { PostCardSkeleton } from '../../Components/PostCard'
import TrendingCard from '../../Components/TrendingCard/TrendingCard'
import AuthorCard from './Components/AuthorCard/AuthorCard'
import AuthorCardSkeleton from './Components/AuthorCard/AuthorCard.skeleton'
import PageContent from './Components/PageContent/PageContent'
import PageContentSkeleton from './Components/PageContent/PageContent.skeleton'
import PostActions from './Components/PostActions/PostActions'
import PostActionsSkeleton from './Components/PostActions/PostActions.skeleton'
import styles from './styles.module.scss'

View File

@@ -19,6 +19,12 @@ query PostDetails($id: Int!, $type: POST_TYPE!) {
type
cover_image
is_published
project {
id
title
thumbnail_image
hashtag
}
# comments_count
# comments {
# id

View File

@@ -642,6 +642,7 @@ export type Story = PostBase & {
excerpt: Scalars['String'];
id: Scalars['Int'];
is_published: Maybe<Scalars['Boolean']>;
project: Maybe<Project>;
tags: Array<Tag>;
title: Scalars['String'];
type: Scalars['String'];
@@ -959,7 +960,7 @@ export type FeedQueryVariables = Exact<{
}>;
export type FeedQuery = { __typename?: 'Query', getFeed: Array<{ __typename?: 'Bounty', id: number, title: string, createdAt: any, excerpt: string, votes_count: number, type: string, cover_image: string | null, deadline: string, reward_amount: number, applicants_count: number, author: { __typename?: 'Author', id: number, name: string, avatar: string, join_date: any }, tags: Array<{ __typename?: 'Tag', id: number, title: string }> } | { __typename?: 'Question', id: number, title: string, createdAt: any, excerpt: string, votes_count: number, type: string, author: { __typename?: 'Author', id: number, name: string, avatar: string, join_date: any }, tags: Array<{ __typename?: 'Tag', id: number, title: string }> } | { __typename?: 'Story', id: number, title: string, createdAt: any, excerpt: string, votes_count: number, type: string, cover_image: string | null, comments_count: number, author: { __typename?: 'Author', id: number, name: string, avatar: string, join_date: any }, tags: Array<{ __typename?: 'Tag', id: number, title: string }> }> };
export type FeedQuery = { __typename?: 'Query', getFeed: Array<{ __typename?: 'Bounty', id: number, title: string, createdAt: any, excerpt: string, votes_count: number, type: string, cover_image: string | null, deadline: string, reward_amount: number, applicants_count: number, author: { __typename?: 'Author', id: number, name: string, avatar: string, join_date: any }, tags: Array<{ __typename?: 'Tag', id: number, title: string }> } | { __typename?: 'Question', id: number, title: string, createdAt: any, excerpt: string, votes_count: number, type: string, author: { __typename?: 'Author', id: number, name: string, avatar: string, join_date: any }, tags: Array<{ __typename?: 'Tag', id: number, title: string }> } | { __typename?: 'Story', id: number, title: string, createdAt: any, excerpt: string, votes_count: number, type: string, cover_image: string | null, comments_count: number, author: { __typename?: 'Author', id: number, name: string, avatar: string, join_date: any }, tags: Array<{ __typename?: 'Tag', id: number, title: string }>, project: { __typename?: 'Project', id: number, title: string, thumbnail_image: string, hashtag: string } | null }> };
export type PostDetailsQueryVariables = Exact<{
id: Scalars['Int'];
@@ -967,7 +968,7 @@ export type PostDetailsQueryVariables = Exact<{
}>;
export type PostDetailsQuery = { __typename?: 'Query', getPostById: { __typename?: 'Bounty', id: number, title: string, createdAt: any, body: string, votes_count: number, type: string, cover_image: string | null, deadline: string, reward_amount: number, applicants_count: number, author: { __typename?: 'Author', id: number, name: string, avatar: string, join_date: any }, tags: Array<{ __typename?: 'Tag', id: number, title: string }>, applications: Array<{ __typename?: 'BountyApplication', id: number, date: string, workplan: string, author: { __typename?: 'Author', id: number, name: string, avatar: string } }> } | { __typename?: 'Question', id: number, title: string, createdAt: any, body: string, votes_count: number, type: string, author: { __typename?: 'Author', id: number, name: string, avatar: string, join_date: any }, tags: Array<{ __typename?: 'Tag', id: number, title: string }> } | { __typename?: 'Story', id: number, title: string, createdAt: any, body: string, votes_count: number, type: string, cover_image: string | null, is_published: boolean | null, author: { __typename?: 'Author', id: number, name: string, avatar: string, join_date: any }, tags: Array<{ __typename?: 'Tag', id: number, title: string }> } };
export type PostDetailsQuery = { __typename?: 'Query', getPostById: { __typename?: 'Bounty', id: number, title: string, createdAt: any, body: string, votes_count: number, type: string, cover_image: string | null, deadline: string, reward_amount: number, applicants_count: number, author: { __typename?: 'Author', id: number, name: string, avatar: string, join_date: any }, tags: Array<{ __typename?: 'Tag', id: number, title: string }>, applications: Array<{ __typename?: 'BountyApplication', id: number, date: string, workplan: string, author: { __typename?: 'Author', id: number, name: string, avatar: string } }> } | { __typename?: 'Question', id: number, title: string, createdAt: any, body: string, votes_count: number, type: string, author: { __typename?: 'Author', id: number, name: string, avatar: string, join_date: any }, tags: Array<{ __typename?: 'Tag', id: number, title: string }> } | { __typename?: 'Story', id: number, title: string, createdAt: any, body: string, votes_count: number, type: string, cover_image: string | null, is_published: boolean | null, author: { __typename?: 'Author', id: number, name: string, avatar: string, join_date: any }, tags: Array<{ __typename?: 'Tag', id: number, title: string }>, project: { __typename?: 'Project', id: number, title: string, thumbnail_image: string, hashtag: string } | null } };
type UserBasicInfo_MyProfile_Fragment = { __typename?: 'MyProfile', id: number, name: string, avatar: string, join_date: any, role: string | null, jobTitle: string | null, lightning_address: string | null, website: string | null, twitter: string | null, discord: string | null, github: string | null, linkedin: string | null, bio: string | null, location: string | null };
@@ -1885,6 +1886,12 @@ export const FeedDocument = gql`
type
cover_image
comments_count
project {
id
title
thumbnail_image
hashtag
}
}
... on Bounty {
id
@@ -1982,6 +1989,12 @@ export const PostDetailsDocument = gql`
type
cover_image
is_published
project {
id
title
thumbnail_image
hashtag
}
}
... on Bounty {
id