mirror of
https://github.com/aljazceru/landscape-template.git
synced 2026-01-31 12:14:30 +01:00
feat: built stories component in profile
This commit is contained in:
@@ -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'
|
||||
}
|
||||
|
||||
@@ -245,6 +245,7 @@ type User {
|
||||
location: String
|
||||
name: String!
|
||||
role: String
|
||||
stories: [Story!]!
|
||||
twitter: String
|
||||
website: String
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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" } });
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -14,5 +14,15 @@ query profile($profileId: Int!) {
|
||||
linkedin
|
||||
bio
|
||||
location
|
||||
stories {
|
||||
id
|
||||
title
|
||||
createdAt
|
||||
tags {
|
||||
id
|
||||
title
|
||||
icon
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
Reference in New Issue
Block a user