feat: built stories component in profile

This commit is contained in:
MTG2000
2022-07-14 11:39:59 +03:00
parent adb11c3af7
commit 7cd6fc749c
12 changed files with 337 additions and 145 deletions

View File

@@ -408,6 +408,7 @@ export interface NexusGenFieldTypes {
location: string | null; // String
name: string; // String!
role: string | null; // String
stories: NexusGenRootTypes['Story'][]; // [Story!]!
twitter: string | null; // String
website: string | null; // String
}
@@ -615,6 +616,7 @@ export interface NexusGenFieldTypeNames {
location: 'String'
name: 'String'
role: 'String'
stories: 'Story'
twitter: 'String'
website: 'String'
}

View File

@@ -245,6 +245,7 @@ type User {
location: String
name: String!
role: String
stories: [Story!]!
twitter: String
website: String
}

View File

@@ -114,143 +114,7 @@ const StoryInputType = inputObjectType({
t.boolean('is_published')
}
})
const createStory = extendType({
type: 'Mutation',
definition(t) {
t.field('createStory', {
type: 'Story',
args: { data: StoryInputType },
async resolve(_root, args, ctx) {
const { id, title, body, cover_image, tags, is_published } = args.data;
const user = await getUserByPubKey(ctx.userPubKey);
// Do some validation
if (!user)
throw new ApolloError("Not Authenticated");
let was_published = false;
if (id) {
const oldPost = await prisma.story.findFirst({
where: { id },
select: {
user_id: true,
is_published: true
}
})
was_published = oldPost.is_published;
if (user.id !== oldPost.user_id)
throw new ApolloError("Not post author")
}
// TODO: validate post data
// Preprocess & insert
const htmlBody = marked.parse(body);
const excerpt = htmlBody.replace(/<[^>]+>/g, '').slice(0, 120);
if (id) {
await prisma.story.update({
where: { id },
data: {
tags: {
set: []
},
}
});
return prisma.story.update({
where: { id },
data: {
title,
body,
cover_image,
excerpt,
is_published: was_published || is_published,
tags: {
connectOrCreate:
tags.map(tag => {
tag = tag.toLowerCase().trim();
return {
where: {
title: tag,
},
create: {
title: tag
}
}
})
},
}
})
}
return prisma.story.create({
data: {
title,
body,
cover_image,
excerpt,
is_published,
tags: {
connectOrCreate:
tags.map(tag => {
tag = tag.toLowerCase().trim();
return {
where: {
title: tag,
},
create: {
title: tag
}
}
})
},
user: {
connect: {
id: user.id,
}
}
}
})
}
})
},
})
const deleteStory = extendType({
type: 'Mutation',
definition(t) {
t.field('deleteStory', {
type: 'Story',
args: { id: nonNull(intArg()) },
async resolve(_root, args, ctx) {
const { id } = args;
const user = await getUserByPubKey(ctx.userPubKey);
// Do some validation
if (!user)
throw new ApolloError("Not Authenticated");
const oldPost = await prisma.story.findFirst({
where: { id },
select: {
user_id: true
}
})
if (user.id !== oldPost.user_id)
throw new ApolloError("Not post author")
return prisma.story.delete({
where: {
id
}
})
}
})
},
})
const BountyApplication = objectType({
name: 'BountyApplication',
@@ -417,6 +281,7 @@ const getTrendingPosts = extendType({
})
const getMyDrafts = extendType({
type: "Query",
definition(t) {
@@ -475,6 +340,144 @@ const getPostById = extendType({
}
})
const createStory = extendType({
type: 'Mutation',
definition(t) {
t.field('createStory', {
type: 'Story',
args: { data: StoryInputType },
async resolve(_root, args, ctx) {
const { id, title, body, cover_image, tags, is_published } = args.data;
const user = await getUserByPubKey(ctx.userPubKey);
// Do some validation
if (!user)
throw new ApolloError("Not Authenticated");
let was_published = false;
if (id) {
const oldPost = await prisma.story.findFirst({
where: { id },
select: {
user_id: true,
is_published: true
}
})
was_published = oldPost.is_published;
if (user.id !== oldPost.user_id)
throw new ApolloError("Not post author")
}
// TODO: validate post data
// Preprocess & insert
const htmlBody = marked.parse(body);
const excerpt = htmlBody.replace(/<[^>]+>/g, '').slice(0, 120);
if (id) {
await prisma.story.update({
where: { id },
data: {
tags: {
set: []
},
}
});
return prisma.story.update({
where: { id },
data: {
title,
body,
cover_image,
excerpt,
is_published: was_published || is_published,
tags: {
connectOrCreate:
tags.map(tag => {
tag = tag.toLowerCase().trim();
return {
where: {
title: tag,
},
create: {
title: tag
}
}
})
},
}
})
}
return prisma.story.create({
data: {
title,
body,
cover_image,
excerpt,
is_published,
tags: {
connectOrCreate:
tags.map(tag => {
tag = tag.toLowerCase().trim();
return {
where: {
title: tag,
},
create: {
title: tag
}
}
})
},
user: {
connect: {
id: user.id,
}
}
}
})
}
})
},
})
const deleteStory = extendType({
type: 'Mutation',
definition(t) {
t.field('deleteStory', {
type: 'Story',
args: { id: nonNull(intArg()) },
async resolve(_root, args, ctx) {
const { id } = args;
const user = await getUserByPubKey(ctx.userPubKey);
// Do some validation
if (!user)
throw new ApolloError("Not Authenticated");
const oldPost = await prisma.story.findFirst({
where: { id },
select: {
user_id: true
}
})
if (user.id !== oldPost.user_id)
throw new ApolloError("Not post author")
return prisma.story.delete({
where: {
id
}
})
}
})
},
})

View File

@@ -23,6 +23,13 @@ const User = objectType({
t.string('linkedin')
t.string('bio')
t.string('location')
t.nonNull.list.nonNull.field('stories', {
type: "Story",
resolve: (parent) => {
return prisma.story.findMany({ where: { user_id: parent.id, is_published: true }, orderBy: { createdAt: "desc" } });
}
});
}
})

View File

@@ -6,6 +6,7 @@ import AboutCard from "./AboutCard/AboutCard"
import { Helmet } from 'react-helmet'
import { useAppSelector } from 'src/utils/hooks';
import styles from './styles.module.scss'
import StoriesCard from "./StoriesCard/StoriesCard"
export default function ProfilePage() {
@@ -39,8 +40,9 @@ export default function ProfilePage() {
<div className={`page-container ${styles.grid}`}
>
<aside></aside>
<main className="">
<main className="flex flex-col gap-24">
<AboutCard user={profileQuery.data.profile} isOwner={isOwner} />
<StoriesCard stories={profileQuery.data.profile.stories} isOwner={isOwner} />
</main>
<aside></aside>
</div>

View File

@@ -0,0 +1,31 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { MOCK_DATA } from 'src/mocks/data';
import StoriesCard from './StoriesCard';
export default {
title: 'Profiles/Profile Page/Stories Card',
component: StoriesCard,
argTypes: {
backgroundColor: { control: 'color' },
},
} as ComponentMeta<typeof StoriesCard>;
const Template: ComponentStory<typeof StoriesCard> = (args) => <StoriesCard {...args} ></StoriesCard>
export const Default = Template.bind({});
Default.args = {
stories: MOCK_DATA['posts'].stories
}
export const Empty = Template.bind({});
Empty.args = {
stories: [],
}
export const EmptyOwner = Template.bind({});
EmptyOwner.args = {
stories: [],
isOwner: true
}

View File

@@ -0,0 +1,69 @@
import React from 'react'
import { Link } from 'react-router-dom'
import Badge from 'src/Components/Badge/Badge'
import Button from 'src/Components/Button/Button'
import { Story } from 'src/features/Posts/types'
import { getDateDifference } from 'src/utils/helperFunctions'
import { Tag } from 'src/utils/interfaces'
import { createRoute } from 'src/utils/routing'
interface Props {
isOwner?: boolean;
stories: Array<
Pick<Story,
| 'id'
| 'title'
| 'createdAt'
>
&
{
tags: Array<Pick<Tag, 'id' | 'icon' | 'title'>>
}
>
}
export default function StoriesCard({ stories, isOwner }: Props) {
return (
<div className="rounded-16 bg-white border-2 border-gray-200 p-24">
<p className="text-body2 font-bold">Stories ({stories.length})</p>
{stories.length > 0 &&
<ul className="">
{stories.map(story =>
<li key={story.id} className='py-24 border-b-[1px] border-gray-200 last-of-type:border-b-0 ' >
<Link
className="hover:underline text-body3 font-medium"
role={'button'}
to={createRoute({ type: "story", id: story.id, title: story.title })}
>
{story.title}
</Link>
<div className="flex flex-wrap items-center gap-8 text-body5 mt-8">
<p className="text-gray-600 mr-12">{getDateDifference(story.createdAt, { dense: true })} ago</p>
{story.tags.slice(0, 3).map(tag => <Badge key={tag.id} size='sm'>
{tag.icon} {tag.title}
</Badge>)}
{story.tags.length > 3 && <Badge size='sm'>
+{story.tags.length - 3}
</Badge>}
</div>
</li>)}
</ul>}
{stories.length === 0 &&
<div className="flex flex-col gap-16 mt-24">
<p className="text-body3 font-medium">
😐 No Stories Added Yet
</p>
<p className="text-body5 text-gray-500">
The maker have not written any stories yet
</p>
{isOwner && <Button
href='/blog/create-post'
color='primary'
>
Write your first story
</Button>}
</div>
}
</div>
)
}

View File

@@ -14,5 +14,15 @@ query profile($profileId: Int!) {
linkedin
bio
location
stories {
id
title
createdAt
tags {
id
title
icon
}
}
}
}

View File

@@ -390,6 +390,7 @@ export type User = {
location: Maybe<Scalars['String']>;
name: Scalars['String'];
role: Maybe<Scalars['String']>;
stories: Array<Story>;
twitter: Maybe<Scalars['String']>;
website: Maybe<Scalars['String']>;
};
@@ -518,7 +519,7 @@ export type ProfileQueryVariables = Exact<{
}>;
export type ProfileQuery = { __typename?: 'Query', profile: { __typename?: 'User', id: number, name: string, avatar: string, join_date: any, role: string | null, email: string | null, jobTitle: string | null, lightning_address: string | null, website: string | null, twitter: string | null, github: string | null, linkedin: string | null, bio: string | null, location: string | null } | null };
export type ProfileQuery = { __typename?: 'Query', profile: { __typename?: 'User', id: number, name: string, avatar: string, join_date: any, role: string | null, email: string | null, jobTitle: string | null, lightning_address: string | null, website: string | null, twitter: string | null, github: string | null, linkedin: string | null, bio: string | null, location: string | null, stories: Array<{ __typename?: 'Story', id: number, title: string, createdAt: any, tags: Array<{ __typename?: 'Tag', title: string, icon: string | null, id: number }> }> } | null };
export type UpdateProfileAboutMutationVariables = Exact<{
data: InputMaybe<UpdateProfileInput>;
@@ -1360,6 +1361,16 @@ export const ProfileDocument = gql`
linkedin
bio
location
stories {
id
title
createdAt
tags {
title
icon
id
}
}
}
}
`;

View File

@@ -95,6 +95,21 @@ export let posts = {
comments: generatePostComments(3),
},
{
id: 6,
title: 'The End Is Nigh',
body: postBody,
cover_image: getCoverImage(),
comments_count: 3,
createdAt: getDate(),
votes_count: 120,
excerpt: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. In odio libero accumsan...',
type: "Story",
tags: randomItems(3, ...tags),
author: getAuthor(),
comments: generatePostComments(3),
},
] as Story[],
bounties: [
{
@@ -113,7 +128,24 @@ export let posts = {
reward_amount: 200_000,
applications: getApplications(2),
}
},
{
type: "Bounty",
id: 51,
title: 'Wanted, Dead OR Alive!!',
body: postBody,
cover_image: getCoverImage(),
applicants_count: 31,
createdAt: getDate(),
votes_count: 120,
excerpt: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. In odio libero accumsan...',
tags: randomItems(3, ...tags),
author: getAuthor(),
deadline: "25 May",
reward_amount: 200_000,
applications: getApplications(2),
},
] as Bounty[],
questions: [
{
@@ -132,6 +164,22 @@ export let posts = {
author: getAuthor(),
comments: generatePostComments(3)
},
{
type: "Question",
id: 33,
title: 'What is a man but miserable pile of secrets?',
body: postBody,
answers_count: 3,
createdAt: getDate(),
votes_count: 70,
excerpt: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. In odio libero accumsan...',
tags: [
{ id: 1, title: "lnurl" },
{ id: 2, title: "webln" },
],
author: getAuthor(),
comments: generatePostComments(3)
},
] as Question[]
}

View File

@@ -1,4 +1,5 @@
import { User } from "src/graphql";
import { posts } from "./posts";
export const user: User = {
id: 123,
@@ -14,5 +15,6 @@ export const user: User = {
location: "Germany, Berlin",
role: "user",
twitter: "john-doe",
website: "https://mtg-dev.tech"
website: "https://mtg-dev.tech",
stories: posts.stories
}

View File

@@ -111,14 +111,20 @@ export function getPropertyFromUnknown<Value = string>(obj: unknown, prop: strin
return null
}
export function getDateDifference(date: string) {
export function getDateDifference(date: string, { dense }: { dense?: boolean } = {}) {
const now = dayjs();
const mins = now.diff(date, 'minute');
if (mins < 60) return mins + 'm';
if (mins < 60)
return mins + (dense ? 'm' : " minutes");
const hrs = now.diff(date, 'hour');
if (hrs < 24) return hrs + 'h';
if (hrs < 24)
return hrs + (dense ? 'h' : " hours");
const days = now.diff(date, 'day');
if (days < 30) return days + 'd';
if (days < 30)
return days + (dense ? 'd' : " days");
const months = now.diff(date, 'month');
return months + 'mo'
return months + (dense ? 'mo' : " months")
}