mirror of
https://github.com/aljazceru/landscape-template.git
synced 2026-01-24 08:44:28 +01:00
Merge pull request #128 from peakshift/feature/list-your-product-ui
Feature - List Your Project UI components and pages
This commit is contained in:
@@ -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!
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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() {
|
||||
<Route path={PAGES_ROUTES.projects.hottest} element={<HottestPage />} />
|
||||
<Route path={PAGES_ROUTES.projects.byCategoryId} element={<CategoryPage />} />
|
||||
<Route path={PAGES_ROUTES.projects.default} element={<ExplorePage />} />
|
||||
<Route path={PAGES_ROUTES.projects.listProject} element={<ListProjectPage />} />
|
||||
|
||||
<Route path={PAGES_ROUTES.blog.storyById} element={<PostDetailsPage postType='story' />} />
|
||||
<Route path={PAGES_ROUTES.blog.feed} element={<FeedPage />} />
|
||||
|
||||
@@ -34,7 +34,7 @@ export default function CoverImageInput(props: Props) {
|
||||
wrapperClass='h-full'
|
||||
render={({ img, isUploading, isDraggingOnWindow }) =>
|
||||
<div className="w-full h-full group relative ">
|
||||
{!img && <div className='w-full h-full flex flex-col justify-center items-center bg-gray-500 outline outline-2 outline-gray-200'>
|
||||
{!img && <div className={`w-full h-full flex flex-col justify-center items-center bg-gray-500 outline outline-2 outline-gray-200 ${props.rounded ?? 'rounded-12'}`}>
|
||||
<p className="text-center text-gray-100 text-body1 md:text-h1 mb-8"><FaImage /></p>
|
||||
<div className={`text-gray-100 text-center text-body4`}>
|
||||
Drop a <span className="font-bold">COVER IMAGE</span> here or <br /> <span className="text-blue-300 underline">Click to browse</span>
|
||||
@@ -56,7 +56,7 @@ export default function CoverImageInput(props: Props) {
|
||||
</>}
|
||||
{isUploading &&
|
||||
<div
|
||||
className="absolute inset-0 bg-gray-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold transition-transform"
|
||||
className={`absolute inset-0 bg-gray-400 ${props.rounded ?? 'rounded-12'} bg-opacity-60 flex flex-col justify-center items-center text-white font-bold transition-transform`}
|
||||
>
|
||||
<RotatingLines
|
||||
strokeColor="#fff"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import UploadPreview, { PreviewComponentProps, PreviewMethods } from '@rpldy/upload-preview'
|
||||
import { useAbortItem, useItemAbortListener, useItemCancelListener, useItemErrorListener, useItemProgressListener } from '@rpldy/uploady';
|
||||
import { useAbortItem, useItemAbortListener, useItemCancelListener, useItemErrorListener, useItemFinishListener, useItemProgressListener } from '@rpldy/uploady';
|
||||
import { useState } from 'react'
|
||||
import ScreenShotsThumbnail from './ScreenshotThumbnail'
|
||||
|
||||
@@ -20,15 +20,11 @@ function CustomImagePreview({ id, url }: PreviewComponentProps) {
|
||||
useItemProgressListener(item => {
|
||||
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)
|
||||
|
||||
@@ -45,7 +45,7 @@ export default function ScreenshotThumbnail({ url, isLoading, isError, onCancel
|
||||
Failed...
|
||||
</div>}
|
||||
{!isEmpty &&
|
||||
<button className="absolute bg-gray-900 hover:bg-opacity-100 bg-opacity-60 text-white rounded-full w-32 h-32 top-8 right-8" onClick={() => onCancel?.()}><FaTimes /></button>
|
||||
<button type='button' className="absolute bg-gray-900 hover:bg-opacity-100 bg-opacity-60 text-white rounded-full w-32 h-32 top-8 right-8 flex flex-col justify-center items-center" onClick={() => onCancel?.()}><FaTimes /></button>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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<ScreenshotType> }>({
|
||||
WrapFormController<{ screenshots: Array<ImageInput> }>({
|
||||
logValues: true,
|
||||
name: "screenshots",
|
||||
defaultValues: {
|
||||
@@ -29,7 +30,7 @@ Empty.args = {
|
||||
|
||||
export const WithValues = Template.bind({});
|
||||
WithValues.decorators = [
|
||||
WrapFormController<{ screenshots: Array<ScreenshotType> }>({
|
||||
WrapFormController<{ screenshots: Array<ImageInput> }>({
|
||||
logValues: true,
|
||||
name: "screenshots",
|
||||
defaultValues: {
|
||||
@@ -51,7 +52,7 @@ WithValues.args = {
|
||||
|
||||
export const Full = Template.bind({});
|
||||
Full.decorators = [
|
||||
WrapFormController<{ screenshots: Array<ScreenshotType> }>({
|
||||
WrapFormController<{ screenshots: Array<ImageInput> }>({
|
||||
logValues: true,
|
||||
name: "screenshots",
|
||||
defaultValues: {
|
||||
|
||||
@@ -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 (
|
||||
<Uploady
|
||||
accept="image/*"
|
||||
multiple={true}
|
||||
inputFieldName='file'
|
||||
grouped={false}
|
||||
enhancer={mockSenderEnhancer}
|
||||
listeners={{
|
||||
[UPLOADER_EVENTS.BATCH_ADD]: (batch) => {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(200px,1fr))] gap-16 mt-24">
|
||||
{canUploadMore && <DropZoneButton />}
|
||||
{uploadedFiles.map(f => <ScreenshotThumbnail
|
||||
key={f.id}
|
||||
<DropZoneButton extraProps={{ canUploadMore }} />
|
||||
{uploadedFiles.map((f, idx) => <ScreenshotThumbnail
|
||||
key={f.local_id}
|
||||
url={f.url}
|
||||
onCancel={() => {
|
||||
onChange(uploadedFiles.filter(file => file.id !== f.id))
|
||||
onChange(removeArrayItemAtIndex(uploadedFiles, idx))
|
||||
}} />)}
|
||||
<ImagePreviews />
|
||||
{(placeholdersCount > 0) &&
|
||||
@@ -92,21 +85,22 @@ export default function ScreenshotsInput(props: Props) {
|
||||
}
|
||||
|
||||
const DropZone = forwardRef<any, any>((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<any, any>((props, ref) => {
|
||||
[onClick]
|
||||
);
|
||||
|
||||
if (!canUploadMore) return null
|
||||
|
||||
return <UploadDropZone
|
||||
{...buttonProps}
|
||||
ref={ref}
|
||||
|
||||
32
src/Components/Inputs/TextInput/TextInput.tsx
Normal file
32
src/Components/Inputs/TextInput/TextInput.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from "react";
|
||||
|
||||
type Props = {
|
||||
isError?: boolean;
|
||||
className?: string;
|
||||
inputClass?: string;
|
||||
renderBefore?: () => JSX.Element
|
||||
renderAfter?: () => JSX.Element
|
||||
} & React.ComponentPropsWithoutRef<'input'>
|
||||
|
||||
export default React.forwardRef<HTMLInputElement, Props>(function TextInput({ className, inputClass, isError, renderBefore, renderAfter, ...props }, ref) {
|
||||
|
||||
return (
|
||||
<div className={`
|
||||
relative w-full border bg-white rounded-12 flex
|
||||
focus-within:ring focus-within:ring-opacity-50
|
||||
${isError ?
|
||||
"border-red-300 focus-within:border-red-300 focus-within:outline-red-400 focus-within:ring-red-200"
|
||||
:
|
||||
"border-gray-300 focus-within:border-primary-300 focus-within:outline-primary-400 focus-within:ring-primary-200"}
|
||||
${className}`}>
|
||||
{renderBefore?.()}
|
||||
<input
|
||||
type='text'
|
||||
className={`input-text ${inputClass}`}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
{renderAfter?.()}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
27
src/Components/Inputs/TextareaInput/TextareaInput.tsx
Normal file
27
src/Components/Inputs/TextareaInput/TextareaInput.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from "react";
|
||||
|
||||
type Props = {
|
||||
isError?: boolean;
|
||||
className?: string;
|
||||
inputClass?: string
|
||||
} & React.ComponentPropsWithoutRef<'textarea'>
|
||||
|
||||
export default React.forwardRef<HTMLTextAreaElement, Props>(function TextareaInput({ className, inputClass, isError, ...props }, ref) {
|
||||
|
||||
return (
|
||||
<div className={`
|
||||
relative w-full border bg-white rounded-12 flex
|
||||
focus-within:ring focus-within:ring-opacity-50
|
||||
${isError ?
|
||||
"border-red-300 focus-within:border-red-300 focus-within:outline-red-400 focus-within:ring-red-200"
|
||||
:
|
||||
"border-gray-300 focus-within:border-primary-300 focus-within:outline-primary-400 focus-within:ring-primary-200"}
|
||||
${className}`}>
|
||||
<textarea
|
||||
className={`input-text ${inputClass}`}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
29
src/Components/Inputs/UsersInput/UsersInput.stories.tsx
Normal file
29
src/Components/Inputs/UsersInput/UsersInput.stories.tsx
Normal file
@@ -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<typeof UsersInput>;
|
||||
|
||||
|
||||
const Template: ComponentStory<typeof UsersInput> = (args) => <div>
|
||||
<p className="text-body4 mb-8 text-gray-700">
|
||||
Search for users:
|
||||
</p>
|
||||
<UsersInput classes={{ input: "max-w-[320px]" }} {...args}></UsersInput>
|
||||
</div>
|
||||
|
||||
export const Default = Template.bind({});
|
||||
164
src/Components/Inputs/UsersInput/UsersInput.tsx
Normal file
164
src/Components/Inputs/UsersInput/UsersInput.tsx
Normal file
@@ -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<User>) => {
|
||||
return (
|
||||
<div>
|
||||
<components.Option {...props} className='!flex items-center gap-16 !py-16'>
|
||||
<Avatar src={props.data.avatar} width={48} />
|
||||
<div>
|
||||
<p className="font-medium self-center">
|
||||
{props.data.name}
|
||||
</p>
|
||||
<p className="text-body5 text-gray-500">
|
||||
{props.data.jobTitle}
|
||||
</p>
|
||||
</div>
|
||||
</components.Option>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
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 ?? <span className='text-gray-400'><FiSearch /> <span className='align-middle'>Search by username</span></span>
|
||||
|
||||
const handleChange = (newValue: OnChangeValue<User, false>,) => {
|
||||
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 (
|
||||
<div className={`${classes?.container}`}>
|
||||
<AsyncSelect
|
||||
value={null}
|
||||
inputValue={inputValue}
|
||||
onInputChange={setInputValue}
|
||||
defaultOptions={false}
|
||||
loadOptions={fetchOptions}
|
||||
loadingMessage={() => 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)',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{/* <div className="flex mt-16 gap-8 flex-wrap">
|
||||
{(value as Tag[]).map((tag, idx) => <Badge color="gray" key={tag.title} onRemove={() => handleRemove(idx)} >{tag.title}</Badge>)}
|
||||
</div> */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
8
src/Components/Inputs/UsersInput/searchUsers.graphql
Normal file
8
src/Components/Inputs/UsersInput/searchUsers.graphql
Normal file
@@ -0,0 +1,8 @@
|
||||
query SearchUsers($value: String!) {
|
||||
searchUsers(value: $value) {
|
||||
id
|
||||
name
|
||||
avatar
|
||||
jobTitle
|
||||
}
|
||||
}
|
||||
@@ -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 (
|
||||
<img src={src} className={`shrink-0 rounded-full object-cover border-2 bg-white border-gray-100 ${className}`} style={{
|
||||
width: width,
|
||||
aspectRatio: '1/1'
|
||||
}} alt={alt ?? "avatar"} />
|
||||
<>
|
||||
<img ref={setTriggerRef} src={src} className={`shrink-0 rounded-full object-cover border-2 bg-white border-gray-100 ${className}`} style={{
|
||||
width: width,
|
||||
aspectRatio: '1/1'
|
||||
}} alt={alt ?? "avatar"} />
|
||||
{
|
||||
(renderTooltip && visible) && (
|
||||
<div
|
||||
ref={setTooltipRef}
|
||||
{...getTooltipProps({ className: 'tooltip-container' })}
|
||||
>
|
||||
<div {...getArrowProps({ className: 'tooltip-arrow' })} />
|
||||
{renderTooltip()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import Skeleton from 'react-loading-skeleton'
|
||||
|
||||
export default function ProjectCardMiniSkeleton() {
|
||||
return (
|
||||
<div className="bg-gray-25 select-none px-16 py-16 flex min-w-[300px] gap-16 border border-gray-200 rounded-10 items-center" >
|
||||
<div className="bg-gray-25 select-none px-16 py-16 flex flex-[0_0_100%] max-w-[296px] gap-16 border border-gray-200 rounded-10 items-center" >
|
||||
<Skeleton circle width={64} height={64} containerClassName='flex-shrink-0' />
|
||||
<div className="justify-around items-start min-w-0">
|
||||
<p className="text-body4 w-full font-bold overflow-ellipsis overflow-hidden whitespace-nowrap"><Skeleton width="15ch" /></p>
|
||||
|
||||
@@ -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() {
|
||||
<div className="max-w-[90%]">
|
||||
{headerLinks[1].title}
|
||||
</div>
|
||||
<Button color="white" href={headerLinks[1].link.url} newTab className="mt-24">
|
||||
<Button color="white" href={headerLinks[1].link.url} newTab={headerLinks[1].link.newTab ?? false} className="mt-24">
|
||||
{headerLinks[1].link.content}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -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<typeof CapabilitiesInput>;
|
||||
|
||||
|
||||
const Template: ComponentStory<typeof CapabilitiesInput> = (args) => WrapFormController('v', [])(<CapabilitiesInput {...args as any} ></CapabilitiesInput>)
|
||||
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex flex-wrap gap-8">
|
||||
{categoriesQuery.loading ?
|
||||
Array(10).fill(0).map((_, idx) =>
|
||||
<div
|
||||
key={idx}
|
||||
className="bg-gray-100 border border-gray-200 p-8 rounded-10">
|
||||
<span className='invisible'>{"loading category skeleton".slice(random(6, 12))}</span>
|
||||
</div>)
|
||||
:
|
||||
categoriesQuery.data?.getAllCapabilities.map(item =>
|
||||
<Button
|
||||
key={item.id}
|
||||
color='none'
|
||||
size='sm'
|
||||
className={`
|
||||
border border-gray-200
|
||||
${props.value.includes(item.id) ?
|
||||
'text-primary-600 bg-primary-100'
|
||||
:
|
||||
"bg-gray-100"
|
||||
}
|
||||
`}
|
||||
onClick={() => handleClick(item.id)}
|
||||
>
|
||||
{item.icon} {item.title}
|
||||
</Button>)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
query GetAllCapabilities {
|
||||
getAllCapabilities {
|
||||
id
|
||||
title
|
||||
icon
|
||||
}
|
||||
}
|
||||
@@ -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<typeof CategoriesInput>;
|
||||
|
||||
|
||||
const Template: ComponentStory<typeof CategoriesInput> = (args) => WrapFormController('v', [])(<CategoriesInput {...args as any} ></CategoriesInput>)
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex flex-wrap gap-8">
|
||||
{categoriesQuery.loading ?
|
||||
Array(10).fill(0).map((_, idx) =>
|
||||
<div
|
||||
key={idx}
|
||||
className="bg-gray-100 border border-gray-200 p-8 rounded-10">
|
||||
<span className='invisible'>{"loading category skeleton".slice(random(6, 12))}</span>
|
||||
</div>)
|
||||
:
|
||||
categoriesQuery.data?.allCategories.map(category =>
|
||||
<Button
|
||||
key={category.id}
|
||||
color='none'
|
||||
size='sm'
|
||||
className={`
|
||||
border border-gray-200
|
||||
${props.value === category.id ?
|
||||
'text-primary-600 bg-primary-100'
|
||||
:
|
||||
"bg-gray-100"
|
||||
}
|
||||
`}
|
||||
onClick={() => props.onChange?.(category.id)}
|
||||
>
|
||||
{category.icon} {category.title}
|
||||
</Button>)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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<typeof ExtrasTab>;
|
||||
|
||||
|
||||
const Template: ComponentStory<typeof ExtrasTab> = (args) => <ExtrasTab {...args as any} ></ExtrasTab>
|
||||
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
}
|
||||
|
||||
|
||||
@@ -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<IListProjectForm>();
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-24">
|
||||
<Card>
|
||||
<h2 className="text-body2 font-bolder">🚀 Launch status</h2>
|
||||
<p className="text-body4 font-light text-gray-600 mt-8">Has this product been launched already, or is it still a work in progress?</p>
|
||||
<div className="mt-24 flex flex-col gap-24">
|
||||
<div className="flex gap-16">
|
||||
<input
|
||||
{...register("launch_status")}
|
||||
type="radio"
|
||||
name="launch_status"
|
||||
value={ProjectLaunchStatusEnum.Wip}
|
||||
/>
|
||||
<div>
|
||||
<p className="text-body4 font-medium">WIP 🛠️</p>
|
||||
<p className="text-body5 text-gray-500 mt-4">It’s still a Work In Progress.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-16">
|
||||
<input
|
||||
{...register("launch_status")}
|
||||
type="radio"
|
||||
name="launch_status"
|
||||
value={ProjectLaunchStatusEnum.Launched}
|
||||
/>
|
||||
<div>
|
||||
<p className="text-body4 font-medium">Launched 🚀</p>
|
||||
<p className="text-body5 text-gray-500 mt-4">The product is ready for launch, or has been launched already.</p>
|
||||
</div>
|
||||
</div>
|
||||
{errors.launch_status && <p className='input-error'>{errors.launch_status?.message}</p>}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<h2 className="text-body2 font-bolder">⚔️️ Tournaments</h2>
|
||||
<p className="text-body4 font-light text-gray-600 mt-8">Is your application part of a tournament? If so, select the tournament(s) that apply and it will automatically be listed for you.</p>
|
||||
<div className="mt-24">
|
||||
<Controller
|
||||
control={control}
|
||||
name="tournaments"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TournamentsInput
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.tournaments && <p className='input-error'>{errors.tournaments?.message}</p>}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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<UpdateProjectInput, {
|
||||
members: NestedValue<ProjectMember[]>
|
||||
capabilities: NestedValue<UpdateProjectInput['capabilities']>
|
||||
recruit_roles: NestedValue<UpdateProjectInput['recruit_roles']>
|
||||
tournaments: NestedValue<UpdateProjectInput['tournaments']>
|
||||
cover_image: NestedValue<UpdateProjectInput['cover_image']>
|
||||
thumbnail_image: NestedValue<UpdateProjectInput['thumbnail_image']>
|
||||
}>
|
||||
|
||||
export type ProjectMember = {
|
||||
id: number,
|
||||
name: string,
|
||||
jobTitle: string | null,
|
||||
avatar: string,
|
||||
role: Team_Member_Role,
|
||||
}
|
||||
|
||||
const schema: yup.SchemaOf<IListProjectForm> = 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<Props>) {
|
||||
|
||||
const [params] = useSearchParams();
|
||||
|
||||
const id = params.get('id') ? Number(params.get('id')) : null;
|
||||
|
||||
const isUpdating = !!id;
|
||||
const navigate = useNavigate()
|
||||
|
||||
|
||||
const methods = useForm<IListProjectForm>({
|
||||
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<IListProjectForm>,
|
||||
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<IListProjectForm> = data => console.log(data);
|
||||
|
||||
|
||||
if (query.loading)
|
||||
return <LoadingPage />
|
||||
|
||||
return (
|
||||
<FormProvider {...methods} >
|
||||
<UpdateProjectContextProvider permissions={query.data?.getProject.permissions ?? Object.values(ProjectPermissionEnum)}>
|
||||
<form onSubmit={methods.handleSubmit(onSubmit)}>
|
||||
{props.children}
|
||||
</form>
|
||||
</UpdateProjectContextProvider>
|
||||
</FormProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
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)]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<State | undefined>(undefined)
|
||||
|
||||
const UpdateProjectContextProvider = React.memo(function (props: PropsWithChildren<Props>) {
|
||||
|
||||
return (
|
||||
<context.Provider value={{ permissions: props.permissions }}>
|
||||
{props.children}
|
||||
</context.Provider>
|
||||
)
|
||||
})
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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<typeof ProjectDetailsTab>;
|
||||
|
||||
|
||||
const Template: ComponentStory<typeof ProjectDetailsTab> = (args) => <ProjectDetailsTab {...args as any} ></ProjectDetailsTab>
|
||||
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
}
|
||||
|
||||
|
||||
@@ -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<IListProjectForm>();
|
||||
|
||||
const isUpdating = !!getValues('id');
|
||||
|
||||
|
||||
return (
|
||||
<div className="md:col-span-2 flex flex-col gap-24">
|
||||
<Card className="" defaultPadding={false}>
|
||||
<div className="bg-gray-600 relative h-[160px] rounded-t-12 md:rounded-t-16">
|
||||
<Controller
|
||||
control={control}
|
||||
name="cover_image"
|
||||
render={({ field: { onChange, value, onBlur, ref } }) => <CoverImageInput
|
||||
value={value}
|
||||
rounded='rounded-t-12 md:rounded-t-16'
|
||||
onChange={e => {
|
||||
onChange(e)
|
||||
}}
|
||||
/>
|
||||
|
||||
}
|
||||
/>
|
||||
<div className="absolute left-24 bottom-0 translate-y-1/2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="thumbnail_image"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<AvatarInput value={value} onChange={onChange} width={120} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-16 md:p-24 mt-64">
|
||||
{(errors.cover_image || errors.thumbnail_image) && <div className="mb-16">
|
||||
{errors.cover_image && <p className="input-error">
|
||||
{errors.cover_image.message}
|
||||
</p>}
|
||||
{errors.thumbnail_image && <p className="input-error">
|
||||
{errors.thumbnail_image.message}
|
||||
</p>}
|
||||
</div>}
|
||||
<p className="text-body5 font-medium">
|
||||
Project name<sup className="text-red-500">*</sup>
|
||||
</p>
|
||||
<TextInput
|
||||
className="mt-8"
|
||||
isError={!!errors.title}
|
||||
placeholder='e.g BOLT🔩FUN'
|
||||
{...register("title")}
|
||||
/>
|
||||
{errors.title && <p className="input-error">
|
||||
{errors.title.message}
|
||||
</p>}
|
||||
<p className="text-body5 mt-16 font-medium">
|
||||
Project link<sup className="text-red-500">*</sup>
|
||||
</p>
|
||||
<TextInput
|
||||
className="mt-8"
|
||||
isError={!!errors.website}
|
||||
placeholder='https://lightning.xyz'
|
||||
{...register("website")}
|
||||
/>
|
||||
{errors.website && <p className="input-error">
|
||||
{errors.website.message}
|
||||
</p>}
|
||||
<p className="text-body5 mt-16 font-medium">
|
||||
Tagline<sup className="text-red-500">*</sup>
|
||||
</p>
|
||||
<TextInput
|
||||
className="mt-8"
|
||||
isError={!!errors.tagline}
|
||||
placeholder='Your product’s one liner'
|
||||
{...register("tagline")}
|
||||
/>
|
||||
{errors.tagline && <p className="input-error">
|
||||
{errors.tagline.message}
|
||||
</p>}
|
||||
<p className="text-body5 mt-16 font-medium">
|
||||
Description<sup className="text-red-500">*</sup>
|
||||
</p>
|
||||
<TextareaInput
|
||||
className="mt-8"
|
||||
isError={!!errors.description}
|
||||
rows={5}
|
||||
placeholder='Provide a short description your product...'
|
||||
{...register("description")}
|
||||
/>
|
||||
{errors.description && <p className="input-error">
|
||||
{errors.description.message}
|
||||
</p>}
|
||||
<p className="text-body5 font-medium mt-16">
|
||||
Project tag<sup className="text-red-500">*</sup>
|
||||
</p>
|
||||
<TextInput
|
||||
className="mt-8"
|
||||
isError={!!errors.hashtag}
|
||||
placeholder='my_project_name'
|
||||
inputClass="pl-8"
|
||||
renderBefore={() => <span className="flex flex-col justify-center pl-16 shrink-0">#</span>}
|
||||
{...register("hashtag")}
|
||||
/>
|
||||
{errors.hashtag && <p className="input-error">
|
||||
{errors.hashtag.message}
|
||||
</p>}
|
||||
{(isUpdating && dirtyFields.hashtag) &&
|
||||
<InfoCard className="mt-8 bg-warning-50 border-warning-100">
|
||||
<span className="font-medium text-orange-600">⚠️ Warning:</span> when you change the tag of your project, existing links that use this tag will no longer work & will need to be updateded.
|
||||
</InfoCard>}
|
||||
{!isUpdating &&
|
||||
<InfoCard className="mt-8">
|
||||
<span className="font-medium text-gray-900">ℹ️ Project tag</span> 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.
|
||||
</InfoCard>
|
||||
}
|
||||
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="">
|
||||
<h2 className="text-body2 font-bolder">🔗 Links</h2>
|
||||
<p className="text-body4 font-light text-gray-600 mt-8">Make sure that people can find your project online. </p>
|
||||
<div className="flex flex-col gap-8 mt-24">
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
className="mt-8"
|
||||
isError={!!errors.twitter}
|
||||
renderBefore={() => <FiTwitter className="text-blue-400 text-body2 pl-16 py-0 w-auto shrink-0 self-center" />}
|
||||
placeholder='https://twitter.com/project_handle'
|
||||
{...register("twitter")}
|
||||
/>
|
||||
{errors.twitter && <p className="input-error">
|
||||
{errors.twitter.message}
|
||||
</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
className="mt-8"
|
||||
isError={!!errors.discord}
|
||||
renderBefore={() => <FaDiscord className="text-violet-500 text-body2 pl-16 py-0 w-auto shrink-0 self-center" />}
|
||||
placeholder='https://discord.com/'
|
||||
{...register("discord")}
|
||||
/>
|
||||
{errors.discord && <p className="input-error">
|
||||
{errors.discord.message}
|
||||
</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
className="mt-8"
|
||||
isError={!!errors.github}
|
||||
renderBefore={() => <FiGithub className="text-gray-700 text-body2 pl-16 py-0 w-auto shrink-0 self-center" />}
|
||||
placeholder='https://github.com/'
|
||||
{...register("github")}
|
||||
/>
|
||||
{errors.github && <p className="input-error">
|
||||
{errors.github.message}
|
||||
</p>}
|
||||
</div>
|
||||
<div>
|
||||
<TextInput
|
||||
className="mt-8"
|
||||
isError={!!errors.slack}
|
||||
renderBefore={() => <FaSlack className="text-pink-500 text-body2 pl-16 py-0 w-auto shrink-0 self-center" />}
|
||||
placeholder='https://slack.com/'
|
||||
{...register("slack")}
|
||||
/>
|
||||
{errors.slack && <p className="input-error">
|
||||
{errors.slack.message}
|
||||
</p>}
|
||||
</div>
|
||||
<div>
|
||||
<TextInput
|
||||
className="mt-8"
|
||||
isError={!!errors.telegram}
|
||||
renderBefore={() => <FaTelegram className="text-teal-500 text-body2 pl-16 py-0 w-auto shrink-0 self-center" />}
|
||||
placeholder='https://t.me/XXXXXX'
|
||||
{...register("telegram")}
|
||||
/>
|
||||
{errors.telegram && <p className="input-error">
|
||||
{errors.telegram.message}
|
||||
</p>}
|
||||
</div>
|
||||
<div>
|
||||
|
||||
<TextInput
|
||||
className="mt-8"
|
||||
isError={!!errors.lightning_address}
|
||||
renderBefore={() => <BsLightningChargeFill className="text-yellow-400 text-body2 pl-16 py-0 w-auto shrink-0 self-center" />}
|
||||
placeholder='lightning_address@XXX.com'
|
||||
{...register("lightning_address")}
|
||||
/>
|
||||
{errors.lightning_address && <p className="input-error">
|
||||
{errors.lightning_address.message}
|
||||
</p>}
|
||||
</div>
|
||||
</div>
|
||||
</Card >
|
||||
|
||||
<Card>
|
||||
<h2 className="text-body2 font-bolder">🌶️ Category<sup className="text-red-500">*</sup></h2>
|
||||
<p className="text-body4 font-light text-gray-600 mt-8">Select one of the categories below.</p>
|
||||
<div className="mt-24">
|
||||
<Controller
|
||||
control={control}
|
||||
name="category_id"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<CategoriesInput
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.category_id && <p className='input-error'>{errors.category_id?.message}</p>}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<h2 className="text-body2 font-bolder">🦾 Capabilities</h2>
|
||||
<p className="text-body4 font-light text-gray-600 mt-8">Let other makers know what lightning capabilities your application has.</p>
|
||||
<div className="mt-24">
|
||||
<Controller
|
||||
control={control}
|
||||
name="capabilities"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<CapabilitiesInput
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.capabilities && <p className='input-error'>{errors.capabilities?.message}</p>}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<h2 className="text-body2 font-bolder">📷 Screenshots</h2>
|
||||
<p className="text-body4 font-light text-gray-600 mt-8">Choose up to 4 screenshots from your project</p>
|
||||
<div className="mt-24">
|
||||
<Controller
|
||||
control={control}
|
||||
name="screenshots"
|
||||
render={({ field: { onChange, value, onBlur, ref } }) => <ScreenshotsInput
|
||||
value={value}
|
||||
onChange={e => {
|
||||
onChange(e)
|
||||
}}
|
||||
/>
|
||||
|
||||
}
|
||||
/>
|
||||
{errors.capabilities && <p className='input-error'>{errors.capabilities?.message}</p>}
|
||||
</div>
|
||||
</Card>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
query IsValidProjectHashtag($hashtag: String!, $projectId: Int) {
|
||||
checkValidProjectHashtag(hashtag: $hashtag, projectId: $projectId)
|
||||
}
|
||||
@@ -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<typeof ProjectListedModal>;
|
||||
|
||||
|
||||
const Template: ComponentStory<typeof ProjectListedModal> = (args) => <ProjectListedModal {...args as any} ></ProjectListedModal>
|
||||
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<motion.div
|
||||
custom={direction}
|
||||
variants={modalCardVariants}
|
||||
initial='initial'
|
||||
animate="animate"
|
||||
exit='exit'
|
||||
className="modal-card max-w-[442px] p-24 rounded-xl relative"
|
||||
>
|
||||
<Portal id='confetti'>
|
||||
<Confetti recycle={false} width={size.width} height={size.height} numberOfPieces={isSmallScreen ? 200 : 400} />
|
||||
</Portal>
|
||||
<IoClose className='absolute text-body2 top-24 right-24 hover:cursor-pointer' onClick={onClose} />
|
||||
<h2 className='text-h5 font-bold text-center'>Product listed!</h2>
|
||||
<div className="flex flex-col gap-16 justify-center items-center my-24">
|
||||
<Avatar src={project.img} width={80} />
|
||||
<p className="text-body3 font-medium">{project.name}</p>
|
||||
</div>
|
||||
<p className="text-body4 font-light text-gray-600 mt-24 text-center">
|
||||
Nice work, you successfully listed your product! Here are a few ideas to get your started.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col gap-16 my-32">
|
||||
<div className='!flex items-center gap-16'>
|
||||
<div className={`rounded-8 w-48 h-48 text-center py-12 shrink-0 bg-gray-100`}>
|
||||
✍️
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium self-center">
|
||||
Stories
|
||||
</p>
|
||||
<p className="text-body5 text-gray-500">
|
||||
Tell the maker community about your product.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='!flex items-center gap-16'>
|
||||
<div className={`rounded-8 w-48 h-48 text-center py-12 shrink-0 bg-gray-100`}>
|
||||
⚔️
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium self-center">
|
||||
Start hacking
|
||||
</p>
|
||||
<p className="text-body5 text-gray-500">
|
||||
Kickstart your hacking journey with a tournament.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-16" >
|
||||
<Button
|
||||
color='primary'
|
||||
fullWidth
|
||||
newTab
|
||||
href={createRoute({ type: "edit-story" })}
|
||||
>✍️ Write a story</Button>
|
||||
<Button
|
||||
color='white'
|
||||
fullWidth
|
||||
newTab
|
||||
href='/tournaments'
|
||||
>⚔️ Explore tournaments</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
import { lazyModal } from 'src/utils/helperFunctions';
|
||||
|
||||
export const { LazyComponent: ProjectListedModal } = lazyModal(() => import('./ProjectListedModal'))
|
||||
@@ -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 (
|
||||
<div className="flex flex-wrap gap-8">
|
||||
{query.loading ?
|
||||
Array(10).fill(0).map((_, idx) =>
|
||||
<div
|
||||
key={idx}
|
||||
className="bg-gray-100 border border-gray-200 p-8 rounded-10">
|
||||
<span className='invisible'>{"loading category skeleton".slice(random(6, 12))}</span>
|
||||
</div>)
|
||||
:
|
||||
query.data?.getAllMakersRoles.map(item =>
|
||||
<Button
|
||||
key={item.id}
|
||||
color='none'
|
||||
size='sm'
|
||||
className={`
|
||||
border border-gray-200
|
||||
${props.value.includes(item.id) ?
|
||||
'text-primary-600 bg-primary-100'
|
||||
:
|
||||
"bg-gray-100"
|
||||
}
|
||||
`}
|
||||
onClick={() => handleClick(item.id)}
|
||||
>
|
||||
{item.icon} {item.title}
|
||||
</Button>)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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: '✍️️'
|
||||
},
|
||||
]
|
||||
@@ -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<typeof RecruitRolesInput>;
|
||||
|
||||
|
||||
const Template: ComponentStory<typeof RecruitRolesInput> = (args) => WrapFormController('v', [])(<RecruitRolesInput {...args as any} ></RecruitRolesInput>)
|
||||
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
}
|
||||
|
||||
|
||||
@@ -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<IListProjectForm>();
|
||||
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<IListProjectForm>(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 <Button
|
||||
color="primary"
|
||||
fullWidth
|
||||
onClick={clickSubmit}
|
||||
disabled={!isDirty || isLoading}
|
||||
>
|
||||
{isLoading ? "Saving..." : "Save Changes"}
|
||||
</Button>
|
||||
else if (props.currentTab === 'project-details')
|
||||
return <Button
|
||||
color="primary"
|
||||
fullWidth
|
||||
onClick={props.onNext}
|
||||
>
|
||||
Next step: {tabs.team.text}
|
||||
</Button>
|
||||
else if (props.currentTab === 'team')
|
||||
return <Button
|
||||
color="primary"
|
||||
fullWidth
|
||||
onClick={props.onNext}
|
||||
>
|
||||
Next step: {tabs.extras.text}
|
||||
</Button>
|
||||
else
|
||||
return <Button
|
||||
color="primary"
|
||||
fullWidth
|
||||
onClick={clickSubmit}
|
||||
disabled={!isDirty || isLoading}
|
||||
>
|
||||
{isLoading ? "Listing your product..." : "List your product"}
|
||||
</Button>
|
||||
}, [clickSubmit, isDirty, isLoading, isUpdating, props.currentTab, props.onNext])
|
||||
|
||||
|
||||
return (
|
||||
<Card className='flex flex-col gap-24'>
|
||||
<div className='flex gap-8 items-center'>
|
||||
{img ?
|
||||
<Avatar width={48} src={img.url} /> :
|
||||
<div className="bg-gray-50 border border-gray-200 rounded-full w-48 h-48 shrink-0"></div>
|
||||
}
|
||||
<div className='overflow-hidden'>
|
||||
<p className={`text-body4 text-black font-medium overflow-hidden text-ellipsis`}>{name || "Product preview"}</p>
|
||||
{<p className={`text-body6 text-gray-600 text-ellipsis overflow-hidden whitespace-nowrap`}>{tagline || "Provide some more details."}</p>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-b border-gray-200"></div>
|
||||
{/* <p className="hidden md:block text-body5">{trimText(profileQuery.data.profile.bio, 120)}</p> */}
|
||||
<div className="flex flex-col gap-16">
|
||||
{ctaBtn}
|
||||
<Button
|
||||
color="gray"
|
||||
onClick={clickCancel}
|
||||
disabled={!isDirty || isLoading}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<div
|
||||
key={user.id}
|
||||
className="border-b py-16 last-of-type:border-b-0 flex gap-16 items-center">
|
||||
|
||||
<Avatar width={40} src={user.avatar} />
|
||||
<div className='grow overflow-hidden'>
|
||||
<p className="font-medium self-center overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{user.name}
|
||||
</p>
|
||||
<p className="text-body5 text-gray-500 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{user.jobTitle}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="ml-auto flex gap-12 md:gap-16 shrink-0">
|
||||
{canUpdateRole ? <Menu
|
||||
offsetY={12}
|
||||
align='end'
|
||||
menuButton={<MenuButton className='border text-body5 border-gray-200 p-8 rounded-8 text-gray-500'>{user.role} <FaChevronDown className='ml-4 text-gray-400' /></MenuButton>} transition>
|
||||
{[Team_Member_Role.Admin, Team_Member_Role.Maker].map(role =>
|
||||
<MenuItem
|
||||
className={'text-body5'}
|
||||
onClick={() => onUpdateRole(role)}
|
||||
key={role}>{role}</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
:
|
||||
<span className="text-gray-500">{user.role}</span>
|
||||
}
|
||||
{canDelete && <button onClick={() => onRemove()} className=''>
|
||||
<FaRegTrashAlt className='text-red-400' />
|
||||
</button>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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<typeof TeamMembersInput>;
|
||||
|
||||
|
||||
const Template: ComponentStory<typeof TeamMembersInput> = (args) => WrapFormController('v', [])(<TeamMembersInput {...args as any} ></TeamMembersInput>)
|
||||
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
}
|
||||
|
||||
|
||||
@@ -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<infer U> ? 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<typeof UsersInput>['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 && <UsersInput onSelect={addMember} />}
|
||||
{value.length > 0 &&
|
||||
<div className='flex flex-col mt-24'>
|
||||
{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 <MemberRow
|
||||
key={member.id}
|
||||
user={member}
|
||||
canUpdateRole={canEdit}
|
||||
canDelete={canEdit}
|
||||
onRemove={() => removeMember(member.id)}
|
||||
onUpdateRole={role => setMemberRole(member.id, role)}
|
||||
/>
|
||||
})}
|
||||
</div>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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<typeof TeamTab>;
|
||||
|
||||
|
||||
const Template: ComponentStory<typeof TeamTab> = (args) => <TeamTab {...args as any} ></TeamTab>
|
||||
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
}
|
||||
|
||||
|
||||
@@ -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<IListProjectForm>();
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-24">
|
||||
<Card >
|
||||
<h2 className="text-body2 font-bolder">⚡️ Team</h2>
|
||||
<p className="text-body4 font-light text-gray-600 mt-8">Let us know who is on this product’s team.</p>
|
||||
<div className="mt-24">
|
||||
<Controller
|
||||
control={control}
|
||||
name="members"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TeamMembersInput
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.members && <p className='input-error'>{errors.members?.message}</p>}
|
||||
</div>
|
||||
<div className="bg-gray-50 p-16 rounded-12 border border-gray-200 mt-24">
|
||||
<p className="text-body5">
|
||||
<span className="font-bold">ℹ️ Onboard your team:</span> 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.
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<h2 className="text-body2 font-bolder">💪️ Recruit</h2>
|
||||
<p className="text-body4 font-light text-gray-600 mt-8">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.</p>
|
||||
<div className="mt-24">
|
||||
<Controller
|
||||
control={control}
|
||||
name="recruit_roles"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<RecruitRolesInput
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.recruit_roles && <p className='input-error'>{errors.recruit_roles?.message}</p>}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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<typeof TournamentsInput>;
|
||||
|
||||
|
||||
const Template: ComponentStory<typeof TournamentsInput> = (args) => WrapFormController('v', [])(<TournamentsInput {...args as any} ></TournamentsInput>)
|
||||
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex flex-wrap gap-8">
|
||||
{query.loading ?
|
||||
Array(4).fill(0).map((_, idx) =>
|
||||
<div
|
||||
key={idx}
|
||||
className="bg-gray-100 border border-gray-200 p-8 rounded-10">
|
||||
<span className='invisible'>{"loading category skeleton".slice(random(6, 12))}</span>
|
||||
</div>)
|
||||
:
|
||||
((query.data?.getTournamentToRegister && query.data?.getTournamentToRegister.length < 0) ?
|
||||
query.data?.getTournamentToRegister.map(item =>
|
||||
<Button
|
||||
key={item.id}
|
||||
color='none'
|
||||
size='sm'
|
||||
className={`
|
||||
border text-gray-800
|
||||
${props.value.includes(item.id) ?
|
||||
'title-primary-600 bg-primary-100 border-primary-200'
|
||||
:
|
||||
"bg-gray-100 border-gray-200"
|
||||
}
|
||||
`}
|
||||
onClick={() => handleClick(item.id)}
|
||||
>
|
||||
{item.title}
|
||||
</Button>)
|
||||
:
|
||||
<p className='text-gray-400 font-medium'>
|
||||
There is no running tournaments currently.
|
||||
</p>)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
query GetTournamentsToRegister {
|
||||
getTournamentToRegister {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
140
src/features/Projects/pages/ListProjectPage/ListProjectPage.tsx
Normal file
140
src/features/Projects/pages/ListProjectPage/ListProjectPage.tsx
Normal file
@@ -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<TabsKeys>(tabs['project-details'].path)
|
||||
const { viewportRef, } = useCarousel({
|
||||
align: 'start', slidesToScroll: 2,
|
||||
containScroll: "trimSnaps",
|
||||
})
|
||||
const location = useLocation()
|
||||
|
||||
const meQuery = useMeQuery({
|
||||
|
||||
});
|
||||
|
||||
if (meQuery.loading) return <LoadingPage />
|
||||
|
||||
if (meQuery.error || !meQuery.data?.me) return <Navigate to={'/login'} state={{ from: location.pathname }} />
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>List a project</title>
|
||||
<meta property="og:title" content='List a project' />
|
||||
</Helmet>
|
||||
<div className="page-container">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-24">
|
||||
{isMediumScreen ?
|
||||
<aside >
|
||||
<Card className="sticky-side-element">
|
||||
<p className="text-body2 font-bolder text-black mb-16">List a project</p>
|
||||
<ul className=' flex flex-col gap-8'>
|
||||
{links.map((link, idx) =>
|
||||
<li key={idx}>
|
||||
<button
|
||||
// className={({ isActive }) => `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}
|
||||
</button>
|
||||
</li>)}
|
||||
</ul>
|
||||
</Card>
|
||||
</aside>
|
||||
:
|
||||
<aside
|
||||
className=" bg-white z-10 w-full sticky-top-element !col-start-1"
|
||||
>
|
||||
<div className="relative group overflow-hidden">
|
||||
<div className="border-b-2 border-gray-200" ref={viewportRef}>
|
||||
<div className="select-none w-full flex gap-16">
|
||||
{links.map((link, idx) =>
|
||||
<button
|
||||
key={idx}
|
||||
className={`flex min-w-max items-start cursor-pointer font-bold py-12 px-8
|
||||
active:scale-95 transition-transform`}
|
||||
style={{
|
||||
...(link.path === curTab && {
|
||||
borderBottom: '2px solid var(--primary)',
|
||||
marginBottom: -2
|
||||
}),
|
||||
}
|
||||
}
|
||||
onClick={() => setCurTab(link.path)}
|
||||
>
|
||||
{link.text}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
}
|
||||
<main className="md:col-span-3">
|
||||
<FormContainer>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-24">
|
||||
<div className="md:col-span-2">
|
||||
{curTab === tabs["project-details"].path && <ProjectDetailsTab />}
|
||||
{curTab === tabs["team"].path && <TeamTab />}
|
||||
{curTab === tabs["extras"].path && <ExtrasTab />}
|
||||
</div>
|
||||
<div className="self-start sticky-side-element">
|
||||
<SaveChangesCard
|
||||
currentTab={curTab}
|
||||
onNext={() => {
|
||||
if (curTab === 'project-details') setCurTab(tabs['team'].path)
|
||||
else if (curTab === 'team') setCurTab(tabs['extras'].path)
|
||||
}}
|
||||
onBackToFirstPage={() => setCurTab(tabs["project-details"].path)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</FormContainer>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<motion.div
|
||||
custom={direction}
|
||||
@@ -26,30 +25,40 @@ export default function ProjectDetailsCardSkeleton({ onClose, direction, ...prop
|
||||
initial='initial'
|
||||
animate="animate"
|
||||
exit='exit'
|
||||
className={`modal-card max-w-[768px] ${props.isPageModal && !isMdScreen && 'rounded-0 w-full min-h-screen'}`}
|
||||
className={`modal-card max-w-[676px] ${props.isPageModal && !isMdScreen && 'rounded-0 w-full min-h-screen'}`}
|
||||
|
||||
>
|
||||
<div className="relative h-[80px] lg:h-[152px]">
|
||||
<div className="relative h-[100px] lg:h-[80px]">
|
||||
<Skeleton height='100%' className='!leading-inherit' />
|
||||
<button className="w-40 h-40 md:w-48 md:h-48 bg-white z-10 absolute top-1/2 left-32 -translate-y-1/2 rounded-full hover:bg-gray-200 text-center" onClick={onClose}><MdClose className=' inline-block text-body2 lg:text-body1' /></button>
|
||||
<button className="w-32 h-32 bg-gray-600 bg-opacity-80 text-white absolute top-24 right-24 rounded-full hover:bg-gray-800 text-center" onClick={onClose}><MdClose className=' inline-block' /></button>
|
||||
</div>
|
||||
<div className="p-24">
|
||||
<div className="flex gap-24 items-center h-[93px]">
|
||||
<div className="flex-shrink-0 w-[93px] h-[93px] rounded-md overflow-hidden">
|
||||
<Skeleton height='100%' />
|
||||
<div className="flex flex-col mt-[-80px] md:flex-row md:mt-0 gap-24 items-start relative">
|
||||
<div className="flex-shrink-0 w-[108px] h-[108px] ">
|
||||
<Skeleton height='100%' className='rounded-24 border-2 border-white' />
|
||||
</div>
|
||||
<div className='flex flex-col items-start justify-between self-stretch'>
|
||||
<h3 className="text-h3 font-regular"> <Skeleton width='13ch' /></h3>
|
||||
<span className="text-blue-400 font-regular text-body4" > <Skeleton width='6ch' /></span>
|
||||
<div className='flex gap-8'>
|
||||
<Badge size='sm' isLoading />
|
||||
<Badge size='sm' isLoading />
|
||||
<div className='flex flex-col gap-8 items-start justify-between'>
|
||||
<h3 className="text-body1 font-bold"><Skeleton width='13ch' /></h3>
|
||||
<p className="text-body4 text-gray-600"><Skeleton width='30ch' /></p>
|
||||
<div>
|
||||
<span className="font-medium text-body4 text-gray-600"><Skeleton width='10ch' /></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-shrink-0 w-full md:w-auto md:flex ml-auto gap-16 self-stretch">
|
||||
{/* <Button color='primary' size='md' className=" my-16" href={project.website} newTab >Visit <BsJoystick /></Button> */}
|
||||
{/* <VoteButton onVote={onVote} /> */}
|
||||
{/* <VoteButton fullWidth votes={project.votes_count} direction='vertical' onVote={onVote} /> */}
|
||||
{/* {isWalletConnected ?
|
||||
:
|
||||
<Button onClick={onConnectWallet} size='md' className="border border-gray-200 bg-gray-100 hover:bg-gray-50 active:bg-gray-100 my-16"><AiFillThunderbolt className='inline-block text-thunder transform scale-125' /> Connect Wallet to Vote</Button>
|
||||
} */}
|
||||
<Button fullWidth variant='outline' color='gray' className='!px-8'>
|
||||
<p className='opacity-0'>votes</p>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-40 text-body4 leading-normal h-[120px]">
|
||||
|
||||
<Skeleton width='98%' />
|
||||
<Skeleton width='90%' />
|
||||
<Skeleton width='70%' />
|
||||
@@ -57,8 +66,6 @@ export default function ProjectDetailsCardSkeleton({ onClose, direction, ...prop
|
||||
</p>
|
||||
|
||||
<div className="mt-40">
|
||||
<h3 className="text-h5 font-bold mb-16">Screenshots</h3>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 justify-items-center">
|
||||
{
|
||||
Array(4).fill(0).map((_, idx) => <div key={idx} className="w-full relative pt-[56%] cursor-pointer bg-gray-200 shadow-sm rounded-10 overflow-hidden">
|
||||
|
||||
@@ -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
|
||||
</div>
|
||||
</div>
|
||||
|
||||
if (loading || !project)
|
||||
if (loading || !data?.getProject)
|
||||
return <ProjectCardSkeleton onClose={closeModal} direction={direction} isPageModal={props.isPageModal} />;
|
||||
|
||||
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 (
|
||||
<div
|
||||
className={`modal-card max-w-[768px] ${(props.isPageModal && !isMdScreen) && '!rounded-0 w-full min-h-screen'}`}
|
||||
className={`modal-card max-w-[676px] ${(props.isPageModal && !isMdScreen) && '!rounded-0 w-full min-h-screen'}`}
|
||||
>
|
||||
<div className="relative h-[80px] lg:h-[152px]">
|
||||
{/* Cover Image */}
|
||||
<div className="relative h-[120px] lg:h-[80px]">
|
||||
<img className="w-full h-full object-cover" src={project.cover_image} alt="" />
|
||||
<button className="w-40 h-40 md:w-48 md:h-48 bg-white absolute top-1/2 left-32 -translate-y-1/2 rounded-full hover:bg-gray-200 text-center" onClick={closeModal}><MdClose className=' inline-block text-body2 lg:text-body1' /></button>
|
||||
<div className="absolute top-16 md:top-24 left-24 flex gap-8 bg-gray-800 bg-opacity-60 text-white rounded-48 py-4 px-12 text-body6 font-medium">
|
||||
{project.launch_status === ProjectLaunchStatusEnum.Launched && `🚀 Launched`}
|
||||
{project.launch_status === ProjectLaunchStatusEnum.Wip && `🔧 WIP`}
|
||||
</div>
|
||||
<div className="absolute top-16 md:top-24 right-24 flex gap-8">
|
||||
{project.permissions.includes(ProjectPermissionEnum.UpdateInfo) &&
|
||||
<Link className="w-32 h-32 bg-gray-800 bg-opacity-60 text-white rounded-full hover:bg-opacity-40 text-center flex flex-col justify-center items-center" onClick={() => props.onClose?.()} to={createRoute({ type: "edit-project", id: project.id })}><FiEdit2 /></Link>}
|
||||
<button className="w-32 h-32 bg-gray-800 bg-opacity-60 text-white rounded-full hover:bg-opacity-40 text-center flex flex-col justify-center items-center" onClick={closeModal}><IoMdClose className=' inline-block' /></button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-24">
|
||||
<div className="flex gap-24 items-start">
|
||||
<div className="flex-shrink-0 w-[93px] h-[93px]">
|
||||
<img className="w-full h-full rounded-md border" src={project?.thumbnail_image} alt="" />
|
||||
<div className="p-24 flex flex-col gap-24">
|
||||
|
||||
{/* Title & Basic Info */}
|
||||
<div className="flex flex-col mt-[-80px] md:flex-row md:mt-0 gap-24 md:items-center relative">
|
||||
<div className="flex-shrink-0 w-[108px] h-[108px]">
|
||||
<img className="w-full h-full border-2 border-white rounded-24" src={project.thumbnail_image} alt="" />
|
||||
</div>
|
||||
<div className='flex flex-col items-start justify-between self-stretch'>
|
||||
<h3 className="text-h3 font-regular">{project?.title}</h3>
|
||||
<a className="text-blue-400 font-regular text-body4 truncate max-w-[20ch]" target='_blank' rel="noreferrer" href={project?.website}>{project?.website?.replace(/(^\w+:|^)\/\//, '').replace(/\/$/, "")}</a>
|
||||
<div className='flex flex-col gap-8 items-start justify-between'>
|
||||
<a href={project.website} target='_blank' rel="noreferrer"><h3 className="text-body1 font-bold">{project.title}</h3></a>
|
||||
<p className="text-body4 text-gray-600">{project.tagline}</p>
|
||||
<div>
|
||||
<span className="chip-small font-light text-body5 py-4 px-12 mr-8"> {project?.category.title}</span>
|
||||
|
||||
<span className="chip-small bg-warning-50 font-light text-body5 py-4 px-12"><MdLocalFireDepartment className='inline-block text-fire transform text-body4 align-middle' /> {numberFormatter(project?.votes_count)}</span>
|
||||
|
||||
<span className="font-medium text-body4 text-gray-600">{project.category.icon} {project.category.title}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0 hidden md:flex ml-auto gap-16">
|
||||
<Button color='primary' size='md' className=" my-16" href={project.website} newTab >Visit <BsJoystick /></Button>
|
||||
{isWalletConnected ?
|
||||
<VoteButton onVote={onVote} />
|
||||
<div className="flex-shrink-0 w-full md:w-auto md:flex ml-auto gap-16 self-stretch">
|
||||
{/* <Button color='primary' size='md' className=" my-16" href={project.website} newTab >Visit <BsJoystick /></Button> */}
|
||||
{/* <VoteButton onVote={onVote} /> */}
|
||||
{/* <VoteButton fullWidth votes={project.votes_count} direction='vertical' onVote={onVote} /> */}
|
||||
{/* {isWalletConnected ?
|
||||
:
|
||||
<Button onClick={onConnectWallet} size='md' className="border border-gray-200 bg-gray-100 hover:bg-gray-50 active:bg-gray-100 my-16"><AiFillThunderbolt className='inline-block text-thunder transform scale-125' /> Connect Wallet to Vote</Button>
|
||||
}
|
||||
} */}
|
||||
<Button fullWidth variant='outline' color='gray' className='!px-8' onClick={() => onVote()}>
|
||||
<div className="flex justify-center items-center gap-8 md:flex-col ">
|
||||
<MdLocalFireDepartment />{<span className="align-middle w-[4ch]"> {numberFormatter(project.votes_count)}</span>}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-40 text-body4 leading-normal whitespace-pre-line" dangerouslySetInnerHTML={{
|
||||
__html: linkifyHtml(project?.description, {
|
||||
className: ' text-blue-500 underline',
|
||||
defaultProtocol: 'https',
|
||||
target: "_blank",
|
||||
rel: 'noreferrer'
|
||||
})
|
||||
}}></p>
|
||||
<div className="md:hidden">
|
||||
<Button color='primary' size='md' fullWidth href={project.website} newTab className="w-full mt-24 mb-16">Visit <BsJoystick /></Button>
|
||||
{isWalletConnected ?
|
||||
<VoteButton fullWidth onVote={onVote} />
|
||||
:
|
||||
<Button size='md' fullWidth className="bg-gray-200 hover:bg-gray-100 mb-24" onClick={onConnectWallet}><AiFillThunderbolt className='inline-block text-thunder transform scale-125' /> Connect Wallet to Vote</Button>
|
||||
}
|
||||
|
||||
|
||||
{/* About */}
|
||||
<div>
|
||||
<p className="text-body6 uppercase font-medium text-gray-400 mb-8">About</p>
|
||||
<div className=" text-body4 text-gray-600 leading-normal whitespace-pre-line" dangerouslySetInnerHTML={{
|
||||
__html: linkifyHtml(project.description, {
|
||||
className: ' text-blue-500 underline',
|
||||
defaultProtocol: 'https',
|
||||
target: "_blank",
|
||||
rel: 'noreferrer'
|
||||
})
|
||||
}}></div>
|
||||
|
||||
{/* Links */}
|
||||
<div className="mt-16 flex flex-wrap gap-16">
|
||||
{links.filter(link => !!link.value).map((link, idx) =>
|
||||
(link.url ? <a
|
||||
key={idx}
|
||||
href={link.url!}
|
||||
className={`w-40 aspect-square rounded-full flex justify-center items-center ${link.colors}`}
|
||||
target='_blank'
|
||||
rel="noreferrer">
|
||||
<link.icon className="scale-125" />
|
||||
</a>
|
||||
:
|
||||
<CopyToClipboard
|
||||
text={link.value!}
|
||||
onCopy={() => NotificationsService.info(" Copied to clipboard", { icon: "📋" })}
|
||||
>
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => { }}
|
||||
className={`w-40 aspect-square rounded-full flex justify-center items-center ${link.colors}`}
|
||||
>
|
||||
<link.icon className="scale-125" />
|
||||
</button>
|
||||
</CopyToClipboard>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{project.screenshots.length > 0 && <>
|
||||
<div className="mt-40">
|
||||
<h3 className="text-h5 font-bold mb-16">Screenshots</h3>
|
||||
<div className="">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 justify-items-center">
|
||||
{project.screenshots.slice(0, 4).map((screenshot, idx) => <div
|
||||
key={idx}
|
||||
@@ -165,9 +244,36 @@ export default function ProjectDetailsCard({ direction, projectId, ...props }: P
|
||||
onClose={() => setScreenshotsOpen(-1)}
|
||||
/>
|
||||
</>}
|
||||
<hr className="my-40" />
|
||||
<div className="text-center">
|
||||
<h3 className="text-body4 font-regular">Are you the creator of this project?</h3>
|
||||
|
||||
{project.capabilities.length > 0 &&
|
||||
<div>
|
||||
<p className="text-body6 uppercase font-medium text-gray-400 mb-8">CAPABILITIES</p>
|
||||
<div className="flex flex-wrap gap-8">
|
||||
{project.capabilities.map(cap => <Badge key={cap.id} size='sm'>{cap.icon} {cap.title}</Badge>)}
|
||||
</div>
|
||||
</div>}
|
||||
<hr className="" />
|
||||
{project.members.length > 0 &&
|
||||
<div>
|
||||
<p className="text-body6 uppercase font-medium text-gray-400 mb-8">MAKERS</p>
|
||||
<div className="flex flex-wrap gap-8">
|
||||
{project.members.map(m => <Link key={m.user.id} to={createRoute({ type: "profile", id: m.user.id, username: m.user.name })}>
|
||||
<Avatar
|
||||
width={40}
|
||||
src={m.user.avatar}
|
||||
renderTooltip={() => <div className='bg-white px-12 py-8 border border-gray-200 rounded-12 flex flex-wrap gap-12 shadow-lg'>
|
||||
<Avatar width={48} src={m.user.avatar} />
|
||||
<div className='overflow-hidden'>
|
||||
<p className={`text-black font-medium overflow-hidden text-ellipsis`}>{m.user.name}</p>
|
||||
<p className={`text-body6 text-gray-600`}>{m.user.jobTitle}</p>
|
||||
</div>
|
||||
</div>}
|
||||
/>
|
||||
</Link>)}
|
||||
</div>
|
||||
</div>}
|
||||
{/* <div className="text-center">
|
||||
<h3 className="text-body4 font-regular">Are you the creator of this project</h3>
|
||||
<Button
|
||||
color='gray'
|
||||
size='md'
|
||||
@@ -176,7 +282,7 @@ export default function ProjectDetailsCard({ direction, projectId, ...props }: P
|
||||
newTab
|
||||
// onClick={onClaim}
|
||||
>Claim 🖐</Button>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
import { Project, ProjectCategory } from "src/utils/interfaces";
|
||||
import { Project, ProjectPermissionEnum } from "src/graphql";
|
||||
import { ProjectCategory } from "src/utils/interfaces";
|
||||
|
||||
|
||||
export let categories = [
|
||||
@@ -93,9 +94,29 @@ export let projects = [
|
||||
"lightning_address": "hello@getalby.com",
|
||||
"votes_count": 335,
|
||||
tags: [],
|
||||
awards: [],
|
||||
capabilities: [],
|
||||
discord: "https://discord.com",
|
||||
github: null,
|
||||
hashtag: "#hashtag",
|
||||
launch_status: "Launched",
|
||||
members: [],
|
||||
permissions: Object.values(ProjectPermissionEnum),
|
||||
recruit_roles: [],
|
||||
slack: null,
|
||||
telegram: null,
|
||||
twitter: null,
|
||||
tournaments: [],
|
||||
tagline: "My project tagline",
|
||||
lnurl_callback_url: "",
|
||||
"category": {
|
||||
"id": 9,
|
||||
"title": "Misc / Other"
|
||||
"title": "Misc / Other",
|
||||
icon: "🎁",
|
||||
apps_count: 0,
|
||||
cover_image: "null",
|
||||
project: [],
|
||||
votes_sum: 100,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -109,9 +130,29 @@ export let projects = [
|
||||
"lightning_address": "divineorgan67@walletofsatoshi.com",
|
||||
"votes_count": 232,
|
||||
tags: [],
|
||||
awards: [],
|
||||
capabilities: [],
|
||||
discord: "https://discord.com",
|
||||
github: null,
|
||||
hashtag: "#hashtag",
|
||||
launch_status: "Launched",
|
||||
members: [],
|
||||
permissions: Object.values(ProjectPermissionEnum),
|
||||
recruit_roles: [],
|
||||
slack: null,
|
||||
telegram: null,
|
||||
twitter: null,
|
||||
tournaments: [],
|
||||
tagline: "My project tagline",
|
||||
lnurl_callback_url: "",
|
||||
"category": {
|
||||
"id": 1,
|
||||
"title": "Finance"
|
||||
"title": "Finance",
|
||||
icon: "🎁",
|
||||
apps_count: 0,
|
||||
cover_image: "null",
|
||||
project: [],
|
||||
votes_sum: 100,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -129,9 +170,29 @@ export let projects = [
|
||||
"lightning_address": "johns@getalby.com",
|
||||
"votes_count": 220,
|
||||
tags: [],
|
||||
awards: [],
|
||||
capabilities: [],
|
||||
discord: "https://discord.com",
|
||||
github: null,
|
||||
hashtag: "#hashtag",
|
||||
launch_status: "Launched",
|
||||
members: [],
|
||||
permissions: Object.values(ProjectPermissionEnum),
|
||||
recruit_roles: [],
|
||||
slack: null,
|
||||
telegram: null,
|
||||
twitter: null,
|
||||
tournaments: [],
|
||||
tagline: "My project tagline",
|
||||
lnurl_callback_url: "",
|
||||
"category": {
|
||||
"id": 1,
|
||||
"title": "Finance"
|
||||
"title": "Finance",
|
||||
icon: "🎁",
|
||||
apps_count: 0,
|
||||
cover_image: "null",
|
||||
project: [],
|
||||
votes_sum: 100,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -145,9 +206,29 @@ export let projects = [
|
||||
"lightning_address": "moritz@getalby.com",
|
||||
"votes_count": 205,
|
||||
tags: [],
|
||||
awards: [],
|
||||
capabilities: [],
|
||||
discord: "https://discord.com",
|
||||
github: null,
|
||||
hashtag: "#hashtag",
|
||||
launch_status: "Launched",
|
||||
members: [],
|
||||
permissions: Object.values(ProjectPermissionEnum),
|
||||
recruit_roles: [],
|
||||
slack: null,
|
||||
telegram: null,
|
||||
twitter: null,
|
||||
tournaments: [],
|
||||
tagline: "My project tagline",
|
||||
lnurl_callback_url: "",
|
||||
"category": {
|
||||
"id": 7,
|
||||
"title": "Media & News"
|
||||
"title": "Media & News",
|
||||
icon: "🎁",
|
||||
apps_count: 0,
|
||||
cover_image: "null",
|
||||
project: [],
|
||||
votes_sum: 100,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -161,9 +242,29 @@ export let projects = [
|
||||
"lightning_address": "moritz@getalby.com",
|
||||
"votes_count": 45,
|
||||
tags: [],
|
||||
awards: [],
|
||||
capabilities: [],
|
||||
discord: "https://discord.com",
|
||||
github: null,
|
||||
hashtag: "#hashtag",
|
||||
launch_status: "Launched",
|
||||
members: [],
|
||||
permissions: Object.values(ProjectPermissionEnum),
|
||||
recruit_roles: [],
|
||||
slack: null,
|
||||
telegram: null,
|
||||
twitter: null,
|
||||
tournaments: [],
|
||||
tagline: "My project tagline",
|
||||
lnurl_callback_url: "",
|
||||
"category": {
|
||||
"id": 7,
|
||||
"title": "Media & News"
|
||||
"title": "Media & News",
|
||||
icon: "🎁",
|
||||
apps_count: 0,
|
||||
cover_image: "null",
|
||||
project: [],
|
||||
votes_sum: 100,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -177,9 +278,29 @@ export let projects = [
|
||||
"lightning_address": "moritz@getalby.com",
|
||||
"votes_count": 25,
|
||||
tags: [],
|
||||
awards: [],
|
||||
capabilities: [],
|
||||
discord: "https://discord.com",
|
||||
github: null,
|
||||
hashtag: "#hashtag",
|
||||
launch_status: "Launched",
|
||||
members: [],
|
||||
permissions: Object.values(ProjectPermissionEnum),
|
||||
recruit_roles: [],
|
||||
slack: null,
|
||||
telegram: null,
|
||||
twitter: null,
|
||||
tournaments: [],
|
||||
tagline: "My project tagline",
|
||||
lnurl_callback_url: "",
|
||||
"category": {
|
||||
"id": 8,
|
||||
"title": "Shopping"
|
||||
"title": "Shopping",
|
||||
icon: "🎁",
|
||||
apps_count: 0,
|
||||
cover_image: "null",
|
||||
project: [],
|
||||
votes_sum: 100,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -193,9 +314,29 @@ export let projects = [
|
||||
"lightning_address": "johns@getalby.com",
|
||||
"votes_count": 11,
|
||||
tags: [],
|
||||
awards: [],
|
||||
capabilities: [],
|
||||
discord: "https://discord.com",
|
||||
github: null,
|
||||
hashtag: "#hashtag",
|
||||
launch_status: "Launched",
|
||||
members: [],
|
||||
permissions: Object.values(ProjectPermissionEnum),
|
||||
recruit_roles: [],
|
||||
slack: null,
|
||||
telegram: null,
|
||||
twitter: null,
|
||||
tournaments: [],
|
||||
tagline: "My project tagline",
|
||||
lnurl_callback_url: "",
|
||||
"category": {
|
||||
"id": 3,
|
||||
"title": "Art & Collectibles"
|
||||
"title": "Art & Collectibles",
|
||||
icon: "🎁",
|
||||
apps_count: 0,
|
||||
cover_image: "null",
|
||||
project: [],
|
||||
votes_sum: 100,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -209,9 +350,29 @@ export let projects = [
|
||||
"lightning_address": "moritz@getalby.com",
|
||||
"votes_count": 10,
|
||||
tags: [],
|
||||
awards: [],
|
||||
capabilities: [],
|
||||
discord: "https://discord.com",
|
||||
github: null,
|
||||
hashtag: "#hashtag",
|
||||
launch_status: "Launched",
|
||||
members: [],
|
||||
permissions: Object.values(ProjectPermissionEnum),
|
||||
recruit_roles: [],
|
||||
slack: null,
|
||||
telegram: null,
|
||||
twitter: null,
|
||||
tournaments: [],
|
||||
tagline: "My project tagline",
|
||||
lnurl_callback_url: "",
|
||||
"category": {
|
||||
"id": 4,
|
||||
"title": "Gaming"
|
||||
"title": "Gaming",
|
||||
icon: "🎁",
|
||||
apps_count: 0,
|
||||
cover_image: "null",
|
||||
project: [],
|
||||
votes_sum: 100,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -225,9 +386,29 @@ export let projects = [
|
||||
"lightning_address": "moritz@getalby.com",
|
||||
"votes_count": 10,
|
||||
tags: [],
|
||||
awards: [],
|
||||
capabilities: [],
|
||||
discord: "https://discord.com",
|
||||
github: null,
|
||||
hashtag: "#hashtag",
|
||||
launch_status: "Launched",
|
||||
members: [],
|
||||
permissions: Object.values(ProjectPermissionEnum),
|
||||
recruit_roles: [],
|
||||
slack: null,
|
||||
telegram: null,
|
||||
twitter: null,
|
||||
tournaments: [],
|
||||
tagline: "My project tagline",
|
||||
lnurl_callback_url: "",
|
||||
"category": {
|
||||
"id": 9,
|
||||
"title": "Misc / Other",
|
||||
icon: "🎁",
|
||||
apps_count: 0,
|
||||
cover_image: "null",
|
||||
project: [],
|
||||
votes_sum: 100,
|
||||
|
||||
}
|
||||
},
|
||||
@@ -242,9 +423,29 @@ export let projects = [
|
||||
"lightning_address": "moritz@getalby.com",
|
||||
"votes_count": 10,
|
||||
tags: [],
|
||||
awards: [],
|
||||
capabilities: [],
|
||||
discord: "https://discord.com",
|
||||
github: null,
|
||||
hashtag: "#hashtag",
|
||||
launch_status: "Launched",
|
||||
members: [],
|
||||
permissions: Object.values(ProjectPermissionEnum),
|
||||
recruit_roles: [],
|
||||
slack: null,
|
||||
telegram: null,
|
||||
twitter: null,
|
||||
tournaments: [],
|
||||
tagline: "My project tagline",
|
||||
lnurl_callback_url: "",
|
||||
"category": {
|
||||
"id": 8,
|
||||
"title": "Shopping"
|
||||
"title": "Shopping",
|
||||
icon: "🎁",
|
||||
apps_count: 0,
|
||||
cover_image: "null",
|
||||
project: [],
|
||||
votes_sum: 100,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -258,9 +459,29 @@ export let projects = [
|
||||
"lightning_address": "moritz@getalby.com",
|
||||
"votes_count": 5,
|
||||
tags: [],
|
||||
awards: [],
|
||||
capabilities: [],
|
||||
discord: "https://discord.com",
|
||||
github: null,
|
||||
hashtag: "#hashtag",
|
||||
launch_status: "Launched",
|
||||
members: [],
|
||||
permissions: Object.values(ProjectPermissionEnum),
|
||||
recruit_roles: [],
|
||||
slack: null,
|
||||
telegram: null,
|
||||
twitter: null,
|
||||
tournaments: [],
|
||||
tagline: "My project tagline",
|
||||
lnurl_callback_url: "",
|
||||
"category": {
|
||||
"id": 4,
|
||||
"title": "Gaming"
|
||||
"title": "Gaming",
|
||||
icon: "🎁",
|
||||
apps_count: 0,
|
||||
cover_image: "null",
|
||||
project: [],
|
||||
votes_sum: 100,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -278,9 +499,29 @@ export let projects = [
|
||||
"lightning_address": "subirachs@getalby.com",
|
||||
"votes_count": 0,
|
||||
tags: [],
|
||||
awards: [],
|
||||
capabilities: [],
|
||||
discord: "https://discord.com",
|
||||
github: null,
|
||||
hashtag: "#hashtag",
|
||||
launch_status: "Launched",
|
||||
members: [],
|
||||
permissions: Object.values(ProjectPermissionEnum),
|
||||
recruit_roles: [],
|
||||
slack: null,
|
||||
telegram: null,
|
||||
twitter: null,
|
||||
tournaments: [],
|
||||
tagline: "My project tagline",
|
||||
lnurl_callback_url: "",
|
||||
"category": {
|
||||
"id": 11,
|
||||
"title": "Shock the Web ⚡️"
|
||||
"title": "Shock the Web ⚡️",
|
||||
icon: "🎁",
|
||||
apps_count: 0,
|
||||
cover_image: "null",
|
||||
project: [],
|
||||
votes_sum: 100,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -296,9 +537,29 @@ export let projects = [
|
||||
"lightning_address": "kiwiidb@getalby.com",
|
||||
"votes_count": 0,
|
||||
tags: [],
|
||||
awards: [],
|
||||
capabilities: [],
|
||||
discord: "https://discord.com",
|
||||
github: null,
|
||||
hashtag: "#hashtag",
|
||||
launch_status: "Launched",
|
||||
members: [],
|
||||
permissions: Object.values(ProjectPermissionEnum),
|
||||
recruit_roles: [],
|
||||
slack: null,
|
||||
telegram: null,
|
||||
twitter: null,
|
||||
tournaments: [],
|
||||
tagline: "My project tagline",
|
||||
lnurl_callback_url: "",
|
||||
"category": {
|
||||
"id": 11,
|
||||
"title": "Shock the Web ⚡️"
|
||||
"title": "Shock the Web ⚡️",
|
||||
icon: "🎁",
|
||||
apps_count: 0,
|
||||
cover_image: "null",
|
||||
project: [],
|
||||
votes_sum: 100,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -315,9 +576,29 @@ export let projects = [
|
||||
"lightning_address": "tobitcoin@getalby.com",
|
||||
"votes_count": 0,
|
||||
tags: [],
|
||||
awards: [],
|
||||
capabilities: [],
|
||||
discord: "https://discord.com",
|
||||
github: null,
|
||||
hashtag: "#hashtag",
|
||||
launch_status: "Launched",
|
||||
members: [],
|
||||
permissions: Object.values(ProjectPermissionEnum),
|
||||
recruit_roles: [],
|
||||
slack: null,
|
||||
telegram: null,
|
||||
twitter: null,
|
||||
tournaments: [],
|
||||
tagline: "My project tagline",
|
||||
lnurl_callback_url: "",
|
||||
"category": {
|
||||
"id": 11,
|
||||
"title": "Shock the Web ⚡️"
|
||||
"title": "Shock the Web ⚡️",
|
||||
icon: "🎁",
|
||||
apps_count: 0,
|
||||
cover_image: "null",
|
||||
project: [],
|
||||
votes_sum: 100,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -341,9 +622,29 @@ export let projects = [
|
||||
"lightning_address": "atlantabitdevs@getalby.com",
|
||||
"votes_count": 0,
|
||||
tags: [],
|
||||
awards: [],
|
||||
capabilities: [],
|
||||
discord: "https://discord.com",
|
||||
github: null,
|
||||
hashtag: "#hashtag",
|
||||
launch_status: "Launched",
|
||||
members: [],
|
||||
permissions: Object.values(ProjectPermissionEnum),
|
||||
recruit_roles: [],
|
||||
slack: null,
|
||||
telegram: null,
|
||||
twitter: null,
|
||||
tournaments: [],
|
||||
tagline: "My project tagline",
|
||||
lnurl_callback_url: "",
|
||||
"category": {
|
||||
"id": 11,
|
||||
"title": "Shock the Web ⚡️"
|
||||
"title": "Shock the Web ⚡️",
|
||||
icon: "🎁",
|
||||
apps_count: 0,
|
||||
cover_image: "null",
|
||||
project: [],
|
||||
votes_sum: 100,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -359,9 +660,29 @@ export let projects = [
|
||||
"lightning_address": "reneaaron@getalby.com",
|
||||
"votes_count": 0,
|
||||
tags: [],
|
||||
awards: [],
|
||||
capabilities: [],
|
||||
discord: "https://discord.com",
|
||||
github: null,
|
||||
hashtag: "#hashtag",
|
||||
launch_status: "Launched",
|
||||
members: [],
|
||||
permissions: Object.values(ProjectPermissionEnum),
|
||||
recruit_roles: [],
|
||||
slack: null,
|
||||
telegram: null,
|
||||
twitter: null,
|
||||
tournaments: [],
|
||||
tagline: "My project tagline",
|
||||
lnurl_callback_url: "",
|
||||
"category": {
|
||||
"id": 11,
|
||||
"title": "Shock the Web ⚡️"
|
||||
"title": "Shock the Web ⚡️",
|
||||
icon: "🎁",
|
||||
apps_count: 0,
|
||||
cover_image: "null",
|
||||
project: [],
|
||||
votes_sum: 100,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -382,9 +703,29 @@ export let projects = [
|
||||
"lightning_address": "alivesession77@walletofsatoshi.com",
|
||||
"votes_count": 0,
|
||||
tags: [],
|
||||
awards: [],
|
||||
capabilities: [],
|
||||
discord: "https://discord.com",
|
||||
github: null,
|
||||
hashtag: "#hashtag",
|
||||
launch_status: "Launched",
|
||||
members: [],
|
||||
permissions: Object.values(ProjectPermissionEnum),
|
||||
recruit_roles: [],
|
||||
slack: null,
|
||||
telegram: null,
|
||||
twitter: null,
|
||||
tournaments: [],
|
||||
tagline: "My project tagline",
|
||||
lnurl_callback_url: "",
|
||||
"category": {
|
||||
"id": 11,
|
||||
"title": "Shock the Web ⚡️"
|
||||
"title": "Shock the Web ⚡️",
|
||||
icon: "🎁",
|
||||
apps_count: 0,
|
||||
cover_image: "null",
|
||||
project: [],
|
||||
votes_sum: 100,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -398,9 +739,44 @@ export let projects = [
|
||||
"lightning_address": "johns@getalby.com",
|
||||
"votes_count": 0,
|
||||
tags: [],
|
||||
awards: [],
|
||||
capabilities: [{
|
||||
id: 1,
|
||||
title: "Lnurl",
|
||||
icon: "🎛️",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Ln-Auth",
|
||||
icon: "🔑",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Ln-Pay",
|
||||
icon: "💳",
|
||||
},
|
||||
],
|
||||
discord: "https://discord.com",
|
||||
github: null,
|
||||
hashtag: "#hashtag",
|
||||
launch_status: "Launched",
|
||||
members: [],
|
||||
permissions: Object.values(ProjectPermissionEnum),
|
||||
recruit_roles: [],
|
||||
slack: null,
|
||||
telegram: null,
|
||||
twitter: null,
|
||||
tournaments: [],
|
||||
tagline: "My project tagline",
|
||||
lnurl_callback_url: "",
|
||||
"category": {
|
||||
"id": 1,
|
||||
"title": "Finance"
|
||||
"title": "Finance",
|
||||
icon: "🎁",
|
||||
apps_count: 0,
|
||||
cover_image: "null",
|
||||
project: [],
|
||||
votes_sum: 100,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -420,9 +796,29 @@ export let projects = [
|
||||
"lightning_address": "divineorgan67@walletofsatoshi.com",
|
||||
"votes_count": 0,
|
||||
tags: [],
|
||||
awards: [],
|
||||
capabilities: [],
|
||||
discord: "https://discord.com",
|
||||
github: null,
|
||||
hashtag: "#hashtag",
|
||||
launch_status: "Launched",
|
||||
members: [],
|
||||
permissions: Object.values(ProjectPermissionEnum),
|
||||
recruit_roles: [],
|
||||
slack: null,
|
||||
telegram: null,
|
||||
twitter: null,
|
||||
tournaments: [],
|
||||
tagline: "My project tagline",
|
||||
lnurl_callback_url: "",
|
||||
"category": {
|
||||
"id": 11,
|
||||
"title": "Shock the Web ⚡️"
|
||||
"title": "Shock the Web ⚡️",
|
||||
icon: "🎁",
|
||||
apps_count: 0,
|
||||
cover_image: "null",
|
||||
project: [],
|
||||
votes_sum: 100,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -436,9 +832,29 @@ export let projects = [
|
||||
"lightning_address": "moritz@getalby.com",
|
||||
"votes_count": 0,
|
||||
tags: [],
|
||||
awards: [],
|
||||
capabilities: [],
|
||||
discord: "https://discord.com",
|
||||
github: null,
|
||||
hashtag: "#hashtag",
|
||||
launch_status: "Launched",
|
||||
members: [],
|
||||
permissions: Object.values(ProjectPermissionEnum),
|
||||
recruit_roles: [],
|
||||
slack: null,
|
||||
telegram: null,
|
||||
twitter: null,
|
||||
tournaments: [],
|
||||
tagline: "My project tagline",
|
||||
lnurl_callback_url: "",
|
||||
"category": {
|
||||
"id": 1,
|
||||
"title": "Finance"
|
||||
"title": "Finance",
|
||||
icon: "🎁",
|
||||
apps_count: 0,
|
||||
cover_image: "null",
|
||||
project: [],
|
||||
votes_sum: 100,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -452,9 +868,29 @@ export let projects = [
|
||||
"lightning_address": "moritz@getalby.com",
|
||||
"votes_count": 0,
|
||||
tags: [],
|
||||
awards: [],
|
||||
capabilities: [],
|
||||
discord: "https://discord.com",
|
||||
github: null,
|
||||
hashtag: "#hashtag",
|
||||
launch_status: "Launched",
|
||||
members: [],
|
||||
permissions: Object.values(ProjectPermissionEnum),
|
||||
recruit_roles: [],
|
||||
slack: null,
|
||||
telegram: null,
|
||||
twitter: null,
|
||||
tournaments: [],
|
||||
tagline: "My project tagline",
|
||||
lnurl_callback_url: "",
|
||||
"category": {
|
||||
"id": 9,
|
||||
"title": "Misc / Other"
|
||||
"title": "Misc / Other",
|
||||
icon: "🎁",
|
||||
apps_count: 0,
|
||||
cover_image: "null",
|
||||
project: [],
|
||||
votes_sum: 100,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -468,9 +904,29 @@ export let projects = [
|
||||
"lightning_address": "moritz@getalby.com",
|
||||
"votes_count": 0,
|
||||
tags: [],
|
||||
awards: [],
|
||||
capabilities: [],
|
||||
discord: "https://discord.com",
|
||||
github: null,
|
||||
hashtag: "#hashtag",
|
||||
launch_status: "Launched",
|
||||
members: [],
|
||||
permissions: Object.values(ProjectPermissionEnum),
|
||||
recruit_roles: [],
|
||||
slack: null,
|
||||
telegram: null,
|
||||
twitter: null,
|
||||
tournaments: [],
|
||||
tagline: "My project tagline",
|
||||
lnurl_callback_url: "",
|
||||
"category": {
|
||||
"id": 3,
|
||||
"title": "Art & Collectibles"
|
||||
"title": "Art & Collectibles",
|
||||
icon: "🎁",
|
||||
apps_count: 0,
|
||||
cover_image: "null",
|
||||
project: [],
|
||||
votes_sum: 100,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -484,9 +940,29 @@ export let projects = [
|
||||
"lightning_address": "moritz@getalby.com",
|
||||
"votes_count": 0,
|
||||
tags: [],
|
||||
awards: [],
|
||||
capabilities: [],
|
||||
discord: "https://discord.com",
|
||||
github: null,
|
||||
hashtag: "#hashtag",
|
||||
launch_status: "Launched",
|
||||
members: [],
|
||||
permissions: Object.values(ProjectPermissionEnum),
|
||||
recruit_roles: [],
|
||||
slack: null,
|
||||
telegram: null,
|
||||
twitter: null,
|
||||
tournaments: [],
|
||||
tagline: "My project tagline",
|
||||
lnurl_callback_url: "",
|
||||
"category": {
|
||||
"id": 7,
|
||||
"title": "Media & News"
|
||||
"title": "Media & News",
|
||||
icon: "🎁",
|
||||
apps_count: 0,
|
||||
cover_image: "null",
|
||||
project: [],
|
||||
votes_sum: 100,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -500,9 +976,29 @@ export let projects = [
|
||||
"lightning_address": "moritz@geralby.com",
|
||||
"votes_count": 0,
|
||||
tags: [],
|
||||
awards: [],
|
||||
capabilities: [],
|
||||
discord: "https://discord.com",
|
||||
github: null,
|
||||
hashtag: "#hashtag",
|
||||
launch_status: "Launched",
|
||||
members: [],
|
||||
permissions: Object.values(ProjectPermissionEnum),
|
||||
recruit_roles: [],
|
||||
slack: null,
|
||||
telegram: null,
|
||||
twitter: null,
|
||||
tournaments: [],
|
||||
tagline: "My project tagline",
|
||||
lnurl_callback_url: "",
|
||||
"category": {
|
||||
"id": 7,
|
||||
"title": "Media & News"
|
||||
"title": "Media & News",
|
||||
icon: "🎁",
|
||||
apps_count: 0,
|
||||
cover_image: "null",
|
||||
project: [],
|
||||
votes_sum: 100,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -516,9 +1012,29 @@ export let projects = [
|
||||
"lightning_address": "moritz@getalby.com",
|
||||
"votes_count": 0,
|
||||
tags: [],
|
||||
awards: [],
|
||||
capabilities: [],
|
||||
discord: "https://discord.com",
|
||||
github: null,
|
||||
hashtag: "#hashtag",
|
||||
launch_status: "Launched",
|
||||
members: [],
|
||||
permissions: Object.values(ProjectPermissionEnum),
|
||||
recruit_roles: [],
|
||||
slack: null,
|
||||
telegram: null,
|
||||
twitter: null,
|
||||
tournaments: [],
|
||||
tagline: "My project tagline",
|
||||
lnurl_callback_url: "",
|
||||
"category": {
|
||||
"id": 6,
|
||||
"title": "Analytics"
|
||||
"title": "Analytics",
|
||||
icon: "🎁",
|
||||
apps_count: 0,
|
||||
cover_image: "null",
|
||||
project: [],
|
||||
votes_sum: 100,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -532,9 +1048,29 @@ export let projects = [
|
||||
"lightning_address": "moritz@getalby.com",
|
||||
"votes_count": 0,
|
||||
tags: [],
|
||||
awards: [],
|
||||
capabilities: [],
|
||||
discord: "https://discord.com",
|
||||
github: null,
|
||||
hashtag: "#hashtag",
|
||||
launch_status: "Launched",
|
||||
members: [],
|
||||
permissions: Object.values(ProjectPermissionEnum),
|
||||
recruit_roles: [],
|
||||
slack: null,
|
||||
telegram: null,
|
||||
twitter: null,
|
||||
tournaments: [],
|
||||
tagline: "My project tagline",
|
||||
lnurl_callback_url: "",
|
||||
"category": {
|
||||
"id": 2,
|
||||
"title": "Social"
|
||||
"title": "Social",
|
||||
icon: "🎁",
|
||||
apps_count: 0,
|
||||
cover_image: "null",
|
||||
project: [],
|
||||
votes_sum: 100,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -548,9 +1084,29 @@ export let projects = [
|
||||
"lightning_address": "moritz@getalby.com",
|
||||
"votes_count": 0,
|
||||
tags: [],
|
||||
awards: [],
|
||||
capabilities: [],
|
||||
discord: "https://discord.com",
|
||||
github: null,
|
||||
hashtag: "#hashtag",
|
||||
launch_status: "Launched",
|
||||
members: [],
|
||||
permissions: Object.values(ProjectPermissionEnum),
|
||||
recruit_roles: [],
|
||||
slack: null,
|
||||
telegram: null,
|
||||
twitter: null,
|
||||
tournaments: [],
|
||||
tagline: "My project tagline",
|
||||
lnurl_callback_url: "",
|
||||
"category": {
|
||||
"id": 8,
|
||||
"title": "Shopping"
|
||||
"title": "Shopping",
|
||||
icon: "🎁",
|
||||
apps_count: 0,
|
||||
cover_image: "null",
|
||||
project: [],
|
||||
votes_sum: 100,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -564,9 +1120,29 @@ export let projects = [
|
||||
"lightning_address": "moritz@getalby.com",
|
||||
"votes_count": 0,
|
||||
tags: [],
|
||||
awards: [],
|
||||
capabilities: [],
|
||||
discord: "https://discord.com",
|
||||
github: null,
|
||||
hashtag: "#hashtag",
|
||||
launch_status: "Launched",
|
||||
members: [],
|
||||
permissions: Object.values(ProjectPermissionEnum),
|
||||
recruit_roles: [],
|
||||
slack: null,
|
||||
telegram: null,
|
||||
twitter: null,
|
||||
tournaments: [],
|
||||
tagline: "My project tagline",
|
||||
lnurl_callback_url: "",
|
||||
"category": {
|
||||
"id": 4,
|
||||
"title": "Gaming"
|
||||
"title": "Gaming",
|
||||
icon: "🎁",
|
||||
apps_count: 0,
|
||||
cover_image: "null",
|
||||
project: [],
|
||||
votes_sum: 100,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -580,9 +1156,29 @@ export let projects = [
|
||||
"lightning_address": "moritz@getalby.com",
|
||||
"votes_count": 0,
|
||||
tags: [],
|
||||
awards: [],
|
||||
capabilities: [],
|
||||
discord: "https://discord.com",
|
||||
github: null,
|
||||
hashtag: "#hashtag",
|
||||
launch_status: "Launched",
|
||||
members: [],
|
||||
permissions: Object.values(ProjectPermissionEnum),
|
||||
recruit_roles: [],
|
||||
slack: null,
|
||||
telegram: null,
|
||||
twitter: null,
|
||||
tournaments: [],
|
||||
tagline: "My project tagline",
|
||||
lnurl_callback_url: "",
|
||||
"category": {
|
||||
"id": 4,
|
||||
"title": "Gaming"
|
||||
"title": "Gaming",
|
||||
icon: "🎁",
|
||||
apps_count: 0,
|
||||
cover_image: "null",
|
||||
project: [],
|
||||
votes_sum: 100,
|
||||
}
|
||||
}
|
||||
] as Project[]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
import { graphql } from 'msw'
|
||||
import { allCategories, getAllHackathons, getAllMakersRoles, getAllMakersSkills, getCategory, getFeed, getMakersInTournament, getMyDrafts, getPostById, getProject, getTournamentById, getTrendingPosts, hottestProjects, me, newProjects, popularTags, profile, projectsByCategory, searchProjects } from './resolvers'
|
||||
import { allCategories, getAllHackathons, getAllMakersRoles, getAllMakersSkills, getCategory, getFeed, getMakersInTournament, getMyDrafts, getPostById, getProject, getTournamentById, getTrendingPosts, hottestProjects, me, newProjects, popularTags, profile, projectsByCategory, searchProjects, searchUsers } from './resolvers'
|
||||
import {
|
||||
NavCategoriesQuery,
|
||||
ExploreProjectsQuery,
|
||||
@@ -29,6 +29,8 @@ import {
|
||||
MeQuery,
|
||||
ProfileQuery,
|
||||
GetMyDraftsQuery,
|
||||
SearchUsersQuery,
|
||||
SearchUsersQueryVariables,
|
||||
MyProfileAboutQuery,
|
||||
MyProfilePreferencesQuery,
|
||||
GetTournamentByIdQuery,
|
||||
@@ -266,6 +268,16 @@ export const handlers = [
|
||||
)
|
||||
}),
|
||||
|
||||
|
||||
graphql.query<SearchUsersQuery, SearchUsersQueryVariables>('SearchUsers', async (req, res, ctx) => {
|
||||
await delay()
|
||||
return res(
|
||||
ctx.data({
|
||||
searchUsers: searchUsers(req.variables.value)
|
||||
})
|
||||
)
|
||||
}),
|
||||
|
||||
graphql.query<GetMyDraftsQuery>('GetMyDrafts', async (req, res, ctx) => {
|
||||
await delay()
|
||||
|
||||
|
||||
@@ -89,6 +89,10 @@ export function getAllMakersRoles() {
|
||||
export function getAllMakersSkills() {
|
||||
return MOCK_DATA['allMakersSkills']
|
||||
}
|
||||
export function searchUsers(value: string) {
|
||||
return MOCK_DATA['users'].filter(u => u.name.toLowerCase().indexOf(value.toLowerCase()) !== -1);
|
||||
}
|
||||
|
||||
export function getMyDrafts(): Query['getMyDrafts'] {
|
||||
return MOCK_DATA['posts'].stories;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { Login_ScanningWalletCard, Login_ExternalWalletCard, Login_NativeWalletCard, Login_SuccessCard } from "src/Components/Modals/Login";
|
||||
import { ProjectDetailsCard } from "src/features/Projects/pages/ProjectPage/ProjectDetailsCard";
|
||||
import { ProjectListedModal } from "src/features/Projects/pages/ListProjectPage/Components/ProjectListedModal";
|
||||
import VoteCard from "src/features/Projects/pages/ProjectPage/VoteCard/VoteCard";
|
||||
import { InsertVideoModal } from 'src/Components/Inputs/TextEditor/InsertVideoModal'
|
||||
import { InsertLinkModal } from 'src/Components/Inputs/TextEditor/InsertLinkModal'
|
||||
@@ -55,11 +56,11 @@ export const ALL_MODALS = {
|
||||
ConfirmModal,
|
||||
VoteCard,
|
||||
NoWeblnModal,
|
||||
ProjectListedModal,
|
||||
|
||||
// User Wallets Keys
|
||||
LinkingAccountModal,
|
||||
RemoveWalletKeyModal,
|
||||
|
||||
// Text Editor Modals
|
||||
InsertImageModal,
|
||||
InsertVideoModal,
|
||||
|
||||
@@ -38,6 +38,14 @@ export class NotificationsService {
|
||||
})
|
||||
}
|
||||
|
||||
static warn(msg: string, options?: AlertOptions) {
|
||||
toast.warn(msg, {
|
||||
onClose: options?.onClose,
|
||||
autoClose: options?.autoClose ?? 2500,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
static error(msg: string, options?: AlertOptions & Partial<{ error: any }>) {
|
||||
if (options?.error && DEBUG) console.log(options?.error)
|
||||
toast.error(msg, {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
$screen-xs-min: 320px;
|
||||
|
||||
@import "./tw.scss", "./shared.scss", "./vendors.scss", "./scrollbar.scss",
|
||||
"./ui_state.scss";
|
||||
"./ui_state.scss", "./portals.scss";
|
||||
@import "/src/styles/mixins/index.scss";
|
||||
|
||||
html {
|
||||
|
||||
6
src/styles/portals.scss
Normal file
6
src/styles/portals.scss
Normal file
@@ -0,0 +1,6 @@
|
||||
#confetti {
|
||||
z-index: 3000;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -182,3 +182,8 @@ export const getSpanDate = (_date1: string, _date2: string) => {
|
||||
return `${dayjs(_date1).format('H:mm')} - ${dayjs(_date2).format('H:mm, Do MMM')}`
|
||||
|
||||
}
|
||||
|
||||
|
||||
export function removeArrayItemAtIndex<T>(arr: T[], indexToRemove: number) {
|
||||
return [...arr.slice(0, indexToRemove), ...arr.slice(indexToRemove + 1)]
|
||||
}
|
||||
@@ -23,7 +23,7 @@ export type ControlledStateHandler<T, IsMulti extends boolean> = {
|
||||
}
|
||||
|
||||
|
||||
export type Override<A, B> = Omit<A, keyof B> & B;
|
||||
export type Override<A, B extends { [Key in keyof A]?: any }> = Omit<A, keyof B> & B;
|
||||
|
||||
|
||||
export type Image = string;
|
||||
@@ -14,6 +14,10 @@ type RouteOptions =
|
||||
title?: string,
|
||||
username?: string,
|
||||
}
|
||||
| {
|
||||
type: "edit-story",
|
||||
id?: number,
|
||||
}
|
||||
| {
|
||||
type: "bounty",
|
||||
id: string | number,
|
||||
@@ -31,6 +35,13 @@ type RouteOptions =
|
||||
id: string | number,
|
||||
username?: string,
|
||||
}
|
||||
| {
|
||||
type: "projects-page"
|
||||
}
|
||||
| {
|
||||
type: "edit-project",
|
||||
id?: number,
|
||||
}
|
||||
| {
|
||||
type: "edit-profile",
|
||||
}
|
||||
@@ -74,6 +85,12 @@ export function createRoute(options: RouteOptions) {
|
||||
if (options.type === 'tournament')
|
||||
return `/tournaments/${options.id}` + (options.tab ? `/${options.tab}` : "")
|
||||
|
||||
if (options.type === 'projects-page')
|
||||
return '/projects'
|
||||
|
||||
if (options.type === 'edit-project')
|
||||
return `/projects/list-project` + (options.id ? `?id=${options.id}` : '')
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -81,7 +98,8 @@ export const PAGES_ROUTES = {
|
||||
projects: {
|
||||
default: "/projects",
|
||||
hottest: "/projects/hottest",
|
||||
byCategoryId: "/projects/category/:id"
|
||||
byCategoryId: "/projects/category/:id",
|
||||
listProject: "/projects/list-project"
|
||||
},
|
||||
blog: {
|
||||
feed: "/feed",
|
||||
|
||||
@@ -16,7 +16,7 @@ import "src/styles/index.scss";
|
||||
import 'react-loading-skeleton/dist/skeleton.css'
|
||||
import { ApolloProvider } from '@apollo/client';
|
||||
import { apolloClient } from '../apollo';
|
||||
import { Controller, FormProvider, useForm, UseFormProps } from 'react-hook-form';
|
||||
import { Controller, FormProvider, useForm, UseFormProps, } from 'react-hook-form';
|
||||
import ModalsContainer from 'src/Components/Modals/ModalsContainer/ModalsContainer';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import { NotificationsService } from 'src/services';
|
||||
@@ -156,6 +156,7 @@ export function WrapFormController<T = any>(options: Partial<UseFormProps<T> & {
|
||||
|
||||
|
||||
|
||||
|
||||
export const WithModals: DecoratorFn = (Component) => <>
|
||||
<Component />
|
||||
<ModalsContainer />
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import React, { ReactElement, ReactNode } from "react"
|
||||
import { Controller, useForm } from "react-hook-form"
|
||||
import { RootState } from "src/redux/store"
|
||||
|
||||
export type ModifyArgs = Partial<{
|
||||
@@ -8,3 +10,30 @@ export type ModifyArgs = Partial<{
|
||||
}>
|
||||
|
||||
|
||||
|
||||
export function WrapFormController<K extends string, V extends any>(key: K, defaultValue: V) {
|
||||
|
||||
const Func = (Story: ReactElement) => {
|
||||
const { control } = useForm({
|
||||
defaultValues: {
|
||||
[key]: defaultValue as any
|
||||
}
|
||||
})
|
||||
return <Controller
|
||||
control={control}
|
||||
name={key}
|
||||
render={({ field: { onChange, value, onBlur } }) => {
|
||||
console.log(value);
|
||||
return React.cloneElement(Story, { value, onChange, onBlur })
|
||||
// <Story
|
||||
// value={value}
|
||||
// onChange={onChange}
|
||||
// onBlur={onBlur}
|
||||
// />
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
||||
return Func;
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user