From 7cd6fc749c6b422d2f97d47786ca2c86974cfd7a Mon Sep 17 00:00:00 2001 From: MTG2000 Date: Thu, 14 Jul 2022 11:39:59 +0300 Subject: [PATCH] feat: built stories component in profile --- api/functions/graphql/nexus-typegen.ts | 2 + api/functions/graphql/schema.graphql | 1 + api/functions/graphql/types/post.js | 275 +++++++++--------- api/functions/graphql/types/users.js | 7 + .../pages/ProfilePage/ProfilePage.tsx | 4 +- .../StoriesCard/StoriesCard.stories.tsx | 31 ++ .../ProfilePage/StoriesCard/StoriesCard.tsx | 69 +++++ .../pages/ProfilePage/profile.graphql | 10 + src/graphql/index.tsx | 13 +- src/mocks/data/posts.ts | 50 +++- src/mocks/data/users.ts | 4 +- src/utils/helperFunctions.tsx | 16 +- 12 files changed, 337 insertions(+), 145 deletions(-) create mode 100644 src/features/Profiles/pages/ProfilePage/StoriesCard/StoriesCard.stories.tsx create mode 100644 src/features/Profiles/pages/ProfilePage/StoriesCard/StoriesCard.tsx diff --git a/api/functions/graphql/nexus-typegen.ts b/api/functions/graphql/nexus-typegen.ts index fe3a13a..b4af5bd 100644 --- a/api/functions/graphql/nexus-typegen.ts +++ b/api/functions/graphql/nexus-typegen.ts @@ -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' } diff --git a/api/functions/graphql/schema.graphql b/api/functions/graphql/schema.graphql index b334d3f..1860cf9 100644 --- a/api/functions/graphql/schema.graphql +++ b/api/functions/graphql/schema.graphql @@ -245,6 +245,7 @@ type User { location: String name: String! role: String + stories: [Story!]! twitter: String website: String } diff --git a/api/functions/graphql/types/post.js b/api/functions/graphql/types/post.js index 3beddca..88df036 100644 --- a/api/functions/graphql/types/post.js +++ b/api/functions/graphql/types/post.js @@ -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 + } + }) + } + }) + }, +}) + diff --git a/api/functions/graphql/types/users.js b/api/functions/graphql/types/users.js index 3a83b5c..dc01deb 100644 --- a/api/functions/graphql/types/users.js +++ b/api/functions/graphql/types/users.js @@ -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" } }); + } + }); } }) diff --git a/src/features/Profiles/pages/ProfilePage/ProfilePage.tsx b/src/features/Profiles/pages/ProfilePage/ProfilePage.tsx index 339992e..7cdd415 100644 --- a/src/features/Profiles/pages/ProfilePage/ProfilePage.tsx +++ b/src/features/Profiles/pages/ProfilePage/ProfilePage.tsx @@ -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() {
-
+
+
diff --git a/src/features/Profiles/pages/ProfilePage/StoriesCard/StoriesCard.stories.tsx b/src/features/Profiles/pages/ProfilePage/StoriesCard/StoriesCard.stories.tsx new file mode 100644 index 0000000..0391e7c --- /dev/null +++ b/src/features/Profiles/pages/ProfilePage/StoriesCard/StoriesCard.stories.tsx @@ -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; + + +const Template: ComponentStory = (args) => + +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 +} \ No newline at end of file diff --git a/src/features/Profiles/pages/ProfilePage/StoriesCard/StoriesCard.tsx b/src/features/Profiles/pages/ProfilePage/StoriesCard/StoriesCard.tsx new file mode 100644 index 0000000..a94f72f --- /dev/null +++ b/src/features/Profiles/pages/ProfilePage/StoriesCard/StoriesCard.tsx @@ -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 + & + { + tags: Array> + } + > +} + +export default function StoriesCard({ stories, isOwner }: Props) { + return ( +
+

Stories ({stories.length})

+ {stories.length > 0 && +
    + {stories.map(story => +
  • + + {story.title} + +
    +

    {getDateDifference(story.createdAt, { dense: true })} ago

    + {story.tags.slice(0, 3).map(tag => + {tag.icon} {tag.title} + )} + {story.tags.length > 3 && + +{story.tags.length - 3} + } +
    +
  • )} +
} + {stories.length === 0 && +
+

+ 😐 No Stories Added Yet +

+

+ The maker have not written any stories yet +

+ {isOwner && } +
+ } +
+ ) +} diff --git a/src/features/Profiles/pages/ProfilePage/profile.graphql b/src/features/Profiles/pages/ProfilePage/profile.graphql index d4209e7..60feb92 100644 --- a/src/features/Profiles/pages/ProfilePage/profile.graphql +++ b/src/features/Profiles/pages/ProfilePage/profile.graphql @@ -14,5 +14,15 @@ query profile($profileId: Int!) { linkedin bio location + stories { + id + title + createdAt + tags { + id + title + icon + } + } } } diff --git a/src/graphql/index.tsx b/src/graphql/index.tsx index 574cfde..d5f434c 100644 --- a/src/graphql/index.tsx +++ b/src/graphql/index.tsx @@ -390,6 +390,7 @@ export type User = { location: Maybe; name: Scalars['String']; role: Maybe; + stories: Array; twitter: Maybe; website: Maybe; }; @@ -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; @@ -1360,6 +1361,16 @@ export const ProfileDocument = gql` linkedin bio location + stories { + id + title + createdAt + tags { + title + icon + id + } + } } } `; diff --git a/src/mocks/data/posts.ts b/src/mocks/data/posts.ts index bff8bea..7f8dee4 100644 --- a/src/mocks/data/posts.ts +++ b/src/mocks/data/posts.ts @@ -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[] } diff --git a/src/mocks/data/users.ts b/src/mocks/data/users.ts index 3046b28..95eb3ab 100644 --- a/src/mocks/data/users.ts +++ b/src/mocks/data/users.ts @@ -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 } diff --git a/src/utils/helperFunctions.tsx b/src/utils/helperFunctions.tsx index aaa8d18..093088e 100644 --- a/src/utils/helperFunctions.tsx +++ b/src/utils/helperFunctions.tsx @@ -111,14 +111,20 @@ export function getPropertyFromUnknown(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") } \ No newline at end of file