mirror of
https://github.com/aljazceru/landscape-template.git
synced 2026-01-01 13:34:30 +01:00
Merge branch 'dev'
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -30,4 +30,4 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
TODO
|
||||
NOTES
|
||||
NOTES
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -7,6 +7,7 @@ const Tag = objectType({
|
||||
t.nonNull.int('id');
|
||||
t.nonNull.string('title');
|
||||
t.string('icon');
|
||||
t.string('description');
|
||||
t.boolean('isOfficial');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Story" ALTER COLUMN "cover_image" DROP NOT NULL;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Tag" ADD COLUMN "description" TEXT;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Story" ADD COLUMN "is_published" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Story" ALTER COLUMN "is_published" SET DEFAULT true;
|
||||
@@ -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[]
|
||||
|
||||
61
src/App.tsx
61
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() {
|
||||
|
||||
/>
|
||||
</Helmet>
|
||||
<Navbar />
|
||||
<Suspense fallback={<LoadingPage />}>
|
||||
<Routes>
|
||||
<Route path="/products/hottest" element={<HottestPage />} />
|
||||
<Route path="/products/category/:id" element={<CategoryPage />} />
|
||||
<Route path="/products" element={<ExplorePage />} />
|
||||
|
||||
<Route path="/blog/post/:type/:id/*" element={<PostDetailsPage />} />
|
||||
<Route path="/blog/preview-post/:type" element={<PreviewPostPage />} />
|
||||
<Route path="/blog/create-post" element={<ProtectedRoute><CreatePostPage /></ProtectedRoute>} />
|
||||
<Route path="/blog" element={<FeedPage />} />
|
||||
|
||||
<Route path="/hackathons" element={<HackathonsPage />} />
|
||||
<Route element={<NavbarLayout />}>
|
||||
<Route path="/products/hottest" element={<HottestPage />} />
|
||||
<Route path="/products/category/:id" element={<CategoryPage />} />
|
||||
<Route path="/products" element={<ExplorePage />} />
|
||||
|
||||
<Route path="/donate" element={<DonatePage />} />
|
||||
<Route path="/blog/post/:type/:id/*" element={<PostDetailsPage />} />
|
||||
<Route path="/blog" element={<FeedPage />} />
|
||||
|
||||
<Route path="/profile/:id/*" element={<ProfilePage />} />
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/logout" element={<LogoutPage />} />
|
||||
<Route path="/hackathons" element={<HackathonsPage />} />
|
||||
|
||||
<Route path="/donate" element={<DonatePage />} />
|
||||
|
||||
<Route path="/profile/:id/*" element={<ProfilePage />} />
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/logout" element={<LogoutPage />} />
|
||||
|
||||
<Route path="/" element={<Navigate to="/products" />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/" element={<Navigate to="/products" />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
<ModalsContainer />
|
||||
|
||||
@@ -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<any, Props>(({
|
||||
multiple,
|
||||
value,
|
||||
max = 3,
|
||||
@@ -45,9 +44,8 @@ export default function FilesInput({
|
||||
allowedType = 'images',
|
||||
uploadText = 'Upload files',
|
||||
...props
|
||||
}: Props) {
|
||||
}, ref) => {
|
||||
|
||||
const ref = useRef<HTMLInputElement>(null!)
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
@@ -132,4 +130,7 @@ export default function FilesInput({
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
export default FilesInput;
|
||||
@@ -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<OfficialTagsQuery['officialTags'][number], 'id'>
|
||||
|
||||
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<Option>) => {
|
||||
return (
|
||||
<div>
|
||||
<components.Option {...props} className='flex items-start'>
|
||||
<span className={`rounded-8 w-40 h-40 text-center py-8`}>
|
||||
<components.Option {...props} className='!flex items-center gap-16 !py-16'>
|
||||
<div className={`rounded-8 w-40 h-40 text-center py-8 shrink-0 bg-gray-100`}>
|
||||
{props.data.icon}
|
||||
</span>
|
||||
<span className="self-center px-16">
|
||||
{props.data.label}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium self-center">
|
||||
{props.data.label}
|
||||
</p>
|
||||
<p className="text-body5 text-gray-500">
|
||||
{props.data.description}
|
||||
</p>
|
||||
</div>
|
||||
</components.Option>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const { ValueContainer, Placeholder } = components;
|
||||
const CustomValueContainer = ({ children, ...props }: any) => {
|
||||
|
||||
return (
|
||||
<ValueContainer {...props}>
|
||||
{React.Children.map(children, child =>
|
||||
child && child.type !== Placeholder ? child : null
|
||||
)}
|
||||
<Placeholder {...props} isFocused={props.isFocused}>
|
||||
{props.selectProps.placeholder}
|
||||
</Placeholder>
|
||||
</ValueContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const colourStyles: StylesConfig = {
|
||||
|
||||
control: (styles, state) => ({
|
||||
...styles,
|
||||
padding: '1px 4px',
|
||||
borderRadius: 8,
|
||||
padding: '1px 0',
|
||||
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": {
|
||||
boxShadow: 'none !important'
|
||||
},
|
||||
}),
|
||||
multiValue: styles => ({
|
||||
...styles,
|
||||
padding: '4px 12px',
|
||||
borderRadius: 48,
|
||||
fontWeight: 500
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -83,7 +119,7 @@ export default function TagsInput({
|
||||
|
||||
|
||||
const handleChange = (newValue: OnChangeValue<Option, true>,) => {
|
||||
onChange([...value, ...newValue.map(transformer.optionToTag)]);
|
||||
onChange([...newValue.map(transformer.optionToTag)]);
|
||||
onBlur();
|
||||
}
|
||||
|
||||
@@ -94,29 +130,34 @@ 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 (
|
||||
<div className={`${classes?.container}`}>
|
||||
<CreatableSelect
|
||||
<Select
|
||||
isLoading={officalTags.loading}
|
||||
options={tagsOptions}
|
||||
isMulti
|
||||
isDisabled={maxReached}
|
||||
placeholder={maxReached ? `Max. ${max} tags reached. Remove a tag to add another.` : placeholder}
|
||||
isClearable
|
||||
|
||||
|
||||
value={[]}
|
||||
isOptionDisabled={() => maxReached}
|
||||
placeholder={currentPlaceholder}
|
||||
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}
|
||||
onBlur={onBlur}
|
||||
components={{
|
||||
Option: OptionComponent,
|
||||
MultiValue: () => <></>
|
||||
// ValueContainer: CustomValueContainer
|
||||
}}
|
||||
|
||||
styles={colourStyles as any}
|
||||
theme={(theme) => ({
|
||||
...theme,
|
||||
@@ -127,9 +168,9 @@ export default function TagsInput({
|
||||
},
|
||||
})}
|
||||
/>
|
||||
<div className="flex mt-16 gap-8 flex-wrap">
|
||||
{/* <div className="flex mt-16 gap-8 flex-wrap">
|
||||
{(value as Tag[]).map((tag, idx) => <Badge color="gray" key={tag.title} onRemove={() => handleRemove(idx)} >{tag.title}</Badge>)}
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,5 +3,6 @@ query OfficialTags {
|
||||
id
|
||||
title
|
||||
icon
|
||||
description
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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?.();
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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 <Navigate to={notAuthorizedRedirectPath} replace />;
|
||||
return <Navigate to={notAuthorizedRedirectPath} replace state={{ from: location.pathname }} />;
|
||||
|
||||
|
||||
if (!isAllowed) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ export default function BountyCard({ bounty }: Props) {
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-12 overflow-hidden border-2">
|
||||
<img src={bounty.cover_image} className='h-[200px] w-full object-cover bg-gray-100' alt="" />
|
||||
{bounty.cover_image && <img src={bounty.cover_image} className='h-[200px] w-full object-cover bg-gray-100' alt="" />}
|
||||
<div className="p-24">
|
||||
<Header author={bounty.author} date={bounty.createdAt} />
|
||||
<div className="flex flex-col gap-8 md:gap-0 md:flex-row justify-between">
|
||||
|
||||
@@ -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
|
||||
<div className={`remirror-theme ${styles.wrapper} post-body bg-white`}>
|
||||
<Remirror
|
||||
manager={manager}
|
||||
initialContent={initialContent}
|
||||
initialContent={initialContent?.()}
|
||||
>
|
||||
<TextEditorComponents.SaveModule name={name} />
|
||||
<Toolbar />
|
||||
|
||||
@@ -17,6 +17,7 @@ export default function Toolbar() {
|
||||
{/* <TextEditorComponents.ToolButton cmd='leftAlign' />
|
||||
<TextEditorComponents.ToolButton cmd='centerAlign' />
|
||||
<TextEditorComponents.ToolButton cmd='rightAlign' /> */}
|
||||
<TextEditorComponents.ToolButton cmd='blockquote' />
|
||||
<TextEditorComponents.ToolButton cmd='code' />
|
||||
<TextEditorComponents.ToolButton cmd='codeBlock' />
|
||||
<TextEditorComponents.ToolButton cmd='bulletList' />
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<IStoryFormInputs>()
|
||||
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) &&
|
||||
<div className="border-2 border-gray-200 rounded-16 p-16">
|
||||
<p className="text-body2 font-bolder mb-16">Saved Drafts</p>
|
||||
<ul className=''>
|
||||
{myDraftsQuery.data.getMyDrafts.map(draft =>
|
||||
<li key={draft.id} className='py-16 border-b-[1px] border-gray-200 last-of-type:border-b-0 ' >
|
||||
<p
|
||||
className="hover:underline"
|
||||
role={'button'}
|
||||
onClick={() => loadDraft(draft.id)}
|
||||
>
|
||||
{draft.title}
|
||||
</p>
|
||||
<div className="flex gap-4 text-body5">
|
||||
<p className="text-gray-400">Last edited {getDateDifference(draft.updatedAt)} ago</p>
|
||||
<Button size='sm' color='none' className='text-blue-500 !p-0' onClick={() => deleteDraft(draft.id)}>Delete draft</Button>
|
||||
</div>
|
||||
</li>)}
|
||||
</ul>
|
||||
</div>}
|
||||
{loading && <LoadingPage />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
query GetMyDrafts($type: POST_TYPE!) {
|
||||
getMyDrafts(type: $type) {
|
||||
... on Story {
|
||||
id
|
||||
title
|
||||
updatedAt
|
||||
}
|
||||
... on Bounty {
|
||||
id
|
||||
title
|
||||
updatedAt
|
||||
}
|
||||
... on Question {
|
||||
id
|
||||
title
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import React, { forwardRef } from 'react'
|
||||
import { useFormContext } from 'react-hook-form'
|
||||
import { IStoryFormInputs } from '../../CreateStoryPage/CreateStoryPage';
|
||||
|
||||
const ErrorsContainer = forwardRef<HTMLDivElement>((props, ref) => {
|
||||
|
||||
const { formState: { isValid, isSubmitted, errors } } = useFormContext<IStoryFormInputs>();
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
{(!isValid && isSubmitted) && <ul className='bg-red-50 p-8 pl-24 border-l-4 rounded-8 border-red-600 list-disc text-body4 text-medium'>
|
||||
{errors.title && <li className="input-error text-body5 text-medium">
|
||||
{errors.title.message}
|
||||
</li>}
|
||||
{errors.cover_image && <li className="input-error text-body5 text-medium">
|
||||
{errors.cover_image.message}
|
||||
</li>}
|
||||
{errors.tags && <li className="input-error text-body5 text-medium">
|
||||
{errors.tags.message}
|
||||
</li>}
|
||||
{errors.body && <li className="input-error text-body5 text-medium">
|
||||
{errors.body.message}
|
||||
</li>}
|
||||
</ul>}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default ErrorsContainer;
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { MOCK_DATA } from 'src/mocks/data';
|
||||
|
||||
import PreviewPostCard from './PreviewPostCard';
|
||||
|
||||
export default {
|
||||
title: 'Posts/Post Details Page/Components/Preview Post Card',
|
||||
component: PreviewPostCard,
|
||||
argTypes: {
|
||||
backgroundColor: { control: 'color' },
|
||||
},
|
||||
} as ComponentMeta<typeof PreviewPostCard>;
|
||||
|
||||
|
||||
const Template: ComponentStory<typeof PreviewPostCard> = (args) => <div className="max-w-[890px]"><PreviewPostCard {...args as any} ></PreviewPostCard></div>
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
post: MOCK_DATA.posts.stories[0]
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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<Post,
|
||||
| 'title'
|
||||
| 'createdAt'
|
||||
| 'body'
|
||||
| 'author'
|
||||
> & {
|
||||
tags: Array<{ title: string }>
|
||||
cover_image?: string | File
|
||||
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,8 +38,7 @@ export default function PreviewPostContent({ post }: Props) {
|
||||
className='w-full h-[120px] md:h-[240px] object-cover rounded-12 mb-16'
|
||||
alt="" />}
|
||||
<div className="flex flex-col gap-24">
|
||||
<Header size="lg" showTimeAgo={false} author={post.author} date={post.createdAt} />
|
||||
<h1 className="text-h2 font-bolder">{post.title}</h1>
|
||||
<h1 className="text-[42px] font-bolder">{post.title}</h1>
|
||||
{post.tags.length > 0 && <div className="flex gap-8">
|
||||
{post.tags.map((tag, idx) => <Badge key={idx} size='sm'>
|
||||
{tag.title}
|
||||
@@ -1,93 +1,47 @@
|
||||
import { useState } from 'react'
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import { Controller, FormProvider, NestedValue, Resolver, SubmitHandler, 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 { 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).min(1, "You need to add a cover image")
|
||||
|
||||
}).required();
|
||||
|
||||
|
||||
interface IFormInputs {
|
||||
id: number | null
|
||||
title: string
|
||||
tags: NestedValue<{ title: string }[]>
|
||||
cover_image: NestedValue<File[]> | NestedValue<string[]>
|
||||
body: string
|
||||
interface Props {
|
||||
isUpdating?: boolean;
|
||||
isPublished?: boolean;
|
||||
onSuccess?: (isDraft: boolean) => void,
|
||||
onValidationError?: () => void
|
||||
}
|
||||
|
||||
const storageService = new StorageService<CreateStoryType>('story-edit');
|
||||
|
||||
|
||||
export type CreateStoryType = Override<IFormInputs, {
|
||||
tags: { title: string }[]
|
||||
cover_image: File[] | string[]
|
||||
}>
|
||||
|
||||
export default function StoryForm() {
|
||||
export default function StoryForm(props: Props) {
|
||||
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const { story } = useAppSelector(state => ({
|
||||
story: state.staging.story
|
||||
}))
|
||||
const navigate = useNavigate();
|
||||
const { handleSubmit, control, register, trigger, getValues, watch, reset } = useFormContext<IStoryFormInputs>();
|
||||
|
||||
const formMethods = useForm<IFormInputs>({
|
||||
resolver: yupResolver(schema) as Resolver<IFormInputs>,
|
||||
defaultValues: {
|
||||
id: story?.id ?? null,
|
||||
title: story?.title ?? '',
|
||||
cover_image: story?.cover_image ?? [],
|
||||
tags: story?.tags ?? [],
|
||||
body: story?.body ?? '',
|
||||
},
|
||||
});
|
||||
const { handleSubmit, control, register, formState: { errors, }, trigger, getValues, } = formMethods;
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const navigate = useNavigate()
|
||||
const [editMode, setEditMode] = useState(true)
|
||||
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 presistPost = useThrottledCallback((value) => storageService.set(value), [], 1000)
|
||||
useEffect(() => {
|
||||
const subscription = watch(({ id, is_published, ...values }) => presistPost(values));
|
||||
return () => subscription.unsubscribe();
|
||||
}, [presistPost, watch]);
|
||||
|
||||
|
||||
const clickPreview = async () => {
|
||||
const isValid = await trigger();
|
||||
@@ -95,13 +49,30 @@ export default function StoryForm() {
|
||||
if (isValid) {
|
||||
const data = getValues()
|
||||
dispatch(stageStory(data))
|
||||
navigate('/blog/preview-post/Story')
|
||||
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<IFormInputs>(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()
|
||||
props.onSuccess?.(!!data.createStory?.is_published);
|
||||
setLoading(false)
|
||||
},
|
||||
onError: (error) => {
|
||||
NotificationsService.error('Unexpected error happened, please try again', { error })
|
||||
setLoading(false)
|
||||
},
|
||||
refetchQueries: ['GetMyDrafts']
|
||||
});
|
||||
|
||||
const clickSubmit = (publish_now: boolean) => handleSubmit<IStoryFormInputs>(data => {
|
||||
setLoading(true);
|
||||
createStory({
|
||||
variables: {
|
||||
@@ -110,28 +81,38 @@ 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,
|
||||
is_published: publish_now,
|
||||
cover_image: (data.cover_image[0] ?? null) as string | null,
|
||||
},
|
||||
}
|
||||
})
|
||||
})
|
||||
storageService.clear();
|
||||
}, props.onValidationError);
|
||||
|
||||
|
||||
const isUpdating = story?.id;
|
||||
|
||||
|
||||
const postId = watch('id') ?? -1;
|
||||
|
||||
|
||||
return (
|
||||
<FormProvider {...formMethods}>
|
||||
<form
|
||||
onSubmit={clickSubmit}
|
||||
>
|
||||
<form
|
||||
onSubmit={clickSubmit(true)}
|
||||
>
|
||||
<div className="flex gap-16 mb-24">
|
||||
<button type='button' className={`rounded-8 px-16 py-8 ${editMode ? 'bg-primary-100 text-primary-700' : "text-gray-500"} active:scale-95 transition-transform`} onClick={() => setEditMode(true)}>Edit</button>
|
||||
<button type='button' className={`rounded-8 px-16 py-8 ${!editMode ? 'bg-primary-100 text-primary-700' : "text-gray-500"} active:scale-95 transition-transform`} onClick={clickPreview}>Preview</button>
|
||||
</div>
|
||||
{editMode && <>
|
||||
<div
|
||||
className='bg-white border-2 border-gray-200 rounded-16 overflow-hidden'>
|
||||
<div className="p-32">
|
||||
<Controller
|
||||
control={control}
|
||||
name="cover_image"
|
||||
render={({ field: { onChange, value, onBlur } }) => (
|
||||
render={({ field: { onChange, value, onBlur, ref } }) => (
|
||||
<FilesInput
|
||||
ref={ref}
|
||||
value={value}
|
||||
onBlur={onBlur}
|
||||
onChange={onChange}
|
||||
@@ -139,65 +120,56 @@ export default function StoryForm() {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<p className='input-error'>{errors.cover_image?.message}</p>
|
||||
|
||||
|
||||
<p className="text-body5 mt-16">
|
||||
Title
|
||||
</p>
|
||||
<div className="input-wrapper mt-8 relative">
|
||||
|
||||
<div className="mt-16 relative">
|
||||
<input
|
||||
autoFocus
|
||||
type='text'
|
||||
className="input-text"
|
||||
placeholder='Your Story Title'
|
||||
className="p-0 text-[42px] border-0 focus:border-0 focus:outline-none focus:ring-0 font-bolder placeholder:!text-gray-400"
|
||||
placeholder='New story title here...'
|
||||
{...register("title")}
|
||||
/>
|
||||
</div>
|
||||
{errors.title && <p className="input-error">
|
||||
{errors.title.message}
|
||||
</p>}
|
||||
|
||||
<p className="text-body5 mt-16">
|
||||
Tags
|
||||
</p>
|
||||
<TagsInput
|
||||
placeholder="Add up to 5 tags. Search popular ones or add your own"
|
||||
classes={{ container: 'mt-8' }}
|
||||
placeholder="Add up to 5 popular tags..."
|
||||
classes={{ container: 'mt-16' }}
|
||||
/>
|
||||
{errors.tags && <p className="input-error">
|
||||
{errors.tags.message}
|
||||
</p>}
|
||||
|
||||
</div>
|
||||
<ContentEditor
|
||||
initialContent={story?.body}
|
||||
key={postId}
|
||||
initialContent={() => getValues().body}
|
||||
placeholder="Write your story content here..."
|
||||
name="body"
|
||||
/>
|
||||
|
||||
{errors.body && <p className="input-error py-8 px-16">
|
||||
{errors.body.message}
|
||||
</p>}
|
||||
</div>
|
||||
<div className="flex gap-16 mt-32">
|
||||
<Button
|
||||
type='submit'
|
||||
color="primary"
|
||||
disabled={loading}
|
||||
>
|
||||
{isUpdating ?
|
||||
loading ? "Updating..." : "Update" :
|
||||
loading ? "Publishing..." : "Publish"
|
||||
}
|
||||
</Button>
|
||||
|
||||
</>}
|
||||
{!editMode && <PreviewPostCard post={{ ...getValues(), cover_image: getValues().cover_image[0] }} />}
|
||||
<div className="flex gap-16 mt-32">
|
||||
<Button
|
||||
type='submit'
|
||||
color="primary"
|
||||
disabled={loading}
|
||||
>
|
||||
{props.isUpdating ?
|
||||
"Update" :
|
||||
"Publish"
|
||||
}
|
||||
</Button>
|
||||
{!props.isPublished &&
|
||||
<Button
|
||||
color="gray"
|
||||
onClick={clickPreview}
|
||||
disabled={loading}
|
||||
onClick={clickSubmit(false)}
|
||||
>
|
||||
Preview
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider >
|
||||
Save as Draft
|
||||
</Button>}
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ mutation createStory($data: StoryInputType) {
|
||||
title
|
||||
}
|
||||
votes_count
|
||||
is_published
|
||||
type
|
||||
cover_image
|
||||
comments_count
|
||||
|
||||
@@ -4,26 +4,26 @@ 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 PostTypeList from "./PostTypeList";
|
||||
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();
|
||||
|
||||
return (<>
|
||||
<Helmet>
|
||||
<title>Create Post</title>
|
||||
{postType === 'story' && <title>Create Story</title>}
|
||||
{postType === 'bounty' && <title>Create Bounty</title>}
|
||||
{postType === 'question' && <title>Create Question</title>}
|
||||
</Helmet>
|
||||
<div
|
||||
className="page-container grid gap-24 grid-cols-1 lg:grid-cols-[1fr_min(100%,910px)_1fr]"
|
||||
className="page-container grid gap-24 grid-cols-1 lg:grid-cols-[1fr_4fr]"
|
||||
// style={{ gridTemplateColumns: "326px 1fr" }}
|
||||
>
|
||||
<div className="">
|
||||
@@ -36,14 +36,12 @@ export default function CreatePostPage() {
|
||||
<FiArrowLeft className={"text-body3"} />
|
||||
</button>
|
||||
</div>
|
||||
<div style={{
|
||||
width: "min(100%,910px)"
|
||||
}}>
|
||||
<div >
|
||||
{postType === 'story' && <>
|
||||
<h2 className="text-h2 font-bolder text-gray-800 mb-32">
|
||||
{/* <h2 className="text-h2 font-bolder text-gray-800 mb-32">
|
||||
Write a Story
|
||||
</h2>
|
||||
<StoryForm />
|
||||
</h2> */}
|
||||
<CreateStoryPage />
|
||||
</>}
|
||||
{postType === 'bounty' && <>
|
||||
<h2 className="text-h2 font-bolder text-gray-800 mb-32">
|
||||
|
||||
@@ -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, '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();
|
||||
|
||||
|
||||
export interface IStoryFormInputs {
|
||||
id: number | null
|
||||
title: string
|
||||
tags: NestedValue<{ title: string }[]>
|
||||
cover_image: NestedValue<File[]> | NestedValue<string[]>
|
||||
body: string
|
||||
is_published: boolean | null
|
||||
}
|
||||
|
||||
|
||||
|
||||
export type CreateStoryType = Override<IStoryFormInputs, {
|
||||
tags: { title: string }[]
|
||||
cover_image: File[] | string[]
|
||||
}>
|
||||
|
||||
const storageService = new StorageService<CreateStoryType>('story-edit');
|
||||
|
||||
|
||||
export default function CreateStoryPage() {
|
||||
|
||||
|
||||
const { story } = useAppSelector(state => ({
|
||||
story: state.staging.story || storageService.get()
|
||||
}))
|
||||
|
||||
const formMethods = useForm<IStoryFormInputs>({
|
||||
resolver: yupResolver(schema) as Resolver<IStoryFormInputs>,
|
||||
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<HTMLDivElement>(null!);
|
||||
const [formKey, setFormKey] = useState(1)
|
||||
|
||||
const resetForm = () => setFormKey(v => v + 1)
|
||||
|
||||
|
||||
return (
|
||||
<FormProvider {...formMethods}>
|
||||
<div className={styles.grid}>
|
||||
|
||||
<div id="form">
|
||||
<StoryForm
|
||||
key={formKey}
|
||||
isPublished={!!story?.is_published}
|
||||
isUpdating={!!story?.id}
|
||||
onSuccess={() => resetForm()}
|
||||
onValidationError={() => errorsContainerRef.current.scrollIntoView({ behavior: 'smooth', block: "center" })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="errors">
|
||||
<ErrorsContainer ref={errorsContainerRef} />
|
||||
</div>
|
||||
<div id="drafts">
|
||||
<DraftsContainer type={Post_Type.Story} onDraftLoad={resetForm} />
|
||||
</div>
|
||||
</div>
|
||||
</FormProvider>
|
||||
)
|
||||
}
|
||||
@@ -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 .";
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
@@ -79,14 +81,13 @@ export default function FeedPage() {
|
||||
top: `${navHeight + 16}px`,
|
||||
maxHeight: `calc(100vh - ${navHeight}px - 16px)`,
|
||||
}}>
|
||||
{isLoggedIn &&
|
||||
<Button
|
||||
href='/blog/create-post'
|
||||
color='primary'
|
||||
fullWidth
|
||||
>
|
||||
Write a story
|
||||
</Button>}
|
||||
<Button
|
||||
href='/blog/create-post'
|
||||
color='primary'
|
||||
fullWidth
|
||||
>
|
||||
Write a story
|
||||
</Button>
|
||||
<div className="my-24"></div>
|
||||
<div className="my-24"></div>
|
||||
<PopularTagsFilter
|
||||
|
||||
@@ -28,10 +28,10 @@ export default function AuthorCard({ author }: Props) {
|
||||
</div>
|
||||
<Button
|
||||
fullWidth
|
||||
href={`/profile/${author.id}`}
|
||||
href={createRoute({ type: 'profile', id: author.id, username: author.name })}
|
||||
color="primary"
|
||||
className="mt-16">
|
||||
Follow
|
||||
Maker's Profile
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -49,7 +49,7 @@ export default function StoryPageContent({ story }: Props) {
|
||||
Delete
|
||||
</MenuItem>
|
||||
</Menu>}
|
||||
<h1 className="text-h2 font-bolder">{story.title}</h1>
|
||||
<h1 className="text-[42px] font-bolder">{story.title}</h1>
|
||||
<Header size="lg" showTimeAgo={false} author={story.author} date={story.createdAt} />
|
||||
{story.tags.length > 0 && <div className="flex gap-8">
|
||||
{story.tags.map(tag => <Badge key={tag.id} size='sm'>
|
||||
|
||||
@@ -28,18 +28,18 @@ 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")
|
||||
};
|
||||
|
||||
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({
|
||||
|
||||
@@ -18,6 +18,7 @@ query PostDetails($id: Int!, $type: POST_TYPE!) {
|
||||
votes_count
|
||||
type
|
||||
cover_image
|
||||
is_published
|
||||
comments_count
|
||||
comments {
|
||||
id
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { MOCK_DATA } from 'src/mocks/data';
|
||||
|
||||
import PreviewPostContent from './PreviewPostContent';
|
||||
|
||||
export default {
|
||||
title: 'Posts/Post Details Page/Components/Story Page Content',
|
||||
component: PreviewPostContent,
|
||||
argTypes: {
|
||||
backgroundColor: { control: 'color' },
|
||||
},
|
||||
} as ComponentMeta<typeof PreviewPostContent>;
|
||||
|
||||
|
||||
const Template: ComponentStory<typeof PreviewPostContent> = (args) => <div className="max-w-[890px]"><PreviewPostContent {...args as any} ></PreviewPostContent></div>
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
post: MOCK_DATA.posts.stories[0]
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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 <NotFoundPage />
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{post.title}</title>
|
||||
<meta property="og:title" content={post.title} />
|
||||
</Helmet>
|
||||
<div
|
||||
className={`page-container grid pt-16 w-full gap-32 ${styles.grid}`}
|
||||
>
|
||||
<aside id='actions' className='no-scrollbar'>
|
||||
<div className="sticky"
|
||||
style={{
|
||||
top: `${navHeight + 16}px`,
|
||||
maxHeight: `calc(100vh - ${navHeight}px - 16px)`,
|
||||
}}>
|
||||
<PostActions
|
||||
post={{
|
||||
id: 123,
|
||||
votes_count: 123
|
||||
}}
|
||||
isPreview
|
||||
/>
|
||||
</div>
|
||||
</aside>
|
||||
<PreviewPostContent post={{ ...post, createdAt: new Date().toISOString(), author: author!, cover_image: post.cover_image[0] }} />
|
||||
<aside id='author' className='no-scrollbar min-w-0'>
|
||||
<div className="flex flex-col gap-24"
|
||||
style={{
|
||||
top: `${navHeight + 16}px`,
|
||||
maxHeight: `calc(100vh - ${navHeight}px - 16px)`,
|
||||
overflowY: "scroll",
|
||||
}}>
|
||||
<AuthorCard author={author!} />
|
||||
<TrendingCard />
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -30,9 +30,10 @@ const schema: yup.SchemaOf<IFormInputs> = 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")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -40,15 +40,17 @@ export type Bounty = PostBase & {
|
||||
applications: Array<BountyApplication>;
|
||||
author: Author;
|
||||
body: Scalars['String'];
|
||||
cover_image: Scalars['String'];
|
||||
cover_image: Maybe<Scalars['String']>;
|
||||
createdAt: Scalars['Date'];
|
||||
deadline: Scalars['String'];
|
||||
excerpt: Scalars['String'];
|
||||
id: Scalars['Int'];
|
||||
is_published: Maybe<Scalars['Boolean']>;
|
||||
reward_amount: Scalars['Int'];
|
||||
tags: Array<Tag>;
|
||||
title: Scalars['String'];
|
||||
type: Scalars['String'];
|
||||
updatedAt: Scalars['Date'];
|
||||
votes_count: Scalars['Int'];
|
||||
};
|
||||
|
||||
@@ -174,7 +176,9 @@ export type PostBase = {
|
||||
createdAt: Scalars['Date'];
|
||||
excerpt: Scalars['String'];
|
||||
id: Scalars['Int'];
|
||||
is_published: Maybe<Scalars['Boolean']>;
|
||||
title: Scalars['String'];
|
||||
updatedAt: Scalars['Date'];
|
||||
votes_count: Scalars['Int'];
|
||||
};
|
||||
|
||||
@@ -214,6 +218,7 @@ export type Query = {
|
||||
getDonationsStats: DonationsStats;
|
||||
getFeed: Array<Post>;
|
||||
getLnurlDetailsForProject: LnurlDetails;
|
||||
getMyDrafts: Array<Post>;
|
||||
getPostById: Post;
|
||||
getProject: Project;
|
||||
getTrendingPosts: Array<Post>;
|
||||
@@ -258,6 +263,11 @@ export type QueryGetLnurlDetailsForProjectArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryGetMyDraftsArgs = {
|
||||
type: Post_Type;
|
||||
};
|
||||
|
||||
|
||||
export type QueryGetPostByIdArgs = {
|
||||
id: Scalars['Int'];
|
||||
type: Post_Type;
|
||||
@@ -308,9 +318,11 @@ export type Question = PostBase & {
|
||||
createdAt: Scalars['Date'];
|
||||
excerpt: Scalars['String'];
|
||||
id: Scalars['Int'];
|
||||
is_published: Maybe<Scalars['Boolean']>;
|
||||
tags: Array<Tag>;
|
||||
title: Scalars['String'];
|
||||
type: Scalars['String'];
|
||||
updatedAt: Scalars['Date'];
|
||||
votes_count: Scalars['Int'];
|
||||
};
|
||||
|
||||
@@ -320,26 +332,30 @@ export type Story = PostBase & {
|
||||
body: Scalars['String'];
|
||||
comments: Array<PostComment>;
|
||||
comments_count: Scalars['Int'];
|
||||
cover_image: Scalars['String'];
|
||||
cover_image: Maybe<Scalars['String']>;
|
||||
createdAt: Scalars['Date'];
|
||||
excerpt: Scalars['String'];
|
||||
id: Scalars['Int'];
|
||||
is_published: Maybe<Scalars['Boolean']>;
|
||||
tags: Array<Tag>;
|
||||
title: Scalars['String'];
|
||||
type: Scalars['String'];
|
||||
updatedAt: Scalars['Date'];
|
||||
votes_count: Scalars['Int'];
|
||||
};
|
||||
|
||||
export type StoryInputType = {
|
||||
body: Scalars['String'];
|
||||
cover_image: Scalars['String'];
|
||||
cover_image: InputMaybe<Scalars['String']>;
|
||||
id: InputMaybe<Scalars['Int']>;
|
||||
is_published: InputMaybe<Scalars['Boolean']>;
|
||||
tags: Array<Scalars['String']>;
|
||||
title: Scalars['String'];
|
||||
};
|
||||
|
||||
export type Tag = {
|
||||
__typename?: 'Tag';
|
||||
description: Maybe<Scalars['String']>;
|
||||
icon: Maybe<Scalars['String']>;
|
||||
id: Scalars['Int'];
|
||||
isOfficial: Maybe<Scalars['Boolean']>;
|
||||
@@ -401,7 +417,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; }>;
|
||||
|
||||
@@ -453,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<StoryInputType>;
|
||||
}>;
|
||||
|
||||
|
||||
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, 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'];
|
||||
@@ -480,7 +503,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 +511,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, 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'];
|
||||
@@ -557,6 +580,7 @@ export const OfficialTagsDocument = gql`
|
||||
id
|
||||
title
|
||||
icon
|
||||
description
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -916,6 +940,55 @@ export function useTrendingPostsLazyQuery(baseOptions?: Apollo.LazyQueryHookOpti
|
||||
export type TrendingPostsQueryHookResult = ReturnType<typeof useTrendingPostsQuery>;
|
||||
export type TrendingPostsLazyQueryHookResult = ReturnType<typeof useTrendingPostsLazyQuery>;
|
||||
export type TrendingPostsQueryResult = Apollo.QueryResult<TrendingPostsQuery, TrendingPostsQueryVariables>;
|
||||
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<GetMyDraftsQuery, GetMyDraftsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<GetMyDraftsQuery, GetMyDraftsQueryVariables>(GetMyDraftsDocument, options);
|
||||
}
|
||||
export function useGetMyDraftsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetMyDraftsQuery, GetMyDraftsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<GetMyDraftsQuery, GetMyDraftsQueryVariables>(GetMyDraftsDocument, options);
|
||||
}
|
||||
export type GetMyDraftsQueryHookResult = ReturnType<typeof useGetMyDraftsQuery>;
|
||||
export type GetMyDraftsLazyQueryHookResult = ReturnType<typeof useGetMyDraftsLazyQuery>;
|
||||
export type GetMyDraftsQueryResult = Apollo.QueryResult<GetMyDraftsQuery, GetMyDraftsQueryVariables>;
|
||||
export const CreateStoryDocument = gql`
|
||||
mutation createStory($data: StoryInputType) {
|
||||
createStory(data: $data) {
|
||||
@@ -928,6 +1001,7 @@ export const CreateStoryDocument = gql`
|
||||
title
|
||||
}
|
||||
votes_count
|
||||
is_published
|
||||
type
|
||||
cover_image
|
||||
comments_count
|
||||
@@ -1159,6 +1233,7 @@ export const PostDetailsDocument = gql`
|
||||
votes_count
|
||||
type
|
||||
cover_image
|
||||
is_published
|
||||
comments_count
|
||||
comments {
|
||||
id
|
||||
|
||||
@@ -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[]
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import Wallet_Service from './wallet.service'
|
||||
|
||||
export {
|
||||
Wallet_Service
|
||||
}
|
||||
export { default as Wallet_Service } from './wallet.service'
|
||||
export * from './storage.service'
|
||||
export * from './notifications.service'
|
||||
|
||||
24
src/services/storage.service.ts
Normal file
24
src/services/storage.service.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
|
||||
export class StorageService<T = any> {
|
||||
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;
|
||||
}
|
||||
|
||||
clear() {
|
||||
localStorage.removeItem(this.key)
|
||||
}
|
||||
}
|
||||
@@ -91,3 +91,9 @@ $screen-xl-max: 50000px;
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin gt-xl {
|
||||
@media screen and (min-width: #{$screen-xl-min}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -66,4 +70,35 @@
|
||||
aspect-ratio: 16/9;
|
||||
margin: 36px auto;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin-inline-start: 32px;
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
li {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
ul li {
|
||||
list-style: disc;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
@@ -102,4 +103,22 @@ export function capitalize(s?: string) {
|
||||
return s && s[0].toUpperCase() + s.slice(1);
|
||||
}
|
||||
|
||||
export const withHttp = (url: string) => !/^https?:\/\//i.test(url) ? `http://${url}` : url;
|
||||
export const withHttp = (url: string) => !/^https?:\/\//i.test(url) ? `http://${url}` : url;
|
||||
|
||||
export function getPropertyFromUnknown<Value = string>(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
|
||||
}
|
||||
|
||||
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'
|
||||
}
|
||||
@@ -9,3 +9,4 @@ export * from "./useVote";
|
||||
export * from './useWindowSize'
|
||||
export * from './useMediaQuery'
|
||||
export * from './useCurrentSection'
|
||||
export * from './usePreload'
|
||||
|
||||
13
src/utils/hooks/usePreload.ts
Normal file
13
src/utils/hooks/usePreload.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const Components = {
|
||||
PostPage: () => import('../../features/Posts/pages/PostDetailsPage/PostDetailsPage'),
|
||||
}
|
||||
|
||||
type ComponentToLoad = keyof typeof Components;
|
||||
|
||||
export const usePreload = (componentToLoad: ComponentToLoad) => {
|
||||
useEffect(() => {
|
||||
Components[componentToLoad]()
|
||||
}, [componentToLoad])
|
||||
}
|
||||
@@ -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<T> = {
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './routes'
|
||||
export * from './routes'
|
||||
export * from './loadable'
|
||||
9
src/utils/routing/layouts/NavbarLayout.tsx
Normal file
9
src/utils/routing/layouts/NavbarLayout.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Outlet, } from 'react-router-dom';
|
||||
import Navbar from "src/Components/Navbar/Navbar";
|
||||
|
||||
export const NavbarLayout = () => {
|
||||
return <>
|
||||
<Navbar />
|
||||
<Outlet />
|
||||
</>
|
||||
};
|
||||
2
src/utils/routing/layouts/index.ts
Normal file
2
src/utils/routing/layouts/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
export * from './NavbarLayout'
|
||||
8
src/utils/routing/loadable.tsx
Normal file
8
src/utils/routing/loadable.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Suspense } from "react";
|
||||
import LoadingPage from "src/Components/LoadingPage/LoadingPage";
|
||||
|
||||
export const Loadable = (Component: any, Loading = LoadingPage) => (props: any) => (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Component {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
Reference in New Issue
Block a user