diff --git a/api/functions/graphql/nexus-typegen.ts b/api/functions/graphql/nexus-typegen.ts
index d405ede..7120730 100644
--- a/api/functions/graphql/nexus-typegen.ts
+++ b/api/functions/graphql/nexus-typegen.ts
@@ -28,6 +28,29 @@ declare global {
}
export interface NexusGenInputs {
+ CreateProjectInput: { // input type
+ capabilities: number[]; // [Int!]!
+ category_id: number; // Int!
+ cover_image: NexusGenInputs['ImageInput']; // ImageInput!
+ description: string; // String!
+ discord?: string | null; // String
+ github?: string | null; // String
+ hashtag: string; // String!
+ id?: number | null; // Int
+ launch_status: NexusGenEnums['ProjectLaunchStatusEnum']; // ProjectLaunchStatusEnum!
+ lightning_address?: string | null; // String
+ members: NexusGenInputs['TeamMemberInput'][]; // [TeamMemberInput!]!
+ recruit_roles: number[]; // [Int!]!
+ screenshots: NexusGenInputs['ImageInput'][]; // [ImageInput!]!
+ slack?: string | null; // String
+ tagline: string; // String!
+ telegram?: string | null; // String
+ thumbnail_image: NexusGenInputs['ImageInput']; // ImageInput!
+ title: string; // String!
+ tournaments: number[]; // [Int!]!
+ twitter?: string | null; // String
+ website: string; // String!
+ }
ImageInput: { // input type
id?: string | null; // String
name?: string | null; // String
@@ -70,6 +93,33 @@ export interface NexusGenInputs {
tags: string[]; // [String!]!
title: string; // String!
}
+ TeamMemberInput: { // input type
+ id: number; // Int!
+ role: NexusGenEnums['TEAM_MEMBER_ROLE']; // TEAM_MEMBER_ROLE!
+ }
+ UpdateProjectInput: { // input type
+ capabilities: number[]; // [Int!]!
+ category_id: number; // Int!
+ cover_image: NexusGenInputs['ImageInput']; // ImageInput!
+ description: string; // String!
+ discord?: string | null; // String
+ github?: string | null; // String
+ hashtag: string; // String!
+ id?: number | null; // Int
+ launch_status: NexusGenEnums['ProjectLaunchStatusEnum']; // ProjectLaunchStatusEnum!
+ lightning_address?: string | null; // String
+ members: NexusGenInputs['TeamMemberInput'][]; // [TeamMemberInput!]!
+ recruit_roles: number[]; // [Int!]!
+ screenshots: NexusGenInputs['ImageInput'][]; // [ImageInput!]!
+ slack?: string | null; // String
+ tagline: string; // String!
+ telegram?: string | null; // String
+ thumbnail_image: NexusGenInputs['ImageInput']; // ImageInput!
+ title: string; // String!
+ tournaments: number[]; // [Int!]!
+ twitter?: string | null; // String
+ website: string; // String!
+ }
UpdateTournamentRegistrationInput: { // input type
email?: string | null; // String
hacking_status?: NexusGenEnums['TournamentMakerHackingStatusEnum'] | null; // TournamentMakerHackingStatusEnum
@@ -82,7 +132,10 @@ export interface NexusGenInputs {
export interface NexusGenEnums {
POST_TYPE: "Bounty" | "Question" | "Story"
+ ProjectLaunchStatusEnum: "Launched" | "WIP"
+ ProjectPermissionEnum: "DeleteProject" | "UpdateAdmins" | "UpdateInfo" | "UpdateMembers"
RoleLevelEnum: 3 | 0 | 1 | 2 | 4
+ TEAM_MEMBER_ROLE: "Admin" | "Maker" | "Owner"
TournamentEventTypeEnum: 2 | 3 | 0 | 1
TournamentMakerHackingStatusEnum: 1 | 0
VOTE_ITEM_TYPE: "Bounty" | "PostComment" | "Project" | "Question" | "Story" | "User"
@@ -131,11 +184,19 @@ export interface NexusGenObjects {
id: number; // Int!
workplan: string; // String!
}
+ Capability: { // root type
+ icon: string; // String!
+ id: number; // Int!
+ title: string; // String!
+ }
Category: { // root type
icon?: string | null; // String
id: number; // Int!
title: string; // String!
}
+ CreateProjectResponse: { // root type
+ project: NexusGenRootTypes['Project']; // Project!
+ }
Donation: { // root type
amount: number; // Int!
createdAt: NexusGenScalars['Date']; // Date!
@@ -214,13 +275,25 @@ export interface NexusGenObjects {
}
Project: { // root type
description: string; // String!
+ discord?: string | null; // String
+ github?: string | null; // String
+ hashtag: string; // String!
id: number; // Int!
+ launch_status: NexusGenEnums['ProjectLaunchStatusEnum']; // ProjectLaunchStatusEnum!
lightning_address?: string | null; // String
lnurl_callback_url?: string | null; // String
+ slack?: string | null; // String
+ tagline: string; // String!
+ telegram?: string | null; // String
title: string; // String!
+ twitter?: string | null; // String
votes_count: number; // Int!
website: string; // String!
}
+ ProjectMember: { // root type
+ role: NexusGenEnums['TEAM_MEMBER_ROLE']; // TEAM_MEMBER_ROLE!
+ user: NexusGenRootTypes['User']; // User!
+ }
Query: {};
Question: { // root type
body: string; // String!
@@ -379,6 +452,11 @@ export interface NexusGenFieldTypes {
id: number; // Int!
workplan: string; // String!
}
+ Capability: { // field return type
+ icon: string; // String!
+ id: number; // Int!
+ title: string; // String!
+ }
Category: { // field return type
apps_count: number; // Int!
cover_image: string | null; // String
@@ -388,6 +466,9 @@ export interface NexusGenFieldTypes {
title: string; // String!
votes_sum: number; // Int!
}
+ CreateProjectResponse: { // field return type
+ project: NexusGenRootTypes['Project']; // Project!
+ }
Donation: { // field return type
amount: number; // Int!
by: NexusGenRootTypes['User'] | null; // User
@@ -438,12 +519,15 @@ export interface NexusGenFieldTypes {
Mutation: { // field return type
confirmDonation: NexusGenRootTypes['Donation']; // Donation!
confirmVote: NexusGenRootTypes['Vote']; // Vote!
+ createProject: NexusGenRootTypes['CreateProjectResponse'] | null; // CreateProjectResponse
createStory: NexusGenRootTypes['Story'] | null; // Story
+ deleteProject: NexusGenRootTypes['Project'] | null; // Project
deleteStory: NexusGenRootTypes['Story'] | null; // Story
donate: NexusGenRootTypes['Donation']; // Donation!
registerInTournament: NexusGenRootTypes['User'] | null; // User
updateProfileDetails: NexusGenRootTypes['MyProfile'] | null; // MyProfile
updateProfileRoles: NexusGenRootTypes['MyProfile'] | null; // MyProfile
+ updateProject: NexusGenRootTypes['CreateProjectResponse'] | null; // CreateProjectResponse
updateTournamentRegistration: NexusGenRootTypes['ParticipationInfo'] | null; // ParticipationInfo
updateUserPreferences: NexusGenRootTypes['MyProfile']; // MyProfile!
vote: NexusGenRootTypes['Vote']; // Vote!
@@ -489,23 +573,41 @@ export interface NexusGenFieldTypes {
}
Project: { // field return type
awards: NexusGenRootTypes['Award'][]; // [Award!]!
+ capabilities: NexusGenRootTypes['Capability'][]; // [Capability!]!
category: NexusGenRootTypes['Category']; // Category!
cover_image: string; // String!
description: string; // String!
+ discord: string | null; // String
+ github: string | null; // String
+ hashtag: string; // String!
id: number; // Int!
+ launch_status: NexusGenEnums['ProjectLaunchStatusEnum']; // ProjectLaunchStatusEnum!
lightning_address: string | null; // String
lnurl_callback_url: string | null; // String
+ members: NexusGenRootTypes['ProjectMember'][]; // [ProjectMember!]!
+ permissions: NexusGenEnums['ProjectPermissionEnum'][]; // [ProjectPermissionEnum!]!
recruit_roles: NexusGenRootTypes['MakerRole'][]; // [MakerRole!]!
screenshots: string[]; // [String!]!
+ slack: string | null; // String
+ tagline: string; // String!
tags: NexusGenRootTypes['Tag'][]; // [Tag!]!
+ telegram: string | null; // String
thumbnail_image: string; // String!
title: string; // String!
+ tournaments: NexusGenRootTypes['Tournament'][]; // [Tournament!]!
+ twitter: string | null; // String
votes_count: number; // Int!
website: string; // String!
}
+ ProjectMember: { // field return type
+ role: NexusGenEnums['TEAM_MEMBER_ROLE']; // TEAM_MEMBER_ROLE!
+ user: NexusGenRootTypes['User']; // User!
+ }
Query: { // field return type
allCategories: NexusGenRootTypes['Category'][]; // [Category!]!
allProjects: NexusGenRootTypes['Project'][]; // [Project!]!
+ checkValidProjectHashtag: boolean; // Boolean!
+ getAllCapabilities: NexusGenRootTypes['Capability'][]; // [Capability!]!
getAllHackathons: NexusGenRootTypes['Hackathon'][]; // [Hackathon!]!
getAllMakersRoles: NexusGenRootTypes['GenericMakerRole'][]; // [GenericMakerRole!]!
getAllMakersSkills: NexusGenRootTypes['MakerSkill'][]; // [MakerSkill!]!
@@ -519,6 +621,7 @@ export interface NexusGenFieldTypes {
getProject: NexusGenRootTypes['Project']; // Project!
getProjectsInTournament: NexusGenRootTypes['TournamentProjectsResponse']; // TournamentProjectsResponse!
getTournamentById: NexusGenRootTypes['Tournament']; // Tournament!
+ getTournamentToRegister: NexusGenRootTypes['Tournament'][]; // [Tournament!]!
getTrendingPosts: NexusGenRootTypes['Post'][]; // [Post!]!
hottestProjects: NexusGenRootTypes['Project'][]; // [Project!]!
me: NexusGenRootTypes['MyProfile'] | null; // MyProfile
@@ -528,6 +631,7 @@ export interface NexusGenFieldTypes {
profile: NexusGenRootTypes['User'] | null; // User
projectsByCategory: NexusGenRootTypes['Project'][]; // [Project!]!
searchProjects: NexusGenRootTypes['Project'][]; // [Project!]!
+ searchUsers: NexusGenRootTypes['User'][]; // [User!]!
similarMakers: NexusGenRootTypes['User'][]; // [User!]!
tournamentParticipationInfo: NexusGenRootTypes['ParticipationInfo'] | null; // ParticipationInfo
}
@@ -736,6 +840,11 @@ export interface NexusGenFieldTypeNames {
id: 'Int'
workplan: 'String'
}
+ Capability: { // field return type name
+ icon: 'String'
+ id: 'Int'
+ title: 'String'
+ }
Category: { // field return type name
apps_count: 'Int'
cover_image: 'String'
@@ -745,6 +854,9 @@ export interface NexusGenFieldTypeNames {
title: 'String'
votes_sum: 'Int'
}
+ CreateProjectResponse: { // field return type name
+ project: 'Project'
+ }
Donation: { // field return type name
amount: 'Int'
by: 'User'
@@ -795,12 +907,15 @@ export interface NexusGenFieldTypeNames {
Mutation: { // field return type name
confirmDonation: 'Donation'
confirmVote: 'Vote'
+ createProject: 'CreateProjectResponse'
createStory: 'Story'
+ deleteProject: 'Project'
deleteStory: 'Story'
donate: 'Donation'
registerInTournament: 'User'
updateProfileDetails: 'MyProfile'
updateProfileRoles: 'MyProfile'
+ updateProject: 'CreateProjectResponse'
updateTournamentRegistration: 'ParticipationInfo'
updateUserPreferences: 'MyProfile'
vote: 'Vote'
@@ -846,23 +961,41 @@ export interface NexusGenFieldTypeNames {
}
Project: { // field return type name
awards: 'Award'
+ capabilities: 'Capability'
category: 'Category'
cover_image: 'String'
description: 'String'
+ discord: 'String'
+ github: 'String'
+ hashtag: 'String'
id: 'Int'
+ launch_status: 'ProjectLaunchStatusEnum'
lightning_address: 'String'
lnurl_callback_url: 'String'
+ members: 'ProjectMember'
+ permissions: 'ProjectPermissionEnum'
recruit_roles: 'MakerRole'
screenshots: 'String'
+ slack: 'String'
+ tagline: 'String'
tags: 'Tag'
+ telegram: 'String'
thumbnail_image: 'String'
title: 'String'
+ tournaments: 'Tournament'
+ twitter: 'String'
votes_count: 'Int'
website: 'String'
}
+ ProjectMember: { // field return type name
+ role: 'TEAM_MEMBER_ROLE'
+ user: 'User'
+ }
Query: { // field return type name
allCategories: 'Category'
allProjects: 'Project'
+ checkValidProjectHashtag: 'Boolean'
+ getAllCapabilities: 'Capability'
getAllHackathons: 'Hackathon'
getAllMakersRoles: 'GenericMakerRole'
getAllMakersSkills: 'MakerSkill'
@@ -876,6 +1009,7 @@ export interface NexusGenFieldTypeNames {
getProject: 'Project'
getProjectsInTournament: 'TournamentProjectsResponse'
getTournamentById: 'Tournament'
+ getTournamentToRegister: 'Tournament'
getTrendingPosts: 'Post'
hottestProjects: 'Project'
me: 'MyProfile'
@@ -885,6 +1019,7 @@ export interface NexusGenFieldTypeNames {
profile: 'User'
projectsByCategory: 'Project'
searchProjects: 'Project'
+ searchUsers: 'User'
similarMakers: 'User'
tournamentParticipationInfo: 'ParticipationInfo'
}
@@ -1064,9 +1199,15 @@ export interface NexusGenArgTypes {
payment_request: string; // String!
preimage: string; // String!
}
+ createProject: { // args
+ input?: NexusGenInputs['CreateProjectInput'] | null; // CreateProjectInput
+ }
createStory: { // args
data?: NexusGenInputs['StoryInputType'] | null; // StoryInputType
}
+ deleteProject: { // args
+ id: number; // Int!
+ }
deleteStory: { // args
id: number; // Int!
}
@@ -1083,6 +1224,9 @@ export interface NexusGenArgTypes {
updateProfileRoles: { // args
data?: NexusGenInputs['ProfileRolesInput'] | null; // ProfileRolesInput
}
+ updateProject: { // args
+ input?: NexusGenInputs['UpdateProjectInput'] | null; // UpdateProjectInput
+ }
updateTournamentRegistration: { // args
data?: NexusGenInputs['UpdateTournamentRegistrationInput'] | null; // UpdateTournamentRegistrationInput
tournament_id: number; // Int!
@@ -1106,6 +1250,10 @@ export interface NexusGenArgTypes {
skip?: number | null; // Int
take: number | null; // Int
}
+ checkValidProjectHashtag: { // args
+ hashtag: string; // String!
+ projectId?: number | null; // Int
+ }
getAllHackathons: { // args
sortBy?: string | null; // String
tag?: number | null; // Int
@@ -1171,6 +1319,9 @@ export interface NexusGenArgTypes {
skip?: number | null; // Int
take: number | null; // Int
}
+ searchUsers: { // args
+ value: string; // String!
+ }
similarMakers: { // args
id: number; // Int!
}
diff --git a/api/functions/graphql/schema.graphql b/api/functions/graphql/schema.graphql
index ebfa9b4..2481ecb 100644
--- a/api/functions/graphql/schema.graphql
+++ b/api/functions/graphql/schema.graphql
@@ -67,6 +67,12 @@ type BountyApplication {
workplan: String!
}
+type Capability {
+ icon: String!
+ id: Int!
+ title: String!
+}
+
type Category {
apps_count: Int!
cover_image: String
@@ -77,6 +83,34 @@ type Category {
votes_sum: Int!
}
+input CreateProjectInput {
+ capabilities: [Int!]!
+ category_id: Int!
+ cover_image: ImageInput!
+ description: String!
+ discord: String
+ github: String
+ hashtag: String!
+ id: Int
+ launch_status: ProjectLaunchStatusEnum!
+ lightning_address: String
+ members: [TeamMemberInput!]!
+ recruit_roles: [Int!]!
+ screenshots: [ImageInput!]!
+ slack: String
+ tagline: String!
+ telegram: String
+ thumbnail_image: ImageInput!
+ title: String!
+ tournaments: [Int!]!
+ twitter: String
+ website: String!
+}
+
+type CreateProjectResponse {
+ project: Project!
+}
+
"""Date custom scalar type"""
scalar Date
@@ -152,12 +186,15 @@ input MakerSkillInput {
type Mutation {
confirmDonation(payment_request: String!, preimage: String!): Donation!
confirmVote(payment_request: String!, preimage: String!): Vote!
+ createProject(input: CreateProjectInput): CreateProjectResponse
createStory(data: StoryInputType): Story
+ deleteProject(id: Int!): Project
deleteStory(id: Int!): Story
donate(amount_in_sat: Int!): Donation!
registerInTournament(data: RegisterInTournamentInput, tournament_id: Int!): User
updateProfileDetails(data: ProfileDetailsInput): MyProfile
updateProfileRoles(data: ProfileRolesInput): MyProfile
+ updateProject(input: UpdateProjectInput): CreateProjectResponse
updateTournamentRegistration(data: UpdateTournamentRegistrationInput, tournament_id: Int!): ParticipationInfo
updateUserPreferences(userKeys: [UserKeyInputType!]): MyProfile!
vote(amount_in_sat: Int!, item_id: Int!, item_type: VOTE_ITEM_TYPE!): Vote!
@@ -246,24 +283,55 @@ input ProfileRolesInput {
type Project {
awards: [Award!]!
+ capabilities: [Capability!]!
category: Category!
cover_image: String!
description: String!
+ discord: String
+ github: String
+ hashtag: String!
id: Int!
+ launch_status: ProjectLaunchStatusEnum!
lightning_address: String
lnurl_callback_url: String
+ members: [ProjectMember!]!
+ permissions: [ProjectPermissionEnum!]!
recruit_roles: [MakerRole!]!
screenshots: [String!]!
+ slack: String
+ tagline: String!
tags: [Tag!]!
+ telegram: String
thumbnail_image: String!
title: String!
+ tournaments: [Tournament!]!
+ twitter: String
votes_count: Int!
website: String!
}
+enum ProjectLaunchStatusEnum {
+ Launched
+ WIP
+}
+
+type ProjectMember {
+ role: TEAM_MEMBER_ROLE!
+ user: User!
+}
+
+enum ProjectPermissionEnum {
+ DeleteProject
+ UpdateAdmins
+ UpdateInfo
+ UpdateMembers
+}
+
type Query {
allCategories: [Category!]!
allProjects(skip: Int = 0, take: Int = 50): [Project!]!
+ checkValidProjectHashtag(hashtag: String!, projectId: Int): Boolean!
+ getAllCapabilities: [Capability!]!
getAllHackathons(sortBy: String, tag: Int): [Hackathon!]!
getAllMakersRoles: [GenericMakerRole!]!
getAllMakersSkills: [MakerSkill!]!
@@ -277,6 +345,7 @@ type Query {
getProject(id: Int!): Project!
getProjectsInTournament(roleId: Int, search: String, skip: Int = 0, take: Int = 10, tournamentId: Int!): TournamentProjectsResponse!
getTournamentById(id: Int!): Tournament!
+ getTournamentToRegister: [Tournament!]!
getTrendingPosts: [Post!]!
hottestProjects(skip: Int = 0, take: Int = 50): [Project!]!
me: MyProfile
@@ -286,6 +355,7 @@ type Query {
profile(id: Int!): User
projectsByCategory(category_id: Int!, skip: Int = 0, take: Int = 10): [Project!]!
searchProjects(search: String!, skip: Int = 0, take: Int = 50): [Project!]!
+ searchUsers(value: String!): [User!]!
similarMakers(id: Int!): [User!]!
tournamentParticipationInfo(tournamentId: Int!): ParticipationInfo
}
@@ -343,6 +413,12 @@ input StoryInputType {
title: String!
}
+enum TEAM_MEMBER_ROLE {
+ Admin
+ Maker
+ Owner
+}
+
type Tag {
description: String
icon: String
@@ -351,6 +427,11 @@ type Tag {
title: String!
}
+input TeamMemberInput {
+ id: Int!
+ role: TEAM_MEMBER_ROLE!
+}
+
type Tournament {
cover_image: String!
description: String!
@@ -430,6 +511,30 @@ type TournamentProjectsResponse {
projects: [Project!]!
}
+input UpdateProjectInput {
+ capabilities: [Int!]!
+ category_id: Int!
+ cover_image: ImageInput!
+ description: String!
+ discord: String
+ github: String
+ hashtag: String!
+ id: Int
+ launch_status: ProjectLaunchStatusEnum!
+ lightning_address: String
+ members: [TeamMemberInput!]!
+ recruit_roles: [Int!]!
+ screenshots: [ImageInput!]!
+ slack: String
+ tagline: String!
+ telegram: String
+ thumbnail_image: ImageInput!
+ title: String!
+ tournaments: [Int!]!
+ twitter: String
+ website: String!
+}
+
input UpdateTournamentRegistrationInput {
email: String
hacking_status: TournamentMakerHackingStatusEnum
diff --git a/api/functions/graphql/types/project.js b/api/functions/graphql/types/project.js
index 6a360c6..ef7bf46 100644
--- a/api/functions/graphql/types/project.js
+++ b/api/functions/graphql/types/project.js
@@ -1,23 +1,33 @@
+const { ApolloError } = require('apollo-server-lambda');
const {
intArg,
objectType,
stringArg,
extendType,
nonNull,
-} = require('nexus')
+ enumType,
+ inputObjectType,
+} = require('nexus');
+const { getUserByPubKey } = require('../../../auth/utils/helperFuncs');
const { prisma } = require('../../../prisma');
+const { deleteImage } = require('../../../services/imageUpload.service');
+const { logError } = require('../../../utils/logger');
const { resolveImgObjectToUrl } = require('../../../utils/resolveImageUrl');
-
const { paginationArgs, getLnurlDetails, lightningAddressToLnurl } = require('./helpers');
+const { ImageInput } = require('./misc');
const { MakerRole } = require('./users');
+
const Project = objectType({
name: 'Project',
definition(t) {
t.nonNull.int('id');
t.nonNull.string('title');
+ t.nonNull.string('tagline');
+ t.nonNull.string('website');
t.nonNull.string('description');
+ t.nonNull.string('hashtag');
t.nonNull.string('cover_image', {
async resolve(parent) {
return prisma.project.findUnique({ where: { id: parent.id } }).cover_image_rel().then(resolveImgObjectToUrl)
@@ -28,6 +38,14 @@ const Project = objectType({
return prisma.project.findUnique({ where: { id: parent.id } }).thumbnail_image_rel().then(resolveImgObjectToUrl)
}
});
+ t.nonNull.field('launch_status', {
+ type: ProjectLaunchStatusEnum
+ });
+ t.string('twitter');
+ t.string('discord');
+ t.string('github');
+ t.string('slack');
+ t.string('telegram');
t.nonNull.list.nonNull.string('screenshots', {
async resolve(parent) {
if (!parent.screenshots_ids) return null
@@ -42,7 +60,6 @@ const Project = objectType({
});
}
});
- t.nonNull.string('website');
t.string('lightning_address');
t.string('lnurl_callback_url');
t.nonNull.int('votes_count');
@@ -68,6 +85,40 @@ const Project = objectType({
}
})
+ t.nonNull.list.nonNull.field('members', {
+ type: ProjectMember,
+ resolve: (parent) => {
+ return prisma.projectMember.findMany({
+ where: {
+ projectId: parent.id
+ },
+ include: {
+ user: true
+ }
+ })
+ }
+ })
+
+
+ t.nonNull.list.nonNull.field('tournaments', {
+ type: "Tournament",
+ resolve: (parent) => {
+ return prisma.tournamentProject.findMany({
+ where: { project_id: parent.id },
+ include: {
+ tournament: true
+ }
+ }).then(res => res.map(item => item.tournament))
+ }
+ })
+
+ t.nonNull.list.nonNull.field('capabilities', {
+ type: Capability,
+ resolve: async (parent) => {
+ return prisma.project.findUnique({ where: { id: parent.id } }).capabilities()
+ }
+ })
+
t.nonNull.list.nonNull.field('recruit_roles', {
type: MakerRole,
resolve: async (parent) => {
@@ -89,6 +140,57 @@ const Project = objectType({
})
}
})
+
+ t.nonNull.list.nonNull.field('permissions', {
+ type: ProjectPermissionEnum,
+ resolve: async (parent, _, ctx) => {
+ const user = await getUserByPubKey(ctx.userPubKey)
+ if (!user) return [];
+
+ const role = (await prisma.projectMember.findUnique({ where: { projectId_userId: { projectId: parent.id, userId: user.id } } }))?.role;
+
+ if (!role) return [];
+
+ if (role === ROLE_ADMIN) return [PROJECT_PERMISSIONS.UpdateMembers, PROJECT_PERMISSIONS.UpdateInfo];
+
+ if (role === ROLE_OWNER) return Object.values(PROJECT_PERMISSIONS);
+
+ return []
+ }
+ })
+ }
+})
+
+const ROLE_OWNER = 'Owner'
+const ROLE_ADMIN = 'Admin'
+const ROLE_MAKER = 'Maker'
+
+const TEAM_MEMBER_ROLE = enumType({
+ name: 'TEAM_MEMBER_ROLE',
+ members: [ROLE_OWNER, ROLE_ADMIN, ROLE_MAKER],
+});
+
+const PROJECT_PERMISSIONS = {
+ UpdateInfo: "UpdateInfo",
+ DeleteProject: "DeleteProject",
+ UpdateAdmins: "UpdateAdmins",
+ UpdateMembers: "UpdateMembers",
+}
+
+const ProjectPermissionEnum = enumType({
+ name: 'ProjectPermissionEnum',
+ members: PROJECT_PERMISSIONS,
+});
+
+const ProjectMember = objectType({
+ name: "ProjectMember",
+ definition(t) {
+ t.nonNull.field('user', {
+ type: "User"
+ })
+ t.nonNull.field("role", {
+ type: TEAM_MEMBER_ROLE
+ })
}
})
@@ -110,6 +212,59 @@ const Award = objectType({
})
+const Capability = objectType({
+ name: 'Capability',
+ definition(t) {
+ t.nonNull.int('id');
+ t.nonNull.string('title');
+ t.nonNull.string('icon');
+ }
+})
+
+const checkValidProjectHashtag = extendType({
+ type: "Query",
+ definition(t) {
+ t.nonNull.boolean('checkValidProjectHashtag', {
+ args: {
+ hashtag: nonNull(stringArg()),
+ projectId: intArg(),
+ },
+ async resolve(parent, args, context) {
+ if (args.projectId) {
+ return !(await prisma.project.findFirst({
+ where: {
+ id: {
+ not: args.projectId,
+ },
+ hashtag: {
+ equals: args.hashtag
+ }
+ }
+ }))
+ }
+ return !(await prisma.project.findFirst({
+ where: {
+ hashtag: {
+ equals: args.hashtag
+ }
+ }
+ }))
+ }
+ })
+ }
+})
+
+const getAllCapabilities = extendType({
+ type: "Query",
+ definition(t) {
+ t.nonNull.list.nonNull.field('getAllCapabilities', {
+ type: Capability,
+ async resolve(parent, args, context) {
+ return prisma.capability.findMany();
+ }
+ })
+ }
+})
const getProject = extendType({
@@ -284,10 +439,638 @@ const getLnurlDetailsForProject = extendType({
}
})
+const TeamMemberInput = inputObjectType({
+ name: 'TeamMemberInput',
+ definition(t) {
+ t.nonNull.int('id')
+ t.nonNull.field("role", {
+ type: TEAM_MEMBER_ROLE
+ })
+ }
+})
+
+const ProjectLaunchStatusEnum = enumType({
+ name: 'ProjectLaunchStatusEnum',
+ members: ['WIP', 'Launched'],
+});
+
+const CreateProjectInput = inputObjectType({
+ name: 'CreateProjectInput',
+ definition(t) {
+ t.int('id') // exists in update
+ t.nonNull.string('title');
+ t.nonNull.string('hashtag');
+ t.nonNull.string('website');
+ t.nonNull.string('tagline');
+ t.nonNull.string('description');
+ t.nonNull.field('thumbnail_image', {
+ type: ImageInput
+ })
+ t.nonNull.field('cover_image', {
+ type: ImageInput
+ })
+ t.string('twitter');
+ t.string('discord');
+ t.string('github');
+ t.string('slack');
+ t.string('telegram');
+ t.string('lightning_address');
+ t.nonNull.int('category_id');
+ t.nonNull.list.nonNull.int('capabilities'); // ids
+ t.nonNull.list.nonNull.field('screenshots', {
+ type: ImageInput
+ });
+ t.nonNull.list.nonNull.field('members', {
+ type: TeamMemberInput
+ });
+ t.nonNull.list.nonNull.int('recruit_roles'); // ids
+ t.nonNull.field('launch_status', {
+ type: ProjectLaunchStatusEnum
+ });
+ t.nonNull.list.nonNull.int('tournaments'); // ids
+ }
+})
+
+const CreateProjectResponse = objectType({
+ name: 'CreateProjectResponse',
+ definition(t) {
+ t.nonNull.field('project', { type: Project })
+ }
+})
+
+
+
+const createProject = extendType({
+ type: 'Mutation',
+ definition(t) {
+ t.field('createProject', {
+ type: CreateProjectResponse,
+ args: { input: CreateProjectInput },
+ async resolve(_root, args, ctx) {
+ let {
+ title,
+ tagline,
+ hashtag,
+ description,
+ lightning_address,
+ capabilities,
+ category_id,
+ cover_image,
+ discord,
+ github,
+ slack,
+ telegram,
+ twitter,
+ website,
+ launch_status,
+ members,
+ recruit_roles,
+ screenshots,
+ thumbnail_image,
+ tournaments,
+ } = args.input
+
+ const user = await getUserByPubKey(ctx.userPubKey)
+
+ // Do some validation
+ if (!user) throw new ApolloError('Not Authenticated')
+
+ // Many Owners found. Throw an error
+ if (members.filter((m) => m.role === ROLE_OWNER).length > 1) {
+ throw new ApolloError('Only 1 owner can be defined.')
+ }
+
+ // No owner found. Set the current user as Owner
+ if (!members.find((m) => m.role === ROLE_OWNER)) {
+ const currentUser = members.find((m) => m.id === user.id)
+ if (currentUser) {
+ currentUser.role = ROLE_OWNER
+ } else {
+ members = [{ id: user.id, role: ROLE_OWNER }, ...members]
+ }
+ }
+
+ const coverImage = await prisma.hostedImage.findFirst({
+ where: {
+ provider_image_id: cover_image.id,
+ },
+ })
+
+ const coverImageRel = coverImage
+ ? {
+ cover_image_rel: {
+ connect: {
+ id: coverImage ? coverImage.id : null,
+ },
+ },
+ }
+ : {}
+
+ const thumbnailImage = await prisma.hostedImage.findFirst({
+ where: {
+ provider_image_id: thumbnail_image.id,
+ },
+ })
+
+ const thumbnailImageRel = thumbnailImage
+ ? {
+ thumbnail_image_rel: {
+ connect: {
+ id: thumbnailImage ? thumbnailImage.id : null,
+ },
+ },
+ }
+ : {}
+
+ const screenshots_ids = await prisma.hostedImage.findMany({
+ where: {
+ provider_image_id: {
+ in: screenshots.map((s) => s.id),
+ },
+ },
+ select: {
+ id: true,
+ },
+ })
+
+ const project = await prisma.project.create({
+ data: {
+ title,
+ description,
+ lightning_address,
+ tagline,
+ hashtag,
+ website,
+ discord,
+ github,
+ twitter,
+ slack,
+ telegram,
+ launch_status,
+
+ ...coverImageRel,
+ ...thumbnailImageRel,
+ screenshots_ids: screenshots_ids.map((s) => s.id),
+
+ category: {
+ connect: {
+ id: category_id,
+ },
+ },
+ members: {
+ create: members.map((member) => {
+ return {
+ role: member.role,
+ user: {
+ connect: {
+ id: member.id,
+ },
+ },
+ }
+ }),
+ },
+ recruit_roles: {
+ create: recruit_roles.map((role) => {
+ return {
+ level: 0,
+ role: {
+ connect: {
+ id: role,
+ },
+ },
+ }
+ }),
+ },
+ tournaments: {
+ create: tournaments.map((tournament) => {
+ return {
+ tournament: {
+ connect: {
+ id: tournament,
+ },
+ },
+ }
+ }),
+ },
+ capabilities: {
+ connect: capabilities.map((c) => {
+ return {
+ id: c,
+ }
+ }),
+ },
+ },
+ })
+
+ await prisma.hostedImage
+ .updateMany({
+ where: {
+ id: {
+ in: [coverImage.id, thumbnailImage.id, ...screenshots_ids.map((s) => s.id)],
+ },
+ },
+ data: {
+ is_used: true,
+ },
+ })
+ .catch((error) => {
+ logError(error)
+ throw new ApolloError('Unexpected error happened...')
+ })
+
+ return { project }
+ },
+ })
+ },
+})
+
+const UpdateProjectInput = inputObjectType({
+ name: 'UpdateProjectInput',
+ definition(t) {
+ t.int('id')
+ t.nonNull.string('title')
+ t.nonNull.string('hashtag')
+ t.nonNull.string('website')
+ t.nonNull.string('tagline')
+ t.nonNull.string('description')
+ t.nonNull.field('thumbnail_image', {
+ type: ImageInput,
+ })
+ t.nonNull.field('cover_image', {
+ type: ImageInput,
+ })
+ t.string('twitter')
+ t.string('discord')
+ t.string('github')
+ t.string('slack')
+ t.string('telegram')
+ t.string('lightning_address');
+ t.nonNull.int('category_id')
+ t.nonNull.list.nonNull.int('capabilities')
+ t.nonNull.list.nonNull.field('screenshots', {
+ type: ImageInput,
+ })
+ t.nonNull.list.nonNull.field('members', {
+ type: TeamMemberInput,
+ })
+ t.nonNull.list.nonNull.int('recruit_roles') // ids
+ t.nonNull.field('launch_status', {
+ type: ProjectLaunchStatusEnum,
+ })
+ t.nonNull.list.nonNull.int('tournaments') // ids
+ },
+})
+
+const updateProject = extendType({
+ type: 'Mutation',
+ definition(t) {
+ t.field('updateProject', {
+ type: CreateProjectResponse,
+ args: { input: UpdateProjectInput },
+ async resolve(_root, args, ctx) {
+ const {
+ id,
+ title,
+ tagline,
+ hashtag,
+ description,
+ lightning_address,
+ capabilities,
+ category_id,
+ cover_image,
+ discord,
+ github,
+ slack,
+ telegram,
+ twitter,
+ website,
+ launch_status,
+ members,
+ recruit_roles,
+ screenshots,
+ thumbnail_image,
+ tournaments,
+ } = args.input
+
+ const user = await getUserByPubKey(ctx.userPubKey)
+
+ // Do some validation
+ if (!user) throw new ApolloError('Not Authenticated')
+
+ const project = await prisma.project.findFirst({
+ where: {
+ id,
+ },
+ include: {
+ members: true,
+ },
+ })
+
+ // Verifying current user is a member
+ if (!project.members.some((m) => m.userId === user.id)) {
+ throw new ApolloError("You don't have permission to update this project")
+ }
+
+ // Maker can't change project info
+ if (project.members.find((m) => m.userId === user.id)?.role === ROLE_MAKER) {
+ throw new ApolloError("Makers can't change project info")
+ }
+
+ let newMembers = []
+
+ // Admin can only change makers
+ if (project.members.find((m) => m.userId === user.id)?.role === ROLE_ADMIN) {
+ // Changing Makers
+ const newMakers = members.filter((m) => m.role === ROLE_MAKER)
+
+ // Set old Admins and Owner using current project.memebers because Admin can't change these Roles
+ const currentAdminsOwner = project.members
+ .filter((m) => m.role === ROLE_ADMIN || m.role === ROLE_OWNER)
+ .map((m) => ({ id: m.userId, role: m.role }))
+
+ newMembers = [...newMakers, ...currentAdminsOwner]
+ } else {
+ // Curent user is Owner. Can change all users roles
+ newMembers = members
+ }
+
+ let imagesToDelete = []
+ let imagesToAdd = []
+
+ let coverImageRel = {}
+ if (cover_image.id) {
+ const coverImage = await prisma.hostedImage.findFirst({
+ where: {
+ provider_image_id: cover_image.id,
+ },
+ })
+
+ coverImageRel = coverImage
+ ? {
+ cover_image_rel: {
+ connect: {
+ id: coverImage ? coverImage.id : null,
+ },
+ },
+ }
+ : {}
+
+ if (coverImage) {
+ imagesToAdd.push(coverImage.id)
+ }
+
+ imagesToDelete.push(project.cover_image_id)
+ }
+
+ let thumbnailImageRel = {}
+ if (thumbnail_image.id) {
+ const thumbnailImage = await prisma.hostedImage.findFirst({
+ where: {
+ provider_image_id: thumbnail_image.id,
+ },
+ })
+
+ thumbnailImageRel = thumbnailImage
+ ? {
+ thumbnail_image_rel: {
+ connect: {
+ id: thumbnailImage ? thumbnailImage.id : null,
+ },
+ },
+ }
+ : {}
+
+ if (thumbnailImage) {
+ imagesToAdd.push(thumbnailImage.id)
+ }
+
+ imagesToDelete.push(project.thumbnail_image_id)
+ }
+
+ let screenshots_ids = []
+ for (const screenshot of screenshots) {
+ if (screenshot.id) {
+ const newScreenshot = await prisma.hostedImage.findFirst({
+ where: {
+ provider_image_id: screenshot.id,
+ },
+ select: {
+ id: true,
+ },
+ })
+ if (newScreenshot) {
+ screenshots_ids.push(newScreenshot.id)
+ imagesToAdd.push(newScreenshot.id)
+ }
+ } else {
+ const newScreenshot = await prisma.hostedImage.findFirst({
+ where: {
+ url: screenshot.url,
+ },
+ select: {
+ id: true,
+ },
+ })
+ if (newScreenshot) {
+ screenshots_ids.push(newScreenshot.id)
+ }
+ }
+ }
+ const screenshotsIdsToDelete = project.screenshots_ids.filter((x) => !screenshots_ids.includes(x))
+ imagesToDelete = [...imagesToDelete, ...screenshotsIdsToDelete]
+
+ const updatedProject = await prisma.project
+ .update({
+ where: {
+ id,
+ },
+ data: {
+ title,
+ description,
+ lightning_address,
+ tagline,
+ hashtag,
+ website,
+ discord,
+ github,
+ twitter,
+ slack,
+ telegram,
+ launch_status,
+
+ ...coverImageRel,
+ ...thumbnailImageRel,
+ screenshots_ids,
+
+ category: {
+ connect: {
+ id: category_id,
+ },
+ },
+ members: {
+ deleteMany: {},
+ create: newMembers.map((member) => {
+ return {
+ role: member.role,
+ user: {
+ connect: {
+ id: member.id,
+ },
+ },
+ }
+ }),
+ },
+ recruit_roles: {
+ deleteMany: {},
+ create: recruit_roles.map((role) => {
+ return {
+ level: 0,
+ role: {
+ connect: {
+ id: role,
+ },
+ },
+ }
+ }),
+ },
+ tournaments: {
+ deleteMany: {},
+ create: tournaments.map((tournament) => {
+ return {
+ tournament: {
+ connect: {
+ id: tournament,
+ },
+ },
+ }
+ }),
+ },
+ capabilities: {
+ set: capabilities.map((c) => {
+ return {
+ id: c,
+ }
+ }),
+ },
+ },
+ })
+ .catch((error) => {
+ logError(error)
+ throw new ApolloError('Unexpected error happened...')
+ })
+
+ if (imagesToAdd.length > 0) {
+ await prisma.hostedImage
+ .updateMany({
+ where: {
+ id: {
+ in: imagesToAdd,
+ },
+ },
+ data: {
+ is_used: true,
+ },
+ })
+ .catch((error) => {
+ logError(error)
+ throw new ApolloError('Unexpected error happened...')
+ })
+ }
+
+ imagesToDelete.map(async (i) => await deleteImage(i))
+
+ return { project: updatedProject }
+ },
+ })
+ },
+})
+
+const deleteProject = extendType({
+ type: 'Mutation',
+ definition(t) {
+ t.field('deleteProject', {
+ type: 'Project',
+ args: { id: nonNull(intArg()) },
+ async resolve(_root, args, ctx) {
+ const { id } = args
+ const user = await getUserByPubKey(ctx.userPubKey)
+
+ // Do some validation
+ if (!user) throw new ApolloError('Not Authenticated')
+
+ const project = await prisma.project.findFirst({
+ where: { id },
+ include: {
+ members: true,
+ },
+ })
+
+ if (!project) throw new ApolloError('Project not found')
+
+ if (project.members.find((m) => m.userId === user.id)?.role !== ROLE_OWNER)
+ throw new ApolloError("You don't have the right to delete this project")
+
+ // Award is not implemented yet
+ // await prisma.award.deleteMany({
+ // where: {
+ // projectId: project.id
+ // }
+ // })
+
+ await prisma.projectRecruitRoles.deleteMany({
+ where: {
+ projectId: project.id,
+ },
+ })
+
+ await prisma.projectMember.deleteMany({
+ where: {
+ projectId: project.id,
+ },
+ })
+
+ await prisma.tournamentProject.deleteMany({
+ where: {
+ project_id: project.id,
+ },
+ })
+
+ const deletedProject = await prisma.project.delete({
+ where: {
+ id,
+ },
+ })
+
+ const imagesToDelete = await prisma.hostedImage.findMany({
+ where: {
+ OR: [
+ { id: project.cover_image_id },
+ { id: project.thumbnail_image_id },
+ {
+ id: {
+ in: project.screenshots_ids,
+ },
+ },
+ ],
+ },
+ select: {
+ id: true,
+ },
+ })
+ imagesToDelete.map(async (i) => await deleteImage(i.id))
+
+ return deletedProject
+ },
+ })
+ },
+})
+
+
module.exports = {
// Types
Project,
Award,
+ TEAM_MEMBER_ROLE,
// Queries
getProject,
allProjects,
@@ -295,5 +1078,12 @@ module.exports = {
hottestProjects,
searchProjects,
projectsByCategory,
- getLnurlDetailsForProject
+ getLnurlDetailsForProject,
+ getAllCapabilities,
+ checkValidProjectHashtag,
+
+ // Mutations
+ createProject,
+ updateProject,
+ deleteProject,
}
\ No newline at end of file
diff --git a/api/functions/graphql/types/tournament.js b/api/functions/graphql/types/tournament.js
index 7e05b78..196f376 100644
--- a/api/functions/graphql/types/tournament.js
+++ b/api/functions/graphql/types/tournament.js
@@ -76,8 +76,6 @@ const TournamentMakerHackingStatusEnum = enumType({
},
});
-
-
const TournamentEvent = objectType({
name: 'TournamentEvent',
definition(t) {
@@ -214,6 +212,28 @@ const getTournamentById = extendType({
}
})
+
+const getTournamentToRegister = extendType({
+ type: "Query",
+ definition(t) {
+ t.nonNull.list.nonNull.field('getTournamentToRegister', {
+ type: Tournament,
+ args: {
+ },
+ resolve() {
+
+ return prisma.tournament.findMany({
+ where: {
+ end_date: {
+ gt: new Date()
+ },
+ }
+ })
+ }
+ })
+ }
+})
+
const ParticipationInfo = objectType({
name: "ParticipationInfo",
definition(t) {
@@ -538,6 +558,7 @@ module.exports = {
getMakersInTournament,
getProjectsInTournament,
tournamentParticipationInfo,
+ getTournamentToRegister,
// Mutations
registerInTournament,
diff --git a/api/functions/graphql/types/tournaments.js b/api/functions/graphql/types/tournaments.js
deleted file mode 100644
index e69de29..0000000
diff --git a/api/functions/graphql/types/users.js b/api/functions/graphql/types/users.js
index dfd3afd..b67b64c 100644
--- a/api/functions/graphql/types/users.js
+++ b/api/functions/graphql/types/users.js
@@ -1,6 +1,6 @@
const { prisma } = require('../../../prisma');
-const { objectType, extendType, intArg, nonNull, inputObjectType, interfaceType, list, enumType } = require("nexus");
+const { objectType, extendType, intArg, nonNull, inputObjectType, stringArg, interfaceType, list, enumType } = require("nexus");
const { getUserByPubKey } = require("../../../auth/utils/helperFuncs");
const { removeNulls } = require("./helpers");
const { ImageInput } = require('./misc');
@@ -245,6 +245,29 @@ const profile = extendType({
}
})
+const searchUsers = extendType({
+ type: "Query",
+ definition(t) {
+ t.nonNull.list.nonNull.field('searchUsers', {
+ type: "User",
+ args: {
+ value: nonNull(stringArg())
+ },
+ async resolve(_, { value }) {
+ return prisma.user.findMany({
+ where: {
+ name: {
+ contains: value,
+ mode: "insensitive"
+ }
+ },
+ })
+ }
+ })
+ }
+})
+
+
const similarMakers = extendType({
type: "Query",
definition(t) {
@@ -530,6 +553,7 @@ module.exports = {
// Queries
me,
profile,
+ searchUsers,
similarMakers,
getAllMakersRoles,
getAllMakersSkills,
diff --git a/api/prisma/index.js b/api/prisma/index.js
index 5befab5..a63f077 100644
--- a/api/prisma/index.js
+++ b/api/prisma/index.js
@@ -1,15 +1,19 @@
const { PrismaClient } = process.env.PRISMA_GENERATE_DATAPROXY ? require('@prisma/client/edge') : require('@prisma/client');
const createGlobalModule = require('../utils/createGlobalModule');
+
const createPrismaClient = () => {
console.log("New Prisma Client");
- return new PrismaClient({
- log: ["info"],
- });
+ try {
+ return new PrismaClient();
+ } catch (error) {
+ console.log(error);
+ }
}
const prisma = createGlobalModule('prisma', createPrismaClient)
+
module.exports = {
prisma
}
\ No newline at end of file
diff --git a/src/App.tsx b/src/App.tsx
index 4234abe..7de6cf1 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -11,6 +11,7 @@ import ProtectedRoute from "./Components/ProtectedRoute/ProtectedRoute";
import { Helmet } from "react-helmet";
import { NavbarLayout } from "./utils/routing/layouts";
import { Loadable, PAGES_ROUTES } from "./utils/routing";
+import ListProjectPage from "./features/Projects/pages/ListProjectPage/ListProjectPage";
@@ -98,6 +99,7 @@ function App() {
} />
} />
} />
+ } />
} />
} />
diff --git a/src/Components/Inputs/FilesInputs/CoverImageInput/CoverImageInput.tsx b/src/Components/Inputs/FilesInputs/CoverImageInput/CoverImageInput.tsx
index 975366d..b4975a4 100644
--- a/src/Components/Inputs/FilesInputs/CoverImageInput/CoverImageInput.tsx
+++ b/src/Components/Inputs/FilesInputs/CoverImageInput/CoverImageInput.tsx
@@ -34,7 +34,7 @@ export default function CoverImageInput(props: Props) {
wrapperClass='h-full'
render={({ img, isUploading, isDraggingOnWindow }) =>
- {!img &&
+ {!img &&
Drop a
COVER IMAGE here or
Click to browse
@@ -56,7 +56,7 @@ export default function CoverImageInput(props: Props) {
>}
{isUploading &&
{
if (item.completed > progress) {
setProgress(() => item.completed);
-
- if (item.completed === 100) {
- setItemState(STATES.DONE)
- } else {
- setItemState(STATES.PROGRESS)
- }
}
}, id);
+ useItemFinishListener(() => setItemState(STATES.DONE), id)
+
useItemAbortListener(item => {
@@ -41,7 +37,10 @@ function CustomImagePreview({ id, url }: PreviewComponentProps) {
}, id);
useItemErrorListener(item => {
+ console.log(item);
+
setItemState(STATES.ERROR);
+ setTimeout(() => setItemState(STATES.CANCELLED), 2000)
}, id);
if (itemState === STATES.DONE || itemState === STATES.CANCELLED)
diff --git a/src/Components/Inputs/FilesInputs/ScreenshotsInput/ScreenshotThumbnail.tsx b/src/Components/Inputs/FilesInputs/ScreenshotsInput/ScreenshotThumbnail.tsx
index 0003c43..5f64ac6 100644
--- a/src/Components/Inputs/FilesInputs/ScreenshotsInput/ScreenshotThumbnail.tsx
+++ b/src/Components/Inputs/FilesInputs/ScreenshotsInput/ScreenshotThumbnail.tsx
@@ -45,7 +45,7 @@ export default function ScreenshotThumbnail({ url, isLoading, isError, onCancel
Failed...
}
{!isEmpty &&
-
onCancel?.()}>
+
onCancel?.()}>
}
)
diff --git a/src/Components/Inputs/FilesInputs/ScreenshotsInput/ScreenshotsInput.stories.tsx b/src/Components/Inputs/FilesInputs/ScreenshotsInput/ScreenshotsInput.stories.tsx
index 1575707..e308f85 100644
--- a/src/Components/Inputs/FilesInputs/ScreenshotsInput/ScreenshotsInput.stories.tsx
+++ b/src/Components/Inputs/FilesInputs/ScreenshotsInput/ScreenshotsInput.stories.tsx
@@ -1,13 +1,14 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react';
-import ScreenshotsInput, { ScreenshotType } from './ScreenshotsInput';
+import ScreenshotsInput from './ScreenshotsInput';
import { WrapForm, WrapFormController } from 'src/utils/storybook/decorators';
+import { ImageInput } from 'src/graphql';
export default {
title: 'Shared/Inputs/Files Inputs/Screenshots',
component: ScreenshotsInput,
decorators: [
- WrapFormController<{ screenshots: Array
}>({
+ WrapFormController<{ screenshots: Array }>({
logValues: true,
name: "screenshots",
defaultValues: {
@@ -29,7 +30,7 @@ Empty.args = {
export const WithValues = Template.bind({});
WithValues.decorators = [
- WrapFormController<{ screenshots: Array }>({
+ WrapFormController<{ screenshots: Array }>({
logValues: true,
name: "screenshots",
defaultValues: {
@@ -51,7 +52,7 @@ WithValues.args = {
export const Full = Template.bind({});
Full.decorators = [
- WrapFormController<{ screenshots: Array }>({
+ WrapFormController<{ screenshots: Array }>({
logValues: true,
name: "screenshots",
defaultValues: {
diff --git a/src/Components/Inputs/FilesInputs/ScreenshotsInput/ScreenshotsInput.tsx b/src/Components/Inputs/FilesInputs/ScreenshotsInput/ScreenshotsInput.tsx
index b5bf6c0..c1b8aa2 100644
--- a/src/Components/Inputs/FilesInputs/ScreenshotsInput/ScreenshotsInput.tsx
+++ b/src/Components/Inputs/FilesInputs/ScreenshotsInput/ScreenshotsInput.tsx
@@ -11,6 +11,9 @@ import { getMockSenderEnhancer } from "@rpldy/mock-sender";
import ScreenshotThumbnail from "./ScreenshotThumbnail";
import { FiCamera } from "react-icons/fi";
import { Control, Path, useController } from "react-hook-form";
+import { ImageInput } from "src/graphql";
+import { fetchUploadImageUrl } from "src/api/uploading";
+import { removeArrayItemAtIndex } from "src/utils/helperFunctions";
@@ -20,15 +23,14 @@ const mockSenderEnhancer = getMockSenderEnhancer({
const MAX_UPLOAD_COUNT = 4 as const;
-export interface ScreenshotType {
- id: string,
- name: string,
- url: string;
+
+interface Image extends ImageInput {
+ local_id?: string
}
interface Props {
- value: ScreenshotType[],
- onChange: (new_value: ScreenshotType[]) => void
+ value: Image[],
+ onChange: (new_value: Image[]) => void
}
@@ -46,10 +48,10 @@ export default function ScreenshotsInput(props: Props) {
return (
{
setUploadingCount(v => v + batch.items.length)
@@ -57,31 +59,22 @@ export default function ScreenshotsInput(props: Props) {
[UPLOADER_EVENTS.ITEM_FINALIZE]: () => setUploadingCount(v => v - 1),
[UPLOADER_EVENTS.ITEM_FINISH]: (item) => {
- // Just for mocking purposes
- const dataUrl = URL.createObjectURL(item.file);
-
- const { id, filename, variants } = item?.uploadResponse?.data?.result ?? {
- id: Math.random().toString(),
- filename: item.file.name,
- variants: [
- "",
- dataUrl
- ]
- }
- if (id) {
- onChange([...uploadedFiles, { id, name: filename, url: variants[1] }].slice(-MAX_UPLOAD_COUNT))
+ const { id, filename, variants } = item?.uploadResponse?.data?.result;
+ const url = (variants as string[]).find(v => v.includes('public'));
+ if (id && url) {
+ onChange([...uploadedFiles, { id, local_id: id, name: filename, url: url }].slice(-MAX_UPLOAD_COUNT))
}
}
}}
>
- {canUploadMore &&
}
- {uploadedFiles.map(f =>
+ {uploadedFiles.map((f, idx) =>
{
- onChange(uploadedFiles.filter(file => file.id !== f.id))
+ onChange(removeArrayItemAtIndex(uploadedFiles, idx))
}} />)}
{(placeholdersCount > 0) &&
@@ -92,21 +85,22 @@ export default function ScreenshotsInput(props: Props) {
}
const DropZone = forwardRef((props, ref) => {
- const { onClick, ...buttonProps } = props;
+ const { canUploadMore, onClick, ...buttonProps } = props;
useRequestPreSend(async (data) => {
-
const filename = data.items?.[0].file.name ?? ''
- // const url = await fetchUploadUrl({ filename });
+ const res = await fetchUploadImageUrl({ filename });
+
return {
options: {
destination: {
- url: "URL"
- }
+ url: res.uploadURL
+ },
}
}
+
})
const onZoneClick = useCallback(
@@ -118,6 +112,8 @@ const DropZone = forwardRef((props, ref) => {
[onClick]
);
+ if (!canUploadMore) return null
+
return JSX.Element
+ renderAfter?: () => JSX.Element
+} & React.ComponentPropsWithoutRef<'input'>
+
+export default React.forwardRef(function TextInput({ className, inputClass, isError, renderBefore, renderAfter, ...props }, ref) {
+
+ return (
+
+ {renderBefore?.()}
+
+ {renderAfter?.()}
+
+ )
+})
diff --git a/src/Components/Inputs/TextareaInput/TextareaInput.tsx b/src/Components/Inputs/TextareaInput/TextareaInput.tsx
new file mode 100644
index 0000000..09eb276
--- /dev/null
+++ b/src/Components/Inputs/TextareaInput/TextareaInput.tsx
@@ -0,0 +1,27 @@
+import React from "react";
+
+type Props = {
+ isError?: boolean;
+ className?: string;
+ inputClass?: string
+} & React.ComponentPropsWithoutRef<'textarea'>
+
+export default React.forwardRef(function TextareaInput({ className, inputClass, isError, ...props }, ref) {
+
+ return (
+
+
+
+ )
+})
diff --git a/src/Components/Inputs/UsersInput/UsersInput.stories.tsx b/src/Components/Inputs/UsersInput/UsersInput.stories.tsx
new file mode 100644
index 0000000..464fce7
--- /dev/null
+++ b/src/Components/Inputs/UsersInput/UsersInput.stories.tsx
@@ -0,0 +1,29 @@
+import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { WrapForm } from 'src/utils/storybook/decorators';
+
+import UsersInput from './UsersInput';
+
+export default {
+ title: 'Shared/Inputs/Users Input',
+ component: UsersInput,
+ argTypes: {
+ backgroundColor: { control: 'color' },
+ },
+ decorators: [WrapForm({
+ defaultValues: {
+ tags: [{
+ title: "Webln"
+ }]
+ }
+ })]
+} as ComponentMeta;
+
+
+const Template: ComponentStory = (args) =>
+
+ Search for users:
+
+
+
+
+export const Default = Template.bind({});
diff --git a/src/Components/Inputs/UsersInput/UsersInput.tsx b/src/Components/Inputs/UsersInput/UsersInput.tsx
new file mode 100644
index 0000000..bb8024e
--- /dev/null
+++ b/src/Components/Inputs/UsersInput/UsersInput.tsx
@@ -0,0 +1,164 @@
+
+import AsyncSelect from 'react-select/async';
+import { OnChangeValue, StylesConfig, components, OptionProps } from "react-select";
+import { SearchUsersDocument, SearchUsersQuery, SearchUsersQueryResult } from "src/graphql";
+import { apolloClient } from "src/utils/apollo";
+import Avatar from "src/features/Profiles/Components/Avatar/Avatar";
+import { FiSearch } from 'react-icons/fi';
+import { useState } from 'react';
+import debounce from 'lodash.debounce';
+
+
+
+type User = SearchUsersQuery['searchUsers'][number]
+
+interface Props {
+ classes?: {
+ container?: string
+ input?: string
+ }
+ placeholder?: string,
+ onSelect?: (selectedUser: User) => void
+}
+
+const fetchOptions = debounce((value, callback: any) => {
+ apolloClient.query({
+ query: SearchUsersDocument,
+ variables: {
+ value
+ }
+ })
+ .then((result) => callback((result as SearchUsersQueryResult).data?.searchUsers ?? []))
+ .catch((error: any) => callback(error, null));
+}, 1000);
+
+
+
+
+const OptionComponent = (props: OptionProps) => {
+ return (
+
+
+
+
+
+ {props.data.name}
+
+
+ {props.data.jobTitle}
+
+
+
+
+
+ );
+};
+
+
+const colourStyles: StylesConfig = {
+
+ control: (styles, state) => ({
+ ...styles,
+ padding: '5px 16px',
+ borderRadius: 12,
+ // border: 'none',
+ // boxShadow: 'none',
+
+ ":hover": {
+ cursor: "pointer"
+ },
+ ":focus-within": {
+ '--tw-border-opacity': '1',
+ borderColor: 'rgb(179 160 255 / var(--tw-border-opacity))',
+ outlineColor: '#9E88FF',
+ '--tw-ring-offset-shadow': 'var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)',
+ '--tw-ring-shadow': 'var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color)',
+ boxShadow: 'var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000)',
+ '--tw-ring-color': 'rgb(179 160 255 / var(--tw-ring-opacity))',
+ '--tw-ring-opacity': '0.5'
+ }
+
+ }),
+ multiValueRemove: (styles) => ({
+ ...styles,
+ ":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
+ }),
+ valueContainer: (styles) => ({
+ ...styles,
+ paddingLeft: 0,
+ paddingRight: 0,
+ })
+}
+
+
+export default function UsersInput({
+ classes,
+ ...props }: Props) {
+
+ const [inputValue, setInputValue] = useState("")
+
+ const placeholder = props.placeholder ?? Search by username
+
+ const handleChange = (newValue: OnChangeValue,) => {
+ if (newValue)
+ props.onSelect?.(newValue);
+ }
+
+ let emptyMessage = "Type at least 2 characters";
+ if (inputValue.length >= 2)
+ emptyMessage = "Couldn't find any users..."
+
+
+ let loadingMessage = "Searching...";
+ if (inputValue.length < 2)
+ loadingMessage = "Type at least 2 characters"
+
+ return (
+
+
loadingMessage}
+ placeholder={placeholder}
+ noOptionsMessage={() => emptyMessage}
+ onChange={handleChange as any}
+ components={{
+ Option: OptionComponent,
+ // ValueContainer: CustomValueContainer
+ }}
+ styles={colourStyles as any}
+ theme={(theme) => ({
+ ...theme,
+ borderRadius: 8,
+ colors: {
+ ...theme.colors,
+ primary: 'var(--primary)',
+ },
+ })}
+ />
+ {/*
+ {(value as Tag[]).map((tag, idx) => handleRemove(idx)} >{tag.title} )}
+
*/}
+
+ )
+}
diff --git a/src/Components/Inputs/UsersInput/searchUsers.graphql b/src/Components/Inputs/UsersInput/searchUsers.graphql
new file mode 100644
index 0000000..f503aed
--- /dev/null
+++ b/src/Components/Inputs/UsersInput/searchUsers.graphql
@@ -0,0 +1,8 @@
+query SearchUsers($value: String!) {
+ searchUsers(value: $value) {
+ id
+ name
+ avatar
+ jobTitle
+ }
+}
diff --git a/src/features/Profiles/Components/Avatar/Avatar.tsx b/src/features/Profiles/Components/Avatar/Avatar.tsx
index 3864a52..5ceae65 100644
--- a/src/features/Profiles/Components/Avatar/Avatar.tsx
+++ b/src/features/Profiles/Components/Avatar/Avatar.tsx
@@ -1,16 +1,40 @@
+import { usePopperTooltip } from "react-popper-tooltip";
interface Props {
src: string;
alt?: string;
width?: number | string;
className?: string
+ renderTooltip?: () => JSX.Element
}
-export default function Avatar({ src, alt, className, width = 40 }: Props) {
+export default function Avatar({ src, alt, className, width = 40, renderTooltip }: Props) {
+
+ const {
+ getArrowProps,
+ getTooltipProps,
+ setTooltipRef,
+ setTriggerRef,
+ visible,
+ } = usePopperTooltip();
+
return (
-
+ <>
+
+ {
+ (renderTooltip && visible) && (
+
+ )
+ }
+ >
)
}
diff --git a/src/features/Projects/Components/ProjectCardMini/ProjectCardMini.Skeleton.tsx b/src/features/Projects/Components/ProjectCardMini/ProjectCardMini.Skeleton.tsx
index 0fe212d..d4824d8 100644
--- a/src/features/Projects/Components/ProjectCardMini/ProjectCardMini.Skeleton.tsx
+++ b/src/features/Projects/Components/ProjectCardMini/ProjectCardMini.Skeleton.tsx
@@ -3,7 +3,7 @@ import Skeleton from 'react-loading-skeleton'
export default function ProjectCardMiniSkeleton() {
return (
-
+
diff --git a/src/features/Projects/pages/ExplorePage/Header/Header.tsx b/src/features/Projects/pages/ExplorePage/Header/Header.tsx
index 055bcde..dcfd708 100644
--- a/src/features/Projects/pages/ExplorePage/Header/Header.tsx
+++ b/src/features/Projects/pages/ExplorePage/Header/Header.tsx
@@ -19,6 +19,7 @@ export const bannerData = {
link: {
content: "Register Now",
url: createRoute({ type: "tournament", id: 1 }),
+ newTab: false
},
}
@@ -29,7 +30,8 @@ const headerLinks = [
img: Assets.Images_ExploreHeader1,
link: {
content: "Submit project",
- url: "https://form.jotform.com/220301236112030",
+ url: createRoute({ type: "edit-project" }),
+ newTab: false,
},
},
];
@@ -90,7 +92,7 @@ export default function Header() {
{headerLinks[1].title}
-
+
{headerLinks[1].link.content}
diff --git a/src/features/Projects/pages/ListProjectPage/Components/CapabilitiesInput/CapabilitiesInput.stories.tsx b/src/features/Projects/pages/ListProjectPage/Components/CapabilitiesInput/CapabilitiesInput.stories.tsx
new file mode 100644
index 0000000..2839537
--- /dev/null
+++ b/src/features/Projects/pages/ListProjectPage/Components/CapabilitiesInput/CapabilitiesInput.stories.tsx
@@ -0,0 +1,21 @@
+import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { WrapFormController } from 'src/utils/storybook/utils';
+import CapabilitiesInput from './CapabilitiesInput';
+
+export default {
+ title: 'Projects/List Project Page/Inputs/Capabilites Input',
+ component: CapabilitiesInput,
+ argTypes: {
+ backgroundColor: { control: 'color' },
+ },
+} as ComponentMeta
;
+
+
+const Template: ComponentStory = (args) => WrapFormController('v', [])( )
+
+
+export const Default = Template.bind({});
+Default.args = {
+}
+
+
diff --git a/src/features/Projects/pages/ListProjectPage/Components/CapabilitiesInput/CapabilitiesInput.tsx b/src/features/Projects/pages/ListProjectPage/Components/CapabilitiesInput/CapabilitiesInput.tsx
new file mode 100644
index 0000000..8fa6837
--- /dev/null
+++ b/src/features/Projects/pages/ListProjectPage/Components/CapabilitiesInput/CapabilitiesInput.tsx
@@ -0,0 +1,55 @@
+import React from 'react'
+import Button from 'src/Components/Button/Button';
+import { useGetAllCapabilitiesQuery } from 'src/graphql';
+import { random } from 'src/utils/helperFunctions';
+
+interface Props {
+ value: number[];
+ onChange?: (v: number[]) => void;
+}
+
+export default function CapabilitiesInput(props: Props) {
+
+
+ const categoriesQuery = useGetAllCapabilitiesQuery();
+
+
+ const handleClick = (clickedValue: number) => {
+ if (props.value.includes(clickedValue))
+ props.onChange?.(props.value.filter(v => v !== clickedValue));
+ else
+ props.onChange?.([...props.value, clickedValue])
+ }
+
+
+ return (
+
+ {categoriesQuery.loading ?
+ Array(10).fill(0).map((_, idx) =>
+
+ {"loading category skeleton".slice(random(6, 12))}
+
)
+ :
+ categoriesQuery.data?.getAllCapabilities.map(item =>
+
handleClick(item.id)}
+ >
+ {item.icon} {item.title}
+ )
+ }
+
+ )
+}
\ No newline at end of file
diff --git a/src/features/Projects/pages/ListProjectPage/Components/CapabilitiesInput/getAllCapabilities.graphql b/src/features/Projects/pages/ListProjectPage/Components/CapabilitiesInput/getAllCapabilities.graphql
new file mode 100644
index 0000000..37e4b3f
--- /dev/null
+++ b/src/features/Projects/pages/ListProjectPage/Components/CapabilitiesInput/getAllCapabilities.graphql
@@ -0,0 +1,7 @@
+query GetAllCapabilities {
+ getAllCapabilities {
+ id
+ title
+ icon
+ }
+}
diff --git a/src/features/Projects/pages/ListProjectPage/Components/CategoriesInput/CategoriesInput.stories.tsx b/src/features/Projects/pages/ListProjectPage/Components/CategoriesInput/CategoriesInput.stories.tsx
new file mode 100644
index 0000000..e12ce6f
--- /dev/null
+++ b/src/features/Projects/pages/ListProjectPage/Components/CategoriesInput/CategoriesInput.stories.tsx
@@ -0,0 +1,20 @@
+import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { WrapFormController } from 'src/utils/storybook/utils';
+import CategoriesInput from './CategoriesInput';
+
+export default {
+ title: 'Projects/List Project Page/Inputs/Categories Input',
+ component: CategoriesInput,
+ argTypes: {
+ backgroundColor: { control: 'color' },
+ },
+} as ComponentMeta;
+
+
+const Template: ComponentStory = (args) => WrapFormController('v', [])( )
+
+export const Default = Template.bind({});
+Default.args = {
+}
+
+
diff --git a/src/features/Projects/pages/ListProjectPage/Components/CategoriesInput/CategoriesInput.tsx b/src/features/Projects/pages/ListProjectPage/Components/CategoriesInput/CategoriesInput.tsx
new file mode 100644
index 0000000..c5e8e18
--- /dev/null
+++ b/src/features/Projects/pages/ListProjectPage/Components/CategoriesInput/CategoriesInput.tsx
@@ -0,0 +1,46 @@
+import React from 'react'
+import Button from 'src/Components/Button/Button';
+import { useAllCategoriesQuery } from 'src/graphql'
+import { random } from 'src/utils/helperFunctions';
+
+interface Props {
+ value?: number;
+ onChange?: (v: number) => void;
+}
+
+export default function CategoriesInput(props: Props) {
+
+ const categoriesQuery = useAllCategoriesQuery();
+
+
+ return (
+
+ {categoriesQuery.loading ?
+ Array(10).fill(0).map((_, idx) =>
+
+ {"loading category skeleton".slice(random(6, 12))}
+
)
+ :
+ categoriesQuery.data?.allCategories.map(category =>
+
props.onChange?.(category.id)}
+ >
+ {category.icon} {category.title}
+ )
+ }
+
+ )
+}
diff --git a/src/features/Projects/pages/ListProjectPage/Components/ExtrasTab/ExtrasTab.stories.tsx b/src/features/Projects/pages/ListProjectPage/Components/ExtrasTab/ExtrasTab.stories.tsx
new file mode 100644
index 0000000..ceb7a6f
--- /dev/null
+++ b/src/features/Projects/pages/ListProjectPage/Components/ExtrasTab/ExtrasTab.stories.tsx
@@ -0,0 +1,20 @@
+import { ComponentStory, ComponentMeta } from '@storybook/react';
+import ExtrasTab from './ExtrasTab';
+
+export default {
+ title: 'Projects/List Project Page/Tabs/Extras',
+ component: ExtrasTab,
+ argTypes: {
+ backgroundColor: { control: 'color' },
+ },
+} as ComponentMeta;
+
+
+const Template: ComponentStory = (args) =>
+
+
+export const Default = Template.bind({});
+Default.args = {
+}
+
+
diff --git a/src/features/Projects/pages/ListProjectPage/Components/ExtrasTab/ExtrasTab.tsx b/src/features/Projects/pages/ListProjectPage/Components/ExtrasTab/ExtrasTab.tsx
new file mode 100644
index 0000000..18deacc
--- /dev/null
+++ b/src/features/Projects/pages/ListProjectPage/Components/ExtrasTab/ExtrasTab.tsx
@@ -0,0 +1,68 @@
+import { Controller, useFormContext } from "react-hook-form"
+import Card from "src/Components/Card/Card";
+import TournamentsInput from "../TournamentsInput/TournamentsInput";
+import { IListProjectForm } from "../FormContainer/FormContainer";
+import { ProjectLaunchStatusEnum } from "src/graphql";
+
+interface Props { }
+
+export default function ExtrasTab(props: Props) {
+
+ const { register, formState: { errors, }, control } = useFormContext();
+
+
+
+ return (
+
+
+ 🚀 Launch status
+ Has this product been launched already, or is it still a work in progress?
+
+
+
+
+
WIP 🛠️
+
It’s still a Work In Progress.
+
+
+
+
+
+
Launched 🚀
+
The product is ready for launch, or has been launched already.
+
+
+ {errors.launch_status &&
{errors.launch_status?.message}
}
+
+
+
+
+ ⚔️️ Tournaments
+ Is your application part of a tournament? If so, select the tournament(s) that apply and it will automatically be listed for you.
+
+
(
+
+ )}
+ />
+ {errors.tournaments && {errors.tournaments?.message}
}
+
+
+
+ )
+}
diff --git a/src/features/Projects/pages/ListProjectPage/Components/FormContainer/FormContainer.tsx b/src/features/Projects/pages/ListProjectPage/Components/FormContainer/FormContainer.tsx
new file mode 100644
index 0000000..cceed97
--- /dev/null
+++ b/src/features/Projects/pages/ListProjectPage/Components/FormContainer/FormContainer.tsx
@@ -0,0 +1,230 @@
+import { FormProvider, NestedValue, Resolver, SubmitHandler, useForm } from "react-hook-form"
+import * as yup from "yup";
+import { yupResolver } from "@hookform/resolvers/yup";
+import { IsValidProjectHashtagDocument, ProjectDetailsQuery, ProjectLaunchStatusEnum, ProjectPermissionEnum, Team_Member_Role, UpdateProjectInput, useProjectDetailsQuery } from "src/graphql";
+import { PropsWithChildren } from "react";
+import { useSearchParams } from "react-router-dom";
+import { usePrompt } from "src/utils/hooks";
+import { imageSchema } from "src/utils/validation";
+import { Override } from "src/utils/interfaces";
+import LoadingPage from "src/Components/LoadingPage/LoadingPage";
+import { apolloClient } from "src/utils/apollo";
+import { store } from "src/redux/store";
+import UpdateProjectContextProvider from './updateProjectContext'
+import { useNavigate } from 'react-router-dom'
+import { createRoute } from 'src/utils/routing'
+import { nanoid } from "@reduxjs/toolkit";
+
+
+interface Props {
+
+}
+
+export type IListProjectForm = Override
+ capabilities: NestedValue
+ recruit_roles: NestedValue
+ tournaments: NestedValue
+ cover_image: NestedValue
+ thumbnail_image: NestedValue
+}>
+
+export type ProjectMember = {
+ id: number,
+ name: string,
+ jobTitle: string | null,
+ avatar: string,
+ role: Team_Member_Role,
+}
+
+const schema: yup.SchemaOf = yup.object({
+ id: yup.number().optional(),
+ title: yup.string().trim().required("please provide a title").min(2),
+ hashtag: yup
+ .string()
+ .required("please provide a project tag")
+ .transform(v => v ? '#' + v : undefined)
+ .matches(
+ /^#[^ !@#$%^&*(),.?":{}|<>]*$/,
+ "your project's tag can only contain letters, numbers and '_’"
+ )
+ .min(3, "your project tag must be longer than 2 characters.")
+ .max(35, 'your project tag must be shorter than 35 characters.')
+ .test({
+ name: "is unique hashtag",
+ test: async (value, context) => {
+ // TODO: debounce this validation function
+ try {
+ const res = await apolloClient.query({
+ query: IsValidProjectHashtagDocument,
+ variables: {
+ hashtag: value,
+ projectId: context.parent.id
+ }
+ })
+ if (res.data.checkValidProjectHashtag) return true;
+ return false;
+ } catch (error) {
+ return false;
+ }
+ },
+ message: "this hashtag is already used by another project"
+ }),
+ website: yup.string().trim().url().required().label("project's link"),
+ tagline: yup.string().trim().required("please provide a tagline").min(10),
+ description: yup.string().trim().required("please provide a description for your project").min(50, 'Write at least 10 words descriping your project'),
+ lightning_address: yup
+ .string()
+ .test({
+ name: "is valid lightning_address",
+ test: async value => {
+ try {
+ if (value) {
+ const [name, domain] = value.split("@");
+ const lnurl = `https://${domain}/.well-known/lnurlp/${name}`;
+ const res = await fetch(lnurl);
+ if (res.status === 200) return true;
+ }
+ return true;
+ } catch (error) {
+ return false;
+ }
+ },
+ message: "this lightning address isn't valid"
+ })
+ .nullable()
+ .label("lightning address"),
+ thumbnail_image: imageSchema.required("please pick a thumbnail image").default(undefined),
+ cover_image: imageSchema.required("please pick a cover image").default(undefined),
+ twitter: yup.string().url().nullable(),
+ discord: yup.string().url().nullable(),
+ github: yup.string().url().nullable(),
+ slack: yup.string().url().nullable(),
+ telegram: yup.string().url().nullable(),
+ category_id: yup.number().required("please choose a category"),
+ capabilities: yup.array().of(yup.number().required()).default([]),
+ screenshots: yup.array().of(imageSchema.required()).default([]),
+ members: yup.array().of(yup.object() as any).default([]),
+ recruit_roles: yup.array().of(yup.number().required()).default([]),
+ launch_status: yup.mixed().oneOf([ProjectLaunchStatusEnum.Wip, ProjectLaunchStatusEnum.Launched]).default(ProjectLaunchStatusEnum.Wip),
+ tournaments: yup.array().of(yup.number().required()).default([])
+}).required();
+
+export default function FormContainer(props: PropsWithChildren) {
+
+ const [params] = useSearchParams();
+
+ const id = params.get('id') ? Number(params.get('id')) : null;
+
+ const isUpdating = !!id;
+ const navigate = useNavigate()
+
+
+ const methods = useForm({
+ defaultValues: {
+ cover_image: undefined,
+ thumbnail_image: undefined,
+ id: isUpdating ? id : undefined,
+ title: "",
+ website: "",
+ tagline: "",
+ description: "",
+ category_id: undefined,
+ capabilities: [],
+ screenshots: [],
+ members: prepareMembers([]),
+ recruit_roles: [],
+ launch_status: ProjectLaunchStatusEnum.Wip,
+ tournaments: [],
+ },
+ resolver: yupResolver(schema) as Resolver,
+ mode: 'onTouched'
+ });
+
+ const query = useProjectDetailsQuery({
+ variables: {
+ projectId: id!
+ },
+ skip: !isUpdating,
+ onCompleted: (res) => {
+ if (res.getProject) {
+ const data = res.getProject
+ if (!res.getProject.permissions.includes(ProjectPermissionEnum.UpdateInfo))
+ navigate({ pathname: createRoute({ type: "projects-page" }) })
+ else
+ methods.reset({
+ id: data.id,
+ title: data.title,
+ cover_image: { url: data.cover_image },
+ thumbnail_image: { url: data.thumbnail_image },
+ tagline: data.tagline,
+ website: data.website,
+ description: data.description,
+ hashtag: data.hashtag.slice(1),
+ twitter: data.twitter,
+ discord: data.discord,
+ slack: data.slack,
+ telegram: data.telegram,
+ github: data.github,
+ lightning_address: data.lightning_address,
+ category_id: data.category.id,
+ capabilities: data.capabilities.map(c => c.id),
+ screenshots: data.screenshots.map(url => ({ url, local_id: nanoid(5), })),
+
+ members: prepareMembers(data.members),
+ recruit_roles: data.recruit_roles.map(r => r.id),
+
+ tournaments: [],
+ launch_status: data.launch_status,
+ })
+ }
+ }
+ })
+
+
+ usePrompt('You may have some unsaved changes. You still want to leave?', methods.formState.isDirty)
+
+ const onSubmit: SubmitHandler = data => console.log(data);
+
+
+ if (query.loading)
+ return
+
+ return (
+
+
+
+
+
+ )
+}
+
+
+function prepareMembers(members: ProjectDetailsQuery['getProject']['members']): ProjectMember[] {
+
+ const me = store.getState().user.me;
+
+ if (!me) {
+ window.location.href = '/login';
+ return [];
+ }
+
+ if (members.length === 0)
+ return [{
+ id: me.id,
+ avatar: me.avatar,
+ name: me.name,
+ jobTitle: me.jobTitle,
+ role: Team_Member_Role.Owner,
+ }]
+
+ const _members = members.map(({ role, user }) => ({ role, id: user.id, avatar: user.avatar, name: user.name, jobTitle: user.jobTitle }))
+
+ const myMember = _members.find(m => m.id === me.id);
+
+ if (!myMember) throw new Error("Not a member of the project")
+
+ return [myMember, ..._members.filter(m => m.id !== me.id)]
+}
\ No newline at end of file
diff --git a/src/features/Projects/pages/ListProjectPage/Components/FormContainer/listProject.graphql b/src/features/Projects/pages/ListProjectPage/Components/FormContainer/listProject.graphql
new file mode 100644
index 0000000..6eb9015
--- /dev/null
+++ b/src/features/Projects/pages/ListProjectPage/Components/FormContainer/listProject.graphql
@@ -0,0 +1,72 @@
+fragment ProjectDetails on Project {
+ id
+ title
+ tagline
+ description
+ hashtag
+ cover_image
+ thumbnail_image
+ launch_status
+ twitter
+ discord
+ github
+ slack
+ telegram
+ screenshots
+ website
+ lightning_address
+ votes_count
+ category {
+ id
+ icon
+ title
+ }
+ permissions
+ members {
+ role
+ user {
+ id
+ name
+ jobTitle
+ avatar
+ }
+ }
+ awards {
+ title
+ image
+ url
+ id
+ }
+ tags {
+ id
+ title
+ }
+ recruit_roles {
+ id
+ title
+ icon
+ level
+ }
+
+ capabilities {
+ id
+ title
+ icon
+ }
+}
+
+mutation CreateProject($input: CreateProjectInput) {
+ createProject(input: $input) {
+ project {
+ ...ProjectDetails
+ }
+ }
+}
+
+mutation UpdateProject($input: UpdateProjectInput) {
+ updateProject(input: $input) {
+ project {
+ ...ProjectDetails
+ }
+ }
+}
diff --git a/src/features/Projects/pages/ListProjectPage/Components/FormContainer/updateProjectContext.tsx b/src/features/Projects/pages/ListProjectPage/Components/FormContainer/updateProjectContext.tsx
new file mode 100644
index 0000000..08144b9
--- /dev/null
+++ b/src/features/Projects/pages/ListProjectPage/Components/FormContainer/updateProjectContext.tsx
@@ -0,0 +1,31 @@
+import React, { PropsWithChildren } from 'react'
+import { ProjectDetailsQuery } from 'src/graphql'
+
+interface Props {
+ permissions: ProjectDetailsQuery['getProject']['permissions']
+}
+
+interface State {
+ permissions: ProjectDetailsQuery['getProject']['permissions']
+}
+
+const context = React.createContext(undefined)
+
+const UpdateProjectContextProvider = React.memo(function (props: PropsWithChildren) {
+
+ return (
+
+ {props.children}
+
+ )
+})
+
+export default UpdateProjectContextProvider;
+
+export const useUpdateProjectContext = () => {
+ const res = React.useContext(context);
+
+ if (!res) throw new Error("No context provider was found for useUpdateProjectContext")
+
+ return res;
+}
diff --git a/src/features/Projects/pages/ListProjectPage/Components/ProjectDetailsTab/ProjectDetailsTab.stories.tsx b/src/features/Projects/pages/ListProjectPage/Components/ProjectDetailsTab/ProjectDetailsTab.stories.tsx
new file mode 100644
index 0000000..6303394
--- /dev/null
+++ b/src/features/Projects/pages/ListProjectPage/Components/ProjectDetailsTab/ProjectDetailsTab.stories.tsx
@@ -0,0 +1,20 @@
+import { ComponentStory, ComponentMeta } from '@storybook/react';
+import ProjectDetailsTab from './ProjectDetailsTab';
+
+export default {
+ title: 'Projects/List Project Page/Tabs/Project Details',
+ component: ProjectDetailsTab,
+ argTypes: {
+ backgroundColor: { control: 'color' },
+ },
+} as ComponentMeta;
+
+
+const Template: ComponentStory = (args) =>
+
+
+export const Default = Template.bind({});
+Default.args = {
+}
+
+
diff --git a/src/features/Projects/pages/ListProjectPage/Components/ProjectDetailsTab/ProjectDetailsTab.tsx b/src/features/Projects/pages/ListProjectPage/Components/ProjectDetailsTab/ProjectDetailsTab.tsx
new file mode 100644
index 0000000..9fcdb01
--- /dev/null
+++ b/src/features/Projects/pages/ListProjectPage/Components/ProjectDetailsTab/ProjectDetailsTab.tsx
@@ -0,0 +1,277 @@
+import { Controller, useFormContext } from "react-hook-form"
+import Card from "src/Components/Card/Card";
+import { FaDiscord, FaSlack, FaTelegram } from "react-icons/fa";
+import { FiCamera, FiGithub, FiTwitter } from "react-icons/fi";
+import CategoriesInput from "../CategoriesInput/CategoriesInput";
+import CapabilitiesInput from "../CapabilitiesInput/CapabilitiesInput";
+import { IListProjectForm } from "../FormContainer/FormContainer";
+import AvatarInput from "src/Components/Inputs/FilesInputs/AvatarInput/AvatarInput";
+import CoverImageInput from "src/Components/Inputs/FilesInputs/CoverImageInput/CoverImageInput";
+import ScreenshotsInput from "src/Components/Inputs/FilesInputs/ScreenshotsInput/ScreenshotsInput";
+import { BsLightningChargeFill } from "react-icons/bs";
+import InfoCard from "src/Components/InfoCard/InfoCard";
+import TextInput from "src/Components/Inputs/TextInput/TextInput";
+import TextareaInput from "src/Components/Inputs/TextareaInput/TextareaInput";
+
+interface Props { }
+
+export default function ProjectDetailsTab(props: Props) {
+
+ const { register, formState: { errors, dirtyFields }, control, getValues } = useFormContext();
+
+ const isUpdating = !!getValues('id');
+
+
+ return (
+
+
+
+
{
+ onChange(e)
+ }}
+ />
+
+ }
+ />
+
+
+
+
+ {(errors.cover_image || errors.thumbnail_image) &&
+ {errors.cover_image &&
+ {errors.cover_image.message}
+
}
+ {errors.thumbnail_image &&
+ {errors.thumbnail_image.message}
+
}
+
}
+
+ Project name*
+
+
+ {errors.title &&
+ {errors.title.message}
+
}
+
+ Project link*
+
+
+ {errors.website &&
+ {errors.website.message}
+
}
+
+ Tagline*
+
+
+ {errors.tagline &&
+ {errors.tagline.message}
+
}
+
+ Description*
+
+
+ {errors.description &&
+ {errors.description.message}
+
}
+
+ Project tag*
+
+
# }
+ {...register("hashtag")}
+ />
+ {errors.hashtag &&
+ {errors.hashtag.message}
+
}
+ {(isUpdating && dirtyFields.hashtag) &&
+
+ ⚠️ Warning: when you change the tag of your project, existing links that use this tag will no longer work & will need to be updateded.
+ }
+ {!isUpdating &&
+
+ ℹ️ Project tag allows you to mention your project in stories, or across other platforms like Discord. You can change your project’s tag later, but links that use the old tag will no longer work & need to be updated.
+
+ }
+
+
+
+
+ 🔗 Links
+ Make sure that people can find your project online.
+
+
+
+
}
+ placeholder='https://twitter.com/project_handle'
+ {...register("twitter")}
+ />
+ {errors.twitter &&
+ {errors.twitter.message}
+
}
+
+
+
+
}
+ placeholder='https://discord.com/'
+ {...register("discord")}
+ />
+ {errors.discord &&
+ {errors.discord.message}
+
}
+
+
+
+
}
+ placeholder='https://github.com/'
+ {...register("github")}
+ />
+ {errors.github &&
+ {errors.github.message}
+
}
+
+
+
}
+ placeholder='https://slack.com/'
+ {...register("slack")}
+ />
+ {errors.slack &&
+ {errors.slack.message}
+
}
+
+
+
}
+ placeholder='https://t.me/XXXXXX'
+ {...register("telegram")}
+ />
+ {errors.telegram &&
+ {errors.telegram.message}
+
}
+
+
+
+
}
+ placeholder='lightning_address@XXX.com'
+ {...register("lightning_address")}
+ />
+ {errors.lightning_address &&
+ {errors.lightning_address.message}
+
}
+
+
+
+
+
+ 🌶️ Category*
+ Select one of the categories below.
+
+
(
+
+ )}
+ />
+ {errors.category_id && {errors.category_id?.message}
}
+
+
+
+
+ 🦾 Capabilities
+ Let other makers know what lightning capabilities your application has.
+
+
(
+
+ )}
+ />
+ {errors.capabilities && {errors.capabilities?.message}
}
+
+
+
+
+ 📷 Screenshots
+ Choose up to 4 screenshots from your project
+
+
{
+ onChange(e)
+ }}
+ />
+
+ }
+ />
+ {errors.capabilities && {errors.capabilities?.message}
}
+
+
+
+ )
+}
diff --git a/src/features/Projects/pages/ListProjectPage/Components/ProjectDetailsTab/checkValidProjectHashtag.graphql b/src/features/Projects/pages/ListProjectPage/Components/ProjectDetailsTab/checkValidProjectHashtag.graphql
new file mode 100644
index 0000000..a24cee3
--- /dev/null
+++ b/src/features/Projects/pages/ListProjectPage/Components/ProjectDetailsTab/checkValidProjectHashtag.graphql
@@ -0,0 +1,3 @@
+query IsValidProjectHashtag($hashtag: String!, $projectId: Int) {
+ checkValidProjectHashtag(hashtag: $hashtag, projectId: $projectId)
+}
diff --git a/src/features/Projects/pages/ListProjectPage/Components/ProjectListedModal/ProjectDetailsTab.stories.tsx b/src/features/Projects/pages/ListProjectPage/Components/ProjectListedModal/ProjectDetailsTab.stories.tsx
new file mode 100644
index 0000000..e3dbd67
--- /dev/null
+++ b/src/features/Projects/pages/ListProjectPage/Components/ProjectListedModal/ProjectDetailsTab.stories.tsx
@@ -0,0 +1,26 @@
+import { ComponentStory, ComponentMeta } from '@storybook/react';
+import ProjectListedModal from './ProjectListedModal';
+
+export default {
+ title: 'Projects/List Project Page/Modals/Project Listed Modal',
+ component: ProjectListedModal,
+ argTypes: {
+ backgroundColor: { control: 'color' },
+ },
+} as ComponentMeta;
+
+
+const Template: ComponentStory = (args) =>
+
+
+export const Default = Template.bind({});
+Default.args = {
+ project: {
+ id: 12,
+ name: "BOLT FUN",
+ img: "https://picsum.photos/id/870/150/150.jpg",
+ tagline: "An awesome directory for lightning projects and makers"
+ }
+}
+
+
diff --git a/src/features/Projects/pages/ListProjectPage/Components/ProjectListedModal/ProjectListedModal.tsx b/src/features/Projects/pages/ListProjectPage/Components/ProjectListedModal/ProjectListedModal.tsx
new file mode 100644
index 0000000..0964b6c
--- /dev/null
+++ b/src/features/Projects/pages/ListProjectPage/Components/ProjectListedModal/ProjectListedModal.tsx
@@ -0,0 +1,96 @@
+import { motion } from 'framer-motion'
+import { useAppSelector, useMediaQuery, useWindowSize } from 'src/utils/hooks';
+import { ModalCard, modalCardVariants } from 'src/Components/Modals/ModalsContainer/ModalsContainer'
+import Button from 'src/Components/Button/Button'
+import { IoClose } from 'react-icons/io5';
+import NutImg from './nut.png'
+import AlbyImg from './alby.png'
+import Avatar from 'src/features/Profiles/Components/Avatar/Avatar';
+import { createRoute } from 'src/utils/routing';
+import Confetti from 'react-confetti'
+import { Portal } from 'src/Components/Portal/Portal';
+
+
+interface Props extends ModalCard {
+ project: {
+ id: number,
+ img: string,
+ name: string,
+ tagline: string,
+ }
+}
+
+export default function ProjectListedModal({ onClose, direction, project, ...props }: Props) {
+
+ const size = useWindowSize();
+
+ const isSmallScreen = useMediaQuery('screen and (max-width: 680px)')
+
+ return (
+
+
+
+
+
+ Product listed!
+
+
+ Nice work, you successfully listed your product! Here are a few ideas to get your started.
+
+
+
+
+
+ ✍️
+
+
+
+ Stories
+
+
+ Tell the maker community about your product.
+
+
+
+
+
+ ⚔️
+
+
+
+ Start hacking
+
+
+ Kickstart your hacking journey with a tournament.
+
+
+
+
+
+
+ ✍️ Write a story
+ ⚔️ Explore tournaments
+
+
+ )
+}
diff --git a/src/features/Projects/pages/ListProjectPage/Components/ProjectListedModal/index.ts b/src/features/Projects/pages/ListProjectPage/Components/ProjectListedModal/index.ts
new file mode 100644
index 0000000..c93533d
--- /dev/null
+++ b/src/features/Projects/pages/ListProjectPage/Components/ProjectListedModal/index.ts
@@ -0,0 +1,4 @@
+
+import { lazyModal } from 'src/utils/helperFunctions';
+
+export const { LazyComponent: ProjectListedModal } = lazyModal(() => import('./ProjectListedModal'))
\ No newline at end of file
diff --git a/src/features/Projects/pages/ListProjectPage/Components/RecruitRolesInput/RecruitRolesInput.tsx b/src/features/Projects/pages/ListProjectPage/Components/RecruitRolesInput/RecruitRolesInput.tsx
new file mode 100644
index 0000000..e594680
--- /dev/null
+++ b/src/features/Projects/pages/ListProjectPage/Components/RecruitRolesInput/RecruitRolesInput.tsx
@@ -0,0 +1,100 @@
+
+import Button from 'src/Components/Button/Button';
+import { useGetAllRolesQuery } from 'src/graphql';
+import { random } from 'src/utils/helperFunctions';
+
+interface Props {
+ value: number[];
+ onChange?: (v: number[]) => void;
+}
+
+export default function RecruitRolesInput(props: Props) {
+
+ const query = useGetAllRolesQuery();
+
+ const handleClick = (clickedValue: number) => {
+ if (props.value.includes(clickedValue))
+ props.onChange?.(props.value.filter(v => v !== clickedValue));
+ else
+ props.onChange?.([...props.value, clickedValue])
+ }
+
+
+ return (
+
+ {query.loading ?
+ Array(10).fill(0).map((_, idx) =>
+
+ {"loading category skeleton".slice(random(6, 12))}
+
)
+ :
+ query.data?.getAllMakersRoles.map(item =>
+
handleClick(item.id)}
+ >
+ {item.icon} {item.title}
+ )
+ }
+
+ )
+}
+
+const data = [
+ {
+ text: 'Frontend Dev',
+ icon: '💄️'
+ },
+ {
+ text: 'Backend Dev',
+ icon: '💻'
+ },
+ {
+ text: 'UI/UX Designer',
+ icon: '🌈️️'
+ },
+ {
+ text: 'Comm. Manager',
+ icon: '🎉️️'
+ },
+ {
+ text: 'Founder',
+ icon: '🦄️'
+ },
+ {
+ text: 'Marketer',
+ icon: '🚨️'
+ },
+ {
+ text: 'Content Creator',
+ icon: '🎥️'
+ },
+ {
+ text: 'Researcher',
+ icon: '🔬'
+ },
+ {
+ text: 'Data engineer',
+ icon: '💿️'
+ },
+ {
+ text: 'Growth hacker',
+ icon: '📉️'
+ },
+ {
+ text: 'Technical Writer',
+ icon: '✍️️'
+ },
+]
diff --git a/src/features/Projects/pages/ListProjectPage/Components/RecruitRolesInput/RecruiterRolesInput.stories.tsx b/src/features/Projects/pages/ListProjectPage/Components/RecruitRolesInput/RecruiterRolesInput.stories.tsx
new file mode 100644
index 0000000..81dd2fa
--- /dev/null
+++ b/src/features/Projects/pages/ListProjectPage/Components/RecruitRolesInput/RecruiterRolesInput.stories.tsx
@@ -0,0 +1,21 @@
+import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { WrapFormController } from 'src/utils/storybook/utils';
+import RecruitRolesInput from './RecruitRolesInput';
+
+export default {
+ title: 'Projects/List Project Page/Inputs/Recruiter Roles Input',
+ component: RecruitRolesInput,
+ argTypes: {
+ backgroundColor: { control: 'color' },
+ },
+} as ComponentMeta;
+
+
+const Template: ComponentStory = (args) => WrapFormController('v', [])( )
+
+
+export const Default = Template.bind({});
+Default.args = {
+}
+
+
diff --git a/src/features/Projects/pages/ListProjectPage/Components/SaveChangesCard/SaveChangesCard.tsx b/src/features/Projects/pages/ListProjectPage/Components/SaveChangesCard/SaveChangesCard.tsx
new file mode 100644
index 0000000..c3b6256
--- /dev/null
+++ b/src/features/Projects/pages/ListProjectPage/Components/SaveChangesCard/SaveChangesCard.tsx
@@ -0,0 +1,142 @@
+import Button from 'src/Components/Button/Button'
+import Card from 'src/Components/Card/Card'
+import Avatar from 'src/features/Profiles/Components/Avatar/Avatar'
+import { useFormContext } from "react-hook-form"
+import { IListProjectForm } from "../FormContainer/FormContainer";
+import { useMemo } from 'react'
+import { tabs } from '../../ListProjectPage'
+import { NotificationsService } from 'src/services'
+import { useAppDispatch } from 'src/utils/hooks';
+import { openModal } from 'src/redux/features/modals.slice';
+import { useCreateProjectMutation, useUpdateProjectMutation, UpdateProjectInput } from 'src/graphql'
+
+interface Props {
+ currentTab: keyof typeof tabs
+ onNext: () => void
+ onBackToFirstPage: () => void
+}
+
+export default function SaveChangesCard(props: Props) {
+
+ const { handleSubmit, formState: { isDirty, }, reset, getValues, watch } = useFormContext();
+ const dispatch = useAppDispatch();
+
+ const isUpdating = useMemo(() => !!getValues('id'), [getValues]);
+
+ const [update, updatingStatus] = useUpdateProjectMutation();
+ const [create, creatingStatus] = useCreateProjectMutation()
+
+ const isLoading = updatingStatus.loading || creatingStatus.loading
+
+
+ const [img, name, tagline] = watch(['thumbnail_image', 'title', 'tagline',])
+
+ const clickCancel = () => {
+ if (window.confirm('You might lose some unsaved changes. Are you sure you want to continue?'))
+ reset();
+ }
+
+ const clickSubmit = handleSubmit(async data => {
+ try {
+
+ const input: UpdateProjectInput = {
+ ...data,
+ members: data.members.map(m => ({ id: m.id, role: m.role })),
+ screenshots: data.screenshots.map(s => ({ id: s.id, name: s.name, url: s.url }))
+ }
+
+ await (isUpdating ?
+ update({ variables: { input } })
+ : create({ variables: { input } })
+ )
+ reset(data)
+ } catch (error) {
+ NotificationsService.error("A network error happened...");
+ return;
+ }
+ if (isUpdating)
+ NotificationsService.success("Saved changes successfully")
+ else {
+ dispatch(openModal({
+ Modal: "ProjectListedModal", props: {
+ project: {
+ id: data.id!,
+ name: data.title,
+ img: data.thumbnail_image.url || "https://picsum.photos/id/870/150/150.jpg",
+ tagline: data.tagline,
+ }
+ }
+ }))
+ }
+ }, (errors) => {
+ NotificationsService.error("Please fill all the required fields");
+ props.onBackToFirstPage()
+ })
+
+
+
+ let ctaBtn = useMemo(() => {
+ if (isUpdating)
+ return
+ {isLoading ? "Saving..." : "Save Changes"}
+
+ else if (props.currentTab === 'project-details')
+ return
+ Next step: {tabs.team.text}
+
+ else if (props.currentTab === 'team')
+ return
+ Next step: {tabs.extras.text}
+
+ else
+ return
+ {isLoading ? "Listing your product..." : "List your product"}
+
+ }, [clickSubmit, isDirty, isLoading, isUpdating, props.currentTab, props.onNext])
+
+
+ return (
+
+
+ {img ?
+
:
+
+ }
+
+
{name || "Product preview"}
+ {
{tagline || "Provide some more details."}
}
+
+
+
+ {/* {trimText(profileQuery.data.profile.bio, 120)}
*/}
+
+ {ctaBtn}
+
+ Cancel
+
+
+
+ )
+}
diff --git a/src/features/Projects/pages/ListProjectPage/Components/TeamMembersInput/MemberRow.tsx b/src/features/Projects/pages/ListProjectPage/Components/TeamMembersInput/MemberRow.tsx
new file mode 100644
index 0000000..2a64cd7
--- /dev/null
+++ b/src/features/Projects/pages/ListProjectPage/Components/TeamMembersInput/MemberRow.tsx
@@ -0,0 +1,57 @@
+import { Menu, MenuButton, MenuItem } from '@szhsin/react-menu';
+import { ComponentProps } from 'react'
+import { NestedValue } from 'react-hook-form'
+import { FaChevronDown, FaRegTrashAlt, } from 'react-icons/fa';
+import UsersInput from 'src/Components/Inputs/UsersInput/UsersInput'
+import Avatar from 'src/features/Profiles/Components/Avatar/Avatar';
+import { Team_Member_Role } from 'src/graphql';
+import { Value } from './TeamMembersInput'
+
+interface Props {
+ user: Value[number]
+ onRemove: () => void;
+ onUpdateRole: (role: Team_Member_Role) => void
+ disabled?: boolean;
+ canUpdateRole?: boolean;
+ canDelete?: boolean;
+
+}
+
+export default function MemberRow({ user, onRemove, onUpdateRole, disabled, canUpdateRole, canDelete }: Props) {
+ return (
+
+
+
+
+
+ {user.name}
+
+
+ {user.jobTitle}
+
+
+
+
+ {canUpdateRole ?
{user.role} } transition>
+ {[Team_Member_Role.Admin, Team_Member_Role.Maker].map(role =>
+ onUpdateRole(role)}
+ key={role}>{role}
+ )}
+
+ :
+ {user.role}
+ }
+ {canDelete && onRemove()} className=''>
+
+ }
+
+
+ )
+}
diff --git a/src/features/Projects/pages/ListProjectPage/Components/TeamMembersInput/TeamMembersInput.stories.tsx b/src/features/Projects/pages/ListProjectPage/Components/TeamMembersInput/TeamMembersInput.stories.tsx
new file mode 100644
index 0000000..0427410
--- /dev/null
+++ b/src/features/Projects/pages/ListProjectPage/Components/TeamMembersInput/TeamMembersInput.stories.tsx
@@ -0,0 +1,21 @@
+import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { WrapFormController } from 'src/utils/storybook/utils';
+import TeamMembersInput from './TeamMembersInput';
+
+export default {
+ title: 'Projects/List Project Page/Inputs/Team Members Input',
+ component: TeamMembersInput,
+ argTypes: {
+ backgroundColor: { control: 'color' },
+ },
+} as ComponentMeta;
+
+
+const Template: ComponentStory = (args) => WrapFormController('v', [])( )
+
+
+export const Default = Template.bind({});
+Default.args = {
+}
+
+
diff --git a/src/features/Projects/pages/ListProjectPage/Components/TeamMembersInput/TeamMembersInput.tsx b/src/features/Projects/pages/ListProjectPage/Components/TeamMembersInput/TeamMembersInput.tsx
new file mode 100644
index 0000000..d4895ab
--- /dev/null
+++ b/src/features/Projects/pages/ListProjectPage/Components/TeamMembersInput/TeamMembersInput.tsx
@@ -0,0 +1,78 @@
+
+import { ComponentProps } from 'react'
+import { NestedValue } from 'react-hook-form'
+import UsersInput from 'src/Components/Inputs/UsersInput/UsersInput'
+import { ProjectPermissionEnum, Team_Member_Role } from 'src/graphql';
+import { IListProjectForm } from '../FormContainer/FormContainer';
+import { useUpdateProjectContext } from '../FormContainer/updateProjectContext';
+import MemberRow from './MemberRow';
+
+export type Value = IListProjectForm['members'] extends NestedValue ? U : never;
+
+type Props = {
+ value: Value,
+ onChange?: (new_value: Value) => void
+}
+
+export default function TeamMembersInput({ value, onChange = () => { } }: Props) {
+
+
+ const { permissions } = useUpdateProjectContext()
+
+ const canAddNew = permissions.includes(ProjectPermissionEnum.UpdateAdmins)
+ const canUpdateMembers = permissions.includes(ProjectPermissionEnum.UpdateMembers)
+ const canUpdateAdmins = permissions.includes(ProjectPermissionEnum.UpdateAdmins)
+
+ const addMember: ComponentProps['onSelect'] = (user) => {
+ if (value.some(u => u.id === user.id))
+ return;
+ onChange([
+ ...value,
+ {
+ id: user.id,
+ name: user.name,
+ avatar: user.avatar,
+ jobTitle: user.jobTitle,
+ role: Team_Member_Role.Maker,
+ }])
+ }
+
+ const setMemberRole = (id: number, role: Team_Member_Role) => {
+ onChange(value.map(u => {
+ if (u.id !== id) return u;
+ return {
+ ...u,
+ role,
+ }
+ }))
+ }
+
+ const removeMember = (id: number) => {
+ onChange(value.filter(u => u.id !== id));
+ }
+
+ return (
+ <>
+ {canAddNew && }
+ {value.length > 0 &&
+
+ {value.map(member => {
+
+ let canEdit = false;
+
+ if (member.role === Team_Member_Role.Admin) canEdit = canUpdateAdmins;
+ if (member.role === Team_Member_Role.Maker) canEdit = canUpdateMembers;
+
+ return removeMember(member.id)}
+ onUpdateRole={role => setMemberRole(member.id, role)}
+ />
+ })}
+
}
+ >
+ )
+}
diff --git a/src/features/Projects/pages/ListProjectPage/Components/TeamTab/TeamTab.stories.tsx b/src/features/Projects/pages/ListProjectPage/Components/TeamTab/TeamTab.stories.tsx
new file mode 100644
index 0000000..a2a7fa2
--- /dev/null
+++ b/src/features/Projects/pages/ListProjectPage/Components/TeamTab/TeamTab.stories.tsx
@@ -0,0 +1,20 @@
+import { ComponentStory, ComponentMeta } from '@storybook/react';
+import TeamTab from './TeamTab';
+
+export default {
+ title: 'Projects/List Project Page/Tabs/Team',
+ component: TeamTab,
+ argTypes: {
+ backgroundColor: { control: 'color' },
+ },
+} as ComponentMeta;
+
+
+const Template: ComponentStory = (args) =>
+
+
+export const Default = Template.bind({});
+Default.args = {
+}
+
+
diff --git a/src/features/Projects/pages/ListProjectPage/Components/TeamTab/TeamTab.tsx b/src/features/Projects/pages/ListProjectPage/Components/TeamTab/TeamTab.tsx
new file mode 100644
index 0000000..aaba86f
--- /dev/null
+++ b/src/features/Projects/pages/ListProjectPage/Components/TeamTab/TeamTab.tsx
@@ -0,0 +1,62 @@
+import { Controller, useFormContext } from "react-hook-form"
+import Card from "src/Components/Card/Card";
+import TeamMembersInput from "../TeamMembersInput/TeamMembersInput";
+import RecruitRolesInput from "../RecruitRolesInput/RecruitRolesInput";
+import { IListProjectForm } from "../FormContainer/FormContainer";
+
+
+interface Props {
+}
+
+export default function TeamTab() {
+
+ const { formState: { errors, }, control } = useFormContext();
+
+
+
+ return (
+
+
+ ⚡️ Team
+ Let us know who is on this product’s team.
+
+
(
+
+ )}
+ />
+ {errors.members && {errors.members?.message}
}
+
+
+
+ ℹ️ Onboard your team: Make sure you onboard any other team members so they can help you manage this project and its development progress. To add them, they will first need to create a maker profile.
+
+
+
+
+
+ 💪️ Recruit
+ Are you looking to recruit more makers to your project? Select the roles you’re looking for below and let makers discover your project at ⚔️ Tournaments.
+
+
(
+
+ )}
+ />
+ {errors.recruit_roles && {errors.recruit_roles?.message}
}
+
+
+
+ )
+}
diff --git a/src/features/Projects/pages/ListProjectPage/Components/TournamentsInput/TournamentsInput.stories.tsx b/src/features/Projects/pages/ListProjectPage/Components/TournamentsInput/TournamentsInput.stories.tsx
new file mode 100644
index 0000000..aa7f5b3
--- /dev/null
+++ b/src/features/Projects/pages/ListProjectPage/Components/TournamentsInput/TournamentsInput.stories.tsx
@@ -0,0 +1,21 @@
+import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { WrapFormController } from 'src/utils/storybook/utils';
+import TournamentsInput from './TournamentsInput';
+
+export default {
+ title: 'Projects/List Project Page/Inputs/Tournaments Input',
+ component: TournamentsInput,
+ argTypes: {
+ backgroundColor: { control: 'color' },
+ },
+} as ComponentMeta;
+
+
+const Template: ComponentStory = (args) => WrapFormController('v', [])( )
+
+
+export const Default = Template.bind({});
+Default.args = {
+}
+
+
diff --git a/src/features/Projects/pages/ListProjectPage/Components/TournamentsInput/TournamentsInput.tsx b/src/features/Projects/pages/ListProjectPage/Components/TournamentsInput/TournamentsInput.tsx
new file mode 100644
index 0000000..edf6b75
--- /dev/null
+++ b/src/features/Projects/pages/ListProjectPage/Components/TournamentsInput/TournamentsInput.tsx
@@ -0,0 +1,60 @@
+import Button from 'src/Components/Button/Button';
+import { useGetTournamentsToRegisterQuery } from 'src/graphql';
+import { random } from 'src/utils/helperFunctions';
+
+interface Props {
+ value: number[];
+ onChange?: (v: number[]) => void;
+}
+
+export default function TournamentsInput(props: Props) {
+
+ const query = useGetTournamentsToRegisterQuery();
+
+
+ const handleClick = (clickedValue: number) => {
+ if (props.value.includes(clickedValue))
+ props.onChange?.(props.value.filter(v => v !== clickedValue));
+ else
+ props.onChange?.([...props.value, clickedValue])
+ }
+
+
+ return (
+
+ {query.loading ?
+ Array(4).fill(0).map((_, idx) =>
+
+ {"loading category skeleton".slice(random(6, 12))}
+
)
+ :
+ ((query.data?.getTournamentToRegister && query.data?.getTournamentToRegister.length < 0) ?
+ query.data?.getTournamentToRegister.map(item =>
+
handleClick(item.id)}
+ >
+ {item.title}
+ )
+ :
+
+ There is no running tournaments currently.
+
)
+ }
+
+ )
+}
+
+
diff --git a/src/features/Projects/pages/ListProjectPage/Components/TournamentsInput/tournamentsToRegister.graphql b/src/features/Projects/pages/ListProjectPage/Components/TournamentsInput/tournamentsToRegister.graphql
new file mode 100644
index 0000000..84c0927
--- /dev/null
+++ b/src/features/Projects/pages/ListProjectPage/Components/TournamentsInput/tournamentsToRegister.graphql
@@ -0,0 +1,6 @@
+query GetTournamentsToRegister {
+ getTournamentToRegister {
+ id
+ title
+ }
+}
diff --git a/src/features/Projects/pages/ListProjectPage/ListProjectPage.tsx b/src/features/Projects/pages/ListProjectPage/ListProjectPage.tsx
new file mode 100644
index 0000000..0129d15
--- /dev/null
+++ b/src/features/Projects/pages/ListProjectPage/ListProjectPage.tsx
@@ -0,0 +1,140 @@
+import { useCarousel, useMediaQuery, } from "src/utils/hooks";
+import { Helmet } from 'react-helmet'
+import { MEDIA_QUERIES } from "src/utils/theme";
+import Card from "src/Components/Card/Card";
+import LoadingPage from "src/Components/LoadingPage/LoadingPage";
+import ProjectDetailsTab from "./Components/ProjectDetailsTab/ProjectDetailsTab";
+import TeamTab from "./Components/TeamTab/TeamTab";
+import ExtrasTab from "./Components/ExtrasTab/ExtrasTab";
+import FormContainer from "./Components/FormContainer/FormContainer";
+import { useState } from "react";
+import SaveChangesCard from "./Components/SaveChangesCard/SaveChangesCard";
+import { useMeQuery } from "src/graphql";
+import { Navigate, useLocation } from 'react-router-dom'
+
+
+export const tabs = {
+ 'project-details': {
+ text: "🚀️ Project details",
+ path: 'project-details',
+ },
+ 'team': {
+ text: "⚡️ Team",
+ path: 'team',
+ },
+ 'extras': {
+ text: "💎 Extras",
+ path: 'extras',
+ }
+} as const;
+
+const links = [tabs['project-details'], tabs['team'], tabs['extras']];
+
+type TabsKeys = keyof typeof tabs;
+
+
+export default function ListProjectPage() {
+
+ const isMediumScreen = useMediaQuery(MEDIA_QUERIES.isMedium)
+ const [curTab, setCurTab] = useState(tabs['project-details'].path)
+ const { viewportRef, } = useCarousel({
+ align: 'start', slidesToScroll: 2,
+ containScroll: "trimSnaps",
+ })
+ const location = useLocation()
+
+ const meQuery = useMeQuery({
+
+ });
+
+ if (meQuery.loading) return
+
+ if (meQuery.error || !meQuery.data?.me) return
+
+ return (
+ <>
+
+ List a project
+
+
+
+
+ {isMediumScreen ?
+
+
+ List a project
+
+ {links.map((link, idx) =>
+
+ `flex items-start rounded-8 cursor-pointer font-bold p-12
+ // active:scale-95 transition-transform
+ // ${isActive ? 'bg-gray-100' : 'hover:bg-gray-50'}
+ // `}
+ className={`flex w-full items-start rounded-8 cursor-pointer font-bold p-12
+ active:scale-95 transition-transform
+ ${link.path === curTab ? 'bg-gray-100' : 'hover:bg-gray-50'}
+ `}
+ onClick={() => setCurTab(link.path)}
+ >
+ {link.text}
+
+ )}
+
+
+
+ :
+
+
+
+
+ {links.map((link, idx) =>
+ setCurTab(link.path)}
+ >
+ {link.text}
+
+ )}
+
+
+
+
+ }
+
+
+
+
+ {curTab === tabs["project-details"].path &&
}
+ {curTab === tabs["team"].path &&
}
+ {curTab === tabs["extras"].path &&
}
+
+
+ {
+ if (curTab === 'project-details') setCurTab(tabs['team'].path)
+ else if (curTab === 'team') setCurTab(tabs['extras'].path)
+ }}
+ onBackToFirstPage={() => setCurTab(tabs["project-details"].path)}
+ />
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/src/features/Projects/pages/ProjectPage/ProjectDetailsCard/ProjectDetails.graphql b/src/features/Projects/pages/ProjectPage/ProjectDetailsCard/ProjectDetails.graphql
index d8203e8..66ea49e 100644
--- a/src/features/Projects/pages/ProjectPage/ProjectDetailsCard/ProjectDetails.graphql
+++ b/src/features/Projects/pages/ProjectPage/ProjectDetailsCard/ProjectDetails.graphql
@@ -2,18 +2,36 @@ query ProjectDetails($projectId: Int!) {
getProject(id: $projectId) {
id
title
+ tagline
description
+ hashtag
cover_image
thumbnail_image
+ launch_status
+ twitter
+ discord
+ github
+ slack
+ telegram
screenshots
website
lightning_address
- lnurl_callback_url
votes_count
category {
id
+ icon
title
}
+ permissions
+ members {
+ role
+ user {
+ id
+ name
+ jobTitle
+ avatar
+ }
+ }
awards {
title
image
@@ -24,5 +42,17 @@ query ProjectDetails($projectId: Int!) {
id
title
}
+ recruit_roles {
+ id
+ title
+ icon
+ level
+ }
+
+ capabilities {
+ id
+ title
+ icon
+ }
}
}
diff --git a/src/features/Projects/pages/ProjectPage/ProjectDetailsCard/ProjectDetailsCard.Skeleton.tsx b/src/features/Projects/pages/ProjectPage/ProjectDetailsCard/ProjectDetailsCard.Skeleton.tsx
index 8da03ca..2501813 100644
--- a/src/features/Projects/pages/ProjectPage/ProjectDetailsCard/ProjectDetailsCard.Skeleton.tsx
+++ b/src/features/Projects/pages/ProjectPage/ProjectDetailsCard/ProjectDetailsCard.Skeleton.tsx
@@ -5,6 +5,7 @@ import Skeleton from 'react-loading-skeleton';
import Badge from 'src/Components/Badge/Badge';
import { useMediaQuery } from 'src/utils/hooks';
import { MEDIA_QUERIES } from 'src/utils/theme';
+import Button from 'src/Components/Button/Button';
interface Props extends ModalCard {
@@ -17,8 +18,6 @@ export default function ProjectDetailsCardSkeleton({ onClose, direction, ...prop
const isMdScreen = useMediaQuery(MEDIA_QUERIES.isMedium)
-
-
return (
-
+
-
+
-
-
-
+
+
+
-
-
-
-
-
-
+
+
+ {/*
Visit */}
+ {/*
*/}
+ {/*
*/}
+ {/* {isWalletConnected ?
+ :
+
Connect Wallet to Vote
+ } */}
+
+ votes
+
+
-
@@ -57,8 +66,6 @@ export default function ProjectDetailsCardSkeleton({ onClose, direction, ...prop
-
Screenshots
-
{
Array(4).fill(0).map((_, idx) =>
diff --git a/src/features/Projects/pages/ProjectPage/ProjectDetailsCard/ProjectDetailsCard.tsx b/src/features/Projects/pages/ProjectPage/ProjectDetailsCard/ProjectDetailsCard.tsx
index 6d8b40b..879360b 100644
--- a/src/features/Projects/pages/ProjectPage/ProjectDetailsCard/ProjectDetailsCard.tsx
+++ b/src/features/Projects/pages/ProjectPage/ProjectDetailsCard/ProjectDetailsCard.tsx
@@ -1,22 +1,28 @@
import { useEffect, useState } from 'react'
-import { BsJoystick } from 'react-icons/bs'
-import { MdClose, MdLocalFireDepartment } from 'react-icons/md';
+import { MdLocalFireDepartment } from 'react-icons/md';
import { ModalCard } from 'src/Components/Modals/ModalsContainer/ModalsContainer';
import { useAppDispatch, useAppSelector, useMediaQuery } from 'src/utils/hooks';
import { openModal, scheduleModal } from 'src/redux/features/modals.slice';
import { setProject } from 'src/redux/features/project.slice';
import Button from 'src/Components/Button/Button';
-import { AiFillThunderbolt } from 'react-icons/ai';
import ProjectCardSkeleton from './ProjectDetailsCard.Skeleton'
-import VoteButton from 'src/features/Projects/pages/ProjectPage/VoteButton/VoteButton';
-import { Wallet_Service } from 'src/services'
-import { useProjectDetailsQuery } from 'src/graphql';
+// import VoteButton from 'src/features/Projects/pages/ProjectPage/VoteButton/VoteButton';
+import { NotificationsService, Wallet_Service } from 'src/services'
+import { ProjectLaunchStatusEnum, ProjectPermissionEnum, useProjectDetailsQuery } from 'src/graphql';
import Lightbox from 'src/Components/Lightbox/Lightbox'
import linkifyHtml from 'linkify-html';
import ErrorMessage from 'src/Components/Errors/ErrorMessage/ErrorMessage';
import { setVoteAmount } from 'src/redux/features/vote.slice';
import { numberFormatter } from 'src/utils/helperFunctions';
import { MEDIA_QUERIES } from 'src/utils/theme';
+import { FaDiscord, } from 'react-icons/fa';
+import { FiEdit2, FiGithub, FiGlobe, FiTwitter } from 'react-icons/fi';
+import CopyToClipboard from 'react-copy-to-clipboard';
+import Badge from 'src/Components/Badge/Badge';
+import Avatar from 'src/features/Profiles/Components/Avatar/Avatar';
+import { Link } from 'react-router-dom';
+import { createRoute } from 'src/utils/routing';
+import { IoMdClose } from 'react-icons/io';
interface Props extends ModalCard {
@@ -29,13 +35,12 @@ export default function ProjectDetailsCard({ direction, projectId, ...props }: P
const [screenshotsOpen, setScreenshotsOpen] = useState(-1);
- const { isWalletConnected, project } = useAppSelector(state => ({
+ const { isWalletConnected } = useAppSelector(state => ({
isWalletConnected: state.wallet.isConnected,
- project: state.project.project
}));
const isMdScreen = useMediaQuery(MEDIA_QUERIES.isMedium)
- const { loading, error } = useProjectDetailsQuery({
+ const { data, loading, error } = useProjectDetailsQuery({
variables: { projectId: projectId! },
onCompleted: data => {
dispatch(setProject(data.getProject))
@@ -65,13 +70,49 @@ export default function ProjectDetailsCard({ direction, projectId, ...props }: P
- if (loading || !project)
+ if (loading || !data?.getProject)
return
;
const onConnectWallet = async () => {
Wallet_Service.connectWallet()
}
+ const project = data.getProject;
+
+ const links = [
+ {
+ value: project.discord,
+ text: project.discord,
+ icon: FaDiscord,
+ colors: "bg-violet-100 text-violet-900",
+ },
+ {
+ value: project.website,
+ text: project.website?.replace(/(^\w+:|^)\/\//, '').replace(/\/$/, ""),
+ icon: FiGlobe,
+ colors: "bg-gray-100 text-gray-900",
+ url: project.website
+ },
+ {
+ value: project.twitter,
+ text: project.twitter,
+ icon: FiTwitter,
+ colors: "bg-blue-100 text-blue-500",
+ url: project.twitter
+ },
+ {
+ value: project.github,
+ text: project.github,
+ icon: FiGithub,
+ colors: "bg-pink-100 text-pink-600",
+ url: project.github
+ },
+ ];
+
+
+
+ const canEdit = project.permissions.includes(ProjectPermissionEnum.UpdateInfo);
+
const onVote = (votes?: number) => {
dispatch(setVoteAmount(votes ?? 10));
dispatch(openModal({
@@ -99,55 +140,93 @@ export default function ProjectDetailsCard({ direction, projectId, ...props }: P
return (
-
+ {/* Cover Image */}
+
-
+
+ {project.launch_status === ProjectLaunchStatusEnum.Launched && `🚀 Launched`}
+ {project.launch_status === ProjectLaunchStatusEnum.Wip && `🔧 WIP`}
+
+
+ {project.permissions.includes(ProjectPermissionEnum.UpdateInfo) &&
+ props.onClose?.()} to={createRoute({ type: "edit-project", id: project.id })}> }
+
+
-
-
-
-
+
+
+ {/* Title & Basic Info */}
+
+
+
-
-
{project?.title}
-
{project?.website?.replace(/(^\w+:|^)\/\//, '').replace(/\/$/, "")}
+
+
{project.title}
+
{project.tagline}
- {project?.category.title}
-
- {numberFormatter(project?.votes_count)}
-
+ {project.category.icon} {project.category.title}
-
-
Visit
- {isWalletConnected ?
-
+
+ {/*
Visit */}
+ {/*
*/}
+ {/*
*/}
+ {/* {isWalletConnected ?
:
Connect Wallet to Vote
- }
+ } */}
+
onVote()}>
+
+ { {numberFormatter(project.votes_count)} }
+
+
-
-
-
Visit
- {isWalletConnected ?
-
- :
-
Connect Wallet to Vote
- }
+
+
+ {/* About */}
+
+
About
+
+
+ {/* Links */}
+
+ {links.filter(link => !!link.value).map((link, idx) =>
+ (link.url ?
+
+
+ :
+
NotificationsService.info(" Copied to clipboard", { icon: "📋" })}
+ >
+ { }}
+ className={`w-40 aspect-square rounded-full flex justify-center items-center ${link.colors}`}
+ >
+
+
+
+ ))}
+
{project.screenshots.length > 0 && <>
-
-
Screenshots
+
{project.screenshots.slice(0, 4).map((screenshot, idx) =>
setScreenshotsOpen(-1)}
/>
>}
-
-
-
Are you the creator of this project?
+
+ {project.capabilities.length > 0 &&
+
+
CAPABILITIES
+
+ {project.capabilities.map(cap => {cap.icon} {cap.title} )}
+
+
}
+
+ {project.members.length > 0 &&
+
+
MAKERS
+
+ {project.members.map(m =>
+
+
+
+
{m.user.name}
+
{m.user.jobTitle}
+
+
}
+ />
+ )}
+
+
}
+ {/*
+
Are you the creator of this project
Claim 🖐
-
+
*/}
)
diff --git a/src/graphql/index.tsx b/src/graphql/index.tsx
index d178469..3e1b397 100644
--- a/src/graphql/index.tsx
+++ b/src/graphql/index.tsx
@@ -91,6 +91,13 @@ export type BountyApplication = {
workplan: Scalars['String'];
};
+export type Capability = {
+ __typename?: 'Capability';
+ icon: Scalars['String'];
+ id: Scalars['Int'];
+ title: Scalars['String'];
+};
+
export type Category = {
__typename?: 'Category';
apps_count: Scalars['Int'];
@@ -102,6 +109,35 @@ export type Category = {
votes_sum: Scalars['Int'];
};
+export type CreateProjectInput = {
+ capabilities: Array
;
+ category_id: Scalars['Int'];
+ cover_image: ImageInput;
+ description: Scalars['String'];
+ discord?: InputMaybe;
+ github?: InputMaybe;
+ hashtag: Scalars['String'];
+ id?: InputMaybe;
+ launch_status: ProjectLaunchStatusEnum;
+ lightning_address?: InputMaybe;
+ members: Array;
+ recruit_roles: Array;
+ screenshots: Array;
+ slack?: InputMaybe;
+ tagline: Scalars['String'];
+ telegram?: InputMaybe;
+ thumbnail_image: ImageInput;
+ title: Scalars['String'];
+ tournaments: Array;
+ twitter?: InputMaybe;
+ website: Scalars['String'];
+};
+
+export type CreateProjectResponse = {
+ __typename?: 'CreateProjectResponse';
+ project: Project;
+};
+
export type Donation = {
__typename?: 'Donation';
amount: Scalars['Int'];
@@ -182,12 +218,15 @@ export type Mutation = {
__typename?: 'Mutation';
confirmDonation: Donation;
confirmVote: Vote;
+ createProject: Maybe;
createStory: Maybe;
+ deleteProject: Maybe;
deleteStory: Maybe;
donate: Donation;
registerInTournament: Maybe;
updateProfileDetails: Maybe;
updateProfileRoles: Maybe;
+ updateProject: Maybe;
updateTournamentRegistration: Maybe;
updateUserPreferences: MyProfile;
vote: Vote;
@@ -206,11 +245,21 @@ export type MutationConfirmVoteArgs = {
};
+export type MutationCreateProjectArgs = {
+ input: InputMaybe;
+};
+
+
export type MutationCreateStoryArgs = {
data: InputMaybe;
};
+export type MutationDeleteProjectArgs = {
+ id: Scalars['Int'];
+};
+
+
export type MutationDeleteStoryArgs = {
id: Scalars['Int'];
};
@@ -237,6 +286,11 @@ export type MutationUpdateProfileRolesArgs = {
};
+export type MutationUpdateProjectArgs = {
+ input: InputMaybe;
+};
+
+
export type MutationUpdateTournamentRegistrationArgs = {
data: InputMaybe;
tournament_id: Scalars['Int'];
@@ -346,25 +400,57 @@ export type ProfileRolesInput = {
export type Project = {
__typename?: 'Project';
awards: Array;
+ capabilities: Array;
category: Category;
cover_image: Scalars['String'];
description: Scalars['String'];
+ discord: Maybe;
+ github: Maybe;
+ hashtag: Scalars['String'];
id: Scalars['Int'];
+ launch_status: ProjectLaunchStatusEnum;
lightning_address: Maybe;
lnurl_callback_url: Maybe;
+ members: Array;
+ permissions: Array;
recruit_roles: Array;
screenshots: Array;
+ slack: Maybe;
+ tagline: Scalars['String'];
tags: Array;
+ telegram: Maybe;
thumbnail_image: Scalars['String'];
title: Scalars['String'];
+ tournaments: Array;
+ twitter: Maybe;
votes_count: Scalars['Int'];
website: Scalars['String'];
};
+export enum ProjectLaunchStatusEnum {
+ Launched = 'Launched',
+ Wip = 'WIP'
+}
+
+export type ProjectMember = {
+ __typename?: 'ProjectMember';
+ role: Team_Member_Role;
+ user: User;
+};
+
+export enum ProjectPermissionEnum {
+ DeleteProject = 'DeleteProject',
+ UpdateAdmins = 'UpdateAdmins',
+ UpdateInfo = 'UpdateInfo',
+ UpdateMembers = 'UpdateMembers'
+}
+
export type Query = {
__typename?: 'Query';
allCategories: Array;
allProjects: Array;
+ checkValidProjectHashtag: Scalars['Boolean'];
+ getAllCapabilities: Array;
getAllHackathons: Array;
getAllMakersRoles: Array;
getAllMakersSkills: Array;
@@ -378,6 +464,7 @@ export type Query = {
getProject: Project;
getProjectsInTournament: TournamentProjectsResponse;
getTournamentById: Tournament;
+ getTournamentToRegister: Array;
getTrendingPosts: Array;
hottestProjects: Array;
me: Maybe;
@@ -387,6 +474,7 @@ export type Query = {
profile: Maybe;
projectsByCategory: Array;
searchProjects: Array;
+ searchUsers: Array;
similarMakers: Array;
tournamentParticipationInfo: Maybe;
};
@@ -398,6 +486,12 @@ export type QueryAllProjectsArgs = {
};
+export type QueryCheckValidProjectHashtagArgs = {
+ hashtag: Scalars['String'];
+ projectId: InputMaybe;
+};
+
+
export type QueryGetAllHackathonsArgs = {
sortBy: InputMaybe;
tag: InputMaybe;
@@ -493,6 +587,11 @@ export type QuerySearchProjectsArgs = {
};
+export type QuerySearchUsersArgs = {
+ value: Scalars['String'];
+};
+
+
export type QuerySimilarMakersArgs = {
id: Scalars['Int'];
};
@@ -557,6 +656,12 @@ export type StoryInputType = {
title: Scalars['String'];
};
+export enum Team_Member_Role {
+ Admin = 'Admin',
+ Maker = 'Maker',
+ Owner = 'Owner'
+}
+
export type Tag = {
__typename?: 'Tag';
description: Maybe;
@@ -566,6 +671,11 @@ export type Tag = {
title: Scalars['String'];
};
+export type TeamMemberInput = {
+ id: Scalars['Int'];
+ role: Team_Member_Role;
+};
+
export type Tournament = {
__typename?: 'Tournament';
cover_image: Scalars['String'];
@@ -653,6 +763,30 @@ export type TournamentProjectsResponse = {
projects: Array;
};
+export type UpdateProjectInput = {
+ capabilities: Array;
+ category_id: Scalars['Int'];
+ cover_image: ImageInput;
+ description: Scalars['String'];
+ discord?: InputMaybe;
+ github?: InputMaybe;
+ hashtag: Scalars['String'];
+ id?: InputMaybe;
+ launch_status: ProjectLaunchStatusEnum;
+ lightning_address?: InputMaybe;
+ members: Array;
+ recruit_roles: Array;
+ screenshots: Array;
+ slack?: InputMaybe;
+ tagline: Scalars['String'];
+ telegram?: InputMaybe;
+ thumbnail_image: ImageInput;
+ title: Scalars['String'];
+ tournaments: Array;
+ twitter?: InputMaybe;
+ website: Scalars['String'];
+};
+
export type UpdateTournamentRegistrationInput = {
email?: InputMaybe;
hacking_status?: InputMaybe;
@@ -725,6 +859,13 @@ export type OfficialTagsQueryVariables = Exact<{ [key: string]: never; }>;
export type OfficialTagsQuery = { __typename?: 'Query', officialTags: Array<{ __typename?: 'Tag', id: number, title: string, icon: string | null, description: string | null }> };
+export type SearchUsersQueryVariables = Exact<{
+ value: Scalars['String'];
+}>;
+
+
+export type SearchUsersQuery = { __typename?: 'Query', searchUsers: Array<{ __typename?: 'User', id: number, name: string, avatar: string, jobTitle: string | null }> };
+
export type NavCategoriesQueryVariables = Exact<{ [key: string]: never; }>;
@@ -896,12 +1037,46 @@ export type HottestProjectsQueryVariables = Exact<{ [key: string]: never; }>;
export type HottestProjectsQuery = { __typename?: 'Query', hottestProjects: Array<{ __typename?: 'Project', id: number, thumbnail_image: string, title: string, votes_count: number, category: { __typename?: 'Category', title: string, id: number } }> };
+export type GetAllCapabilitiesQueryVariables = Exact<{ [key: string]: never; }>;
+
+
+export type GetAllCapabilitiesQuery = { __typename?: 'Query', getAllCapabilities: Array<{ __typename?: 'Capability', id: number, title: string, icon: string }> };
+
+export type ProjectDetailsFragment = { __typename?: 'Project', id: number, title: string, tagline: string, description: string, hashtag: string, cover_image: string, thumbnail_image: string, launch_status: ProjectLaunchStatusEnum, twitter: string | null, discord: string | null, github: string | null, slack: string | null, telegram: string | null, screenshots: Array, website: string, lightning_address: string | null, votes_count: number, permissions: Array, category: { __typename?: 'Category', id: number, icon: string | null, title: string }, members: Array<{ __typename?: 'ProjectMember', role: Team_Member_Role, user: { __typename?: 'User', id: number, name: string, jobTitle: string | null, avatar: string } }>, awards: Array<{ __typename?: 'Award', title: string, image: string, url: string, id: number }>, tags: Array<{ __typename?: 'Tag', id: number, title: string }>, recruit_roles: Array<{ __typename?: 'MakerRole', id: number, title: string, icon: string, level: RoleLevelEnum }>, capabilities: Array<{ __typename?: 'Capability', id: number, title: string, icon: string }> };
+
+export type CreateProjectMutationVariables = Exact<{
+ input: InputMaybe;
+}>;
+
+
+export type CreateProjectMutation = { __typename?: 'Mutation', createProject: { __typename?: 'CreateProjectResponse', project: { __typename?: 'Project', id: number, title: string, tagline: string, description: string, hashtag: string, cover_image: string, thumbnail_image: string, launch_status: ProjectLaunchStatusEnum, twitter: string | null, discord: string | null, github: string | null, slack: string | null, telegram: string | null, screenshots: Array, website: string, lightning_address: string | null, votes_count: number, permissions: Array, category: { __typename?: 'Category', id: number, icon: string | null, title: string }, members: Array<{ __typename?: 'ProjectMember', role: Team_Member_Role, user: { __typename?: 'User', id: number, name: string, jobTitle: string | null, avatar: string } }>, awards: Array<{ __typename?: 'Award', title: string, image: string, url: string, id: number }>, tags: Array<{ __typename?: 'Tag', id: number, title: string }>, recruit_roles: Array<{ __typename?: 'MakerRole', id: number, title: string, icon: string, level: RoleLevelEnum }>, capabilities: Array<{ __typename?: 'Capability', id: number, title: string, icon: string }> } } | null };
+
+export type UpdateProjectMutationVariables = Exact<{
+ input: InputMaybe;
+}>;
+
+
+export type UpdateProjectMutation = { __typename?: 'Mutation', updateProject: { __typename?: 'CreateProjectResponse', project: { __typename?: 'Project', id: number, title: string, tagline: string, description: string, hashtag: string, cover_image: string, thumbnail_image: string, launch_status: ProjectLaunchStatusEnum, twitter: string | null, discord: string | null, github: string | null, slack: string | null, telegram: string | null, screenshots: Array, website: string, lightning_address: string | null, votes_count: number, permissions: Array, category: { __typename?: 'Category', id: number, icon: string | null, title: string }, members: Array<{ __typename?: 'ProjectMember', role: Team_Member_Role, user: { __typename?: 'User', id: number, name: string, jobTitle: string | null, avatar: string } }>, awards: Array<{ __typename?: 'Award', title: string, image: string, url: string, id: number }>, tags: Array<{ __typename?: 'Tag', id: number, title: string }>, recruit_roles: Array<{ __typename?: 'MakerRole', id: number, title: string, icon: string, level: RoleLevelEnum }>, capabilities: Array<{ __typename?: 'Capability', id: number, title: string, icon: string }> } } | null };
+
+export type IsValidProjectHashtagQueryVariables = Exact<{
+ hashtag: Scalars['String'];
+ projectId: InputMaybe;
+}>;
+
+
+export type IsValidProjectHashtagQuery = { __typename?: 'Query', checkValidProjectHashtag: boolean };
+
+export type GetTournamentsToRegisterQueryVariables = Exact<{ [key: string]: never; }>;
+
+
+export type GetTournamentsToRegisterQuery = { __typename?: 'Query', getTournamentToRegister: Array<{ __typename?: 'Tournament', id: number, title: string }> };
+
export type ProjectDetailsQueryVariables = Exact<{
projectId: Scalars['Int'];
}>;
-export type ProjectDetailsQuery = { __typename?: 'Query', getProject: { __typename?: 'Project', id: number, title: string, description: string, cover_image: string, thumbnail_image: string, screenshots: Array, website: string, lightning_address: string | null, lnurl_callback_url: string | null, votes_count: number, category: { __typename?: 'Category', id: number, title: string }, awards: Array<{ __typename?: 'Award', title: string, image: string, url: string, id: number }>, tags: Array<{ __typename?: 'Tag', id: number, title: string }> } };
+export type ProjectDetailsQuery = { __typename?: 'Query', getProject: { __typename?: 'Project', id: number, title: string, tagline: string, description: string, hashtag: string, cover_image: string, thumbnail_image: string, launch_status: ProjectLaunchStatusEnum, twitter: string | null, discord: string | null, github: string | null, slack: string | null, telegram: string | null, screenshots: Array, website: string, lightning_address: string | null, votes_count: number, permissions: Array, category: { __typename?: 'Category', id: number, icon: string | null, title: string }, members: Array<{ __typename?: 'ProjectMember', role: Team_Member_Role, user: { __typename?: 'User', id: number, name: string, jobTitle: string | null, avatar: string } }>, awards: Array<{ __typename?: 'Award', title: string, image: string, url: string, id: number }>, tags: Array<{ __typename?: 'Tag', id: number, title: string }>, recruit_roles: Array<{ __typename?: 'MakerRole', id: number, title: string, icon: string, level: RoleLevelEnum }>, capabilities: Array<{ __typename?: 'Capability', id: number, title: string, icon: string }> } };
export type GetAllRolesQueryVariables = Exact<{ [key: string]: never; }>;
@@ -1010,6 +1185,63 @@ export const UserRolesSkillsFragmentDoc = gql`
}
}
`;
+export const ProjectDetailsFragmentDoc = gql`
+ fragment ProjectDetails on Project {
+ id
+ title
+ tagline
+ description
+ hashtag
+ cover_image
+ thumbnail_image
+ launch_status
+ twitter
+ discord
+ github
+ slack
+ telegram
+ screenshots
+ website
+ lightning_address
+ votes_count
+ category {
+ id
+ icon
+ title
+ }
+ permissions
+ members {
+ role
+ user {
+ id
+ name
+ jobTitle
+ avatar
+ }
+ }
+ awards {
+ title
+ image
+ url
+ id
+ }
+ tags {
+ id
+ title
+ }
+ recruit_roles {
+ id
+ title
+ icon
+ level
+ }
+ capabilities {
+ id
+ title
+ icon
+ }
+}
+ `;
export const OfficialTagsDocument = gql`
query OfficialTags {
officialTags {
@@ -1037,16 +1269,54 @@ export const OfficialTagsDocument = gql`
* });
*/
export function useOfficialTagsQuery(baseOptions?: Apollo.QueryHookOptions) {
- const options = {...defaultOptions, ...baseOptions}
- return Apollo.useQuery(OfficialTagsDocument, options);
- }
+ const options = { ...defaultOptions, ...baseOptions }
+ return Apollo.useQuery(OfficialTagsDocument, options);
+}
export function useOfficialTagsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) {
- const options = {...defaultOptions, ...baseOptions}
- return Apollo.useLazyQuery(OfficialTagsDocument, options);
- }
+ const options = { ...defaultOptions, ...baseOptions }
+ return Apollo.useLazyQuery(OfficialTagsDocument, options);
+}
export type OfficialTagsQueryHookResult = ReturnType;
export type OfficialTagsLazyQueryHookResult = ReturnType;
export type OfficialTagsQueryResult = Apollo.QueryResult;
+export const SearchUsersDocument = gql`
+ query SearchUsers($value: String!) {
+ searchUsers(value: $value) {
+ id
+ name
+ avatar
+ jobTitle
+ }
+}
+ `;
+
+/**
+ * __useSearchUsersQuery__
+ *
+ * To run a query within a React component, call `useSearchUsersQuery` and pass it any options that fit your needs.
+ * When your component renders, `useSearchUsersQuery` 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 } = useSearchUsersQuery({
+ * variables: {
+ * value: // value for 'value'
+ * },
+ * });
+ */
+export function useSearchUsersQuery(baseOptions: Apollo.QueryHookOptions) {
+ const options = { ...defaultOptions, ...baseOptions }
+ return Apollo.useQuery(SearchUsersDocument, options);
+}
+export function useSearchUsersLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) {
+ const options = { ...defaultOptions, ...baseOptions }
+ return Apollo.useLazyQuery(SearchUsersDocument, options);
+}
+export type SearchUsersQueryHookResult = ReturnType;
+export type SearchUsersLazyQueryHookResult = ReturnType;
+export type SearchUsersQueryResult = Apollo.QueryResult;
export const NavCategoriesDocument = gql`
query NavCategories {
allCategories {
@@ -1074,13 +1344,13 @@ export const NavCategoriesDocument = gql`
* });
*/
export function useNavCategoriesQuery(baseOptions?: Apollo.QueryHookOptions) {
- const options = {...defaultOptions, ...baseOptions}
- return Apollo.useQuery(NavCategoriesDocument, options);
- }
+ const options = { ...defaultOptions, ...baseOptions }
+ return Apollo.useQuery(NavCategoriesDocument, options);
+}
export function useNavCategoriesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) {
- const options = {...defaultOptions, ...baseOptions}
- return Apollo.useLazyQuery(NavCategoriesDocument, options);
- }
+ const options = { ...defaultOptions, ...baseOptions }
+ return Apollo.useLazyQuery(NavCategoriesDocument, options);
+}
export type NavCategoriesQueryHookResult = ReturnType;
export type NavCategoriesLazyQueryHookResult = ReturnType;
export type NavCategoriesQueryResult = Apollo.QueryResult;
@@ -1115,13 +1385,13 @@ export const SearchProjectsDocument = gql`
* });
*/
export function useSearchProjectsQuery(baseOptions: Apollo.QueryHookOptions) {
- const options = {...defaultOptions, ...baseOptions}
- return Apollo.useQuery(SearchProjectsDocument, options);
- }
+ const options = { ...defaultOptions, ...baseOptions }
+ return Apollo.useQuery(SearchProjectsDocument, options);
+}
export function useSearchProjectsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) {
- const options = {...defaultOptions, ...baseOptions}
- return Apollo.useLazyQuery(SearchProjectsDocument, options);
- }
+ const options = { ...defaultOptions, ...baseOptions }
+ return Apollo.useLazyQuery(SearchProjectsDocument, options);
+}
export type SearchProjectsQueryHookResult = ReturnType;
export type SearchProjectsLazyQueryHookResult = ReturnType;
export type SearchProjectsQueryResult = Apollo.QueryResult;
@@ -1154,13 +1424,13 @@ export const MeDocument = gql`
* });
*/
export function useMeQuery(baseOptions?: Apollo.QueryHookOptions) {
- const options = {...defaultOptions, ...baseOptions}
- return Apollo.useQuery(MeDocument, options);
- }
+ const options = { ...defaultOptions, ...baseOptions }
+ return Apollo.useQuery(MeDocument, options);
+}
export function useMeLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) {
- const options = {...defaultOptions, ...baseOptions}
- return Apollo.useLazyQuery(MeDocument, options);
- }
+ const options = { ...defaultOptions, ...baseOptions }
+ return Apollo.useLazyQuery(MeDocument, options);
+}
export type MeQueryHookResult = ReturnType;
export type MeLazyQueryHookResult = ReturnType;
export type MeQueryResult = Apollo.QueryResult;
@@ -1191,13 +1461,13 @@ export const DonationsStatsDocument = gql`
* });
*/
export function useDonationsStatsQuery(baseOptions?: Apollo.QueryHookOptions) {
- const options = {...defaultOptions, ...baseOptions}
- return Apollo.useQuery(DonationsStatsDocument, options);
- }
+ const options = { ...defaultOptions, ...baseOptions }
+ return Apollo.useQuery(DonationsStatsDocument, options);
+}
export function useDonationsStatsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) {
- const options = {...defaultOptions, ...baseOptions}
- return Apollo.useLazyQuery(DonationsStatsDocument, options);
- }
+ const options = { ...defaultOptions, ...baseOptions }
+ return Apollo.useLazyQuery(DonationsStatsDocument, options);
+}
export type DonationsStatsQueryHookResult = ReturnType;
export type DonationsStatsLazyQueryHookResult = ReturnType;
export type DonationsStatsQueryResult = Apollo.QueryResult;
@@ -1231,9 +1501,9 @@ export type DonateMutationFn = Apollo.MutationFunction) {
- const options = {...defaultOptions, ...baseOptions}
- return Apollo.useMutation(DonateDocument, options);
- }
+ const options = { ...defaultOptions, ...baseOptions }
+ return Apollo.useMutation(DonateDocument, options);
+}
export type DonateMutationHookResult = ReturnType;
export type DonateMutationResult = Apollo.MutationResult;
export type DonateMutationOptions = Apollo.BaseMutationOptions;
@@ -1267,9 +1537,9 @@ export type ConfirmDonationMutationFn = Apollo.MutationFunction) {
- const options = {...defaultOptions, ...baseOptions}
- return Apollo.useMutation(ConfirmDonationDocument, options);
- }
+ const options = { ...defaultOptions, ...baseOptions }
+ return Apollo.useMutation(ConfirmDonationDocument, options);
+}
export type ConfirmDonationMutationHookResult = ReturnType;
export type ConfirmDonationMutationResult = Apollo.MutationResult;
export type ConfirmDonationMutationOptions = Apollo.BaseMutationOptions;
@@ -1311,13 +1581,13 @@ export const GetHackathonsDocument = gql`
* });
*/
export function useGetHackathonsQuery(baseOptions?: Apollo.QueryHookOptions) {
- const options = {...defaultOptions, ...baseOptions}
- return Apollo.useQuery(GetHackathonsDocument, options);
- }
+ const options = { ...defaultOptions, ...baseOptions }
+ return Apollo.useQuery(GetHackathonsDocument, options);
+}
export function useGetHackathonsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) {
- const options = {...defaultOptions, ...baseOptions}
- return Apollo.useLazyQuery(GetHackathonsDocument, options);
- }
+ const options = { ...defaultOptions, ...baseOptions }
+ return Apollo.useLazyQuery(GetHackathonsDocument, options);
+}
export type GetHackathonsQueryHookResult = ReturnType;
export type GetHackathonsLazyQueryHookResult = ReturnType;
export type GetHackathonsQueryResult = Apollo.QueryResult;
@@ -1368,13 +1638,13 @@ export const TrendingPostsDocument = gql`
* });
*/
export function useTrendingPostsQuery(baseOptions?: Apollo.QueryHookOptions) {
- const options = {...defaultOptions, ...baseOptions}
- return Apollo.useQuery(TrendingPostsDocument, options);
- }
+ const options = { ...defaultOptions, ...baseOptions }
+ return Apollo.useQuery(TrendingPostsDocument, options);
+}
export function useTrendingPostsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) {
- const options = {...defaultOptions, ...baseOptions}
- return Apollo.useLazyQuery(TrendingPostsDocument, options);
- }
+ const options = { ...defaultOptions, ...baseOptions }
+ return Apollo.useLazyQuery(TrendingPostsDocument, options);
+}
export type TrendingPostsQueryHookResult = ReturnType;
export type TrendingPostsLazyQueryHookResult = ReturnType;
export type TrendingPostsQueryResult = Apollo.QueryResult;
@@ -1417,13 +1687,13 @@ export const GetMyDraftsDocument = gql`
* });
*/
export function useGetMyDraftsQuery(baseOptions: Apollo.QueryHookOptions) {
- const options = {...defaultOptions, ...baseOptions}
- return Apollo.useQuery(GetMyDraftsDocument, options);
- }
+ const options = { ...defaultOptions, ...baseOptions }
+ return Apollo.useQuery(GetMyDraftsDocument, options);
+}
export function useGetMyDraftsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions