From 25d31f373d1e8c8ec68ef6dea6277aad1e4d0344 Mon Sep 17 00:00:00 2001 From: MTG2000 Date: Fri, 8 Jul 2022 16:48:34 +0300 Subject: [PATCH 01/15] fix: make cover_image optional, disable custom tags, make ul,ol tags styled a little --- api/functions/graphql/nexus-typegen.ts | 10 +++++----- api/functions/graphql/schema.graphql | 6 +++--- api/functions/graphql/types/post.js | 6 +++--- .../migration.sql | 2 ++ prisma/schema.prisma | 2 +- .../Inputs/FilesInput/FilesInput.tsx | 11 +++++----- src/Components/Inputs/TagsInput/TagsInput.tsx | 5 +++-- .../PostCard/BountyCard/BountyCard.tsx | 2 +- .../Components/StoryForm/StoryForm.tsx | 9 +++++---- .../StoryPageContent/useUpdateStory.tsx | 2 +- .../PreviewPostContent/PreviewPostContent.tsx | 2 +- src/graphql/index.tsx | 12 +++++------ src/styles/mixins/_post_body.scss | 20 +++++++++++++++++++ 13 files changed, 57 insertions(+), 32 deletions(-) create mode 100644 prisma/migrations/20220708131731_make_story_cover_image_optional/migration.sql diff --git a/api/functions/graphql/nexus-typegen.ts b/api/functions/graphql/nexus-typegen.ts index 1f0a9e1..4d16ed1 100644 --- a/api/functions/graphql/nexus-typegen.ts +++ b/api/functions/graphql/nexus-typegen.ts @@ -30,7 +30,7 @@ declare global { export interface NexusGenInputs { StoryInputType: { // input type body: string; // String! - cover_image: string; // String! + cover_image?: string | null; // String id?: number | null; // Int tags: string[]; // [String!]! title: string; // String! @@ -81,7 +81,7 @@ export interface NexusGenObjects { applicants_count: number; // Int! applications: NexusGenRootTypes['BountyApplication'][]; // [BountyApplication!]! body: string; // String! - cover_image: string; // String! + cover_image?: string | null; // String createdAt: NexusGenScalars['Date']; // Date! deadline: string; // String! excerpt: string; // String! @@ -165,7 +165,7 @@ export interface NexusGenObjects { } Story: { // root type body: string; // String! - cover_image: string; // String! + cover_image?: string | null; // String createdAt: NexusGenScalars['Date']; // Date! excerpt: string; // String! id: number; // Int! @@ -236,7 +236,7 @@ export interface NexusGenFieldTypes { applications: NexusGenRootTypes['BountyApplication'][]; // [BountyApplication!]! author: NexusGenRootTypes['Author']; // Author! body: string; // String! - cover_image: string; // String! + cover_image: string | null; // String createdAt: NexusGenScalars['Date']; // Date! deadline: string; // String! excerpt: string; // String! @@ -364,7 +364,7 @@ export interface NexusGenFieldTypes { body: string; // String! comments: NexusGenRootTypes['PostComment'][]; // [PostComment!]! comments_count: number; // Int! - cover_image: string; // String! + cover_image: string | null; // String createdAt: NexusGenScalars['Date']; // Date! excerpt: string; // String! id: number; // Int! diff --git a/api/functions/graphql/schema.graphql b/api/functions/graphql/schema.graphql index 8d9775d..37097da 100644 --- a/api/functions/graphql/schema.graphql +++ b/api/functions/graphql/schema.graphql @@ -22,7 +22,7 @@ type Bounty implements PostBase { applications: [BountyApplication!]! author: Author! body: String! - cover_image: String! + cover_image: String createdAt: Date! deadline: String! excerpt: String! @@ -182,7 +182,7 @@ type Story implements PostBase { body: String! comments: [PostComment!]! comments_count: Int! - cover_image: String! + cover_image: String createdAt: Date! excerpt: String! id: Int! @@ -194,7 +194,7 @@ type Story implements PostBase { input StoryInputType { body: String! - cover_image: String! + cover_image: String id: Int tags: [String!]! title: String! diff --git a/api/functions/graphql/types/post.js b/api/functions/graphql/types/post.js index a1274ad..de4bcac 100644 --- a/api/functions/graphql/types/post.js +++ b/api/functions/graphql/types/post.js @@ -67,7 +67,7 @@ const Story = objectType({ t.nonNull.string('type', { resolve: () => t.typeName }); - t.nonNull.string('cover_image'); + t.string('cover_image'); t.nonNull.list.nonNull.field('comments', { type: "PostComment", resolve: (parent) => prisma.story.findUnique({ where: { id: parent.id } }).comments() @@ -107,7 +107,7 @@ const StoryInputType = inputObjectType({ t.int('id'); t.nonNull.string('title'); t.nonNull.string('body'); - t.nonNull.string('cover_image'); + t.string('cover_image'); t.nonNull.list.nonNull.string('tags'); } }) @@ -263,7 +263,7 @@ const Bounty = objectType({ t.nonNull.string('type', { resolve: () => 'Bounty' }); - t.nonNull.string('cover_image'); + t.string('cover_image'); t.nonNull.string('deadline'); t.nonNull.int('reward_amount'); t.nonNull.int('applicants_count'); diff --git a/prisma/migrations/20220708131731_make_story_cover_image_optional/migration.sql b/prisma/migrations/20220708131731_make_story_cover_image_optional/migration.sql new file mode 100644 index 0000000..a99de4b --- /dev/null +++ b/prisma/migrations/20220708131731_make_story_cover_image_optional/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Story" ALTER COLUMN "cover_image" DROP NOT NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 5e4bec8..072a135 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -119,7 +119,7 @@ model Story { updatedAt DateTime @updatedAt body String excerpt String - cover_image String + cover_image String? votes_count Int @default(0) diff --git a/src/Components/Inputs/FilesInput/FilesInput.tsx b/src/Components/Inputs/FilesInput/FilesInput.tsx index 396f041..85e6cdb 100644 --- a/src/Components/Inputs/FilesInput/FilesInput.tsx +++ b/src/Components/Inputs/FilesInput/FilesInput.tsx @@ -35,8 +35,7 @@ const fileUrlToObject = async (url: string, fileName: string = 'filename') => { const INSERT_IMAGE_ACTION = createAction<{ src: string, alt?: string }>('COVER_IMAGE_INSERTED')({ src: '', alt: "" }) - -export default function FilesInput({ +const FilesInput = React.forwardRef(({ multiple, value, max = 3, @@ -45,9 +44,8 @@ export default function FilesInput({ allowedType = 'images', uploadText = 'Upload files', ...props -}: Props) { +}, ref) => { - const ref = useRef(null!) const dispatch = useAppDispatch(); @@ -132,4 +130,7 @@ export default function FilesInput({ } ) -} +}) + + +export default FilesInput; \ No newline at end of file diff --git a/src/Components/Inputs/TagsInput/TagsInput.tsx b/src/Components/Inputs/TagsInput/TagsInput.tsx index b0a9633..47f3061 100644 --- a/src/Components/Inputs/TagsInput/TagsInput.tsx +++ b/src/Components/Inputs/TagsInput/TagsInput.tsx @@ -1,7 +1,8 @@ import { useController } from "react-hook-form"; import Badge from "src/Components/Badge/Badge"; -import CreatableSelect from 'react-select/creatable'; +// import CreatableSelect from 'react-select/creatable'; +import Select from 'react-select' import { OnChangeValue, StylesConfig, components, OptionProps } from "react-select"; import { useOfficialTagsQuery } from "src/graphql"; @@ -100,7 +101,7 @@ export default function TagsInput({ return (
- - + {bounty.cover_image && }
diff --git a/src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm.tsx b/src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm.tsx index 4e06e5e..b1cb45d 100644 --- a/src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm.tsx +++ b/src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm.tsx @@ -34,7 +34,7 @@ const schema = yup.object({ title: yup.string().trim().required().min(10, 'the title is too short'), tags: yup.array().required().min(1, 'please pick at least one relevant tag'), body: yup.string().required().min(50, 'stories should have a minimum of 10 words'), - cover_image: yup.array().of(FileSchema as any).min(1, "You need to add a cover image") + cover_image: yup.array().of(FileSchema as any) }).required(); @@ -110,7 +110,7 @@ export default function StoryForm() { title: data.title, body: data.body, tags: data.tags.map(t => t.title), - cover_image: (data.cover_image[0] ?? '') as string, + cover_image: (data.cover_image[0] ?? null) as string | null, }, } }) @@ -130,8 +130,9 @@ export default function StoryForm() { ( + render={({ field: { onChange, value, onBlur, ref } }) => ( {errors.tags &&

diff --git a/src/features/Posts/pages/PostDetailsPage/Components/StoryPageContent/useUpdateStory.tsx b/src/features/Posts/pages/PostDetailsPage/Components/StoryPageContent/useUpdateStory.tsx index 4ee3d82..041a689 100644 --- a/src/features/Posts/pages/PostDetailsPage/Components/StoryPageContent/useUpdateStory.tsx +++ b/src/features/Posts/pages/PostDetailsPage/Components/StoryPageContent/useUpdateStory.tsx @@ -28,7 +28,7 @@ export const useUpdateStory = (story: Story) => { const handleEdit = () => { dispatch(stageStory({ ...story, - cover_image: [story.cover_image] + cover_image: story.cover_image ? [story.cover_image] : [] })) navigate("/blog/create-post?type=story") diff --git a/src/features/Posts/pages/PreviewPostPage/PreviewPostContent/PreviewPostContent.tsx b/src/features/Posts/pages/PreviewPostPage/PreviewPostContent/PreviewPostContent.tsx index 461fb8e..6298d04 100644 --- a/src/features/Posts/pages/PreviewPostPage/PreviewPostContent/PreviewPostContent.tsx +++ b/src/features/Posts/pages/PreviewPostPage/PreviewPostContent/PreviewPostContent.tsx @@ -13,7 +13,7 @@ interface Props { | 'author' > & { tags: Array<{ title: string }> - cover_image?: string | File + cover_image?: string | File | null } } diff --git a/src/graphql/index.tsx b/src/graphql/index.tsx index 3259494..3fa3053 100644 --- a/src/graphql/index.tsx +++ b/src/graphql/index.tsx @@ -40,7 +40,7 @@ export type Bounty = PostBase & { applications: Array; author: Author; body: Scalars['String']; - cover_image: Scalars['String']; + cover_image: Maybe; createdAt: Scalars['Date']; deadline: Scalars['String']; excerpt: Scalars['String']; @@ -320,7 +320,7 @@ export type Story = PostBase & { body: Scalars['String']; comments: Array; comments_count: Scalars['Int']; - cover_image: Scalars['String']; + cover_image: Maybe; createdAt: Scalars['Date']; excerpt: Scalars['String']; id: Scalars['Int']; @@ -332,7 +332,7 @@ export type Story = PostBase & { export type StoryInputType = { body: Scalars['String']; - cover_image: Scalars['String']; + cover_image: InputMaybe; id: InputMaybe; tags: Array; title: Scalars['String']; @@ -458,7 +458,7 @@ export type CreateStoryMutationVariables = Exact<{ }>; -export type CreateStoryMutation = { __typename?: 'Mutation', createStory: { __typename?: 'Story', id: number, title: string, createdAt: any, body: string, votes_count: number, type: string, cover_image: string, comments_count: number, tags: Array<{ __typename?: 'Tag', id: number, title: string }> } | null }; +export type CreateStoryMutation = { __typename?: 'Mutation', createStory: { __typename?: 'Story', id: number, title: string, createdAt: any, body: string, votes_count: number, type: string, cover_image: string | null, comments_count: number, tags: Array<{ __typename?: 'Tag', id: number, title: string }> } | null }; export type DeleteStoryMutationVariables = Exact<{ deleteStoryId: Scalars['Int']; @@ -480,7 +480,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, 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, answers_count: number, author: { __typename?: 'Author', id: number, name: string, avatar: string, join_date: any }, tags: Array<{ __typename?: 'Tag', id: number, title: string }>, comments: Array<{ __typename?: 'PostComment', id: number, createdAt: any, body: string, author: { __typename?: 'Author', id: number, name: string, avatar: string, join_date: any } }> } | { __typename?: 'Story', id: number, title: string, createdAt: any, excerpt: string, votes_count: number, type: string, cover_image: string, 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, answers_count: number, author: { __typename?: 'Author', id: number, name: string, avatar: string, join_date: any }, tags: Array<{ __typename?: 'Tag', id: number, title: string }>, comments: Array<{ __typename?: 'PostComment', id: number, createdAt: any, body: string, author: { __typename?: 'Author', id: number, name: string, avatar: string, join_date: any } }> } | { __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 PostDetailsQueryVariables = Exact<{ id: Scalars['Int']; @@ -488,7 +488,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, 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, answers_count: number, author: { __typename?: 'Author', id: number, name: string, avatar: string, join_date: any }, tags: Array<{ __typename?: 'Tag', id: number, title: string }>, comments: Array<{ __typename?: 'PostComment', id: number, createdAt: any, body: string, votes_count: number, parentId: number | null, author: { __typename?: 'Author', id: number, name: string, avatar: string } }> } | { __typename?: 'Story', id: number, title: string, createdAt: any, body: string, votes_count: number, type: string, cover_image: string, comments_count: number, author: { __typename?: 'Author', id: number, name: string, avatar: string, join_date: any }, tags: Array<{ __typename?: 'Tag', id: number, title: string }>, comments: Array<{ __typename?: 'PostComment', id: number, createdAt: any, body: string, votes_count: number, parentId: number | null, author: { __typename?: 'Author', id: number, name: string, avatar: 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, answers_count: number, author: { __typename?: 'Author', id: number, name: string, avatar: string, join_date: any }, tags: Array<{ __typename?: 'Tag', id: number, title: string }>, comments: Array<{ __typename?: 'PostComment', id: number, createdAt: any, body: string, votes_count: number, parentId: number | null, author: { __typename?: 'Author', id: number, name: string, avatar: string } }> } | { __typename?: 'Story', id: number, title: string, createdAt: any, body: 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 }>, comments: Array<{ __typename?: 'PostComment', id: number, createdAt: any, body: string, votes_count: number, parentId: number | null, author: { __typename?: 'Author', id: number, name: string, avatar: string } }> } }; export type ProfileQueryVariables = Exact<{ profileId: Scalars['Int']; diff --git a/src/styles/mixins/_post_body.scss b/src/styles/mixins/_post_body.scss index 93eeb7b..4ac3588 100644 --- a/src/styles/mixins/_post_body.scss +++ b/src/styles/mixins/_post_body.scss @@ -66,4 +66,24 @@ aspect-ratio: 16/9; margin: 36px auto; } + + ul, + ol { + list-style: disc; + margin-inline-start: 32px; + + p { + margin-bottom: 0; + } + li { + margin-bottom: 8px; + } + } + + ul { + list-style: disc; + } + ol { + list-style: decimal; + } } From 6a55cb5b046eeae0b2074d9da1618114b8c65ce0 Mon Sep 17 00:00:00 2001 From: MTG2000 Date: Mon, 11 Jul 2022 12:45:58 +0300 Subject: [PATCH 02/15] feat: add quote bitton, unify ul/ol styles, fix spacing under ul/ol --- .../Inputs/TextEditor/ToolButton/helpers.ts | 8 ++++++- .../Components/ContentEditor/Toolbar.tsx | 1 + .../ContentEditor/styles.module.scss | 10 +++++++-- src/styles/mixins/_post_body.scss | 21 ++++++++++++++++--- 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/Components/Inputs/TextEditor/ToolButton/helpers.ts b/src/Components/Inputs/TextEditor/ToolButton/helpers.ts index 2c7cac2..c7dad6d 100644 --- a/src/Components/Inputs/TextEditor/ToolButton/helpers.ts +++ b/src/Components/Inputs/TextEditor/ToolButton/helpers.ts @@ -1,5 +1,5 @@ import { FiBold, FiItalic, FiType, FiUnderline, FiAlignCenter, FiAlignLeft, FiAlignRight, FiCode } from 'react-icons/fi' -import { FaListOl, FaListUl, FaUndo, FaRedo, FaImage, FaYoutube } from 'react-icons/fa' +import { FaListOl, FaListUl, FaUndo, FaRedo, FaImage, FaYoutube, FaQuoteLeft } from 'react-icons/fa' import { BiCodeCurly } from 'react-icons/bi'; @@ -73,6 +73,12 @@ export const cmdToBtn = { tip: "Redo", Icon: FaRedo, }, + blockquote: { + cmd: 'toggleBlockquote', + activeCmd: 'blockquote', + tip: "Block Quote", + Icon: FaQuoteLeft, + }, code: { cmd: 'toggleCode', activeCmd: 'code', diff --git a/src/features/Posts/pages/CreatePostPage/Components/ContentEditor/Toolbar.tsx b/src/features/Posts/pages/CreatePostPage/Components/ContentEditor/Toolbar.tsx index ef73d3d..c29e678 100644 --- a/src/features/Posts/pages/CreatePostPage/Components/ContentEditor/Toolbar.tsx +++ b/src/features/Posts/pages/CreatePostPage/Components/ContentEditor/Toolbar.tsx @@ -17,6 +17,7 @@ export default function Toolbar() { {/* */} + diff --git a/src/features/Posts/pages/CreatePostPage/Components/ContentEditor/styles.module.scss b/src/features/Posts/pages/CreatePostPage/Components/ContentEditor/styles.module.scss index 9e4a095..3d0bf15 100644 --- a/src/features/Posts/pages/CreatePostPage/Components/ContentEditor/styles.module.scss +++ b/src/features/Posts/pages/CreatePostPage/Components/ContentEditor/styles.module.scss @@ -8,8 +8,6 @@ border-bottom-right-radius: inherit; min-height: var(--rmr-space-7); - @include post-body; - a { color: rgb(54, 139, 236); @@ -18,6 +16,14 @@ cursor: pointer; } } + + li { + & > label { + display: none; + } + } + + @include post-body; } .remirror-editor-wrapper { height: 60vh; diff --git a/src/styles/mixins/_post_body.scss b/src/styles/mixins/_post_body.scss index 4ac3588..9ebac9f 100644 --- a/src/styles/mixins/_post_body.scss +++ b/src/styles/mixins/_post_body.scss @@ -8,6 +8,10 @@ } } + :where(h1, h2, h3, h4, h5, h6, a, ul, ol, pre, a, p, iframe, ul, ol, blockquote) { + margin-bottom: 16px; + } + h1 { font-size: 36px; line-height: 50px; @@ -69,7 +73,6 @@ ul, ol { - list-style: disc; margin-inline-start: 32px; p { @@ -80,10 +83,22 @@ } } - ul { + ul li { list-style: disc; } - ol { + ol li { list-style: decimal; } + + blockquote { + border-left: 3px solid #dee2e6; + margin-left: 0; + margin-right: 0; + padding-left: 10px; + font-style: italic; + + p { + color: #777; + } + } } From 3f6c344d16b162b19e515c55c9c23ec892b8a51b Mon Sep 17 00:00:00 2001 From: MTG2000 Date: Mon, 11 Jul 2022 14:52:01 +0300 Subject: [PATCH 03/15] feat: add description to tags, update the tags input structure, make post title biggest --- api/functions/graphql/nexus-typegen.ts | 3 + api/functions/graphql/schema.graphql | 1 + api/functions/graphql/types/tag.js | 1 + .../migration.sql | 2 + prisma/schema.prisma | 9 +- src/Components/Inputs/TagsInput/TagsInput.tsx | 82 +++++++++++++------ .../Inputs/TagsInput/officalTags.graphql | 1 + .../Components/StoryForm/StoryForm.tsx | 17 ++-- .../pages/CreatePostPage/CreatePostPage.tsx | 8 +- .../Components/AuthorCard/AuthorCard.tsx | 4 +- .../StoryPageContent/StoryPageContent.tsx | 2 +- .../PreviewPostContent/PreviewPostContent.tsx | 2 +- src/graphql/index.tsx | 4 +- src/mocks/data/tags.ts | 12 ++- src/utils/interfaces/misc.interfaces.ts | 6 +- 15 files changed, 101 insertions(+), 53 deletions(-) create mode 100644 prisma/migrations/20220711095800_add_description_to_tags/migration.sql diff --git a/api/functions/graphql/nexus-typegen.ts b/api/functions/graphql/nexus-typegen.ts index 4d16ed1..6fcb7aa 100644 --- a/api/functions/graphql/nexus-typegen.ts +++ b/api/functions/graphql/nexus-typegen.ts @@ -173,6 +173,7 @@ export interface NexusGenObjects { votes_count: number; // Int! } Tag: { // root type + description?: string | null; // String icon?: string | null; // String id: number; // Int! isOfficial?: boolean | null; // Boolean @@ -374,6 +375,7 @@ export interface NexusGenFieldTypes { votes_count: number; // Int! } Tag: { // field return type + description: string | null; // String icon: string | null; // String id: number; // Int! isOfficial: boolean | null; // Boolean @@ -571,6 +573,7 @@ export interface NexusGenFieldTypeNames { votes_count: 'Int' } Tag: { // field return type name + description: 'String' icon: 'String' id: 'Int' isOfficial: 'Boolean' diff --git a/api/functions/graphql/schema.graphql b/api/functions/graphql/schema.graphql index 37097da..2c176b7 100644 --- a/api/functions/graphql/schema.graphql +++ b/api/functions/graphql/schema.graphql @@ -201,6 +201,7 @@ input StoryInputType { } type Tag { + description: String icon: String id: Int! isOfficial: Boolean diff --git a/api/functions/graphql/types/tag.js b/api/functions/graphql/types/tag.js index 354487b..6b48f43 100644 --- a/api/functions/graphql/types/tag.js +++ b/api/functions/graphql/types/tag.js @@ -7,6 +7,7 @@ const Tag = objectType({ t.nonNull.int('id'); t.nonNull.string('title'); t.string('icon'); + t.string('description'); t.boolean('isOfficial'); } }); diff --git a/prisma/migrations/20220711095800_add_description_to_tags/migration.sql b/prisma/migrations/20220711095800_add_description_to_tags/migration.sql new file mode 100644 index 0000000..d2cc7f8 --- /dev/null +++ b/prisma/migrations/20220711095800_add_description_to_tags/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Tag" ADD COLUMN "description" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 072a135..bcb9ff3 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -12,10 +12,11 @@ generator client { // ----------------- model Tag { - id Int @id @default(autoincrement()) - title String @unique - icon String? - isOfficial Boolean @default(false) + id Int @id @default(autoincrement()) + title String @unique + icon String? + description String? + isOfficial Boolean @default(false) project Project[] stories Story[] diff --git a/src/Components/Inputs/TagsInput/TagsInput.tsx b/src/Components/Inputs/TagsInput/TagsInput.tsx index 47f3061..7b67e38 100644 --- a/src/Components/Inputs/TagsInput/TagsInput.tsx +++ b/src/Components/Inputs/TagsInput/TagsInput.tsx @@ -4,18 +4,17 @@ import Badge from "src/Components/Badge/Badge"; // import CreatableSelect from 'react-select/creatable'; import Select from 'react-select' import { OnChangeValue, StylesConfig, components, OptionProps } from "react-select"; -import { useOfficialTagsQuery } from "src/graphql"; +import { OfficialTagsQuery, useOfficialTagsQuery } from "src/graphql"; +import React from "react"; interface Option { readonly label: string; readonly value: string; readonly icon: string | null + readonly description: string | null } -type Tag = { - title: string, - icon: string | null -} +type Tag = Omit interface Props { classes?: { @@ -29,32 +28,52 @@ interface Props { const transformer = { - tagToOption: (tag: Tag): Option => ({ label: tag.title, value: tag.title, icon: tag.icon }), - optionToTag: (o: Option): Tag => ({ title: o.value, icon: null }) + tagToOption: (tag: Tag): Option => ({ label: tag.title, value: tag.title, icon: tag.icon, description: tag.description }), + optionToTag: (o: Option): Tag => ({ title: o.value, icon: o.icon, description: o.description, }) } const OptionComponent = (props: OptionProps

- - + +
{props.data.icon} - - - {props.data.label} - +
+
+

+ {props.data.label} +

+

+ {props.data.description} +

+
); }; +const { ValueContainer, Placeholder } = components; +const CustomValueContainer = ({ children, ...props }: any) => { + + return ( + + {React.Children.map(children, child => + child && child.type !== Placeholder ? child : null + )} + + {props.selectProps.placeholder} + + + ); +}; + const colourStyles: StylesConfig = { control: (styles, state) => ({ ...styles, - padding: '1px 4px', - borderRadius: 8, + padding: '1px 0', + border: 'none' }), indicatorSeparator: (styles, state) => ({ ...styles, @@ -66,6 +85,12 @@ const colourStyles: StylesConfig = { boxShadow: 'none !important' }, }), + multiValue: styles => ({ + ...styles, + padding: '4px 12px', + borderRadius: 48, + fontWeight: 500 + }) } @@ -84,7 +109,7 @@ export default function TagsInput({ const handleChange = (newValue: OnChangeValue,) => { - onChange([...value, ...newValue.map(transformer.optionToTag)]); + onChange([...newValue.map(transformer.optionToTag)]); onBlur(); } @@ -95,9 +120,12 @@ export default function TagsInput({ } + const maxReached = value.length >= max; - const tagsOptions = (officalTags.data?.officialTags ?? []).filter(t => !value.some((v: Tag) => v.title === t.title)).map(transformer.tagToOption); + const currentPlaceholder = maxReached ? '' : value.length > 0 ? "Add Another..." : placeholder; + + const tagsOptions = !maxReached ? (officalTags.data?.officialTags ?? []).filter(t => !value.some((v: Tag) => v.title === t.title)).map(transformer.tagToOption) : []; return (
@@ -105,19 +133,23 @@ export default function TagsInput({ isLoading={officalTags.loading} options={tagsOptions} isMulti - isDisabled={maxReached} - placeholder={maxReached ? `Max. ${max} tags reached. Remove a tag to add another.` : placeholder} + isOptionDisabled={() => maxReached} + placeholder={currentPlaceholder} isClearable + noOptionsMessage={() => { + return maxReached + ? "You've reached the max number of tags." + : "No tags available"; + }} - - value={[]} + closeMenuOnSelect={false} + value={value.map(transformer.tagToOption)} onChange={handleChange as any} onBlur={onBlur} components={{ Option: OptionComponent, - MultiValue: () => <> + // ValueContainer: CustomValueContainer }} - styles={colourStyles as any} theme={(theme) => ({ ...theme, @@ -128,9 +160,9 @@ export default function TagsInput({ }, })} /> -
+ {/*
{(value as Tag[]).map((tag, idx) => handleRemove(idx)} >{tag.title})} -
+
*/}
) } diff --git a/src/Components/Inputs/TagsInput/officalTags.graphql b/src/Components/Inputs/TagsInput/officalTags.graphql index 0cc6388..28ed7eb 100644 --- a/src/Components/Inputs/TagsInput/officalTags.graphql +++ b/src/Components/Inputs/TagsInput/officalTags.graphql @@ -3,5 +3,6 @@ query OfficialTags { id title icon + description } } diff --git a/src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm.tsx b/src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm.tsx index b1cb45d..210cf52 100644 --- a/src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm.tsx +++ b/src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm.tsx @@ -143,15 +143,13 @@ export default function StoryForm() {

{errors.cover_image?.message}

-

- Title -

-
+ +
@@ -159,12 +157,9 @@ export default function StoryForm() { {errors.title.message}

} -

- Tags -

{errors.tags &&

{errors.tags.message} diff --git a/src/features/Posts/pages/CreatePostPage/CreatePostPage.tsx b/src/features/Posts/pages/CreatePostPage/CreatePostPage.tsx index 3e975b2..848cf98 100644 --- a/src/features/Posts/pages/CreatePostPage/CreatePostPage.tsx +++ b/src/features/Posts/pages/CreatePostPage/CreatePostPage.tsx @@ -20,7 +20,9 @@ export default function CreatePostPage() { return (<> - Create Post + {postType === 'story' && Create Story} + {postType === 'bounty' && Create Bounty} + {postType === 'question' && Create Question}

{postType === 'story' && <> -

+ {/*

Write a Story -

+ */} } {postType === 'bounty' && <> diff --git a/src/features/Posts/pages/PostDetailsPage/Components/AuthorCard/AuthorCard.tsx b/src/features/Posts/pages/PostDetailsPage/Components/AuthorCard/AuthorCard.tsx index 65b2c82..178015a 100644 --- a/src/features/Posts/pages/PostDetailsPage/Components/AuthorCard/AuthorCard.tsx +++ b/src/features/Posts/pages/PostDetailsPage/Components/AuthorCard/AuthorCard.tsx @@ -28,10 +28,10 @@ export default function AuthorCard({ author }: Props) {
) diff --git a/src/features/Posts/pages/PostDetailsPage/Components/StoryPageContent/StoryPageContent.tsx b/src/features/Posts/pages/PostDetailsPage/Components/StoryPageContent/StoryPageContent.tsx index 5f8c125..4750a72 100644 --- a/src/features/Posts/pages/PostDetailsPage/Components/StoryPageContent/StoryPageContent.tsx +++ b/src/features/Posts/pages/PostDetailsPage/Components/StoryPageContent/StoryPageContent.tsx @@ -49,7 +49,7 @@ export default function StoryPageContent({ story }: Props) { Delete } -

{story.title}

+

{story.title}

{story.tags.length > 0 &&
{story.tags.map(tag => diff --git a/src/features/Posts/pages/PreviewPostPage/PreviewPostContent/PreviewPostContent.tsx b/src/features/Posts/pages/PreviewPostPage/PreviewPostContent/PreviewPostContent.tsx index 6298d04..c6d9ebf 100644 --- a/src/features/Posts/pages/PreviewPostPage/PreviewPostContent/PreviewPostContent.tsx +++ b/src/features/Posts/pages/PreviewPostPage/PreviewPostContent/PreviewPostContent.tsx @@ -37,7 +37,7 @@ export default function PreviewPostContent({ post }: Props) { alt="" />}
-

{post.title}

+

{post.title}

{post.tags.length > 0 &&
{post.tags.map((tag, idx) => {tag.title} diff --git a/src/graphql/index.tsx b/src/graphql/index.tsx index 3fa3053..40cdd93 100644 --- a/src/graphql/index.tsx +++ b/src/graphql/index.tsx @@ -340,6 +340,7 @@ export type StoryInputType = { export type Tag = { __typename?: 'Tag'; + description: Maybe; icon: Maybe; id: Scalars['Int']; isOfficial: Maybe; @@ -401,7 +402,7 @@ export type Vote = { export type OfficialTagsQueryVariables = Exact<{ [key: string]: never; }>; -export type OfficialTagsQuery = { __typename?: 'Query', officialTags: Array<{ __typename?: 'Tag', id: number, title: string, icon: string | null }> }; +export type OfficialTagsQuery = { __typename?: 'Query', officialTags: Array<{ __typename?: 'Tag', id: number, title: string, icon: string | null, description: string | null }> }; export type NavCategoriesQueryVariables = Exact<{ [key: string]: never; }>; @@ -557,6 +558,7 @@ export const OfficialTagsDocument = gql` id title icon + description } } `; diff --git a/src/mocks/data/tags.ts b/src/mocks/data/tags.ts index c40aeb4..a75bb15 100644 --- a/src/mocks/data/tags.ts +++ b/src/mocks/data/tags.ts @@ -5,27 +5,37 @@ export const tags = [ { id: 1, title: 'Bitcoin', + description: 'Lorem ipsum dolor sit amort consectetur, adipisicing elit. Possimus officia sit numquam nobis iure atque ab sunt nihil voluptatibus', icon: "🅱", + isOfficial: true, }, { id: 2, title: 'Lightning', + description: 'Lorem ipsum dolor sit amort consectetur, adipisicing elit. Possimus officia sit numquam nobis iure atque ab sunt nihil voluptatibus', icon: "⚡", + isOfficial: true, }, { id: 3, title: 'Webln', + description: 'Lorem ipsum dolor sit amort consectetur, adipisicing elit. Possimus officia sit numquam nobis iure atque ab sunt nihil voluptatibus', icon: "🔗", + isOfficial: true, }, { id: 4, title: 'Gaming', + description: 'Lorem ipsum dolor sit amort consectetur, adipisicing elit. Possimus officia sit numquam nobis iure atque ab sunt nihil voluptatibus', icon: "🎮", + isOfficial: true, }, { id: 5, title: 'Design', - icon: '🎨' + description: 'Lorem ipsum dolor sit amort consectetur, adipisicing elit. Possimus officia sit numquam nobis iure atque ab sunt nihil voluptatibus', + icon: '🎨', + isOfficial: true, } ].map(i => ({ __typename: "Tag", ...i })) as Tag[] \ No newline at end of file diff --git a/src/utils/interfaces/misc.interfaces.ts b/src/utils/interfaces/misc.interfaces.ts index 5239bca..f3fc015 100644 --- a/src/utils/interfaces/misc.interfaces.ts +++ b/src/utils/interfaces/misc.interfaces.ts @@ -1,8 +1,6 @@ +import { Tag as ApiTag } from "src/graphql"; -export type Tag = { - id: number - title: string -} +export type Tag = ApiTag; export type ListComponentProps = { From 6319e23dae2127d62bc72fd65ea5a7e148960b6d Mon Sep 17 00:00:00 2001 From: MTG2000 Date: Mon, 11 Jul 2022 15:49:01 +0300 Subject: [PATCH 04/15] fix: update lightning address validation handle 404 responses in lightning addresses validation closes issue #70 --- .../pages/ProfilePage/AboutCard/UpdateAboutForm.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/features/Profiles/pages/ProfilePage/AboutCard/UpdateAboutForm.tsx b/src/features/Profiles/pages/ProfilePage/AboutCard/UpdateAboutForm.tsx index 4f06cb1..17a2b8d 100644 --- a/src/features/Profiles/pages/ProfilePage/AboutCard/UpdateAboutForm.tsx +++ b/src/features/Profiles/pages/ProfilePage/AboutCard/UpdateAboutForm.tsx @@ -30,9 +30,10 @@ const schema: yup.SchemaOf = yup.object({ if (value) { const [name, domain] = value.split("@"); const lnurl = `https://${domain}/.well-known/lnurlp/${name}`; - await fetch(lnurl); + const res = await fetch(lnurl); + if (res.status === 200) return true; } - return true; + return false; } catch (error) { return false; } @@ -141,7 +142,7 @@ export default function UpdateAboutForm({ data, onClose }: Props) { type='text' className="input-text" - placeholder="UK, London" + placeholder="Back-end Developer" {...register("jobTitle")} />
From 5b7ce1d777859e2e7b15190884eb5593d8adaa7a Mon Sep 17 00:00:00 2001 From: MTG2000 Date: Mon, 11 Jul 2022 16:05:36 +0300 Subject: [PATCH 05/15] update: update story title placeholder --- .../pages/CreatePostPage/Components/StoryForm/StoryForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm.tsx b/src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm.tsx index 210cf52..3eb4519 100644 --- a/src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm.tsx +++ b/src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm.tsx @@ -149,7 +149,7 @@ export default function StoryForm() { autoFocus type='text' className="p-0 text-[42px] border-0 focus:border-0 focus:outline-none focus:ring-0 font-bolder placeholder:!text-gray-600" - placeholder='Your Story Title...' + placeholder='New story title here...' {...register("title")} />
From d41aeb948eca98606e8c88d696b04da04ae6d864 Mon Sep 17 00:00:00 2001 From: MTG2000 Date: Mon, 11 Jul 2022 22:54:12 +0300 Subject: [PATCH 06/15] feat: navbar layout, show createStory btn always, enchance protected route to redirect to the route that he came from, remove navbar in create story page Fixes #74 --- src/App.tsx | 61 +++++++++++-------- .../ProtectedRoute/ProtectedRoute.tsx | 8 ++- .../Auth/pages/LoginPage/LoginPage.tsx | 9 ++- .../Posts/pages/FeedPage/FeedPage.tsx | 15 +++-- src/utils/helperFunctions.tsx | 8 ++- src/utils/routing/index.ts | 3 +- src/utils/routing/layouts/NavbarLayout.tsx | 9 +++ src/utils/routing/layouts/index.ts | 2 + src/utils/routing/loadable.tsx | 8 +++ 9 files changed, 83 insertions(+), 40 deletions(-) create mode 100644 src/utils/routing/layouts/NavbarLayout.tsx create mode 100644 src/utils/routing/layouts/index.ts create mode 100644 src/utils/routing/loadable.tsx diff --git a/src/App.tsx b/src/App.tsx index 0cde4f7..7e1f764 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,4 @@ import React, { Suspense, useEffect } from "react"; -import Navbar from "src/Components/Navbar/Navbar"; import ModalsContainer from "src/Components/Modals/ModalsContainer/ModalsContainer"; import { useAppDispatch, useAppSelector } from './utils/hooks'; import { Wallet_Service } from "./services"; @@ -10,23 +9,30 @@ import { useMeQuery } from "./graphql"; import { setUser } from "./redux/features/user.slice"; import ProtectedRoute from "./Components/ProtectedRoute/ProtectedRoute"; import { Helmet } from "react-helmet"; +import { NavbarLayout } from "./utils/routing/layouts"; +import { Loadable } from "./utils/routing"; + + // Pages -const FeedPage = React.lazy(() => import("./features/Posts/pages/FeedPage/FeedPage")) -const PostDetailsPage = React.lazy(() => import("./features/Posts/pages/PostDetailsPage/PostDetailsPage")) -const CreatePostPage = React.lazy(() => import("./features/Posts/pages/CreatePostPage/CreatePostPage")) -const PreviewPostPage = React.lazy(() => import("./features/Posts/pages/PreviewPostPage/PreviewPostPage")) +const FeedPage = Loadable(React.lazy(() => import("./features/Posts/pages/FeedPage/FeedPage"))) +const PostDetailsPage = Loadable(React.lazy(() => import("./features/Posts/pages/PostDetailsPage/PostDetailsPage"))) +const CreatePostPage = Loadable(React.lazy(() => import("./features/Posts/pages/CreatePostPage/CreatePostPage"))) +const PreviewPostPage = Loadable(React.lazy(() => import("./features/Posts/pages/PreviewPostPage/PreviewPostPage"))) + +const HottestPage = Loadable(React.lazy(() => import("src/features/Projects/pages/HottestPage/HottestPage"))) +const CategoryPage = Loadable(React.lazy(() => import("src/features/Projects/pages/CategoryPage/CategoryPage"))) +const ExplorePage = Loadable(React.lazy(() => import("src/features/Projects/pages/ExplorePage"))) + +const HackathonsPage = Loadable(React.lazy(() => import("./features/Hackathons/pages/HackathonsPage/HackathonsPage"))) + +const DonatePage = Loadable(React.lazy(() => import("./features/Donations/pages/DonatePage/DonatePage"))) +const LoginPage = Loadable(React.lazy(() => import("./features/Auth/pages/LoginPage/LoginPage"))) +const LogoutPage = Loadable(React.lazy(() => import("./features/Auth/pages/LogoutPage/LogoutPage"))) +const ProfilePage = Loadable(React.lazy(() => import("./features/Profiles/pages/ProfilePage/ProfilePage"))) -const HottestPage = React.lazy(() => import("src/features/Projects/pages/HottestPage/HottestPage")) -const CategoryPage = React.lazy(() => import("src/features/Projects/pages/CategoryPage/CategoryPage")) -const ExplorePage = React.lazy(() => import("src/features/Projects/pages/ExplorePage")) -const HackathonsPage = React.lazy(() => import("./features/Hackathons/pages/HackathonsPage/HackathonsPage")) -const DonatePage = React.lazy(() => import("./features/Donations/pages/DonatePage/DonatePage")) -const LoginPage = React.lazy(() => import("./features/Auth/pages/LoginPage/LoginPage")) -const LogoutPage = React.lazy(() => import("./features/Auth/pages/LogoutPage/LogoutPage")) -const ProfilePage = React.lazy(() => import("./features/Profiles/pages/ProfilePage/ProfilePage")) function App() { const { isWalletConnected } = useAppSelector(state => ({ @@ -76,27 +82,30 @@ function App() { /> - }> - } /> - } /> - } /> - - } /> } /> } /> - } /> - } /> + }> + } /> + } /> + } /> - } /> + } /> + } /> - } /> - } /> - } /> + } /> + + } /> + + } /> + } /> + } /> + + } /> + - } /> diff --git a/src/Components/ProtectedRoute/ProtectedRoute.tsx b/src/Components/ProtectedRoute/ProtectedRoute.tsx index 6416840..0235323 100644 --- a/src/Components/ProtectedRoute/ProtectedRoute.tsx +++ b/src/Components/ProtectedRoute/ProtectedRoute.tsx @@ -1,5 +1,5 @@ import { PropsWithChildren } from "react"; -import { Navigate, } from "react-router-dom"; +import { Navigate, useLocation } from "react-router-dom"; import { useAppSelector } from "src/utils/hooks"; interface Props { @@ -17,10 +17,14 @@ export default function ProtectedRoute({ const user = useAppSelector(state => state.user.me); + const location = useLocation(); + + + if (user === undefined) return <> if (user === null) - return ; + return ; if (!isAllowed) { diff --git a/src/features/Auth/pages/LoginPage/LoginPage.tsx b/src/features/Auth/pages/LoginPage/LoginPage.tsx index 4dd7352..f3b9ccb 100644 --- a/src/features/Auth/pages/LoginPage/LoginPage.tsx +++ b/src/features/Auth/pages/LoginPage/LoginPage.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useState } from "react" import { Helmet } from "react-helmet"; import { Grid } from "react-loader-spinner"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useLocation } from "react-router-dom"; import { useMeQuery } from "src/graphql" import { CONSTS } from "src/utils"; import { QRCodeSVG } from 'qrcode.react'; @@ -9,6 +9,7 @@ import { IoRocketOutline } from "react-icons/io5"; import Button from "src/Components/Button/Button"; import { FiCopy } from "react-icons/fi"; import useCopyToClipboard from "src/utils/hooks/useCopyToClipboard"; +import { getPropertyFromUnknown, } from "src/utils/helperFunctions"; @@ -58,6 +59,7 @@ const useLnurlQuery = () => { export default function LoginPage() { const [isLoggedIn, setIsLoggedIn] = useState(false); const navigate = useNavigate(); + const location = useLocation(); const [copied, setCopied] = useState(false); const { loadingLnurl, data: { lnurl, session_token }, error } = useLnurlQuery(); @@ -75,7 +77,10 @@ export default function LoginPage() { setIsLoggedIn(true); meQuery.stopPolling(); setTimeout(() => { - navigate('/') + const cameFrom = getPropertyFromUnknown(location.state, 'from'); + const navigateTo = cameFrom ? cameFrom : '/' + + navigate(navigateTo) }, 2000) } diff --git a/src/features/Posts/pages/FeedPage/FeedPage.tsx b/src/features/Posts/pages/FeedPage/FeedPage.tsx index 6173ca4..2f3e20c 100644 --- a/src/features/Posts/pages/FeedPage/FeedPage.tsx +++ b/src/features/Posts/pages/FeedPage/FeedPage.tsx @@ -79,14 +79,13 @@ export default function FeedPage() { top: `${navHeight + 16}px`, maxHeight: `calc(100vh - ${navHeight}px - 16px)`, }}> - {isLoggedIn && - } +
!/^https?:\/\//i.test(url) ? `http://${url}` : url; \ No newline at end of file +export const withHttp = (url: string) => !/^https?:\/\//i.test(url) ? `http://${url}` : url; + +export function getPropertyFromUnknown(obj: unknown, prop: string | number | symbol): Value | null { + if (typeof obj === 'object' && obj !== null && prop in obj) + return (obj as any)[prop as any] as Value; + return null +} \ No newline at end of file diff --git a/src/utils/routing/index.ts b/src/utils/routing/index.ts index 7256eea..de328f8 100644 --- a/src/utils/routing/index.ts +++ b/src/utils/routing/index.ts @@ -1 +1,2 @@ -export * from './routes' \ No newline at end of file +export * from './routes' +export * from './loadable' \ No newline at end of file diff --git a/src/utils/routing/layouts/NavbarLayout.tsx b/src/utils/routing/layouts/NavbarLayout.tsx new file mode 100644 index 0000000..cc63068 --- /dev/null +++ b/src/utils/routing/layouts/NavbarLayout.tsx @@ -0,0 +1,9 @@ +import { Outlet, } from 'react-router-dom'; +import Navbar from "src/Components/Navbar/Navbar"; + +export const NavbarLayout = () => { + return <> + + + +}; diff --git a/src/utils/routing/layouts/index.ts b/src/utils/routing/layouts/index.ts new file mode 100644 index 0000000..6a608f4 --- /dev/null +++ b/src/utils/routing/layouts/index.ts @@ -0,0 +1,2 @@ + +export * from './NavbarLayout' \ No newline at end of file diff --git a/src/utils/routing/loadable.tsx b/src/utils/routing/loadable.tsx new file mode 100644 index 0000000..d8eabce --- /dev/null +++ b/src/utils/routing/loadable.tsx @@ -0,0 +1,8 @@ +import { Suspense } from "react"; +import LoadingPage from "src/Components/LoadingPage/LoadingPage"; + +export const Loadable = (Component: any, Loading = LoadingPage) => (props: any) => ( + }> + + +); From 216133f8939e444cf399a7837bd6e477767820b8 Mon Sep 17 00:00:00 2001 From: MTG2000 Date: Tue, 12 Jul 2022 10:56:39 +0300 Subject: [PATCH 07/15] feat: create preload hook, preload postDetailsPage --- .../Posts/pages/CreatePostPage/CreatePostPage.tsx | 4 ++++ src/features/Posts/pages/FeedPage/FeedPage.tsx | 4 +++- src/utils/hooks/index.ts | 1 + src/utils/hooks/usePreload.ts | 14 ++++++++++++++ 4 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 src/utils/hooks/usePreload.ts diff --git a/src/features/Posts/pages/CreatePostPage/CreatePostPage.tsx b/src/features/Posts/pages/CreatePostPage/CreatePostPage.tsx index 848cf98..bbe3c90 100644 --- a/src/features/Posts/pages/CreatePostPage/CreatePostPage.tsx +++ b/src/features/Posts/pages/CreatePostPage/CreatePostPage.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { Helmet } from "react-helmet"; import { FiArrowLeft } from "react-icons/fi"; import { useNavigate, useParams } from "react-router-dom"; +import { usePreload } from "src/utils/hooks"; import BountyForm from "./Components/BountyForm/BountyForm"; import QuestionForm from "./Components/QuestionForm/QuestionForm"; import StoryForm from "./Components/StoryForm/StoryForm"; @@ -16,6 +17,9 @@ export default function CreatePostPage() { const { type } = useParams() const [postType, setPostType] = useState<'story' | 'bounty' | 'question'>((type as any) ?? 'story'); + + usePreload('PreviewPostPage'); + const navigate = useNavigate(); return (<> diff --git a/src/features/Posts/pages/FeedPage/FeedPage.tsx b/src/features/Posts/pages/FeedPage/FeedPage.tsx index 2f3e20c..4d62def 100644 --- a/src/features/Posts/pages/FeedPage/FeedPage.tsx +++ b/src/features/Posts/pages/FeedPage/FeedPage.tsx @@ -2,7 +2,7 @@ import { useUpdateEffect } from '@react-hookz/web' import { useState } from 'react' import { useFeedQuery } from 'src/graphql' -import { useAppSelector, useInfiniteQuery } from 'src/utils/hooks' +import { useAppSelector, useInfiniteQuery, usePreload } from 'src/utils/hooks' import PostsList from '../../Components/PostsList/PostsList' import TrendingCard from '../../Components/TrendingCard/TrendingCard' import PopularTagsFilter, { FilterTag } from './PopularTagsFilter/PopularTagsFilter' @@ -32,6 +32,8 @@ export default function FeedPage() { const { fetchMore, isFetchingMore, variablesChanged } = useInfiniteQuery(feedQuery, 'getFeed') useUpdateEffect(variablesChanged, [sortByFilter, tagFilter]); + usePreload('PostPage'); + const { navHeight, isLoggedIn } = useAppSelector((state) => ({ navHeight: state.ui.navHeight, isLoggedIn: Boolean(state.user.me), diff --git a/src/utils/hooks/index.ts b/src/utils/hooks/index.ts index f3f318b..af04c8d 100644 --- a/src/utils/hooks/index.ts +++ b/src/utils/hooks/index.ts @@ -9,3 +9,4 @@ export * from "./useVote"; export * from './useWindowSize' export * from './useMediaQuery' export * from './useCurrentSection' +export * from './usePreload' diff --git a/src/utils/hooks/usePreload.ts b/src/utils/hooks/usePreload.ts new file mode 100644 index 0000000..121fc2e --- /dev/null +++ b/src/utils/hooks/usePreload.ts @@ -0,0 +1,14 @@ +import { useEffect } from 'react'; + +const Components = { + PostPage: () => import('../../features/Posts/pages/PostDetailsPage/PostDetailsPage'), + PreviewPostPage: () => import("../../features/Posts/pages/PreviewPostPage/PreviewPostPage") +} + +type ComponentToLoad = keyof typeof Components; + +export const usePreload = (componentToLoad: ComponentToLoad) => { + useEffect(() => { + Components[componentToLoad]() + }, [componentToLoad]) +} \ No newline at end of file From 0b0e1b5bb293b782b77514cb49f00132d6307f38 Mon Sep 17 00:00:00 2001 From: MTG2000 Date: Tue, 12 Jul 2022 13:17:43 +0300 Subject: [PATCH 08/15] feat: build a new preview component, change the story form errors structure and display, store current draft in storage Issues #67 #66 --- src/App.tsx | 2 - src/Components/Navbar/NavDesktop.tsx | 2 - .../PreviewPostCard.stories.tsx} | 8 +- .../PreviewPostCard/PreviewPostCard.tsx} | 13 +- .../Components/StoryForm/StoryForm.tsx | 199 +++++++++++------- .../pages/CreatePostPage/CreatePostPage.tsx | 9 +- .../pages/PreviewPostPage/PreviewPostPage.tsx | 74 ------- src/services/index.ts | 8 +- src/services/storage.service.ts | 20 ++ src/utils/hooks/usePreload.ts | 1 - 10 files changed, 157 insertions(+), 179 deletions(-) rename src/features/Posts/pages/{PreviewPostPage/PreviewPostContent/PreviewPostContent.stories.tsx => CreatePostPage/Components/PreviewPostCard/PreviewPostCard.stories.tsx} (55%) rename src/features/Posts/pages/{PreviewPostPage/PreviewPostContent/PreviewPostContent.tsx => CreatePostPage/Components/PreviewPostCard/PreviewPostCard.tsx} (81%) delete mode 100644 src/features/Posts/pages/PreviewPostPage/PreviewPostPage.tsx create mode 100644 src/services/storage.service.ts diff --git a/src/App.tsx b/src/App.tsx index 7e1f764..cc94daf 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,7 +18,6 @@ import { Loadable } from "./utils/routing"; const FeedPage = Loadable(React.lazy(() => import("./features/Posts/pages/FeedPage/FeedPage"))) const PostDetailsPage = Loadable(React.lazy(() => import("./features/Posts/pages/PostDetailsPage/PostDetailsPage"))) const CreatePostPage = Loadable(React.lazy(() => import("./features/Posts/pages/CreatePostPage/CreatePostPage"))) -const PreviewPostPage = Loadable(React.lazy(() => import("./features/Posts/pages/PreviewPostPage/PreviewPostPage"))) const HottestPage = Loadable(React.lazy(() => import("src/features/Projects/pages/HottestPage/HottestPage"))) const CategoryPage = Loadable(React.lazy(() => import("src/features/Projects/pages/CategoryPage/CategoryPage"))) @@ -84,7 +83,6 @@ function App() { }> - } /> } /> }> diff --git a/src/Components/Navbar/NavDesktop.tsx b/src/Components/Navbar/NavDesktop.tsx index 87d6342..0825ba3 100644 --- a/src/Components/Navbar/NavDesktop.tsx +++ b/src/Components/Navbar/NavDesktop.tsx @@ -21,8 +21,6 @@ import { createRoute } from "src/utils/routing"; export default function NavDesktop() { const [searchOpen, setSearchOpen] = useState(false) - const communityRef = useRef(null); - const [communitymenuProps, toggleCommunityMenu] = useMenuState({ transition: true }); diff --git a/src/features/Posts/pages/PreviewPostPage/PreviewPostContent/PreviewPostContent.stories.tsx b/src/features/Posts/pages/CreatePostPage/Components/PreviewPostCard/PreviewPostCard.stories.tsx similarity index 55% rename from src/features/Posts/pages/PreviewPostPage/PreviewPostContent/PreviewPostContent.stories.tsx rename to src/features/Posts/pages/CreatePostPage/Components/PreviewPostCard/PreviewPostCard.stories.tsx index c1c1f29..85ce3a1 100644 --- a/src/features/Posts/pages/PreviewPostPage/PreviewPostContent/PreviewPostContent.stories.tsx +++ b/src/features/Posts/pages/CreatePostPage/Components/PreviewPostCard/PreviewPostCard.stories.tsx @@ -1,18 +1,18 @@ import { ComponentStory, ComponentMeta } from '@storybook/react'; import { MOCK_DATA } from 'src/mocks/data'; -import PreviewPostContent from './PreviewPostContent'; +import PreviewPostCard from './PreviewPostCard'; export default { title: 'Posts/Post Details Page/Components/Story Page Content', - component: PreviewPostContent, + component: PreviewPostCard, argTypes: { backgroundColor: { control: 'color' }, }, -} as ComponentMeta; +} as ComponentMeta; -const Template: ComponentStory = (args) =>
+const Template: ComponentStory = (args) =>
export const Default = Template.bind({}); Default.args = { diff --git a/src/features/Posts/pages/PreviewPostPage/PreviewPostContent/PreviewPostContent.tsx b/src/features/Posts/pages/CreatePostPage/Components/PreviewPostCard/PreviewPostCard.tsx similarity index 81% rename from src/features/Posts/pages/PreviewPostPage/PreviewPostContent/PreviewPostContent.tsx rename to src/features/Posts/pages/CreatePostPage/Components/PreviewPostCard/PreviewPostCard.tsx index c6d9ebf..c6a1337 100644 --- a/src/features/Posts/pages/PreviewPostPage/PreviewPostContent/PreviewPostContent.tsx +++ b/src/features/Posts/pages/CreatePostPage/Components/PreviewPostCard/PreviewPostCard.tsx @@ -1,23 +1,25 @@ -import Header from "src/features/Posts/Components/PostCard/Header/Header" import { marked } from 'marked'; -import styles from '../../PostDetailsPage/Components/PageContent/styles.module.scss' +import styles from 'src/features/Posts/pages/PostDetailsPage/Components/PageContent/styles.module.scss' import Badge from "src/Components/Badge/Badge"; import { Post } from "src/graphql"; +function isPost(type?: string): type is 'story' { + return type === 'story' + // || type === 'question' || type === 'bounty' +} + interface Props { post: Pick & { tags: Array<{ title: string }> cover_image?: string | File | null } } -export default function PreviewPostContent({ post }: Props) { +export default function PreviewPostContent({ post, }: Props) { let coverImg: string; if (!post.cover_image) @@ -36,7 +38,6 @@ export default function PreviewPostContent({ post }: Props) { className='w-full h-[120px] md:h-[240px] object-cover rounded-12 mb-16' alt="" />}
-

{post.title}

{post.tags.length > 0 &&
{post.tags.map((tag, idx) => diff --git a/src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm.tsx b/src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm.tsx index 3eb4519..77a629a 100644 --- a/src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm.tsx +++ b/src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm.tsx @@ -1,6 +1,6 @@ -import { useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { yupResolver } from "@hookform/resolvers/yup"; -import { Controller, FormProvider, NestedValue, Resolver, SubmitHandler, useForm } from "react-hook-form"; +import { Controller, FormProvider, NestedValue, Resolver, useForm } from "react-hook-form"; import Button from "src/Components/Button/Button"; import FilesInput from "src/Components/Inputs/FilesInput/FilesInput"; import TagsInput from "src/Components/Inputs/TagsInput/TagsInput"; @@ -13,6 +13,9 @@ import { stageStory } from 'src/redux/features/staging.slice' import { Override } from 'src/utils/interfaces'; import { NotificationsService } from "src/services/notifications.service"; import { createRoute } from 'src/utils/routing'; +import PreviewPostCard from '../PreviewPostCard/PreviewPostCard' +import { StorageService } from 'src/services'; +import { useThrottledCallback } from '@react-hookz/web'; const FileSchema = yup.lazy((value: string | File[]) => { @@ -54,16 +57,25 @@ export type CreateStoryType = Override +const storageService = new StorageService('story-edit'); + export default function StoryForm() { const dispatch = useAppDispatch(); const { story } = useAppSelector(state => ({ - story: state.staging.story + story: state.staging.story || storageService.get() })) + + const [editMode, setEditMode] = useState(true) + const navigate = useNavigate(); + const errorsContainerRef = useRef(null!); + + const formMethods = useForm({ resolver: yupResolver(schema) as Resolver, + shouldFocusError: false, defaultValues: { id: story?.id ?? null, title: story?.title ?? '', @@ -72,11 +84,19 @@ export default function StoryForm() { body: story?.body ?? '', }, }); - const { handleSubmit, control, register, formState: { errors, }, trigger, getValues, } = formMethods; - const [loading, setLoading] = useState(false) + const { handleSubmit, control, register, formState: { errors, isValid, isSubmitted }, trigger, getValues, watch } = formMethods; - const navigate = useNavigate() + const presistPost = useThrottledCallback((value) => storageService.set(value), [], 1000) + useEffect(() => { + const subscription = watch((value) => presistPost(value)); + return () => subscription.unsubscribe(); + }, [presistPost, watch]); + + + + + const [loading, setLoading] = useState(false); const [createStory] = useCreateStoryMutation({ onCompleted: (data) => { navigate(createRoute({ type: 'story', id: data.createStory?.id!, title: data.createStory?.title })) @@ -89,13 +109,15 @@ export default function StoryForm() { } }); + const clickPreview = async () => { const isValid = await trigger(); if (isValid) { const data = getValues() dispatch(stageStory(data)) - navigate('/blog/preview-post/Story') + storageService.set(data) + setEditMode(false); } else { clickSubmit(); // I'm doing this so that the react-hook-form attaches onChange listener to inputs validation } @@ -114,86 +136,107 @@ export default function StoryForm() { }, } }) - }) + }, () => errorsContainerRef.current.scrollIntoView({ behavior: 'smooth', block: "center" })) const isUpdating = story?.id; + return ( -
-
-
- ( - - )} - /> -

{errors.cover_image?.message}

- - - -
- -
- {errors.title &&

- {errors.title.message} -

} - - - {errors.tags &&

- {errors.tags.message} -

} +
+ +
+ +
- + {editMode && <> +
+
+ ( + + )} + /> - {errors.body &&

- {errors.body.message} -

} + + +
+ +
+ + + +
+ + +
+ + } + {!editMode && } +
+ + {/* */} +
+ +
+
+ {(!isValid && isSubmitted) &&
    + {errors.title &&
  • + {errors.title.message} +
  • } + {errors.cover_image &&
  • + {errors.cover_image.message} +
  • } + {errors.tags &&
  • + {errors.tags.message} +
  • } + {errors.body &&
  • + {errors.body.message} +
  • } +
} +
-
- - -
- +
) } diff --git a/src/features/Posts/pages/CreatePostPage/CreatePostPage.tsx b/src/features/Posts/pages/CreatePostPage/CreatePostPage.tsx index bbe3c90..48c7e65 100644 --- a/src/features/Posts/pages/CreatePostPage/CreatePostPage.tsx +++ b/src/features/Posts/pages/CreatePostPage/CreatePostPage.tsx @@ -2,11 +2,9 @@ import { useState } from "react"; import { Helmet } from "react-helmet"; import { FiArrowLeft } from "react-icons/fi"; import { useNavigate, useParams } from "react-router-dom"; -import { usePreload } from "src/utils/hooks"; import BountyForm from "./Components/BountyForm/BountyForm"; import QuestionForm from "./Components/QuestionForm/QuestionForm"; import StoryForm from "./Components/StoryForm/StoryForm"; -import PostTypeList from "./PostTypeList"; interface Props { @@ -18,7 +16,6 @@ export default function CreatePostPage() { const [postType, setPostType] = useState<'story' | 'bounty' | 'question'>((type as any) ?? 'story'); - usePreload('PreviewPostPage'); const navigate = useNavigate(); @@ -29,7 +26,7 @@ export default function CreatePostPage() { {postType === 'question' && Create Question}
@@ -42,9 +39,7 @@ export default function CreatePostPage() {
-
+
{postType === 'story' && <> {/*

Write a Story diff --git a/src/features/Posts/pages/PreviewPostPage/PreviewPostPage.tsx b/src/features/Posts/pages/PreviewPostPage/PreviewPostPage.tsx deleted file mode 100644 index 9a9897b..0000000 --- a/src/features/Posts/pages/PreviewPostPage/PreviewPostPage.tsx +++ /dev/null @@ -1,74 +0,0 @@ - -import { Helmet } from 'react-helmet' -import { useParams } from 'react-router-dom' -import NotFoundPage from 'src/features/Shared/pages/NotFoundPage/NotFoundPage' -import { useAppSelector, } from 'src/utils/hooks' -import TrendingCard from '../../Components/TrendingCard/TrendingCard' -import AuthorCard from '../PostDetailsPage/Components/AuthorCard/AuthorCard' -import PostActions from '../PostDetailsPage/Components/PostActions/PostActions' -import styles from '../PostDetailsPage/styles.module.scss'; -import PreviewPostContent from './PreviewPostContent/PreviewPostContent' - -function isPost(type?: string): type is 'story' { - return type === 'story' - // || type === 'question' || type === 'bounty' -} - - -export default function PreviewPostPage() { - - const { type: _type } = useParams() - - const type = _type?.toLowerCase(); - - const { post, author, navHeight } = useAppSelector(state => ({ - post: isPost(type) ? state.staging[type] : null, - author: state.user.me, - navHeight: state.ui.navHeight - })) - - - - if (!post) - return - - return ( - <> - - {post.title} - - -
- - - -
- - ) -} diff --git a/src/services/index.ts b/src/services/index.ts index 8a6ce53..5bd6a97 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1,5 +1,3 @@ -import Wallet_Service from './wallet.service' - -export { - Wallet_Service -} \ No newline at end of file +export { default as Wallet_Service } from './wallet.service' +export * from './storage.service' +export * from './notifications.service' diff --git a/src/services/storage.service.ts b/src/services/storage.service.ts new file mode 100644 index 0000000..071d6f2 --- /dev/null +++ b/src/services/storage.service.ts @@ -0,0 +1,20 @@ + +export class StorageService { + key: string; + + constructor(key: string) { + this.key = key; + } + + set(newValue: T) { + localStorage.setItem(this.key, JSON.stringify(newValue)); + } + + get() { + const str = localStorage.getItem(this.key); + if (!str) + return null; + + return JSON.parse(str) as T; + } +} diff --git a/src/utils/hooks/usePreload.ts b/src/utils/hooks/usePreload.ts index 121fc2e..681a764 100644 --- a/src/utils/hooks/usePreload.ts +++ b/src/utils/hooks/usePreload.ts @@ -2,7 +2,6 @@ import { useEffect } from 'react'; const Components = { PostPage: () => import('../../features/Posts/pages/PostDetailsPage/PostDetailsPage'), - PreviewPostPage: () => import("../../features/Posts/pages/PreviewPostPage/PreviewPostPage") } type ComponentToLoad = keyof typeof Components; From c27f1954b560080e6eda2d4d1d5cb3f708a25065 Mon Sep 17 00:00:00 2001 From: MTG2000 Date: Tue, 12 Jul 2022 14:07:54 +0300 Subject: [PATCH 09/15] fix: fix story name for preview-post-card --- .../Components/PreviewPostCard/PreviewPostCard.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/Posts/pages/CreatePostPage/Components/PreviewPostCard/PreviewPostCard.stories.tsx b/src/features/Posts/pages/CreatePostPage/Components/PreviewPostCard/PreviewPostCard.stories.tsx index 85ce3a1..99d41ff 100644 --- a/src/features/Posts/pages/CreatePostPage/Components/PreviewPostCard/PreviewPostCard.stories.tsx +++ b/src/features/Posts/pages/CreatePostPage/Components/PreviewPostCard/PreviewPostCard.stories.tsx @@ -4,7 +4,7 @@ import { MOCK_DATA } from 'src/mocks/data'; import PreviewPostCard from './PreviewPostCard'; export default { - title: 'Posts/Post Details Page/Components/Story Page Content', + title: 'Posts/Post Details Page/Components/Preview Post Card', component: PreviewPostCard, argTypes: { backgroundColor: { control: 'color' }, From f0962d5763e52b1c42648be33994efb59ccaeeef Mon Sep 17 00:00:00 2001 From: MTG2000 Date: Tue, 12 Jul 2022 16:20:21 +0300 Subject: [PATCH 10/15] update: update gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ecdc8aa..1f28034 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ yarn-debug.log* yarn-error.log* TODO +NOTES From c37d62ad1751b654256f5be41a1cc0269977ea4c Mon Sep 17 00:00:00 2001 From: MTG2000 Date: Tue, 12 Jul 2022 18:05:55 +0300 Subject: [PATCH 11/15] feat: getMyDrafts api, base drafts list, add drafts to database --- api/functions/graphql/nexus-typegen.ts | 6 ++ api/functions/graphql/schema.graphql | 2 + api/functions/graphql/types/post.js | 48 +++++++++++++-- .../migration.sql | 2 + .../migration.sql | 2 + prisma/schema.prisma | 17 +++--- .../Components/StoryForm/StoryForm.tsx | 33 ++++++---- .../pages/CreatePostPage/getMyDrafts.graphql | 16 +++++ src/graphql/index.tsx | 60 +++++++++++++++++++ src/services/storage.service.ts | 4 ++ 10 files changed, 167 insertions(+), 23 deletions(-) create mode 100644 prisma/migrations/20220712141806_add_is_published_field_to_story/migration.sql create mode 100644 prisma/migrations/20220712142314_make_story_published_by_default/migration.sql create mode 100644 src/features/Posts/pages/CreatePostPage/getMyDrafts.graphql diff --git a/api/functions/graphql/nexus-typegen.ts b/api/functions/graphql/nexus-typegen.ts index 6fcb7aa..03ac891 100644 --- a/api/functions/graphql/nexus-typegen.ts +++ b/api/functions/graphql/nexus-typegen.ts @@ -32,6 +32,7 @@ export interface NexusGenInputs { body: string; // String! cover_image?: string | null; // String id?: number | null; // Int + is_published?: boolean | null; // Boolean tags: string[]; // [String!]! title: string; // String! } @@ -335,6 +336,7 @@ export interface NexusGenFieldTypes { getDonationsStats: NexusGenRootTypes['DonationsStats']; // DonationsStats! getFeed: NexusGenRootTypes['Post'][]; // [Post!]! getLnurlDetailsForProject: NexusGenRootTypes['LnurlDetails']; // LnurlDetails! + getMyDrafts: NexusGenRootTypes['Post'][]; // [Post!]! getPostById: NexusGenRootTypes['Post']; // Post! getProject: NexusGenRootTypes['Project']; // Project! getTrendingPosts: NexusGenRootTypes['Post'][]; // [Post!]! @@ -533,6 +535,7 @@ export interface NexusGenFieldTypeNames { getDonationsStats: 'DonationsStats' getFeed: 'Post' getLnurlDetailsForProject: 'LnurlDetails' + getMyDrafts: 'Post' getPostById: 'Post' getProject: 'Project' getTrendingPosts: 'Post' @@ -663,6 +666,9 @@ export interface NexusGenArgTypes { getLnurlDetailsForProject: { // args project_id: number; // Int! } + getMyDrafts: { // args + type: NexusGenEnums['POST_TYPE']; // POST_TYPE! + } getPostById: { // args id: number; // Int! type: NexusGenEnums['POST_TYPE']; // POST_TYPE! diff --git a/api/functions/graphql/schema.graphql b/api/functions/graphql/schema.graphql index 2c176b7..57d0afc 100644 --- a/api/functions/graphql/schema.graphql +++ b/api/functions/graphql/schema.graphql @@ -150,6 +150,7 @@ type Query { getDonationsStats: DonationsStats! getFeed(skip: Int = 0, sortBy: String, tag: Int = 0, take: Int = 10): [Post!]! getLnurlDetailsForProject(project_id: Int!): LnurlDetails! + getMyDrafts(type: POST_TYPE!): [Post!]! getPostById(id: Int!, type: POST_TYPE!): Post! getProject(id: Int!): Project! getTrendingPosts: [Post!]! @@ -196,6 +197,7 @@ input StoryInputType { body: String! cover_image: String id: Int + is_published: Boolean tags: [String!]! title: String! } diff --git a/api/functions/graphql/types/post.js b/api/functions/graphql/types/post.js index de4bcac..710b1de 100644 --- a/api/functions/graphql/types/post.js +++ b/api/functions/graphql/types/post.js @@ -109,6 +109,7 @@ const StoryInputType = inputObjectType({ t.nonNull.string('body'); t.string('cover_image'); t.nonNull.list.nonNull.string('tags'); + t.boolean('is_published') } }) const createStory = extendType({ @@ -118,21 +119,24 @@ const createStory = extendType({ type: 'Story', args: { data: StoryInputType }, async resolve(_root, args, ctx) { - const { id, title, body, cover_image, tags } = args.data; + 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 + user_id: true, + is_published: true } }) + was_published = oldPost.is_published; if (user.id !== oldPost.user_id) throw new ApolloError("Not post author") } @@ -160,6 +164,7 @@ const createStory = extendType({ body, cover_image, excerpt, + is_published: was_published || is_published, tags: { connectOrCreate: tags.map(tag => { @@ -185,6 +190,7 @@ const createStory = extendType({ body, cover_image, excerpt, + is_published, tags: { connectOrCreate: tags.map(tag => { @@ -371,7 +377,8 @@ const getFeed = extendType({ id: tag } }, - }) + }), + is_published: true, }, skip, take, @@ -396,7 +403,8 @@ const getTrendingPosts = extendType({ where: { createdAt: { gte: lastWeekDate - } + }, + is_published: true, }, orderBy: { votes_count: 'desc' }, take: 5, @@ -407,6 +415,37 @@ const getTrendingPosts = extendType({ }) +const getMyDrafts = extendType({ + type: "Query", + definition(t) { + t.nonNull.list.nonNull.field('getMyDrafts', { + type: "Post", + args: { + type: arg({ + type: nonNull('POST_TYPE') + }) + }, + async resolve(parent, { type }, ctx) { + const user = await getUserByPubKey(ctx.userPubKey); + // Do some validation + if (!user) + throw new ApolloError("Not Authenticated"); + + if (type === 'Story') + return prisma.story.findMany({ + where: { + is_published: false, + user_id: user.id + }, + orderBy: { createdAt: 'desc' }, + }).then(asStoryType) + return [] + } + }) + } +}) + + const getPostById = extendType({ type: "Query", definition(t) { @@ -453,6 +492,7 @@ module.exports = { getFeed, getPostById, getTrendingPosts, + getMyDrafts, // Mutations createStory, diff --git a/prisma/migrations/20220712141806_add_is_published_field_to_story/migration.sql b/prisma/migrations/20220712141806_add_is_published_field_to_story/migration.sql new file mode 100644 index 0000000..d91a82b --- /dev/null +++ b/prisma/migrations/20220712141806_add_is_published_field_to_story/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Story" ADD COLUMN "is_published" BOOLEAN NOT NULL DEFAULT false; diff --git a/prisma/migrations/20220712142314_make_story_published_by_default/migration.sql b/prisma/migrations/20220712142314_make_story_published_by_default/migration.sql new file mode 100644 index 0000000..5c790b9 --- /dev/null +++ b/prisma/migrations/20220712142314_make_story_published_by_default/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Story" ALTER COLUMN "is_published" SET DEFAULT true; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index bcb9ff3..85a5194 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -114,14 +114,15 @@ model Award { // ----------------- model Story { - id Int @id @default(autoincrement()) - title String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - body String - excerpt String - cover_image String? - votes_count Int @default(0) + id Int @id @default(autoincrement()) + title String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + body String + excerpt String + cover_image String? + votes_count Int @default(0) + is_published Boolean @default(true) tags Tag[] diff --git a/src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm.tsx b/src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm.tsx index 77a629a..9e753d9 100644 --- a/src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm.tsx +++ b/src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm.tsx @@ -6,7 +6,7 @@ import FilesInput from "src/Components/Inputs/FilesInput/FilesInput"; import TagsInput from "src/Components/Inputs/TagsInput/TagsInput"; import * as yup from "yup"; import ContentEditor from "../ContentEditor/ContentEditor"; -import { useCreateStoryMutation } from 'src/graphql' +import { Post_Type, useCreateStoryMutation, useGetMyDraftsQuery } from 'src/graphql' import { useNavigate } from 'react-router-dom' import { useAppDispatch, useAppSelector } from 'src/utils/hooks'; import { stageStory } from 'src/redux/features/staging.slice' @@ -72,6 +72,7 @@ export default function StoryForm() { const navigate = useNavigate(); const errorsContainerRef = useRef(null!); + const myDraftsQuery = useGetMyDraftsQuery({ variables: { type: Post_Type.Story } }) const formMethods = useForm({ resolver: yupResolver(schema) as Resolver, @@ -119,11 +120,11 @@ export default function StoryForm() { storageService.set(data) setEditMode(false); } else { - clickSubmit(); // I'm doing this so that the react-hook-form attaches onChange listener to inputs validation + clickSubmit(false)(); // I'm doing this so that the react-hook-form attaches onChange listener to inputs validation } } - const clickSubmit = handleSubmit(data => { + const clickSubmit = (publish_now: boolean) => handleSubmit(data => { setLoading(true); createStory({ variables: { @@ -132,10 +133,12 @@ export default function StoryForm() { title: data.title, body: data.body, tags: data.tags.map(t => t.title), + is_published: publish_now, cover_image: (data.cover_image[0] ?? null) as string | null, }, } }) + storageService.clear(); }, () => errorsContainerRef.current.scrollIntoView({ behavior: 'smooth', block: "center" })) @@ -147,7 +150,7 @@ export default function StoryForm() {
@@ -206,19 +209,20 @@ export default function StoryForm() { disabled={loading} > {isUpdating ? - loading ? "Updating..." : "Update" : - loading ? "Publishing..." : "Publish" + "Update" : + "Publish" } - {/* */} + Save as Draft +
-
+
{(!isValid && isSubmitted) &&
    {errors.title &&
  • @@ -235,6 +239,13 @@ export default function StoryForm() {
  • }
}
+ {(!myDraftsQuery.loading && myDraftsQuery.data?.getMyDrafts && myDraftsQuery.data.getMyDrafts.length > 0) && +
+

Saved Drafts

+
    + {myDraftsQuery.data.getMyDrafts.map(draft =>
  • {draft.title}
  • )} +
+
}
diff --git a/src/features/Posts/pages/CreatePostPage/getMyDrafts.graphql b/src/features/Posts/pages/CreatePostPage/getMyDrafts.graphql new file mode 100644 index 0000000..c76ee1e --- /dev/null +++ b/src/features/Posts/pages/CreatePostPage/getMyDrafts.graphql @@ -0,0 +1,16 @@ +query GetMyDrafts($type: POST_TYPE!) { + getMyDrafts(type: $type) { + ... on Story { + id + title + } + ... on Bounty { + id + title + } + ... on Question { + title + id + } + } +} diff --git a/src/graphql/index.tsx b/src/graphql/index.tsx index 40cdd93..520ecf0 100644 --- a/src/graphql/index.tsx +++ b/src/graphql/index.tsx @@ -214,6 +214,7 @@ export type Query = { getDonationsStats: DonationsStats; getFeed: Array; getLnurlDetailsForProject: LnurlDetails; + getMyDrafts: Array; getPostById: Post; getProject: Project; getTrendingPosts: Array; @@ -258,6 +259,11 @@ export type QueryGetLnurlDetailsForProjectArgs = { }; +export type QueryGetMyDraftsArgs = { + type: Post_Type; +}; + + export type QueryGetPostByIdArgs = { id: Scalars['Int']; type: Post_Type; @@ -334,6 +340,7 @@ export type StoryInputType = { body: Scalars['String']; cover_image: InputMaybe; id: InputMaybe; + is_published: InputMaybe; tags: Array; title: Scalars['String']; }; @@ -468,6 +475,13 @@ export type DeleteStoryMutationVariables = Exact<{ export type DeleteStoryMutation = { __typename?: 'Mutation', deleteStory: { __typename?: 'Story', id: number } | null }; +export type GetMyDraftsQueryVariables = Exact<{ + type: Post_Type; +}>; + + +export type GetMyDraftsQuery = { __typename?: 'Query', getMyDrafts: Array<{ __typename?: 'Bounty', id: number, title: string } | { __typename?: 'Question', title: string, id: number } | { __typename?: 'Story', id: number, title: string }> }; + export type PopularTagsQueryVariables = Exact<{ [key: string]: never; }>; @@ -995,6 +1009,52 @@ export function useDeleteStoryMutation(baseOptions?: Apollo.MutationHookOptions< export type DeleteStoryMutationHookResult = ReturnType; export type DeleteStoryMutationResult = Apollo.MutationResult; export type DeleteStoryMutationOptions = Apollo.BaseMutationOptions; +export const GetMyDraftsDocument = gql` + query GetMyDrafts($type: POST_TYPE!) { + getMyDrafts(type: $type) { + ... on Story { + id + title + } + ... on Bounty { + id + title + } + ... on Question { + title + id + } + } +} + `; + +/** + * __useGetMyDraftsQuery__ + * + * To run a query within a React component, call `useGetMyDraftsQuery` and pass it any options that fit your needs. + * When your component renders, `useGetMyDraftsQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGetMyDraftsQuery({ + * variables: { + * type: // value for 'type' + * }, + * }); + */ +export function useGetMyDraftsQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(GetMyDraftsDocument, options); + } +export function useGetMyDraftsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(GetMyDraftsDocument, options); + } +export type GetMyDraftsQueryHookResult = ReturnType; +export type GetMyDraftsLazyQueryHookResult = ReturnType; +export type GetMyDraftsQueryResult = Apollo.QueryResult; export const PopularTagsDocument = gql` query PopularTags { popularTags { diff --git a/src/services/storage.service.ts b/src/services/storage.service.ts index 071d6f2..5a70c54 100644 --- a/src/services/storage.service.ts +++ b/src/services/storage.service.ts @@ -17,4 +17,8 @@ export class StorageService { return JSON.parse(str) as T; } + + clear() { + localStorage.removeItem(this.key) + } } From c824be44a6ce6eaf0a4be656f5c237f2700535a8 Mon Sep 17 00:00:00 2001 From: MTG2000 Date: Tue, 12 Jul 2022 18:17:58 +0300 Subject: [PATCH 12/15] update: disable save draft btn on published posts --- api/functions/graphql/nexus-typegen.ts | 11 +++++++++++ api/functions/graphql/schema.graphql | 4 ++++ api/functions/graphql/types/post.js | 1 + prisma/schema.prisma | 1 + .../Components/StoryForm/StoryForm.tsx | 17 ++++++++++------- .../pages/PostDetailsPage/postDetails.graphql | 1 + src/graphql/index.tsx | 7 ++++++- 7 files changed, 34 insertions(+), 8 deletions(-) diff --git a/api/functions/graphql/nexus-typegen.ts b/api/functions/graphql/nexus-typegen.ts index 03ac891..d27fb29 100644 --- a/api/functions/graphql/nexus-typegen.ts +++ b/api/functions/graphql/nexus-typegen.ts @@ -87,6 +87,7 @@ export interface NexusGenObjects { deadline: string; // String! excerpt: string; // String! id: number; // Int! + is_published?: boolean | null; // Boolean reward_amount: number; // Int! title: string; // String! votes_count: number; // Int! @@ -161,6 +162,7 @@ export interface NexusGenObjects { createdAt: NexusGenScalars['Date']; // Date! excerpt: string; // String! id: number; // Int! + is_published?: boolean | null; // Boolean title: string; // String! votes_count: number; // Int! } @@ -170,6 +172,7 @@ export interface NexusGenObjects { createdAt: NexusGenScalars['Date']; // Date! excerpt: string; // String! id: number; // Int! + is_published?: boolean | null; // Boolean title: string; // String! votes_count: number; // Int! } @@ -243,6 +246,7 @@ export interface NexusGenFieldTypes { deadline: string; // String! excerpt: string; // String! id: number; // Int! + is_published: boolean | null; // Boolean reward_amount: number; // Int! tags: NexusGenRootTypes['Tag'][]; // [Tag!]! title: string; // String! @@ -357,6 +361,7 @@ export interface NexusGenFieldTypes { createdAt: NexusGenScalars['Date']; // Date! excerpt: string; // String! id: number; // Int! + is_published: boolean | null; // Boolean tags: NexusGenRootTypes['Tag'][]; // [Tag!]! title: string; // String! type: string; // String! @@ -371,6 +376,7 @@ export interface NexusGenFieldTypes { createdAt: NexusGenScalars['Date']; // Date! excerpt: string; // String! id: number; // Int! + is_published: boolean | null; // Boolean tags: NexusGenRootTypes['Tag'][]; // [Tag!]! title: string; // String! type: string; // String! @@ -413,6 +419,7 @@ export interface NexusGenFieldTypes { createdAt: NexusGenScalars['Date']; // Date! excerpt: string; // String! id: number; // Int! + is_published: boolean | null; // Boolean title: string; // String! votes_count: number; // Int! } @@ -442,6 +449,7 @@ export interface NexusGenFieldTypeNames { deadline: 'String' excerpt: 'String' id: 'Int' + is_published: 'Boolean' reward_amount: 'Int' tags: 'Tag' title: 'String' @@ -556,6 +564,7 @@ export interface NexusGenFieldTypeNames { createdAt: 'Date' excerpt: 'String' id: 'Int' + is_published: 'Boolean' tags: 'Tag' title: 'String' type: 'String' @@ -570,6 +579,7 @@ export interface NexusGenFieldTypeNames { createdAt: 'Date' excerpt: 'String' id: 'Int' + is_published: 'Boolean' tags: 'Tag' title: 'String' type: 'String' @@ -612,6 +622,7 @@ export interface NexusGenFieldTypeNames { createdAt: 'Date' excerpt: 'String' id: 'Int' + is_published: 'Boolean' title: 'String' votes_count: 'Int' } diff --git a/api/functions/graphql/schema.graphql b/api/functions/graphql/schema.graphql index 57d0afc..7ddaa6d 100644 --- a/api/functions/graphql/schema.graphql +++ b/api/functions/graphql/schema.graphql @@ -27,6 +27,7 @@ type Bounty implements PostBase { deadline: String! excerpt: String! id: Int! + is_published: Boolean reward_amount: Int! tags: [Tag!]! title: String! @@ -113,6 +114,7 @@ interface PostBase { createdAt: Date! excerpt: String! id: Int! + is_published: Boolean title: String! votes_count: Int! } @@ -172,6 +174,7 @@ type Question implements PostBase { createdAt: Date! excerpt: String! id: Int! + is_published: Boolean tags: [Tag!]! title: String! type: String! @@ -187,6 +190,7 @@ type Story implements PostBase { createdAt: Date! excerpt: String! id: Int! + is_published: Boolean tags: [Tag!]! title: String! type: String! diff --git a/api/functions/graphql/types/post.js b/api/functions/graphql/types/post.js index 710b1de..68dccca 100644 --- a/api/functions/graphql/types/post.js +++ b/api/functions/graphql/types/post.js @@ -57,6 +57,7 @@ const PostBase = interfaceType({ t.nonNull.string('body'); t.nonNull.string('excerpt'); t.nonNull.int('votes_count'); + t.boolean('is_published'); }, }) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 85a5194..9031256 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -141,6 +141,7 @@ model Question { body String excerpt String votes_count Int @default(0) + is_published Boolean @default(true) tags Tag[] diff --git a/src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm.tsx b/src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm.tsx index 9e753d9..7f60c3d 100644 --- a/src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm.tsx +++ b/src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm.tsx @@ -48,6 +48,7 @@ interface IFormInputs { tags: NestedValue<{ title: string }[]> cover_image: NestedValue | NestedValue body: string + is_published: boolean | null } @@ -83,6 +84,7 @@ export default function StoryForm() { cover_image: story?.cover_image ?? [], tags: story?.tags ?? [], body: story?.body ?? '', + is_published: story?.is_published ?? false, }, }); const { handleSubmit, control, register, formState: { errors, isValid, isSubmitted }, trigger, getValues, watch } = formMethods; @@ -213,13 +215,14 @@ export default function StoryForm() { "Publish" } - + {!story?.is_published && + }
diff --git a/src/features/Posts/pages/PostDetailsPage/postDetails.graphql b/src/features/Posts/pages/PostDetailsPage/postDetails.graphql index bf1157e..1b4ccfc 100644 --- a/src/features/Posts/pages/PostDetailsPage/postDetails.graphql +++ b/src/features/Posts/pages/PostDetailsPage/postDetails.graphql @@ -18,6 +18,7 @@ query PostDetails($id: Int!, $type: POST_TYPE!) { votes_count type cover_image + is_published comments_count comments { id diff --git a/src/graphql/index.tsx b/src/graphql/index.tsx index 520ecf0..42eae8c 100644 --- a/src/graphql/index.tsx +++ b/src/graphql/index.tsx @@ -45,6 +45,7 @@ export type Bounty = PostBase & { deadline: Scalars['String']; excerpt: Scalars['String']; id: Scalars['Int']; + is_published: Maybe; reward_amount: Scalars['Int']; tags: Array; title: Scalars['String']; @@ -174,6 +175,7 @@ export type PostBase = { createdAt: Scalars['Date']; excerpt: Scalars['String']; id: Scalars['Int']; + is_published: Maybe; title: Scalars['String']; votes_count: Scalars['Int']; }; @@ -314,6 +316,7 @@ export type Question = PostBase & { createdAt: Scalars['Date']; excerpt: Scalars['String']; id: Scalars['Int']; + is_published: Maybe; tags: Array; title: Scalars['String']; type: Scalars['String']; @@ -330,6 +333,7 @@ export type Story = PostBase & { createdAt: Scalars['Date']; excerpt: Scalars['String']; id: Scalars['Int']; + is_published: Maybe; tags: Array; title: Scalars['String']; type: Scalars['String']; @@ -503,7 +507,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, answers_count: number, author: { __typename?: 'Author', id: number, name: string, avatar: string, join_date: any }, tags: Array<{ __typename?: 'Tag', id: number, title: string }>, comments: Array<{ __typename?: 'PostComment', id: number, createdAt: any, body: string, votes_count: number, parentId: number | null, author: { __typename?: 'Author', id: number, name: string, avatar: string } }> } | { __typename?: 'Story', id: number, title: string, createdAt: any, body: 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 }>, comments: Array<{ __typename?: 'PostComment', id: number, createdAt: any, body: string, votes_count: number, parentId: number | null, author: { __typename?: 'Author', id: number, name: string, avatar: 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, answers_count: number, author: { __typename?: 'Author', id: number, name: string, avatar: string, join_date: any }, tags: Array<{ __typename?: 'Tag', id: number, title: string }>, comments: Array<{ __typename?: 'PostComment', id: number, createdAt: any, body: string, votes_count: number, parentId: number | null, author: { __typename?: 'Author', id: number, name: string, avatar: string } }> } | { __typename?: 'Story', id: number, title: string, createdAt: any, body: string, votes_count: number, type: string, cover_image: string | null, is_published: boolean | null, comments_count: number, author: { __typename?: 'Author', id: number, name: string, avatar: string, join_date: any }, tags: Array<{ __typename?: 'Tag', id: number, title: string }>, comments: Array<{ __typename?: 'PostComment', id: number, createdAt: any, body: string, votes_count: number, parentId: number | null, author: { __typename?: 'Author', id: number, name: string, avatar: string } }> } }; export type ProfileQueryVariables = Exact<{ profileId: Scalars['Int']; @@ -1221,6 +1225,7 @@ export const PostDetailsDocument = gql` votes_count type cover_image + is_published comments_count comments { id From a0e13327a618e3ed01450a6a44cb3837341f97d8 Mon Sep 17 00:00:00 2001 From: MTG2000 Date: Wed, 13 Jul 2022 16:14:16 +0300 Subject: [PATCH 13/15] feat: split the create-story component into multiple ones, build drafts api & components, build reset editor on drafts change, delete draft, dateDiff helper --- api/functions/graphql/nexus-typegen.ts | 11 + api/functions/graphql/schema.graphql | 4 + api/functions/graphql/types/post.js | 1 + .../Modals/ConfirmModal/ConfirmModal.tsx | 5 +- .../ContentEditor/ContentEditor.tsx | 6 +- .../DraftsContainer/DraftsContainer.tsx | 119 ++++++++ .../DraftsContainer}/getMyDrafts.graphql | 5 +- .../ErrorsContainer/ErrorsContainer.tsx | 29 ++ .../Components/StoryForm/StoryForm.tsx | 289 +++++++----------- .../Components/StoryForm/createStory.graphql | 1 + .../pages/CreatePostPage/CreatePostPage.tsx | 9 +- .../CreateStoryPage/CreateStoryPage.tsx | 109 +++++++ .../CreateStoryPage/styles.module.scss | 36 +++ .../StoryPageContent/useUpdateStory.tsx | 4 +- src/graphql/index.tsx | 116 +++---- src/redux/features/staging.slice.ts | 2 +- src/styles/mixins/_media_queries.scss | 6 + src/utils/helperFunctions.tsx | 13 + 18 files changed, 513 insertions(+), 252 deletions(-) create mode 100644 src/features/Posts/pages/CreatePostPage/Components/DraftsContainer/DraftsContainer.tsx rename src/features/Posts/pages/CreatePostPage/{ => Components/DraftsContainer}/getMyDrafts.graphql (81%) create mode 100644 src/features/Posts/pages/CreatePostPage/Components/ErrorsContainer/ErrorsContainer.tsx create mode 100644 src/features/Posts/pages/CreatePostPage/CreateStoryPage/CreateStoryPage.tsx create mode 100644 src/features/Posts/pages/CreatePostPage/CreateStoryPage/styles.module.scss diff --git a/api/functions/graphql/nexus-typegen.ts b/api/functions/graphql/nexus-typegen.ts index d27fb29..fe3a13a 100644 --- a/api/functions/graphql/nexus-typegen.ts +++ b/api/functions/graphql/nexus-typegen.ts @@ -90,6 +90,7 @@ export interface NexusGenObjects { is_published?: boolean | null; // Boolean reward_amount: number; // Int! title: string; // String! + updatedAt: NexusGenScalars['Date']; // Date! votes_count: number; // Int! } BountyApplication: { // root type @@ -164,6 +165,7 @@ export interface NexusGenObjects { id: number; // Int! is_published?: boolean | null; // Boolean title: string; // String! + updatedAt: NexusGenScalars['Date']; // Date! votes_count: number; // Int! } Story: { // root type @@ -174,6 +176,7 @@ export interface NexusGenObjects { id: number; // Int! is_published?: boolean | null; // Boolean title: string; // String! + updatedAt: NexusGenScalars['Date']; // Date! votes_count: number; // Int! } Tag: { // root type @@ -251,6 +254,7 @@ export interface NexusGenFieldTypes { tags: NexusGenRootTypes['Tag'][]; // [Tag!]! title: string; // String! type: string; // String! + updatedAt: NexusGenScalars['Date']; // Date! votes_count: number; // Int! } BountyApplication: { // field return type @@ -365,6 +369,7 @@ export interface NexusGenFieldTypes { tags: NexusGenRootTypes['Tag'][]; // [Tag!]! title: string; // String! type: string; // String! + updatedAt: NexusGenScalars['Date']; // Date! votes_count: number; // Int! } Story: { // field return type @@ -380,6 +385,7 @@ export interface NexusGenFieldTypes { tags: NexusGenRootTypes['Tag'][]; // [Tag!]! title: string; // String! type: string; // String! + updatedAt: NexusGenScalars['Date']; // Date! votes_count: number; // Int! } Tag: { // field return type @@ -421,6 +427,7 @@ export interface NexusGenFieldTypes { id: number; // Int! is_published: boolean | null; // Boolean title: string; // String! + updatedAt: NexusGenScalars['Date']; // Date! votes_count: number; // Int! } } @@ -454,6 +461,7 @@ export interface NexusGenFieldTypeNames { tags: 'Tag' title: 'String' type: 'String' + updatedAt: 'Date' votes_count: 'Int' } BountyApplication: { // field return type name @@ -568,6 +576,7 @@ export interface NexusGenFieldTypeNames { tags: 'Tag' title: 'String' type: 'String' + updatedAt: 'Date' votes_count: 'Int' } Story: { // field return type name @@ -583,6 +592,7 @@ export interface NexusGenFieldTypeNames { tags: 'Tag' title: 'String' type: 'String' + updatedAt: 'Date' votes_count: 'Int' } Tag: { // field return type name @@ -624,6 +634,7 @@ export interface NexusGenFieldTypeNames { id: 'Int' is_published: 'Boolean' title: 'String' + updatedAt: 'Date' votes_count: 'Int' } } diff --git a/api/functions/graphql/schema.graphql b/api/functions/graphql/schema.graphql index 7ddaa6d..b334d3f 100644 --- a/api/functions/graphql/schema.graphql +++ b/api/functions/graphql/schema.graphql @@ -32,6 +32,7 @@ type Bounty implements PostBase { tags: [Tag!]! title: String! type: String! + updatedAt: Date! votes_count: Int! } @@ -116,6 +117,7 @@ interface PostBase { id: Int! is_published: Boolean title: String! + updatedAt: Date! votes_count: Int! } @@ -178,6 +180,7 @@ type Question implements PostBase { tags: [Tag!]! title: String! type: String! + updatedAt: Date! votes_count: Int! } @@ -194,6 +197,7 @@ type Story implements PostBase { tags: [Tag!]! title: String! type: String! + updatedAt: Date! votes_count: Int! } diff --git a/api/functions/graphql/types/post.js b/api/functions/graphql/types/post.js index 68dccca..3beddca 100644 --- a/api/functions/graphql/types/post.js +++ b/api/functions/graphql/types/post.js @@ -54,6 +54,7 @@ const PostBase = interfaceType({ t.nonNull.int('id'); t.nonNull.string('title'); t.nonNull.date('createdAt'); + t.nonNull.date('updatedAt'); t.nonNull.string('body'); t.nonNull.string('excerpt'); t.nonNull.int('votes_count'); diff --git a/src/Components/Modals/ConfirmModal/ConfirmModal.tsx b/src/Components/Modals/ConfirmModal/ConfirmModal.tsx index 2b7e6ba..6592c24 100644 --- a/src/Components/Modals/ConfirmModal/ConfirmModal.tsx +++ b/src/Components/Modals/ConfirmModal/ConfirmModal.tsx @@ -11,7 +11,7 @@ interface Props extends ModalCard { message?: string, actionName?: string, color?: 'red' | 'yellow' | 'blue' - callbackAction: PayloadAction<{ confirmed?: boolean }> + callbackAction: PayloadAction<{ confirmed?: boolean, id?: string | number }> } @@ -39,8 +39,9 @@ export default function ConfirmModal({ const dispatch = useAppDispatch(); const handleConfirm = () => { + const action = Object.assign({}, callbackAction); - action.payload = { confirmed: true } + action.payload = { confirmed: true, id: callbackAction.payload.id } dispatch(action) onClose?.(); } diff --git a/src/features/Posts/pages/CreatePostPage/Components/ContentEditor/ContentEditor.tsx b/src/features/Posts/pages/CreatePostPage/Components/ContentEditor/ContentEditor.tsx index c4dde6f..bbf16e4 100644 --- a/src/features/Posts/pages/CreatePostPage/Components/ContentEditor/ContentEditor.tsx +++ b/src/features/Posts/pages/CreatePostPage/Components/ContentEditor/ContentEditor.tsx @@ -25,7 +25,7 @@ import { } from 'remirror/extensions'; import { ExtensionPriority, InvalidContentHandler } from 'remirror'; import { EditorComponent, Remirror, useRemirror } from '@remirror/react'; -import { useCallback } from 'react'; +import { useCallback, useEffect } from 'react'; import TextEditorComponents from 'src/Components/Inputs/TextEditor'; import Toolbar from './Toolbar'; @@ -35,7 +35,7 @@ turndownService.keep(['iframe']); interface Props { placeholder?: string; - initialContent?: string; + initialContent?: () => string; name?: string; } @@ -100,7 +100,7 @@ export default function ContentEditor({ placeholder, initialContent, name }: Pro
diff --git a/src/features/Posts/pages/CreatePostPage/Components/DraftsContainer/DraftsContainer.tsx b/src/features/Posts/pages/CreatePostPage/Components/DraftsContainer/DraftsContainer.tsx new file mode 100644 index 0000000..5cedbb1 --- /dev/null +++ b/src/features/Posts/pages/CreatePostPage/Components/DraftsContainer/DraftsContainer.tsx @@ -0,0 +1,119 @@ +import { createAction } from '@reduxjs/toolkit'; +import React, { useCallback, useState } from 'react' +import { useFormContext } from 'react-hook-form'; +import Button from 'src/Components/Button/Button'; +import LoadingPage from 'src/Components/LoadingPage/LoadingPage'; +import { isStory } from 'src/features/Posts/types'; +import { Post_Type, useDeleteStoryMutation, useGetMyDraftsQuery, usePostDetailsLazyQuery } from 'src/graphql' +import { openModal } from 'src/redux/features/modals.slice'; +import { NotificationsService } from 'src/services'; +import { getDateDifference } from 'src/utils/helperFunctions'; +import { useAppDispatch } from 'src/utils/hooks'; +import { useReduxEffect } from 'src/utils/hooks/useReduxEffect'; +import { IStoryFormInputs } from '../../CreateStoryPage/CreateStoryPage'; + +interface Props { + type: Post_Type, + onDraftLoad?: () => void, +} + +const CONFIRM_DELETE_STORY = createAction<{ confirmed?: boolean, id: number }>('DELETE_STORY_CONFIRMED')({ id: -1 }) + +export default function DraftsContainer({ type, onDraftLoad }: Props) { + + + const myDraftsQuery = useGetMyDraftsQuery({ variables: { type } }); + const [fetchDraft] = usePostDetailsLazyQuery(); + const [deleteStory] = useDeleteStoryMutation({ + refetchQueries: ['GetMyDrafts'] + }) + const { setValue } = useFormContext() + const dispatch = useAppDispatch(); + + const [loading, setLoading] = useState(false) + + const loadDraft = (id: number) => { + if (!loading) + setLoading(true); + fetchDraft({ variables: { type, id } }) + .then(({ data }) => { + // data.data?.getPostById. + if (data?.getPostById) { + if (isStory(data.getPostById)) { + setValue('id', data.getPostById.id); + setValue('title', data.getPostById.title); + setValue('tags', data.getPostById.tags); + setValue('body', data.getPostById.body); + setValue('cover_image', data.getPostById.cover_image ? [data.getPostById.cover_image] : []); + setValue('is_published', data.getPostById.is_published); + } + + } + + onDraftLoad?.() + }) + .catch(() => { + NotificationsService.error("Unexpected error happened...") + }) + .finally(() => { + setLoading(false); + }) + } + + + const onConfirmDelete = useCallback(({ payload: { confirmed, id } }: typeof CONFIRM_DELETE_STORY) => { + if (confirmed) + deleteStory({ + variables: { + deleteStoryId: id + } + }) + }, [deleteStory]) + + useReduxEffect(onConfirmDelete, CONFIRM_DELETE_STORY.type); + + const deleteDraft = (id: number) => { + dispatch(openModal({ + Modal: "ConfirmModal", + props: { + callbackAction: { + type: CONFIRM_DELETE_STORY.type, + payload: { + id + } + }, + actionName: "Delete", + title: "Delete Draft", + message: "Are you sure you want to delete this draft ??", + color: "red" + } + })) + } + + return ( + + <> + {(!myDraftsQuery.loading && myDraftsQuery.data?.getMyDrafts && myDraftsQuery.data.getMyDrafts.length > 0) && +
+

Saved Drafts

+
    + {myDraftsQuery.data.getMyDrafts.map(draft => +
  • +

    loadDraft(draft.id)} + > + {draft.title} +

    +
    +

    Last edited {getDateDifference(draft.updatedAt)} ago

    + +
    +
  • )} +
+
} + {loading && } + + ) +} diff --git a/src/features/Posts/pages/CreatePostPage/getMyDrafts.graphql b/src/features/Posts/pages/CreatePostPage/Components/DraftsContainer/getMyDrafts.graphql similarity index 81% rename from src/features/Posts/pages/CreatePostPage/getMyDrafts.graphql rename to src/features/Posts/pages/CreatePostPage/Components/DraftsContainer/getMyDrafts.graphql index c76ee1e..6bb1eef 100644 --- a/src/features/Posts/pages/CreatePostPage/getMyDrafts.graphql +++ b/src/features/Posts/pages/CreatePostPage/Components/DraftsContainer/getMyDrafts.graphql @@ -3,14 +3,17 @@ query GetMyDrafts($type: POST_TYPE!) { ... on Story { id title + updatedAt } ... on Bounty { id title + updatedAt } ... on Question { - title id + title + updatedAt } } } diff --git a/src/features/Posts/pages/CreatePostPage/Components/ErrorsContainer/ErrorsContainer.tsx b/src/features/Posts/pages/CreatePostPage/Components/ErrorsContainer/ErrorsContainer.tsx new file mode 100644 index 0000000..66bf4f9 --- /dev/null +++ b/src/features/Posts/pages/CreatePostPage/Components/ErrorsContainer/ErrorsContainer.tsx @@ -0,0 +1,29 @@ +import React, { forwardRef } from 'react' +import { useFormContext } from 'react-hook-form' +import { IStoryFormInputs } from '../../CreateStoryPage/CreateStoryPage'; + +const ErrorsContainer = forwardRef((props, ref) => { + + const { formState: { isValid, isSubmitted, errors } } = useFormContext(); + + return ( +
+ {(!isValid && isSubmitted) &&
    + {errors.title &&
  • + {errors.title.message} +
  • } + {errors.cover_image &&
  • + {errors.cover_image.message} +
  • } + {errors.tags &&
  • + {errors.tags.message} +
  • } + {errors.body &&
  • + {errors.body.message} +
  • } +
} +
+ ) +}) + +export default ErrorsContainer; \ No newline at end of file diff --git a/src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm.tsx b/src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm.tsx index 7f60c3d..c7a219c 100644 --- a/src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm.tsx +++ b/src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm.tsx @@ -1,132 +1,77 @@ -import { useEffect, useRef, useState } from 'react' -import { yupResolver } from "@hookform/resolvers/yup"; -import { Controller, FormProvider, NestedValue, Resolver, useForm } from "react-hook-form"; +import { useEffect, useState } from 'react' +import { Controller, useFormContext } from "react-hook-form"; import Button from "src/Components/Button/Button"; import FilesInput from "src/Components/Inputs/FilesInput/FilesInput"; import TagsInput from "src/Components/Inputs/TagsInput/TagsInput"; -import * as yup from "yup"; import ContentEditor from "../ContentEditor/ContentEditor"; -import { Post_Type, useCreateStoryMutation, useGetMyDraftsQuery } from 'src/graphql' +import { useCreateStoryMutation } from 'src/graphql' import { useNavigate } from 'react-router-dom' -import { useAppDispatch, useAppSelector } from 'src/utils/hooks'; +import { useAppDispatch, } from 'src/utils/hooks'; import { stageStory } from 'src/redux/features/staging.slice' -import { Override } from 'src/utils/interfaces'; import { NotificationsService } from "src/services/notifications.service"; import { createRoute } from 'src/utils/routing'; import PreviewPostCard from '../PreviewPostCard/PreviewPostCard' import { StorageService } from 'src/services'; import { useThrottledCallback } from '@react-hookz/web'; +import { CreateStoryType, IStoryFormInputs } from '../../CreateStoryPage/CreateStoryPage'; -const FileSchema = yup.lazy((value: string | File[]) => { - - switch (typeof value) { - case 'object': - return yup.mixed() - .test("fileSize", "File Size is too large", file => file.size <= 5242880) - .test("fileType", "Unsupported File Format, only png/jpg/jpeg images are allowed", - (file: File) => - ["image/jpeg", "image/png", "image/jpg"].includes(file.type)) - case 'string': - return yup.string().url(); - default: - return yup.mixed() - } -}) - -const schema = yup.object({ - title: yup.string().trim().required().min(10, 'the title is too short'), - tags: yup.array().required().min(1, 'please pick at least one relevant tag'), - body: yup.string().required().min(50, 'stories should have a minimum of 10 words'), - cover_image: yup.array().of(FileSchema as any) - -}).required(); - - -interface IFormInputs { - id: number | null - title: string - tags: NestedValue<{ title: string }[]> - cover_image: NestedValue | NestedValue - body: string - is_published: boolean | null +interface Props { + isUpdating?: boolean; + isPublished?: boolean; + onSuccess?: (isDraft: boolean) => void, + onValidationError?: () => void } - - -export type CreateStoryType = Override - const storageService = new StorageService('story-edit'); -export default function StoryForm() { +export default function StoryForm(props: Props) { const dispatch = useAppDispatch(); - const { story } = useAppSelector(state => ({ - story: state.staging.story || storageService.get() - })) + const navigate = useNavigate(); + const { handleSubmit, control, register, trigger, getValues, watch, reset } = useFormContext(); const [editMode, setEditMode] = useState(true) - const navigate = useNavigate(); - const errorsContainerRef = useRef(null!); - - const myDraftsQuery = useGetMyDraftsQuery({ variables: { type: Post_Type.Story } }) - - const formMethods = useForm({ - resolver: yupResolver(schema) as Resolver, - shouldFocusError: false, - defaultValues: { - id: story?.id ?? null, - title: story?.title ?? '', - cover_image: story?.cover_image ?? [], - tags: story?.tags ?? [], - body: story?.body ?? '', - is_published: story?.is_published ?? false, - }, - }); - const { handleSubmit, control, register, formState: { errors, isValid, isSubmitted }, trigger, getValues, watch } = formMethods; + const [loading, setLoading] = useState(false); const presistPost = useThrottledCallback((value) => storageService.set(value), [], 1000) useEffect(() => { - const subscription = watch((value) => presistPost(value)); + const subscription = watch(({ id, is_published, ...values }) => presistPost(values)); return () => subscription.unsubscribe(); }, [presistPost, watch]); - - - const [loading, setLoading] = useState(false); - const [createStory] = useCreateStoryMutation({ - onCompleted: (data) => { - navigate(createRoute({ type: 'story', id: data.createStory?.id!, title: data.createStory?.title })) - setLoading(false) - }, - - onError: (error) => { - NotificationsService.error('Unexpected error happened, please try again', { error }) - setLoading(false) - } - }); - - const clickPreview = async () => { const isValid = await trigger(); if (isValid) { const data = getValues() dispatch(stageStory(data)) - storageService.set(data) setEditMode(false); } else { clickSubmit(false)(); // I'm doing this so that the react-hook-form attaches onChange listener to inputs validation } } - const clickSubmit = (publish_now: boolean) => handleSubmit(data => { + + const [createStory] = useCreateStoryMutation({ + onCompleted: (data) => { + if (data.createStory?.is_published) + navigate(createRoute({ type: 'story', id: data.createStory?.id!, title: data.createStory?.title })) + else + reset() + setLoading(false) + }, + onError: (error) => { + NotificationsService.error('Unexpected error happened, please try again', { error }) + setLoading(false) + }, + refetchQueries: ['GetMyDrafts'] + }); + + const clickSubmit = (publish_now: boolean) => handleSubmit(data => { setLoading(true); createStory({ variables: { @@ -141,116 +86,94 @@ export default function StoryForm() { } }) storageService.clear(); - }, () => errorsContainerRef.current.scrollIntoView({ behavior: 'smooth', block: "center" })) + }, props.onValidationError); - const isUpdating = story?.id; + + + const postId = watch('id') ?? -1; return ( - -
-
-
- - -
- {editMode && <> -
-
- ( - - )} + +
+ + +
+ {editMode && <> +
+
+ ( + + )} + /> -
- -
- - - -
- + -
- } - {!editMode && } -
- - {!story?.is_published && - } + +
- -
-
- {(!isValid && isSubmitted) &&
    - {errors.title &&
  • - {errors.title.message} -
  • } - {errors.cover_image &&
  • - {errors.cover_image.message} -
  • } - {errors.tags &&
  • - {errors.tags.message} -
  • } - {errors.body &&
  • - {errors.body.message} -
  • } -
} -
- {(!myDraftsQuery.loading && myDraftsQuery.data?.getMyDrafts && myDraftsQuery.data.getMyDrafts.length > 0) && -
-

Saved Drafts

-
    - {myDraftsQuery.data.getMyDrafts.map(draft =>
  • {draft.title}
  • )} -
-
} + { + console.log('GET'); + console.log(getValues().body); + + return getValues().body + }} + placeholder="Write your story content here..." + name="body" + /> +
+ + } + {!editMode && } +
+ + {!props.isPublished && + }
- + ) } diff --git a/src/features/Posts/pages/CreatePostPage/Components/StoryForm/createStory.graphql b/src/features/Posts/pages/CreatePostPage/Components/StoryForm/createStory.graphql index f854c83..70fa1f9 100644 --- a/src/features/Posts/pages/CreatePostPage/Components/StoryForm/createStory.graphql +++ b/src/features/Posts/pages/CreatePostPage/Components/StoryForm/createStory.graphql @@ -9,6 +9,7 @@ mutation createStory($data: StoryInputType) { title } votes_count + is_published type cover_image comments_count diff --git a/src/features/Posts/pages/CreatePostPage/CreatePostPage.tsx b/src/features/Posts/pages/CreatePostPage/CreatePostPage.tsx index 48c7e65..bd898ed 100644 --- a/src/features/Posts/pages/CreatePostPage/CreatePostPage.tsx +++ b/src/features/Posts/pages/CreatePostPage/CreatePostPage.tsx @@ -4,17 +4,14 @@ import { FiArrowLeft } from "react-icons/fi"; import { useNavigate, useParams } from "react-router-dom"; import BountyForm from "./Components/BountyForm/BountyForm"; import QuestionForm from "./Components/QuestionForm/QuestionForm"; -import StoryForm from "./Components/StoryForm/StoryForm"; +import CreateStoryPage from "./CreateStoryPage/CreateStoryPage"; -interface Props { - -} export default function CreatePostPage() { const { type } = useParams() - const [postType, setPostType] = useState<'story' | 'bounty' | 'question'>((type as any) ?? 'story'); + const [postType] = useState<'story' | 'bounty' | 'question'>((type as any) ?? 'story'); const navigate = useNavigate(); @@ -44,7 +41,7 @@ export default function CreatePostPage() { {/*

Write a Story

*/} - + } {postType === 'bounty' && <>

diff --git a/src/features/Posts/pages/CreatePostPage/CreateStoryPage/CreateStoryPage.tsx b/src/features/Posts/pages/CreatePostPage/CreateStoryPage/CreateStoryPage.tsx new file mode 100644 index 0000000..63b3753 --- /dev/null +++ b/src/features/Posts/pages/CreatePostPage/CreateStoryPage/CreateStoryPage.tsx @@ -0,0 +1,109 @@ + +import { yupResolver } from "@hookform/resolvers/yup"; +import { useRef, useState } from "react"; +import { FormProvider, NestedValue, Resolver, useForm } from "react-hook-form"; +import { Post_Type } from "src/graphql"; +import { StorageService } from "src/services"; +import { useAppSelector } from "src/utils/hooks"; +import { Override } from "src/utils/interfaces"; +import * as yup from "yup"; +import DraftsContainer from "../Components/DraftsContainer/DraftsContainer"; +import ErrorsContainer from "../Components/ErrorsContainer/ErrorsContainer"; +import StoryForm from "../Components/StoryForm/StoryForm"; +import styles from './styles.module.scss' + +const FileSchema = yup.lazy((value: string | File[]) => { + switch (typeof value) { + case 'object': + return yup.mixed() + .test("fileSize", "File Size is too large", file => file.size <= 5242880) + .test("fileType", "Unsupported File Format, only png/jpg/jpeg images are allowed", + (file: File) => + ["image/jpeg", "image/png", "image/jpg"].includes(file.type)) + case 'string': + return yup.string().url(); + default: + return yup.mixed() + } +}) + +const schema = yup.object({ + title: yup.string().trim().required().min(10, 'the title is too short'), + tags: yup.array().required().min(1, 'please pick at least one relevant tag'), + body: yup.string().required().min(50, 'stories should have a minimum of 10 words'), + cover_image: yup.array().of(FileSchema as any) + +}).required(); + + +export interface IStoryFormInputs { + id: number | null + title: string + tags: NestedValue<{ title: string }[]> + cover_image: NestedValue | NestedValue + body: string + is_published: boolean | null +} + + + +export type CreateStoryType = Override + +const storageService = new StorageService('story-edit'); + + +export default function CreateStoryPage() { + + + const { story } = useAppSelector(state => ({ + story: state.staging.story || storageService.get() + })) + + const formMethods = useForm({ + resolver: yupResolver(schema) as Resolver, + shouldFocusError: false, + defaultValues: { + id: story?.id ?? null, + title: story?.title ?? '', + cover_image: story?.cover_image ?? [], + tags: story?.tags ?? [], + body: story?.body ?? '', + is_published: story?.is_published ?? false, + }, + }); + + + + const errorsContainerRef = useRef(null!); + const [formKey, setFormKey] = useState(1) + + const resetForm = () => setFormKey(v => v + 1) + + + return ( + +
+ +
+ resetForm()} + onValidationError={() => errorsContainerRef.current.scrollIntoView({ behavior: 'smooth', block: "center" })} + /> +
+ +
+ +
+
+ +
+
+
+ ) +} diff --git a/src/features/Posts/pages/CreatePostPage/CreateStoryPage/styles.module.scss b/src/features/Posts/pages/CreatePostPage/CreateStoryPage/styles.module.scss new file mode 100644 index 0000000..fc18f19 --- /dev/null +++ b/src/features/Posts/pages/CreatePostPage/CreateStoryPage/styles.module.scss @@ -0,0 +1,36 @@ +@import "/src/styles/mixins/index.scss"; + +.grid { + display: grid; + grid-template-columns: 1fr; + gap: 32px; + + & > * { + min-width: 0; + } + + grid-template-areas: + "errors" + "form" + "drafts"; + + :global { + #errors { + grid-area: errors; + } + #form { + grid-area: form; + } + #drafts { + grid-area: drafts; + } + } + + @include gt-xl { + grid-template-columns: 1fr calc(min(326px, 25%)); + grid-template-areas: + "form errors" + "form drafts" + "form ."; + } +} diff --git a/src/features/Posts/pages/PostDetailsPage/Components/StoryPageContent/useUpdateStory.tsx b/src/features/Posts/pages/PostDetailsPage/Components/StoryPageContent/useUpdateStory.tsx index 041a689..d920c1b 100644 --- a/src/features/Posts/pages/PostDetailsPage/Components/StoryPageContent/useUpdateStory.tsx +++ b/src/features/Posts/pages/PostDetailsPage/Components/StoryPageContent/useUpdateStory.tsx @@ -34,12 +34,12 @@ export const useUpdateStory = (story: Story) => { navigate("/blog/create-post?type=story") }; - const onInsertImage = useCallback(({ payload: { confirmed } }: typeof CONFIRM_DELETE_STORY) => { + const onConfirmDelete = useCallback(({ payload: { confirmed } }: typeof CONFIRM_DELETE_STORY) => { if (confirmed) deleteMutation() }, [deleteMutation]) - useReduxEffect(onInsertImage, CONFIRM_DELETE_STORY.type); + useReduxEffect(onConfirmDelete, CONFIRM_DELETE_STORY.type); const handleDelete = () => { dispatch(openModal({ diff --git a/src/graphql/index.tsx b/src/graphql/index.tsx index 42eae8c..574cfde 100644 --- a/src/graphql/index.tsx +++ b/src/graphql/index.tsx @@ -50,6 +50,7 @@ export type Bounty = PostBase & { tags: Array; title: Scalars['String']; type: Scalars['String']; + updatedAt: Scalars['Date']; votes_count: Scalars['Int']; }; @@ -177,6 +178,7 @@ export type PostBase = { id: Scalars['Int']; is_published: Maybe; title: Scalars['String']; + updatedAt: Scalars['Date']; votes_count: Scalars['Int']; }; @@ -320,6 +322,7 @@ export type Question = PostBase & { tags: Array; title: Scalars['String']; type: Scalars['String']; + updatedAt: Scalars['Date']; votes_count: Scalars['Int']; }; @@ -337,6 +340,7 @@ export type Story = PostBase & { tags: Array; title: Scalars['String']; type: Scalars['String']; + updatedAt: Scalars['Date']; votes_count: Scalars['Int']; }; @@ -465,12 +469,19 @@ export type TrendingPostsQueryVariables = Exact<{ [key: string]: never; }>; export type TrendingPostsQuery = { __typename?: 'Query', getTrendingPosts: Array<{ __typename?: 'Bounty', id: number, title: string, author: { __typename?: 'Author', id: number, avatar: string } } | { __typename?: 'Question', id: number, title: string, author: { __typename?: 'Author', id: number, avatar: string } } | { __typename?: 'Story', id: number, title: string, author: { __typename?: 'Author', id: number, avatar: string } }> }; +export type GetMyDraftsQueryVariables = Exact<{ + type: Post_Type; +}>; + + +export type GetMyDraftsQuery = { __typename?: 'Query', getMyDrafts: Array<{ __typename?: 'Bounty', id: number, title: string, updatedAt: any } | { __typename?: 'Question', id: number, title: string, updatedAt: any } | { __typename?: 'Story', id: number, title: string, updatedAt: any }> }; + export type CreateStoryMutationVariables = Exact<{ data: InputMaybe; }>; -export type CreateStoryMutation = { __typename?: 'Mutation', createStory: { __typename?: 'Story', id: number, title: string, createdAt: any, body: string, votes_count: number, type: string, cover_image: string | null, comments_count: number, tags: Array<{ __typename?: 'Tag', id: number, title: string }> } | null }; +export type CreateStoryMutation = { __typename?: 'Mutation', createStory: { __typename?: 'Story', id: number, title: string, createdAt: any, body: string, votes_count: number, is_published: boolean | null, type: string, cover_image: string | null, comments_count: number, tags: Array<{ __typename?: 'Tag', id: number, title: string }> } | null }; export type DeleteStoryMutationVariables = Exact<{ deleteStoryId: Scalars['Int']; @@ -479,13 +490,6 @@ export type DeleteStoryMutationVariables = Exact<{ export type DeleteStoryMutation = { __typename?: 'Mutation', deleteStory: { __typename?: 'Story', id: number } | null }; -export type GetMyDraftsQueryVariables = Exact<{ - type: Post_Type; -}>; - - -export type GetMyDraftsQuery = { __typename?: 'Query', getMyDrafts: Array<{ __typename?: 'Bounty', id: number, title: string } | { __typename?: 'Question', title: string, id: number } | { __typename?: 'Story', id: number, title: string }> }; - export type PopularTagsQueryVariables = Exact<{ [key: string]: never; }>; @@ -936,6 +940,55 @@ export function useTrendingPostsLazyQuery(baseOptions?: Apollo.LazyQueryHookOpti export type TrendingPostsQueryHookResult = ReturnType; export type TrendingPostsLazyQueryHookResult = ReturnType; export type TrendingPostsQueryResult = Apollo.QueryResult; +export const GetMyDraftsDocument = gql` + query GetMyDrafts($type: POST_TYPE!) { + getMyDrafts(type: $type) { + ... on Story { + id + title + updatedAt + } + ... on Bounty { + id + title + updatedAt + } + ... on Question { + id + title + updatedAt + } + } +} + `; + +/** + * __useGetMyDraftsQuery__ + * + * To run a query within a React component, call `useGetMyDraftsQuery` and pass it any options that fit your needs. + * When your component renders, `useGetMyDraftsQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGetMyDraftsQuery({ + * variables: { + * type: // value for 'type' + * }, + * }); + */ +export function useGetMyDraftsQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(GetMyDraftsDocument, options); + } +export function useGetMyDraftsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(GetMyDraftsDocument, options); + } +export type GetMyDraftsQueryHookResult = ReturnType; +export type GetMyDraftsLazyQueryHookResult = ReturnType; +export type GetMyDraftsQueryResult = Apollo.QueryResult; export const CreateStoryDocument = gql` mutation createStory($data: StoryInputType) { createStory(data: $data) { @@ -948,6 +1001,7 @@ export const CreateStoryDocument = gql` title } votes_count + is_published type cover_image comments_count @@ -1013,52 +1067,6 @@ export function useDeleteStoryMutation(baseOptions?: Apollo.MutationHookOptions< export type DeleteStoryMutationHookResult = ReturnType; export type DeleteStoryMutationResult = Apollo.MutationResult; export type DeleteStoryMutationOptions = Apollo.BaseMutationOptions; -export const GetMyDraftsDocument = gql` - query GetMyDrafts($type: POST_TYPE!) { - getMyDrafts(type: $type) { - ... on Story { - id - title - } - ... on Bounty { - id - title - } - ... on Question { - title - id - } - } -} - `; - -/** - * __useGetMyDraftsQuery__ - * - * To run a query within a React component, call `useGetMyDraftsQuery` and pass it any options that fit your needs. - * When your component renders, `useGetMyDraftsQuery` returns an object from Apollo Client that contains loading, error, and data properties - * you can use to render your UI. - * - * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; - * - * @example - * const { data, loading, error } = useGetMyDraftsQuery({ - * variables: { - * type: // value for 'type' - * }, - * }); - */ -export function useGetMyDraftsQuery(baseOptions: Apollo.QueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useQuery(GetMyDraftsDocument, options); - } -export function useGetMyDraftsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useLazyQuery(GetMyDraftsDocument, options); - } -export type GetMyDraftsQueryHookResult = ReturnType; -export type GetMyDraftsLazyQueryHookResult = ReturnType; -export type GetMyDraftsQueryResult = Apollo.QueryResult; export const PopularTagsDocument = gql` query PopularTags { popularTags { diff --git a/src/redux/features/staging.slice.ts b/src/redux/features/staging.slice.ts index 15e8a1e..c2008e0 100644 --- a/src/redux/features/staging.slice.ts +++ b/src/redux/features/staging.slice.ts @@ -1,5 +1,5 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; -import { CreateStoryType } from "src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm"; +import { CreateStoryType } from "src/features/Posts/pages/CreatePostPage/CreateStoryPage/CreateStoryPage"; interface StoreState { story: CreateStoryType | null diff --git a/src/styles/mixins/_media_queries.scss b/src/styles/mixins/_media_queries.scss index e449ea6..10363b9 100644 --- a/src/styles/mixins/_media_queries.scss +++ b/src/styles/mixins/_media_queries.scss @@ -91,3 +91,9 @@ $screen-xl-max: 50000px; @content; } } + +@mixin gt-xl { + @media screen and (min-width: #{$screen-xl-min}) { + @content; + } +} diff --git a/src/utils/helperFunctions.tsx b/src/utils/helperFunctions.tsx index e730432..aaa8d18 100644 --- a/src/utils/helperFunctions.tsx +++ b/src/utils/helperFunctions.tsx @@ -1,3 +1,4 @@ +import dayjs from "dayjs"; import React, { ComponentProps, ComponentType, Suspense } from "react"; import { RotatingLines } from "react-loader-spinner"; import { isNullOrUndefined } from "remirror"; @@ -108,4 +109,16 @@ export function getPropertyFromUnknown(obj: unknown, prop: strin if (typeof obj === 'object' && obj !== null && prop in obj) return (obj as any)[prop as any] as Value; return null +} + +export function getDateDifference(date: string) { + const now = dayjs(); + const mins = now.diff(date, 'minute'); + if (mins < 60) return mins + 'm'; + const hrs = now.diff(date, 'hour'); + if (hrs < 24) return hrs + 'h'; + const days = now.diff(date, 'day'); + if (days < 30) return days + 'd'; + const months = now.diff(date, 'month'); + return months + 'mo' } \ No newline at end of file From 7fa8d997b38bfc240f8c9b05eadef44f0c53c8eb Mon Sep 17 00:00:00 2001 From: MTG2000 Date: Wed, 13 Jul 2022 16:46:58 +0300 Subject: [PATCH 14/15] fix: reset form body on draft save --- .../CreatePostPage/Components/StoryForm/StoryForm.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm.tsx b/src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm.tsx index c7a219c..6293088 100644 --- a/src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm.tsx +++ b/src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm.tsx @@ -62,6 +62,7 @@ export default function StoryForm(props: Props) { navigate(createRoute({ type: 'story', id: data.createStory?.id!, title: data.createStory?.title })) else reset() + props.onSuccess?.(!!data.createStory?.is_published); setLoading(false) }, onError: (error) => { @@ -140,12 +141,7 @@ export default function StoryForm(props: Props) {

{ - console.log('GET'); - console.log(getValues().body); - - return getValues().body - }} + initialContent={() => getValues().body} placeholder="Write your story content here..." name="body" /> From adb11c3af7be117a12c91333f6b2abc3eb27ba54 Mon Sep 17 00:00:00 2001 From: MTG2000 Date: Wed, 13 Jul 2022 17:13:38 +0300 Subject: [PATCH 15/15] update: update story error msgs, update tags-input styling --- src/Components/Inputs/TagsInput/TagsInput.tsx | 20 +++++++++++++------ .../CreateStoryPage/CreateStoryPage.tsx | 6 +++--- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/Components/Inputs/TagsInput/TagsInput.tsx b/src/Components/Inputs/TagsInput/TagsInput.tsx index 7b67e38..2592cec 100644 --- a/src/Components/Inputs/TagsInput/TagsInput.tsx +++ b/src/Components/Inputs/TagsInput/TagsInput.tsx @@ -1,6 +1,5 @@ import { useController } from "react-hook-form"; -import Badge from "src/Components/Badge/Badge"; // import CreatableSelect from 'react-select/creatable'; import Select from 'react-select' import { OnChangeValue, StylesConfig, components, OptionProps } from "react-select"; @@ -73,12 +72,23 @@ const colourStyles: StylesConfig = { control: (styles, state) => ({ ...styles, padding: '1px 0', - border: 'none' + border: 'none', + boxShadow: 'none', + + ":hover": { + cursor: "pointer" + } + }), - indicatorSeparator: (styles, state) => ({ + multiValueRemove: (styles) => ({ ...styles, - display: "none" + ":hover": { + background: 'none' + } }), + indicatorsContainer: () => ({ display: 'none' }), + clearIndicator: () => ({ display: 'none' }), + indicatorSeparator: () => ({ display: "none" }), input: (styles, state) => ({ ...styles, " input": { @@ -135,13 +145,11 @@ export default function TagsInput({ isMulti isOptionDisabled={() => maxReached} placeholder={currentPlaceholder} - isClearable noOptionsMessage={() => { return maxReached ? "You've reached the max number of tags." : "No tags available"; }} - closeMenuOnSelect={false} value={value.map(transformer.tagToOption)} onChange={handleChange as any} diff --git a/src/features/Posts/pages/CreatePostPage/CreateStoryPage/CreateStoryPage.tsx b/src/features/Posts/pages/CreatePostPage/CreateStoryPage/CreateStoryPage.tsx index 63b3753..65361eb 100644 --- a/src/features/Posts/pages/CreatePostPage/CreateStoryPage/CreateStoryPage.tsx +++ b/src/features/Posts/pages/CreatePostPage/CreateStoryPage/CreateStoryPage.tsx @@ -28,9 +28,9 @@ const FileSchema = yup.lazy((value: string | File[]) => { }) const schema = yup.object({ - title: yup.string().trim().required().min(10, 'the title is too short'), - tags: yup.array().required().min(1, 'please pick at least one relevant tag'), - body: yup.string().required().min(50, 'stories should have a minimum of 10 words'), + title: yup.string().trim().required().min(10, 'Story title must be 2+ words'), + tags: yup.array().required().min(1, 'Add at least one tag'), + body: yup.string().required().min(50, 'Post must contain at least 10+ words'), cover_image: yup.array().of(FileSchema as any) }).required();