diff --git a/.gitignore b/.gitignore index 624094d..1f28034 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,4 @@ yarn-debug.log* yarn-error.log* TODO -NOTES \ No newline at end of file +NOTES diff --git a/api/functions/graphql/nexus-typegen.ts b/api/functions/graphql/nexus-typegen.ts index 1f0a9e1..fe3a13a 100644 --- a/api/functions/graphql/nexus-typegen.ts +++ b/api/functions/graphql/nexus-typegen.ts @@ -30,8 +30,9 @@ declare global { export interface NexusGenInputs { StoryInputType: { // input type body: string; // String! - cover_image: string; // String! + cover_image?: string | null; // String id?: number | null; // Int + is_published?: boolean | null; // Boolean tags: string[]; // [String!]! title: string; // String! } @@ -81,13 +82,15 @@ 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! id: number; // Int! + is_published?: boolean | null; // Boolean reward_amount: number; // Int! title: string; // String! + updatedAt: NexusGenScalars['Date']; // Date! votes_count: number; // Int! } BountyApplication: { // root type @@ -160,19 +163,24 @@ export interface NexusGenObjects { createdAt: NexusGenScalars['Date']; // Date! excerpt: string; // String! id: number; // Int! + is_published?: boolean | null; // Boolean title: string; // String! + updatedAt: NexusGenScalars['Date']; // Date! votes_count: number; // Int! } 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! + is_published?: boolean | null; // Boolean title: string; // String! + updatedAt: NexusGenScalars['Date']; // Date! votes_count: number; // Int! } Tag: { // root type + description?: string | null; // String icon?: string | null; // String id: number; // Int! isOfficial?: boolean | null; // Boolean @@ -236,15 +244,17 @@ 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! id: number; // Int! + is_published: boolean | null; // Boolean reward_amount: number; // Int! tags: NexusGenRootTypes['Tag'][]; // [Tag!]! title: string; // String! type: string; // String! + updatedAt: NexusGenScalars['Date']; // Date! votes_count: number; // Int! } BountyApplication: { // field return type @@ -334,6 +344,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!]! @@ -354,9 +365,11 @@ 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! + updatedAt: NexusGenScalars['Date']; // Date! votes_count: number; // Int! } Story: { // field return type @@ -364,16 +377,19 @@ 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! + is_published: boolean | null; // Boolean tags: NexusGenRootTypes['Tag'][]; // [Tag!]! title: string; // String! type: string; // String! + updatedAt: NexusGenScalars['Date']; // Date! votes_count: number; // Int! } Tag: { // field return type + description: string | null; // String icon: string | null; // String id: number; // Int! isOfficial: boolean | null; // Boolean @@ -409,7 +425,9 @@ export interface NexusGenFieldTypes { createdAt: NexusGenScalars['Date']; // Date! excerpt: string; // String! id: number; // Int! + is_published: boolean | null; // Boolean title: string; // String! + updatedAt: NexusGenScalars['Date']; // Date! votes_count: number; // Int! } } @@ -438,10 +456,12 @@ export interface NexusGenFieldTypeNames { deadline: 'String' excerpt: 'String' id: 'Int' + is_published: 'Boolean' reward_amount: 'Int' tags: 'Tag' title: 'String' type: 'String' + updatedAt: 'Date' votes_count: 'Int' } BountyApplication: { // field return type name @@ -531,6 +551,7 @@ export interface NexusGenFieldTypeNames { getDonationsStats: 'DonationsStats' getFeed: 'Post' getLnurlDetailsForProject: 'LnurlDetails' + getMyDrafts: 'Post' getPostById: 'Post' getProject: 'Project' getTrendingPosts: 'Post' @@ -551,9 +572,11 @@ export interface NexusGenFieldTypeNames { createdAt: 'Date' excerpt: 'String' id: 'Int' + is_published: 'Boolean' tags: 'Tag' title: 'String' type: 'String' + updatedAt: 'Date' votes_count: 'Int' } Story: { // field return type name @@ -565,12 +588,15 @@ export interface NexusGenFieldTypeNames { createdAt: 'Date' excerpt: 'String' id: 'Int' + is_published: 'Boolean' tags: 'Tag' title: 'String' type: 'String' + updatedAt: 'Date' votes_count: 'Int' } Tag: { // field return type name + description: 'String' icon: 'String' id: 'Int' isOfficial: 'Boolean' @@ -606,7 +632,9 @@ export interface NexusGenFieldTypeNames { createdAt: 'Date' excerpt: 'String' id: 'Int' + is_published: 'Boolean' title: 'String' + updatedAt: 'Date' votes_count: 'Int' } } @@ -660,6 +688,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 8d9775d..b334d3f 100644 --- a/api/functions/graphql/schema.graphql +++ b/api/functions/graphql/schema.graphql @@ -22,15 +22,17 @@ type Bounty implements PostBase { applications: [BountyApplication!]! author: Author! body: String! - cover_image: String! + cover_image: String createdAt: Date! deadline: String! excerpt: String! id: Int! + is_published: Boolean reward_amount: Int! tags: [Tag!]! title: String! type: String! + updatedAt: Date! votes_count: Int! } @@ -113,7 +115,9 @@ interface PostBase { createdAt: Date! excerpt: String! id: Int! + is_published: Boolean title: String! + updatedAt: Date! votes_count: Int! } @@ -150,6 +154,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!]! @@ -171,9 +176,11 @@ type Question implements PostBase { createdAt: Date! excerpt: String! id: Int! + is_published: Boolean tags: [Tag!]! title: String! type: String! + updatedAt: Date! votes_count: Int! } @@ -182,25 +189,29 @@ type Story implements PostBase { body: String! comments: [PostComment!]! comments_count: Int! - cover_image: String! + cover_image: String createdAt: Date! excerpt: String! id: Int! + is_published: Boolean tags: [Tag!]! title: String! type: String! + updatedAt: Date! votes_count: Int! } input StoryInputType { body: String! - cover_image: String! + cover_image: String id: Int + is_published: Boolean tags: [String!]! title: String! } type Tag { + description: String icon: String id: Int! isOfficial: Boolean diff --git a/api/functions/graphql/types/post.js b/api/functions/graphql/types/post.js index a1274ad..3beddca 100644 --- a/api/functions/graphql/types/post.js +++ b/api/functions/graphql/types/post.js @@ -54,9 +54,11 @@ 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'); + t.boolean('is_published'); }, }) @@ -67,7 +69,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,8 +109,9 @@ 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'); + t.boolean('is_published') } }) const createStory = extendType({ @@ -118,21 +121,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 +166,7 @@ const createStory = extendType({ body, cover_image, excerpt, + is_published: was_published || is_published, tags: { connectOrCreate: tags.map(tag => { @@ -185,6 +192,7 @@ const createStory = extendType({ body, cover_image, excerpt, + is_published, tags: { connectOrCreate: tags.map(tag => { @@ -263,7 +271,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'); @@ -371,7 +379,8 @@ const getFeed = extendType({ id: tag } }, - }) + }), + is_published: true, }, skip, take, @@ -396,7 +405,8 @@ const getTrendingPosts = extendType({ where: { createdAt: { gte: lastWeekDate - } + }, + is_published: true, }, orderBy: { votes_count: 'desc' }, take: 5, @@ -407,6 +417,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 +494,7 @@ module.exports = { getFeed, getPostById, getTrendingPosts, + getMyDrafts, // Mutations createStory, 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/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/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/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 5e4bec8..9031256 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[] @@ -113,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[] @@ -139,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/App.tsx b/src/App.tsx index 0cde4f7..cc94daf 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,29 @@ 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 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 +81,29 @@ function App() { /> - }> - } /> - } /> - } /> - - } /> - } /> } /> - } /> - } /> + }> + } /> + } /> + } /> - } /> + } /> + } /> - } /> - } /> - } /> + } /> + + } /> + + } /> + } /> + } /> + + } /> + - } /> 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..2592cec 100644 --- a/src/Components/Inputs/TagsInput/TagsInput.tsx +++ b/src/Components/Inputs/TagsInput/TagsInput.tsx @@ -1,20 +1,19 @@ 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"; +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?: { @@ -28,43 +27,80 @@ 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