Merge pull request #128 from peakshift/feature/list-your-product-ui

Feature - List Your Project UI components and pages
This commit is contained in:
Mohammed Taher Ghazal
2022-09-29 10:17:27 +03:00
committed by GitHub
65 changed files with 4749 additions and 349 deletions

View File

@@ -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!
}

View File

@@ -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

View File

@@ -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,
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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 />} />

View File

@@ -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"

View File

@@ -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)

View File

@@ -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>
)

View File

@@ -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: {

View File

@@ -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}

View 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>
)
})

View 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>
)
})

View 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({});

View 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>
)
}

View File

@@ -0,0 +1,8 @@
query SearchUsers($value: String!) {
searchUsers(value: $value) {
id
name
avatar
jobTitle
}
}

View File

@@ -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>
)
}
</>
)
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 = {
}

View File

@@ -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>
)
}

View File

@@ -0,0 +1,7 @@
query GetAllCapabilities {
getAllCapabilities {
id
title
icon
}
}

View File

@@ -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 = {
}

View File

@@ -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>
)
}

View File

@@ -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 = {
}

View File

@@ -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">Its 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>
)
}

View File

@@ -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)]
}

View File

@@ -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
}
}
}

View File

@@ -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;
}

View File

@@ -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 = {
}

View File

@@ -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 products 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 projects 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 >
)
}

View File

@@ -0,0 +1,3 @@
query IsValidProjectHashtag($hashtag: String!, $projectId: Int) {
checkValidProjectHashtag(hashtag: $hashtag, projectId: $projectId)
}

View File

@@ -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"
}
}

View File

@@ -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>
)
}

View File

@@ -0,0 +1,4 @@
import { lazyModal } from 'src/utils/helperFunctions';
export const { LazyComponent: ProjectListedModal } = lazyModal(() => import('./ProjectListedModal'))

View File

@@ -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: '✍️️'
},
]

View File

@@ -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 = {
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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 = {
}

View File

@@ -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>}
</>
)
}

View File

@@ -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 = {
}

View File

@@ -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 products 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 youre 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>
)
}

View File

@@ -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 = {
}

View File

@@ -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>
)
}

View File

@@ -0,0 +1,6 @@
query GetTournamentsToRegister {
getTournamentToRegister {
id
title
}
}

View 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>
</>
)
}

View File

@@ -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
}
}
}

View File

@@ -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">

View File

@@ -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

View File

@@ -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[]

View File

@@ -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()

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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, {

View File

@@ -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
View File

@@ -0,0 +1,6 @@
#confetti {
z-index: 3000;
position: fixed;
inset: 0;
pointer-events: none;
}

View File

@@ -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)]
}

View File

@@ -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;

View File

@@ -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",

View File

@@ -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 />

View File

@@ -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;
}