mirror of
https://github.com/aljazceru/landscape-template.git
synced 2026-01-15 12:24:21 +01:00
Merge pull request #148 from peakshift/feature/tournament-pages
Draft 0.1: Tournament Page
This commit is contained in:
@@ -38,6 +38,7 @@ export interface NexusGenInputs {
|
||||
ProfileDetailsInput: { // input type
|
||||
avatar?: string | null; // String
|
||||
bio?: string | null; // String
|
||||
discord?: string | null; // String
|
||||
email?: string | null; // String
|
||||
github?: string | null; // String
|
||||
jobTitle?: string | null; // String
|
||||
@@ -52,6 +53,10 @@ export interface NexusGenInputs {
|
||||
roles: NexusGenInputs['MakerRoleInput'][]; // [MakerRoleInput!]!
|
||||
skills: NexusGenInputs['MakerSkillInput'][]; // [MakerSkillInput!]!
|
||||
}
|
||||
RegisterInTournamentInput: { // input type
|
||||
email: string; // String!
|
||||
hacking_status: NexusGenEnums['TournamentMakerHackingStatusEnum']; // TournamentMakerHackingStatusEnum!
|
||||
}
|
||||
StoryInputType: { // input type
|
||||
body: string; // String!
|
||||
cover_image?: string | null; // String
|
||||
@@ -60,6 +65,10 @@ export interface NexusGenInputs {
|
||||
tags: string[]; // [String!]!
|
||||
title: string; // String!
|
||||
}
|
||||
UpdateTournamentRegistrationInput: { // input type
|
||||
email?: string | null; // String
|
||||
hacking_status?: NexusGenEnums['TournamentMakerHackingStatusEnum'] | null; // TournamentMakerHackingStatusEnum
|
||||
}
|
||||
UserKeyInputType: { // input type
|
||||
key: string; // String!
|
||||
name: string; // String!
|
||||
@@ -69,6 +78,8 @@ export interface NexusGenInputs {
|
||||
export interface NexusGenEnums {
|
||||
POST_TYPE: "Bounty" | "Question" | "Story"
|
||||
RoleLevelEnum: 3 | 0 | 1 | 2 | 4
|
||||
TournamentEventTypeEnum: 2 | 3 | 0 | 1
|
||||
TournamentMakerHackingStatusEnum: 1 | 0
|
||||
VOTE_ITEM_TYPE: "Bounty" | "PostComment" | "Project" | "Question" | "Story" | "User"
|
||||
}
|
||||
|
||||
@@ -171,6 +182,7 @@ export interface NexusGenObjects {
|
||||
MyProfile: { // root type
|
||||
avatar: string; // String!
|
||||
bio?: string | null; // String
|
||||
discord?: string | null; // String
|
||||
email?: string | null; // String
|
||||
github?: string | null; // String
|
||||
id: number; // Int!
|
||||
@@ -186,6 +198,11 @@ export interface NexusGenObjects {
|
||||
twitter?: string | null; // String
|
||||
website?: string | null; // String
|
||||
}
|
||||
ParticipationInfo: { // root type
|
||||
createdAt: NexusGenScalars['Date']; // Date!
|
||||
email: string; // String!
|
||||
hacking_status: NexusGenEnums['TournamentMakerHackingStatusEnum']; // TournamentMakerHackingStatusEnum!
|
||||
}
|
||||
PostComment: { // root type
|
||||
author: NexusGenRootTypes['Author']; // Author!
|
||||
body: string; // String!
|
||||
@@ -240,15 +257,56 @@ export interface NexusGenObjects {
|
||||
description: string; // String!
|
||||
end_date: NexusGenScalars['Date']; // Date!
|
||||
id: number; // Int!
|
||||
location: string; // String!
|
||||
start_date: NexusGenScalars['Date']; // Date!
|
||||
thumbnail_image: string; // String!
|
||||
title: string; // String!
|
||||
website: string; // String!
|
||||
}
|
||||
TournamentEvent: { // root type
|
||||
description: string; // String!
|
||||
ends_at: NexusGenScalars['Date']; // Date!
|
||||
id: number; // Int!
|
||||
image: string; // String!
|
||||
location: string; // String!
|
||||
starts_at: NexusGenScalars['Date']; // Date!
|
||||
title: string; // String!
|
||||
type: NexusGenEnums['TournamentEventTypeEnum']; // TournamentEventTypeEnum!
|
||||
website: string; // String!
|
||||
}
|
||||
TournamentFAQ: { // root type
|
||||
answer: string; // String!
|
||||
question: string; // String!
|
||||
}
|
||||
TournamentJudge: { // root type
|
||||
avatar: string; // String!
|
||||
company: string; // String!
|
||||
name: string; // String!
|
||||
}
|
||||
TournamentMakersResponse: { // root type
|
||||
hasNext?: boolean | null; // Boolean
|
||||
hasPrev?: boolean | null; // Boolean
|
||||
makers: NexusGenRootTypes['TournamentParticipant'][]; // [TournamentParticipant!]!
|
||||
}
|
||||
TournamentParticipant: { // root type
|
||||
hacking_status: NexusGenEnums['TournamentMakerHackingStatusEnum']; // TournamentMakerHackingStatusEnum!
|
||||
is_registered?: boolean | null; // Boolean
|
||||
user: NexusGenRootTypes['User']; // User!
|
||||
}
|
||||
TournamentPrize: { // root type
|
||||
amount: string; // String!
|
||||
image: string; // String!
|
||||
title: string; // String!
|
||||
}
|
||||
TournamentProjectsResponse: { // root type
|
||||
hasNext?: boolean | null; // Boolean
|
||||
hasPrev?: boolean | null; // Boolean
|
||||
projects: NexusGenRootTypes['Project'][]; // [Project!]!
|
||||
}
|
||||
User: { // root type
|
||||
avatar: string; // String!
|
||||
bio?: string | null; // String
|
||||
email?: string | null; // String
|
||||
discord?: string | null; // String
|
||||
github?: string | null; // String
|
||||
id: number; // Int!
|
||||
jobTitle?: string | null; // String
|
||||
@@ -271,6 +329,7 @@ export interface NexusGenObjects {
|
||||
payment_request: string; // String!
|
||||
}
|
||||
WalletKey: { // root type
|
||||
createdAt: NexusGenScalars['Date']; // Date!
|
||||
is_current: boolean; // Boolean!
|
||||
key: string; // String!
|
||||
name: string; // String!
|
||||
@@ -391,17 +450,21 @@ export interface NexusGenFieldTypes {
|
||||
createStory: NexusGenRootTypes['Story'] | null; // Story
|
||||
deleteStory: NexusGenRootTypes['Story'] | null; // Story
|
||||
donate: NexusGenRootTypes['Donation']; // Donation!
|
||||
registerInTournament: NexusGenRootTypes['User'] | null; // User
|
||||
updateProfileDetails: NexusGenRootTypes['MyProfile'] | null; // MyProfile
|
||||
updateProfileRoles: NexusGenRootTypes['MyProfile'] | null; // MyProfile
|
||||
updateTournamentRegistration: NexusGenRootTypes['ParticipationInfo'] | null; // ParticipationInfo
|
||||
updateUserPreferences: NexusGenRootTypes['MyProfile']; // MyProfile!
|
||||
vote: NexusGenRootTypes['Vote']; // Vote!
|
||||
}
|
||||
MyProfile: { // field return type
|
||||
avatar: string; // String!
|
||||
bio: string | null; // String
|
||||
discord: string | null; // String
|
||||
email: string | null; // String
|
||||
github: string | null; // String
|
||||
id: number; // Int!
|
||||
in_tournament: boolean; // Boolean!
|
||||
jobTitle: string | null; // String
|
||||
join_date: NexusGenScalars['Date']; // Date!
|
||||
lightning_address: string | null; // String
|
||||
@@ -420,6 +483,11 @@ export interface NexusGenFieldTypes {
|
||||
walletsKeys: NexusGenRootTypes['WalletKey'][]; // [WalletKey!]!
|
||||
website: string | null; // String
|
||||
}
|
||||
ParticipationInfo: { // field return type
|
||||
createdAt: NexusGenScalars['Date']; // Date!
|
||||
email: string; // String!
|
||||
hacking_status: NexusGenEnums['TournamentMakerHackingStatusEnum']; // TournamentMakerHackingStatusEnum!
|
||||
}
|
||||
PostComment: { // field return type
|
||||
author: NexusGenRootTypes['Author']; // Author!
|
||||
body: string; // String!
|
||||
@@ -454,9 +522,12 @@ export interface NexusGenFieldTypes {
|
||||
getDonationsStats: NexusGenRootTypes['DonationsStats']; // DonationsStats!
|
||||
getFeed: NexusGenRootTypes['Post'][]; // [Post!]!
|
||||
getLnurlDetailsForProject: NexusGenRootTypes['LnurlDetails']; // LnurlDetails!
|
||||
getMakersInTournament: NexusGenRootTypes['TournamentMakersResponse']; // TournamentMakersResponse!
|
||||
getMyDrafts: NexusGenRootTypes['Post'][]; // [Post!]!
|
||||
getPostById: NexusGenRootTypes['Post']; // Post!
|
||||
getProject: NexusGenRootTypes['Project']; // Project!
|
||||
getProjectsInTournament: NexusGenRootTypes['TournamentProjectsResponse']; // TournamentProjectsResponse!
|
||||
getTournamentById: NexusGenRootTypes['Tournament']; // Tournament!
|
||||
getTrendingPosts: NexusGenRootTypes['Post'][]; // [Post!]!
|
||||
hottestProjects: NexusGenRootTypes['Project'][]; // [Project!]!
|
||||
me: NexusGenRootTypes['MyProfile'] | null; // MyProfile
|
||||
@@ -467,6 +538,7 @@ export interface NexusGenFieldTypes {
|
||||
projectsByCategory: NexusGenRootTypes['Project'][]; // [Project!]!
|
||||
searchProjects: NexusGenRootTypes['Project'][]; // [Project!]!
|
||||
similarMakers: NexusGenRootTypes['User'][]; // [User!]!
|
||||
tournamentParticipationInfo: NexusGenRootTypes['ParticipationInfo'] | null; // ParticipationInfo
|
||||
}
|
||||
Question: { // field return type
|
||||
author: NexusGenRootTypes['Author']; // Author!
|
||||
@@ -508,19 +580,68 @@ export interface NexusGenFieldTypes {
|
||||
cover_image: string; // String!
|
||||
description: string; // String!
|
||||
end_date: NexusGenScalars['Date']; // Date!
|
||||
events: NexusGenRootTypes['TournamentEvent'][]; // [TournamentEvent!]!
|
||||
events_count: number; // Int!
|
||||
faqs: NexusGenRootTypes['TournamentFAQ'][]; // [TournamentFAQ!]!
|
||||
id: number; // Int!
|
||||
judges: NexusGenRootTypes['TournamentJudge'][]; // [TournamentJudge!]!
|
||||
location: string; // String!
|
||||
makers_count: number; // Int!
|
||||
prizes: NexusGenRootTypes['TournamentPrize'][]; // [TournamentPrize!]!
|
||||
projects_count: number; // Int!
|
||||
start_date: NexusGenScalars['Date']; // Date!
|
||||
tags: NexusGenRootTypes['Tag'][]; // [Tag!]!
|
||||
thumbnail_image: string; // String!
|
||||
title: string; // String!
|
||||
website: string; // String!
|
||||
}
|
||||
TournamentEvent: { // field return type
|
||||
description: string; // String!
|
||||
ends_at: NexusGenScalars['Date']; // Date!
|
||||
id: number; // Int!
|
||||
image: string; // String!
|
||||
links: string[]; // [String!]!
|
||||
location: string; // String!
|
||||
starts_at: NexusGenScalars['Date']; // Date!
|
||||
title: string; // String!
|
||||
type: NexusGenEnums['TournamentEventTypeEnum']; // TournamentEventTypeEnum!
|
||||
website: string; // String!
|
||||
}
|
||||
TournamentFAQ: { // field return type
|
||||
answer: string; // String!
|
||||
question: string; // String!
|
||||
}
|
||||
TournamentJudge: { // field return type
|
||||
avatar: string; // String!
|
||||
company: string; // String!
|
||||
name: string; // String!
|
||||
}
|
||||
TournamentMakersResponse: { // field return type
|
||||
hasNext: boolean | null; // Boolean
|
||||
hasPrev: boolean | null; // Boolean
|
||||
makers: NexusGenRootTypes['TournamentParticipant'][]; // [TournamentParticipant!]!
|
||||
}
|
||||
TournamentParticipant: { // field return type
|
||||
hacking_status: NexusGenEnums['TournamentMakerHackingStatusEnum']; // TournamentMakerHackingStatusEnum!
|
||||
is_registered: boolean | null; // Boolean
|
||||
user: NexusGenRootTypes['User']; // User!
|
||||
}
|
||||
TournamentPrize: { // field return type
|
||||
amount: string; // String!
|
||||
image: string; // String!
|
||||
title: string; // String!
|
||||
}
|
||||
TournamentProjectsResponse: { // field return type
|
||||
hasNext: boolean | null; // Boolean
|
||||
hasPrev: boolean | null; // Boolean
|
||||
projects: NexusGenRootTypes['Project'][]; // [Project!]!
|
||||
}
|
||||
User: { // field return type
|
||||
avatar: string; // String!
|
||||
bio: string | null; // String
|
||||
email: string | null; // String
|
||||
discord: string | null; // String
|
||||
github: string | null; // String
|
||||
id: number; // Int!
|
||||
in_tournament: boolean; // Boolean!
|
||||
jobTitle: string | null; // String
|
||||
join_date: NexusGenScalars['Date']; // Date!
|
||||
lightning_address: string | null; // String
|
||||
@@ -546,6 +667,7 @@ export interface NexusGenFieldTypes {
|
||||
payment_request: string; // String!
|
||||
}
|
||||
WalletKey: { // field return type
|
||||
createdAt: NexusGenScalars['Date']; // Date!
|
||||
is_current: boolean; // Boolean!
|
||||
key: string; // String!
|
||||
name: string; // String!
|
||||
@@ -553,9 +675,10 @@ export interface NexusGenFieldTypes {
|
||||
BaseUser: { // field return type
|
||||
avatar: string; // String!
|
||||
bio: string | null; // String
|
||||
email: string | null; // String
|
||||
discord: string | null; // String
|
||||
github: string | null; // String
|
||||
id: number; // Int!
|
||||
in_tournament: boolean; // Boolean!
|
||||
jobTitle: string | null; // String
|
||||
join_date: NexusGenScalars['Date']; // Date!
|
||||
lightning_address: string | null; // String
|
||||
@@ -684,17 +807,21 @@ export interface NexusGenFieldTypeNames {
|
||||
createStory: 'Story'
|
||||
deleteStory: 'Story'
|
||||
donate: 'Donation'
|
||||
registerInTournament: 'User'
|
||||
updateProfileDetails: 'MyProfile'
|
||||
updateProfileRoles: 'MyProfile'
|
||||
updateTournamentRegistration: 'ParticipationInfo'
|
||||
updateUserPreferences: 'MyProfile'
|
||||
vote: 'Vote'
|
||||
}
|
||||
MyProfile: { // field return type name
|
||||
avatar: 'String'
|
||||
bio: 'String'
|
||||
discord: 'String'
|
||||
email: 'String'
|
||||
github: 'String'
|
||||
id: 'Int'
|
||||
in_tournament: 'Boolean'
|
||||
jobTitle: 'String'
|
||||
join_date: 'Date'
|
||||
lightning_address: 'String'
|
||||
@@ -713,6 +840,11 @@ export interface NexusGenFieldTypeNames {
|
||||
walletsKeys: 'WalletKey'
|
||||
website: 'String'
|
||||
}
|
||||
ParticipationInfo: { // field return type name
|
||||
createdAt: 'Date'
|
||||
email: 'String'
|
||||
hacking_status: 'TournamentMakerHackingStatusEnum'
|
||||
}
|
||||
PostComment: { // field return type name
|
||||
author: 'Author'
|
||||
body: 'String'
|
||||
@@ -747,9 +879,12 @@ export interface NexusGenFieldTypeNames {
|
||||
getDonationsStats: 'DonationsStats'
|
||||
getFeed: 'Post'
|
||||
getLnurlDetailsForProject: 'LnurlDetails'
|
||||
getMakersInTournament: 'TournamentMakersResponse'
|
||||
getMyDrafts: 'Post'
|
||||
getPostById: 'Post'
|
||||
getProject: 'Project'
|
||||
getProjectsInTournament: 'TournamentProjectsResponse'
|
||||
getTournamentById: 'Tournament'
|
||||
getTrendingPosts: 'Post'
|
||||
hottestProjects: 'Project'
|
||||
me: 'MyProfile'
|
||||
@@ -760,6 +895,7 @@ export interface NexusGenFieldTypeNames {
|
||||
projectsByCategory: 'Project'
|
||||
searchProjects: 'Project'
|
||||
similarMakers: 'User'
|
||||
tournamentParticipationInfo: 'ParticipationInfo'
|
||||
}
|
||||
Question: { // field return type name
|
||||
author: 'Author'
|
||||
@@ -801,19 +937,68 @@ export interface NexusGenFieldTypeNames {
|
||||
cover_image: 'String'
|
||||
description: 'String'
|
||||
end_date: 'Date'
|
||||
events: 'TournamentEvent'
|
||||
events_count: 'Int'
|
||||
faqs: 'TournamentFAQ'
|
||||
id: 'Int'
|
||||
judges: 'TournamentJudge'
|
||||
location: 'String'
|
||||
makers_count: 'Int'
|
||||
prizes: 'TournamentPrize'
|
||||
projects_count: 'Int'
|
||||
start_date: 'Date'
|
||||
tags: 'Tag'
|
||||
thumbnail_image: 'String'
|
||||
title: 'String'
|
||||
website: 'String'
|
||||
}
|
||||
TournamentEvent: { // field return type name
|
||||
description: 'String'
|
||||
ends_at: 'Date'
|
||||
id: 'Int'
|
||||
image: 'String'
|
||||
links: 'String'
|
||||
location: 'String'
|
||||
starts_at: 'Date'
|
||||
title: 'String'
|
||||
type: 'TournamentEventTypeEnum'
|
||||
website: 'String'
|
||||
}
|
||||
TournamentFAQ: { // field return type name
|
||||
answer: 'String'
|
||||
question: 'String'
|
||||
}
|
||||
TournamentJudge: { // field return type name
|
||||
avatar: 'String'
|
||||
company: 'String'
|
||||
name: 'String'
|
||||
}
|
||||
TournamentMakersResponse: { // field return type name
|
||||
hasNext: 'Boolean'
|
||||
hasPrev: 'Boolean'
|
||||
makers: 'TournamentParticipant'
|
||||
}
|
||||
TournamentParticipant: { // field return type name
|
||||
hacking_status: 'TournamentMakerHackingStatusEnum'
|
||||
is_registered: 'Boolean'
|
||||
user: 'User'
|
||||
}
|
||||
TournamentPrize: { // field return type name
|
||||
amount: 'String'
|
||||
image: 'String'
|
||||
title: 'String'
|
||||
}
|
||||
TournamentProjectsResponse: { // field return type name
|
||||
hasNext: 'Boolean'
|
||||
hasPrev: 'Boolean'
|
||||
projects: 'Project'
|
||||
}
|
||||
User: { // field return type name
|
||||
avatar: 'String'
|
||||
bio: 'String'
|
||||
email: 'String'
|
||||
discord: 'String'
|
||||
github: 'String'
|
||||
id: 'Int'
|
||||
in_tournament: 'Boolean'
|
||||
jobTitle: 'String'
|
||||
join_date: 'Date'
|
||||
lightning_address: 'String'
|
||||
@@ -839,6 +1024,7 @@ export interface NexusGenFieldTypeNames {
|
||||
payment_request: 'String'
|
||||
}
|
||||
WalletKey: { // field return type name
|
||||
createdAt: 'Date'
|
||||
is_current: 'Boolean'
|
||||
key: 'String'
|
||||
name: 'String'
|
||||
@@ -846,9 +1032,10 @@ export interface NexusGenFieldTypeNames {
|
||||
BaseUser: { // field return type name
|
||||
avatar: 'String'
|
||||
bio: 'String'
|
||||
email: 'String'
|
||||
discord: 'String'
|
||||
github: 'String'
|
||||
id: 'Int'
|
||||
in_tournament: 'Boolean'
|
||||
jobTitle: 'String'
|
||||
join_date: 'Date'
|
||||
lightning_address: 'String'
|
||||
@@ -895,12 +1082,20 @@ export interface NexusGenArgTypes {
|
||||
donate: { // args
|
||||
amount_in_sat: number; // Int!
|
||||
}
|
||||
registerInTournament: { // args
|
||||
data?: NexusGenInputs['RegisterInTournamentInput'] | null; // RegisterInTournamentInput
|
||||
tournament_id: number; // Int!
|
||||
}
|
||||
updateProfileDetails: { // args
|
||||
data?: NexusGenInputs['ProfileDetailsInput'] | null; // ProfileDetailsInput
|
||||
}
|
||||
updateProfileRoles: { // args
|
||||
data?: NexusGenInputs['ProfileRolesInput'] | null; // ProfileRolesInput
|
||||
}
|
||||
updateTournamentRegistration: { // args
|
||||
data?: NexusGenInputs['UpdateTournamentRegistrationInput'] | null; // UpdateTournamentRegistrationInput
|
||||
tournament_id: number; // Int!
|
||||
}
|
||||
updateUserPreferences: { // args
|
||||
userKeys?: NexusGenInputs['UserKeyInputType'][] | null; // [UserKeyInputType!]
|
||||
}
|
||||
@@ -910,6 +1105,11 @@ export interface NexusGenArgTypes {
|
||||
item_type: NexusGenEnums['VOTE_ITEM_TYPE']; // VOTE_ITEM_TYPE!
|
||||
}
|
||||
}
|
||||
MyProfile: {
|
||||
in_tournament: { // args
|
||||
id: number; // Int!
|
||||
}
|
||||
}
|
||||
Query: {
|
||||
allProjects: { // args
|
||||
skip?: number | null; // Int
|
||||
@@ -931,6 +1131,14 @@ export interface NexusGenArgTypes {
|
||||
getLnurlDetailsForProject: { // args
|
||||
project_id: number; // Int!
|
||||
}
|
||||
getMakersInTournament: { // args
|
||||
openToConnect?: boolean | null; // Boolean
|
||||
roleId?: number | null; // Int
|
||||
search?: string | null; // String
|
||||
skip?: number | null; // Int
|
||||
take: number | null; // Int
|
||||
tournamentId: number; // Int!
|
||||
}
|
||||
getMyDrafts: { // args
|
||||
type: NexusGenEnums['POST_TYPE']; // POST_TYPE!
|
||||
}
|
||||
@@ -941,6 +1149,16 @@ export interface NexusGenArgTypes {
|
||||
getProject: { // args
|
||||
id: number; // Int!
|
||||
}
|
||||
getProjectsInTournament: { // args
|
||||
roleId?: number | null; // Int
|
||||
search?: string | null; // String
|
||||
skip?: number | null; // Int
|
||||
take: number | null; // Int
|
||||
tournamentId: number; // Int!
|
||||
}
|
||||
getTournamentById: { // args
|
||||
id: number; // Int!
|
||||
}
|
||||
hottestProjects: { // args
|
||||
skip?: number | null; // Int
|
||||
take: number | null; // Int
|
||||
@@ -965,6 +1183,19 @@ export interface NexusGenArgTypes {
|
||||
similarMakers: { // args
|
||||
id: number; // Int!
|
||||
}
|
||||
tournamentParticipationInfo: { // args
|
||||
tournamentId: number; // Int!
|
||||
}
|
||||
}
|
||||
User: {
|
||||
in_tournament: { // args
|
||||
id: number; // Int!
|
||||
}
|
||||
}
|
||||
BaseUser: {
|
||||
in_tournament: { // args
|
||||
id: number; // Int!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,9 +21,10 @@ type Award {
|
||||
interface BaseUser {
|
||||
avatar: String!
|
||||
bio: String
|
||||
email: String
|
||||
discord: String
|
||||
github: String
|
||||
id: Int!
|
||||
in_tournament(id: Int!): Boolean!
|
||||
jobTitle: String
|
||||
join_date: Date!
|
||||
lightning_address: String
|
||||
@@ -148,8 +149,10 @@ type Mutation {
|
||||
createStory(data: StoryInputType): Story
|
||||
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
|
||||
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!
|
||||
}
|
||||
@@ -157,9 +160,11 @@ type Mutation {
|
||||
type MyProfile implements BaseUser {
|
||||
avatar: String!
|
||||
bio: String
|
||||
discord: String
|
||||
email: String
|
||||
github: String
|
||||
id: Int!
|
||||
in_tournament(id: Int!): Boolean!
|
||||
jobTitle: String
|
||||
join_date: Date!
|
||||
lightning_address: String
|
||||
@@ -185,6 +190,12 @@ enum POST_TYPE {
|
||||
Story
|
||||
}
|
||||
|
||||
type ParticipationInfo {
|
||||
createdAt: Date!
|
||||
email: String!
|
||||
hacking_status: TournamentMakerHackingStatusEnum!
|
||||
}
|
||||
|
||||
union Post = Bounty | Question | Story
|
||||
|
||||
interface PostBase {
|
||||
@@ -210,6 +221,7 @@ type PostComment {
|
||||
input ProfileDetailsInput {
|
||||
avatar: String
|
||||
bio: String
|
||||
discord: String
|
||||
email: String
|
||||
github: String
|
||||
jobTitle: String
|
||||
@@ -253,9 +265,12 @@ type Query {
|
||||
getDonationsStats: DonationsStats!
|
||||
getFeed(skip: Int = 0, sortBy: String, tag: Int = 0, take: Int = 10): [Post!]!
|
||||
getLnurlDetailsForProject(project_id: Int!): LnurlDetails!
|
||||
getMakersInTournament(openToConnect: Boolean, roleId: Int, search: String, skip: Int = 0, take: Int = 10, tournamentId: Int!): TournamentMakersResponse!
|
||||
getMyDrafts(type: POST_TYPE!): [Post!]!
|
||||
getPostById(id: Int!, type: POST_TYPE!): Post!
|
||||
getProject(id: Int!): Project!
|
||||
getProjectsInTournament(roleId: Int, search: String, skip: Int = 0, take: Int = 10, tournamentId: Int!): TournamentProjectsResponse!
|
||||
getTournamentById(id: Int!): Tournament!
|
||||
getTrendingPosts: [Post!]!
|
||||
hottestProjects(skip: Int = 0, take: Int = 50): [Project!]!
|
||||
me: MyProfile
|
||||
@@ -266,6 +281,7 @@ type Query {
|
||||
projectsByCategory(category_id: Int!, skip: Int = 0, take: Int = 10): [Project!]!
|
||||
searchProjects(search: String!, skip: Int = 0, take: Int = 50): [Project!]!
|
||||
similarMakers(id: Int!): [User!]!
|
||||
tournamentParticipationInfo(tournamentId: Int!): ParticipationInfo
|
||||
}
|
||||
|
||||
type Question implements PostBase {
|
||||
@@ -282,6 +298,11 @@ type Question implements PostBase {
|
||||
votes_count: Int!
|
||||
}
|
||||
|
||||
input RegisterInTournamentInput {
|
||||
email: String!
|
||||
hacking_status: TournamentMakerHackingStatusEnum!
|
||||
}
|
||||
|
||||
enum RoleLevelEnum {
|
||||
Advanced
|
||||
Beginner
|
||||
@@ -328,20 +349,93 @@ type Tournament {
|
||||
cover_image: String!
|
||||
description: String!
|
||||
end_date: Date!
|
||||
events: [TournamentEvent!]!
|
||||
events_count: Int!
|
||||
faqs: [TournamentFAQ!]!
|
||||
id: Int!
|
||||
judges: [TournamentJudge!]!
|
||||
location: String!
|
||||
makers_count: Int!
|
||||
prizes: [TournamentPrize!]!
|
||||
projects_count: Int!
|
||||
start_date: Date!
|
||||
tags: [Tag!]!
|
||||
thumbnail_image: String!
|
||||
title: String!
|
||||
website: String!
|
||||
}
|
||||
|
||||
type TournamentEvent {
|
||||
description: String!
|
||||
ends_at: Date!
|
||||
id: Int!
|
||||
image: String!
|
||||
links: [String!]!
|
||||
location: String!
|
||||
starts_at: Date!
|
||||
title: String!
|
||||
type: TournamentEventTypeEnum!
|
||||
website: String!
|
||||
}
|
||||
|
||||
enum TournamentEventTypeEnum {
|
||||
IRLMeetup
|
||||
OnlineMeetup
|
||||
TwitterSpace
|
||||
Workshop
|
||||
}
|
||||
|
||||
type TournamentFAQ {
|
||||
answer: String!
|
||||
question: String!
|
||||
}
|
||||
|
||||
type TournamentJudge {
|
||||
avatar: String!
|
||||
company: String!
|
||||
name: String!
|
||||
}
|
||||
|
||||
enum TournamentMakerHackingStatusEnum {
|
||||
OpenToConnect
|
||||
Solo
|
||||
}
|
||||
|
||||
type TournamentMakersResponse {
|
||||
hasNext: Boolean
|
||||
hasPrev: Boolean
|
||||
makers: [TournamentParticipant!]!
|
||||
}
|
||||
|
||||
type TournamentParticipant {
|
||||
hacking_status: TournamentMakerHackingStatusEnum!
|
||||
is_registered: Boolean
|
||||
user: User!
|
||||
}
|
||||
|
||||
type TournamentPrize {
|
||||
amount: String!
|
||||
image: String!
|
||||
title: String!
|
||||
}
|
||||
|
||||
type TournamentProjectsResponse {
|
||||
hasNext: Boolean
|
||||
hasPrev: Boolean
|
||||
projects: [Project!]!
|
||||
}
|
||||
|
||||
input UpdateTournamentRegistrationInput {
|
||||
email: String
|
||||
hacking_status: TournamentMakerHackingStatusEnum
|
||||
}
|
||||
|
||||
type User implements BaseUser {
|
||||
avatar: String!
|
||||
bio: String
|
||||
email: String
|
||||
discord: String
|
||||
github: String
|
||||
id: Int!
|
||||
in_tournament(id: Int!): Boolean!
|
||||
jobTitle: String
|
||||
join_date: Date!
|
||||
lightning_address: String
|
||||
@@ -383,6 +477,7 @@ type Vote {
|
||||
}
|
||||
|
||||
type WalletKey {
|
||||
createdAt: Date!
|
||||
is_current: Boolean!
|
||||
key: String!
|
||||
name: String!
|
||||
|
||||
@@ -5,6 +5,7 @@ const vote = require('./vote')
|
||||
const post = require('./post')
|
||||
const users = require('./users')
|
||||
const hackathon = require('./hackathon')
|
||||
const tournament = require('./tournament')
|
||||
const donation = require('./donation')
|
||||
const tag = require('./tag')
|
||||
|
||||
@@ -17,5 +18,6 @@ module.exports = {
|
||||
...post,
|
||||
...users,
|
||||
...hackathon,
|
||||
...tournament,
|
||||
...donation,
|
||||
}
|
||||
524
api/functions/graphql/types/tournament.js
Normal file
524
api/functions/graphql/types/tournament.js
Normal file
@@ -0,0 +1,524 @@
|
||||
const {
|
||||
intArg,
|
||||
objectType,
|
||||
stringArg,
|
||||
extendType,
|
||||
nonNull,
|
||||
enumType,
|
||||
inputObjectType,
|
||||
booleanArg,
|
||||
} = require('nexus');
|
||||
const { getUserByPubKey } = require('../../../auth/utils/helperFuncs');
|
||||
const { prisma } = require('../../../prisma');
|
||||
const { paginationArgs, removeNulls } = require('./helpers');
|
||||
|
||||
|
||||
|
||||
const TournamentPrize = objectType({
|
||||
name: 'TournamentPrize',
|
||||
definition(t) {
|
||||
t.nonNull.string('title');
|
||||
t.nonNull.string('amount');
|
||||
t.nonNull.string('image');
|
||||
}
|
||||
})
|
||||
|
||||
const TournamentJudge = objectType({
|
||||
name: 'TournamentJudge',
|
||||
definition(t) {
|
||||
t.nonNull.string('name');
|
||||
t.nonNull.string('company');
|
||||
t.nonNull.string('avatar');
|
||||
}
|
||||
})
|
||||
|
||||
const TournamentFAQ = objectType({
|
||||
name: 'TournamentFAQ',
|
||||
definition(t) {
|
||||
t.nonNull.string('question');
|
||||
t.nonNull.string('answer');
|
||||
}
|
||||
})
|
||||
|
||||
const TournamentParticipant = objectType({
|
||||
name: "TournamentParticipant",
|
||||
definition(t) {
|
||||
t.nonNull.field('hacking_status', { type: TournamentMakerHackingStatusEnum });
|
||||
t.boolean('is_registered')
|
||||
t.nonNull.field('user', { type: "User" })
|
||||
}
|
||||
})
|
||||
|
||||
const TournamentEventTypeEnum = enumType({
|
||||
name: 'TournamentEventTypeEnum',
|
||||
members: {
|
||||
TwitterSpace: 0,
|
||||
Workshop: 1,
|
||||
IRLMeetup: 2,
|
||||
OnlineMeetup: 3,
|
||||
},
|
||||
});
|
||||
|
||||
const TournamentMakerHackingStatusEnum = enumType({
|
||||
name: 'TournamentMakerHackingStatusEnum',
|
||||
members: {
|
||||
Solo: 0,
|
||||
OpenToConnect: 1,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
const TournamentEvent = objectType({
|
||||
name: 'TournamentEvent',
|
||||
definition(t) {
|
||||
t.nonNull.int('id');
|
||||
t.nonNull.string('title');
|
||||
t.nonNull.string('image');
|
||||
t.nonNull.string('description');
|
||||
t.nonNull.date('starts_at');
|
||||
t.nonNull.date('ends_at');
|
||||
t.nonNull.string('location');
|
||||
t.nonNull.string('website');
|
||||
t.nonNull.field('type', { type: TournamentEventTypeEnum })
|
||||
t.nonNull.list.nonNull.string('links', { resolve() { return [] } });
|
||||
}
|
||||
})
|
||||
|
||||
const Tournament = objectType({
|
||||
name: 'Tournament',
|
||||
definition(t) {
|
||||
t.nonNull.int('id');
|
||||
t.nonNull.string('title');
|
||||
t.nonNull.string('description');
|
||||
t.nonNull.string('thumbnail_image');
|
||||
t.nonNull.string('cover_image');
|
||||
t.nonNull.date('start_date');
|
||||
t.nonNull.date('end_date');
|
||||
t.nonNull.string('location');
|
||||
t.nonNull.string('website');
|
||||
|
||||
t.nonNull.int('events_count', {
|
||||
resolve(parent) {
|
||||
return prisma.tournamentEvent.count({
|
||||
where: {
|
||||
tournament_id: parent.id
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
t.nonNull.int('makers_count', {
|
||||
resolve(parent) {
|
||||
return prisma.tournamentParticipant.count({
|
||||
where: {
|
||||
tournament_id: parent.id
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
t.nonNull.int('projects_count', {
|
||||
resolve(parent) {
|
||||
return prisma.tournamentProject.count({
|
||||
where: {
|
||||
tournament_id: parent.id
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
t.nonNull.list.nonNull.field('prizes', {
|
||||
type: TournamentPrize,
|
||||
resolve(parent) {
|
||||
return prisma.tournament.findUnique({ where: { id: parent.id } }).prizes()
|
||||
}
|
||||
});
|
||||
t.nonNull.list.nonNull.field('judges', {
|
||||
type: TournamentJudge,
|
||||
resolve(parent) {
|
||||
return prisma.tournament.findUnique({ where: { id: parent.id } }).judges()
|
||||
}
|
||||
});
|
||||
t.nonNull.list.nonNull.field('faqs', {
|
||||
type: TournamentFAQ,
|
||||
resolve(parent) {
|
||||
return prisma.tournament.findUnique({ where: { id: parent.id } }).faqs()
|
||||
}
|
||||
});
|
||||
t.nonNull.list.nonNull.field('events', {
|
||||
type: TournamentEvent,
|
||||
resolve(parent) {
|
||||
return prisma.tournament.findUnique({ where: { id: parent.id } }).events()
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const TournamentMakersResponse = objectType({
|
||||
name: 'TournamentMakersResponse',
|
||||
definition(t) {
|
||||
t.boolean('hasNext');
|
||||
t.boolean('hasPrev');
|
||||
|
||||
t.nonNull.list.nonNull.field('makers', { type: TournamentParticipant })
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const TournamentProjectsResponse = objectType({
|
||||
name: 'TournamentProjectsResponse',
|
||||
definition(t) {
|
||||
t.boolean('hasNext');
|
||||
t.boolean('hasPrev');
|
||||
|
||||
t.nonNull.list.nonNull.field('projects', { type: "Project" })
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const getTournamentById = extendType({
|
||||
type: "Query",
|
||||
definition(t) {
|
||||
t.nonNull.field('getTournamentById', {
|
||||
type: Tournament,
|
||||
args: {
|
||||
id: nonNull(intArg()),
|
||||
},
|
||||
resolve(_, { id }) {
|
||||
return prisma.tournament.findUnique({
|
||||
where: { id }
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const ParticipationInfo = objectType({
|
||||
name: "ParticipationInfo",
|
||||
definition(t) {
|
||||
t.nonNull.date('createdAt')
|
||||
t.nonNull.string('email')
|
||||
t.nonNull.field('hacking_status', { type: TournamentMakerHackingStatusEnum });
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
const tournamentParticipationInfo = extendType({
|
||||
type: "Query",
|
||||
definition(t) {
|
||||
t.field('tournamentParticipationInfo', {
|
||||
type: ParticipationInfo,
|
||||
args: {
|
||||
tournamentId: nonNull(intArg()),
|
||||
},
|
||||
async resolve(_, args, ctx) {
|
||||
|
||||
const user = await getUserByPubKey(ctx.userPubKey);
|
||||
if (!user)
|
||||
return null
|
||||
|
||||
|
||||
return prisma.tournamentParticipant.findFirst({
|
||||
where: {
|
||||
user_id: user.id,
|
||||
tournament_id: args.tournamentId
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const getMakersInTournament = extendType({
|
||||
type: "Query",
|
||||
definition(t) {
|
||||
t.nonNull.field('getMakersInTournament', {
|
||||
type: TournamentMakersResponse,
|
||||
args: {
|
||||
tournamentId: nonNull(intArg()),
|
||||
...paginationArgs({ take: 10 }),
|
||||
search: stringArg(),
|
||||
roleId: intArg(),
|
||||
openToConnect: booleanArg()
|
||||
},
|
||||
async resolve(_, args, ctx) {
|
||||
|
||||
const user = await getUserByPubKey(ctx.userPubKey);
|
||||
|
||||
|
||||
let filters = [];
|
||||
|
||||
if (args.search) filters.push({
|
||||
OR: [
|
||||
{
|
||||
name: {
|
||||
contains: args.search,
|
||||
mode: 'insensitive'
|
||||
}
|
||||
},
|
||||
{
|
||||
jobTitle: {
|
||||
contains: args.search,
|
||||
mode: 'insensitive'
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
if (args.roleId) filters.push({
|
||||
roles: {
|
||||
some: {
|
||||
roleId: args.roleId
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (args.openToConnect) filters.push({
|
||||
OR: [
|
||||
{
|
||||
github: {
|
||||
not: ''
|
||||
}
|
||||
},
|
||||
{
|
||||
twitter: {
|
||||
not: ''
|
||||
}
|
||||
},
|
||||
{
|
||||
linkedin: {
|
||||
not: ''
|
||||
}
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
if (user?.id) filters.push({
|
||||
id: {
|
||||
not: user.id
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
const makers = (await prisma.tournamentParticipant.findMany({
|
||||
where: {
|
||||
tournament_id: args.tournamentId,
|
||||
...(filters.length > 0 && {
|
||||
user: {
|
||||
AND: filters
|
||||
}
|
||||
}),
|
||||
...(args.openToConnect && {
|
||||
hacking_status: TournamentMakerHackingStatusEnum.value.members.OpenToConnect
|
||||
})
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
skip: args.skip,
|
||||
take: args.take + 1,
|
||||
}))
|
||||
.map(item => ({
|
||||
hacking_status: item.hacking_status,
|
||||
user: item.user
|
||||
}))
|
||||
|
||||
|
||||
|
||||
return {
|
||||
hasNext: makers.length === args.take + 1,
|
||||
hasPrev: args.skip !== 0,
|
||||
makers: makers.slice(0, args.take)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const getProjectsInTournament = extendType({
|
||||
type: "Query",
|
||||
definition(t) {
|
||||
t.nonNull.field('getProjectsInTournament', {
|
||||
type: TournamentProjectsResponse,
|
||||
args: {
|
||||
tournamentId: nonNull(intArg()),
|
||||
...paginationArgs({ take: 10 }),
|
||||
search: stringArg(),
|
||||
roleId: intArg(),
|
||||
},
|
||||
async resolve(_, args) {
|
||||
|
||||
|
||||
let filters = [];
|
||||
|
||||
if (args.search) filters.push({
|
||||
OR: [
|
||||
{
|
||||
title: {
|
||||
contains: args.search,
|
||||
mode: 'insensitive'
|
||||
}
|
||||
},
|
||||
{
|
||||
description: {
|
||||
contains: args.search,
|
||||
mode: 'insensitive'
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
// if (args.roleId) filters.push({
|
||||
// recruit_roles: {
|
||||
// some: {
|
||||
// roleId: args.roleId
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
|
||||
|
||||
|
||||
const projects = (await prisma.tournamentProject.findMany({
|
||||
where: {
|
||||
tournament_id: args.tournamentId,
|
||||
...(filters.length > 0 && {
|
||||
project: {
|
||||
AND: filters
|
||||
}
|
||||
})
|
||||
},
|
||||
include: {
|
||||
project: true,
|
||||
},
|
||||
skip: args.skip,
|
||||
take: args.take + 1,
|
||||
})).map(item => item.project)
|
||||
|
||||
console.log();
|
||||
|
||||
|
||||
return {
|
||||
hasNext: projects.length === args.take + 1,
|
||||
hasPrev: args.skip !== 0,
|
||||
projects: projects.slice(0, args.take)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
const RegisterInTournamentInput = inputObjectType({
|
||||
name: 'RegisterInTournamentInput',
|
||||
definition(t) {
|
||||
t.nonNull.string('email')
|
||||
t.nonNull.field('hacking_status', { type: TournamentMakerHackingStatusEnum })
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const registerInTournament = extendType({
|
||||
type: 'Mutation',
|
||||
definition(t) {
|
||||
t.field('registerInTournament', {
|
||||
type: 'User',
|
||||
args: {
|
||||
data: RegisterInTournamentInput,
|
||||
tournament_id: nonNull(intArg())
|
||||
},
|
||||
async resolve(_root, { tournament_id, data: { email, hacking_status } }, ctx) {
|
||||
const user = await getUserByPubKey(ctx.userPubKey);
|
||||
|
||||
// Do some validation
|
||||
if (!user)
|
||||
throw new Error("You have to login");
|
||||
|
||||
|
||||
// Email verification here:
|
||||
// ....
|
||||
// ....
|
||||
|
||||
return (await prisma.tournamentParticipant.create({
|
||||
data: {
|
||||
tournament_id,
|
||||
user_id: user.id,
|
||||
email,
|
||||
hacking_status
|
||||
},
|
||||
include: {
|
||||
user: true
|
||||
}
|
||||
})).user;
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const UpdateTournamentRegistrationInput = inputObjectType({
|
||||
name: 'UpdateTournamentRegistrationInput',
|
||||
definition(t) {
|
||||
t.string('email')
|
||||
t.field('hacking_status', { type: TournamentMakerHackingStatusEnum })
|
||||
}
|
||||
})
|
||||
|
||||
const updateTournamentRegistration = extendType({
|
||||
type: 'Mutation',
|
||||
definition(t) {
|
||||
t.field('updateTournamentRegistration', {
|
||||
type: ParticipationInfo,
|
||||
args: {
|
||||
data: UpdateTournamentRegistrationInput,
|
||||
tournament_id: nonNull(intArg())
|
||||
},
|
||||
async resolve(_root, { tournament_id, data: { email, hacking_status } }, ctx) {
|
||||
const user = await getUserByPubKey(ctx.userPubKey);
|
||||
|
||||
// Do some validation
|
||||
// if (!user)
|
||||
// throw new Error("You have to login");
|
||||
|
||||
|
||||
// Email verification here:
|
||||
// ....
|
||||
// ....
|
||||
|
||||
return prisma.tournamentParticipant.update({
|
||||
where: {
|
||||
tournament_id_user_id: { tournament_id, user_id: user.id }
|
||||
},
|
||||
data: removeNulls({
|
||||
email,
|
||||
hacking_status
|
||||
}),
|
||||
});
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
module.exports = {
|
||||
// Types
|
||||
Tournament,
|
||||
|
||||
// Enums
|
||||
TournamentEventTypeEnum,
|
||||
|
||||
// Queries
|
||||
getTournamentById,
|
||||
getMakersInTournament,
|
||||
getProjectsInTournament,
|
||||
tournamentParticipationInfo,
|
||||
|
||||
// Mutations
|
||||
registerInTournament,
|
||||
updateTournamentRegistration,
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
const {
|
||||
intArg,
|
||||
objectType,
|
||||
stringArg,
|
||||
extendType,
|
||||
nonNull,
|
||||
} = require('nexus');
|
||||
const { prisma } = require('../../../prisma');
|
||||
|
||||
|
||||
|
||||
const Tournament = objectType({
|
||||
name: 'Tournament',
|
||||
definition(t) {
|
||||
t.nonNull.int('id');
|
||||
t.nonNull.string('title');
|
||||
t.nonNull.string('description');
|
||||
t.nonNull.string('thumbnail_image');
|
||||
t.nonNull.string('cover_image');
|
||||
t.nonNull.date('start_date');
|
||||
t.nonNull.date('end_date');
|
||||
t.nonNull.string('website');
|
||||
t.nonNull.list.nonNull.field('tags', {
|
||||
type: "Tag",
|
||||
resolve: (parent) => {
|
||||
// return prisma.hackathon.findUnique({ where: { id: parent.id } }).tags();
|
||||
return [];
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
const getAllTournaments = extendType({
|
||||
type: "Query",
|
||||
definition(t) {
|
||||
t.nonNull.list.nonNull.field('getAllTournaments', {
|
||||
type: Tournament,
|
||||
args: {
|
||||
sortBy: stringArg(),
|
||||
tag: intArg(),
|
||||
},
|
||||
resolve(_, args) {
|
||||
const { sortBy, tag } = args;
|
||||
return [];
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = {
|
||||
// Types
|
||||
Tournament,
|
||||
// Queries
|
||||
getAllTournaments,
|
||||
}
|
||||
@@ -3,7 +3,7 @@ const { prisma } = require('../../../prisma');
|
||||
const { objectType, extendType, intArg, nonNull, inputObjectType, interfaceType, list, enumType } = require("nexus");
|
||||
const { getUserByPubKey } = require("../../../auth/utils/helperFuncs");
|
||||
const { removeNulls } = require("./helpers");
|
||||
const { Tournament } = require('./tournaments');
|
||||
const { Tournament } = require('./tournament');
|
||||
|
||||
|
||||
|
||||
@@ -16,11 +16,11 @@ const BaseUser = interfaceType({
|
||||
t.nonNull.string('avatar');
|
||||
t.nonNull.date('join_date');
|
||||
t.string('role');
|
||||
t.string('email')
|
||||
t.string('jobTitle')
|
||||
t.string('lightning_address')
|
||||
t.string('website')
|
||||
t.string('twitter')
|
||||
t.string('discord')
|
||||
t.string('github')
|
||||
t.string('linkedin')
|
||||
t.string('bio')
|
||||
@@ -54,8 +54,15 @@ const BaseUser = interfaceType({
|
||||
})
|
||||
t.nonNull.list.nonNull.field('tournaments', {
|
||||
type: Tournament,
|
||||
resolve: (parent) => {
|
||||
return []
|
||||
resolve: async (parent) => {
|
||||
return prisma.tournamentParticipant.findMany({
|
||||
where: {
|
||||
user_id: parent.id
|
||||
},
|
||||
include: {
|
||||
tournament: true
|
||||
}
|
||||
}).then(d => d.map(item => item.tournament))
|
||||
}
|
||||
})
|
||||
t.nonNull.list.nonNull.field('similar_makers', {
|
||||
@@ -82,6 +89,15 @@ const BaseUser = interfaceType({
|
||||
}
|
||||
});
|
||||
|
||||
t.nonNull.boolean('in_tournament', {
|
||||
args: {
|
||||
id: nonNull(intArg())
|
||||
},
|
||||
resolve(parent, args) {
|
||||
return prisma.tournamentParticipant.findFirst({ where: { tournament_id: args.id, user_id: parent.id } }).then(res => !!res)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
},
|
||||
resolveType() {
|
||||
@@ -166,6 +182,8 @@ const MyProfile = objectType({
|
||||
name: 'MyProfile',
|
||||
definition(t) {
|
||||
t.implements('BaseUser')
|
||||
|
||||
t.string('email')
|
||||
t.string('nostr_prv_key')
|
||||
t.string('nostr_pub_key')
|
||||
|
||||
@@ -209,6 +227,11 @@ const profile = extendType({
|
||||
id: nonNull(intArg())
|
||||
},
|
||||
async resolve(parent, { id }, ctx) {
|
||||
|
||||
const user = await getUserByPubKey(ctx.userPubKey)
|
||||
let isMy = false;
|
||||
if (user?.id === id) isMy = true;
|
||||
|
||||
return prisma.user.findUnique({ where: { id } })
|
||||
}
|
||||
})
|
||||
@@ -249,6 +272,7 @@ const ProfileDetailsInput = inputObjectType({
|
||||
t.string('lightning_address')
|
||||
t.string('website')
|
||||
t.string('twitter')
|
||||
t.string('discord')
|
||||
t.string('github')
|
||||
t.string('linkedin')
|
||||
t.string('bio')
|
||||
|
||||
@@ -9,4 +9,8 @@ generates:
|
||||
- "typescript-react-apollo"
|
||||
config:
|
||||
withHooks: true
|
||||
avoidOptionals: true
|
||||
avoidOptionals:
|
||||
field: true
|
||||
inputValue: false
|
||||
object: true
|
||||
defaultValue: true
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "my-app",
|
||||
"name": "makers-bolt-fun",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "my-app",
|
||||
"name": "makers-bolt-fun",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.6.9",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "my-app",
|
||||
"name": "makers-bolt-fun",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
|
||||
232
prisma/seed/data/tournament.seed.js
Normal file
232
prisma/seed/data/tournament.seed.js
Normal file
@@ -0,0 +1,232 @@
|
||||
|
||||
|
||||
const tournament = {
|
||||
__typename: "Tournament",
|
||||
id: 12,
|
||||
title: "Legends of Lightning ⚡️",
|
||||
start_date: "2022-10-12T21:00:00.000Z",
|
||||
end_date: "2022-11-30T22:00:00.000Z",
|
||||
cover_image: "https://imagedelivery.net/wyrwp3c-j0gDDUWgnE7lig/1d5d2c86-fe46-4478-6909-bb3c425c0d00/public",
|
||||
thumbnail_image: "https://imagedelivery.net/wyrwp3c-j0gDDUWgnE7lig/37fb9cd6-e4f1-43f9-c3fe-7c3e119d5600/public",
|
||||
location: "Online",
|
||||
website: "#",
|
||||
description: // this field accepts markdown
|
||||
`## Tournament Details
|
||||
BOLT🔩FUN’s maiden tournament, **Legends of Lightning** ⚡ will be an online global competition for makers to learn, connect, collaborate, and experiment with building innovative applications and tools with bitcoin and lightning.
|
||||
|
||||
Spanning a 2-month period, makers can form teams, hack on projects, and show off their progress, activity, and updates as they compete for up to **$10,000 in bitcoin prizes**.
|
||||
|
||||
BOLT🔩FUN has partnered with a number of events, meetups, and hackathons to provide makers the opportunity to brainstorm, design, build, and accelerate their tournament projects over the course of a couple of months. At the end of the tournament, a panel of judges will access and score all submitted projects - announcing the winners in the second week of December!
|
||||
|
||||
`, // markdown
|
||||
prizes: [
|
||||
{
|
||||
title: "stw3 champion",
|
||||
amount: "$ 5k",
|
||||
image: "https://imagedelivery.net/wyrwp3c-j0gDDUWgnE7lig/39217dcf-c900-46be-153f-169e3a1f0400/public",
|
||||
},
|
||||
{
|
||||
title: "2nd place",
|
||||
amount: "$ 2.5k",
|
||||
image: "https://imagedelivery.net/wyrwp3c-j0gDDUWgnE7lig/39cdb7c8-5fbf-49ff-32cf-fdabc3aa2d00/public",
|
||||
},
|
||||
{
|
||||
title: "3rd place ",
|
||||
amount: "$ 1.5k",
|
||||
image: "https://imagedelivery.net/wyrwp3c-j0gDDUWgnE7lig/75958797-73b2-4a62-52df-9f0f98c53900/public",
|
||||
},
|
||||
{
|
||||
title: "best design ",
|
||||
amount: "$ 1k",
|
||||
image: "https://imagedelivery.net/wyrwp3c-j0gDDUWgnE7lig/fa7b7cdd-7c06-4ebe-1a2d-94af9d2dae00/public",
|
||||
}
|
||||
],
|
||||
|
||||
events: [
|
||||
{
|
||||
title: "Tab Conf 22",
|
||||
starts_at: "2022-10-13T21:00:00.000Z",
|
||||
ends_at: "2022-10-15T22:00:00.000Z",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Diam morbi pellentesque velit congue. Aliquet rutrum a, augue vitae tincidunt ac egestas. Mauris nec fringilla diam eget fusce malesuada cum parturient. Nulla pretium purus odio odio.",
|
||||
image: 'https://picsum.photos/id/10/400/800',
|
||||
links: [],
|
||||
location: "Atlanta, GA",
|
||||
type: 1, /** EVent typs encoding
|
||||
*
|
||||
Twitter Space: 0,
|
||||
Workshop: 1,
|
||||
IRL Meetup: 2,
|
||||
Online Meetup: 3,
|
||||
*/
|
||||
website: "https://2022.tabconf.com/"
|
||||
},
|
||||
{
|
||||
title: "Bitcoin Amsterdam",
|
||||
starts_at: "2022-10-12T21:00:00.000Z",
|
||||
ends_at: "2022-10-14T22:00:00.000Z",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Diam morbi pellentesque velit congue. Aliquet rutrum a, augue vitae tincidunt ac egestas. Mauris nec fringilla diam eget fusce malesuada cum parturient. Nulla pretium purus odio odio.",
|
||||
image: 'https://picsum.photos/id/10/400/800',
|
||||
links: [],
|
||||
location: "Amsterdam, NL",
|
||||
type: 2,
|
||||
website: "https://b.tc/conference/amsterdam"
|
||||
},
|
||||
{
|
||||
title: "Lugano’s Plan ₿",
|
||||
starts_at: "2022-10-28T21:00:00.000Z",
|
||||
ends_at: "2022-11-04T22:00:00.000Z",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Diam morbi pellentesque velit congue. Aliquet rutrum a, augue vitae tincidunt ac egestas. Mauris nec fringilla diam eget fusce malesuada cum parturient. Nulla pretium purus odio odio.",
|
||||
image: 'https://picsum.photos/id/10/400/800',
|
||||
links: [],
|
||||
location: "Lugano, CH",
|
||||
type: 3,
|
||||
website: "https://planb.lugano.ch/"
|
||||
},
|
||||
{
|
||||
title: "Adopting Bitcoin 22",
|
||||
starts_at: "2022-11-15T21:00:00.000Z",
|
||||
ends_at: "2022-11-17T22:00:00.000Z",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Diam morbi pellentesque velit congue. Aliquet rutrum a, augue vitae tincidunt ac egestas. Mauris nec fringilla diam eget fusce malesuada cum parturient. Nulla pretium purus odio odio.",
|
||||
image: 'https://picsum.photos/id/10/400/800',
|
||||
links: [],
|
||||
location: "El Salvador",
|
||||
type: 2,
|
||||
website: "https://adoptingbitcoin.org/2022/"
|
||||
},
|
||||
|
||||
{
|
||||
title: "PlebTLV",
|
||||
starts_at: "2022-10-23T21:00:00.000Z",
|
||||
ends_at: "2022-10-23T22:00:00.000Z",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Diam morbi pellentesque velit congue. Aliquet rutrum a, augue vitae tincidunt ac egestas. Mauris nec fringilla diam eget fusce malesuada cum parturient. Nulla pretium purus odio odio.",
|
||||
image: 'https://picsum.photos/id/10/400/800',
|
||||
links: [],
|
||||
location: "Tel Aviv",
|
||||
type: 2,
|
||||
website: "https://plebtlv.com/"
|
||||
},
|
||||
{
|
||||
title: "Bitcoin Designathon",
|
||||
starts_at: "2022-10-12T21:00:00.000Z",
|
||||
ends_at: "2022-10-16T22:00:00.000Z",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Diam morbi pellentesque velit congue. Aliquet rutrum a, augue vitae tincidunt ac egestas. Mauris nec fringilla diam eget fusce malesuada cum parturient. Nulla pretium purus odio odio.",
|
||||
image: 'https://picsum.photos/id/10/400/800',
|
||||
links: [],
|
||||
location: "Online",
|
||||
type: 2,
|
||||
website: "https://bitcoin.design"
|
||||
},
|
||||
],
|
||||
judges: [
|
||||
{
|
||||
name: "Roy Sheinfeld",
|
||||
avatar: "https://s3-alpha-sig.figma.com/img/5e65/c22c/673b8f74ac43f024b036dbc4e6479e0d?Expires=1662940800&Signature=GR54s7FBcLGcPTVclWdmPjzU92tyrYpdUbbDUYKMUkdQbxq2yQlUhZ-AOLDHhOPY4P2G3aW2yT16b1AUbC8RBx1boH25MSrH-jpn6X57IJA-4ZeHP8zCo-yjTLpb8Gn~vudIi9rPfbwJ34stp-VeOAYMuOHlah3YO-B4MBsBv-NqhP7BMY4zz9vGdBLZhOjYQYdLZ2494Ae6L5FpD1ah3WD3U5qUN9dDvYvAtqYfhQeBOnsG6PfYoq8LouCuERC4S26BeooPg8UdGUCf324-SjEihCoL8mQFq80PSsaAZl5~EBOKRUx14FOprizMusaYN0K06E~fjDIDbM2Rmc9Xjg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA",
|
||||
company: "Breez",
|
||||
twitter: "@therealkingonly"
|
||||
},
|
||||
{
|
||||
name: "John Carvalho",
|
||||
avatar: "https://s3-alpha-sig.figma.com/img/5e65/c22c/673b8f74ac43f024b036dbc4e6479e0d?Expires=1662940800&Signature=GR54s7FBcLGcPTVclWdmPjzU92tyrYpdUbbDUYKMUkdQbxq2yQlUhZ-AOLDHhOPY4P2G3aW2yT16b1AUbC8RBx1boH25MSrH-jpn6X57IJA-4ZeHP8zCo-yjTLpb8Gn~vudIi9rPfbwJ34stp-VeOAYMuOHlah3YO-B4MBsBv-NqhP7BMY4zz9vGdBLZhOjYQYdLZ2494Ae6L5FpD1ah3WD3U5qUN9dDvYvAtqYfhQeBOnsG6PfYoq8LouCuERC4S26BeooPg8UdGUCf324-SjEihCoL8mQFq80PSsaAZl5~EBOKRUx14FOprizMusaYN0K06E~fjDIDbM2Rmc9Xjg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA",
|
||||
company: "Synonym",
|
||||
twitter: "@BitcoinErrorLog"
|
||||
},
|
||||
{
|
||||
name: "Nifty Nei",
|
||||
avatar: "https://s3-alpha-sig.figma.com/img/5e65/c22c/673b8f74ac43f024b036dbc4e6479e0d?Expires=1662940800&Signature=GR54s7FBcLGcPTVclWdmPjzU92tyrYpdUbbDUYKMUkdQbxq2yQlUhZ-AOLDHhOPY4P2G3aW2yT16b1AUbC8RBx1boH25MSrH-jpn6X57IJA-4ZeHP8zCo-yjTLpb8Gn~vudIi9rPfbwJ34stp-VeOAYMuOHlah3YO-B4MBsBv-NqhP7BMY4zz9vGdBLZhOjYQYdLZ2494Ae6L5FpD1ah3WD3U5qUN9dDvYvAtqYfhQeBOnsG6PfYoq8LouCuERC4S26BeooPg8UdGUCf324-SjEihCoL8mQFq80PSsaAZl5~EBOKRUx14FOprizMusaYN0K06E~fjDIDbM2Rmc9Xjg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA",
|
||||
company: "Blockstream",
|
||||
twitter: "@niftynei"
|
||||
},
|
||||
{
|
||||
name: "Oleg Mikhalsky",
|
||||
avatar: "https://s3-alpha-sig.figma.com/img/5e65/c22c/673b8f74ac43f024b036dbc4e6479e0d?Expires=1662940800&Signature=GR54s7FBcLGcPTVclWdmPjzU92tyrYpdUbbDUYKMUkdQbxq2yQlUhZ-AOLDHhOPY4P2G3aW2yT16b1AUbC8RBx1boH25MSrH-jpn6X57IJA-4ZeHP8zCo-yjTLpb8Gn~vudIi9rPfbwJ34stp-VeOAYMuOHlah3YO-B4MBsBv-NqhP7BMY4zz9vGdBLZhOjYQYdLZ2494Ae6L5FpD1ah3WD3U5qUN9dDvYvAtqYfhQeBOnsG6PfYoq8LouCuERC4S26BeooPg8UdGUCf324-SjEihCoL8mQFq80PSsaAZl5~EBOKRUx14FOprizMusaYN0K06E~fjDIDbM2Rmc9Xjg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA",
|
||||
company: "Fulgur Ventures",
|
||||
twitter: "@olegmikh1"
|
||||
},
|
||||
{
|
||||
name: "Alyse Kileen",
|
||||
avatar: "https://s3-alpha-sig.figma.com/img/5e65/c22c/673b8f74ac43f024b036dbc4e6479e0d?Expires=1662940800&Signature=GR54s7FBcLGcPTVclWdmPjzU92tyrYpdUbbDUYKMUkdQbxq2yQlUhZ-AOLDHhOPY4P2G3aW2yT16b1AUbC8RBx1boH25MSrH-jpn6X57IJA-4ZeHP8zCo-yjTLpb8Gn~vudIi9rPfbwJ34stp-VeOAYMuOHlah3YO-B4MBsBv-NqhP7BMY4zz9vGdBLZhOjYQYdLZ2494Ae6L5FpD1ah3WD3U5qUN9dDvYvAtqYfhQeBOnsG6PfYoq8LouCuERC4S26BeooPg8UdGUCf324-SjEihCoL8mQFq80PSsaAZl5~EBOKRUx14FOprizMusaYN0K06E~fjDIDbM2Rmc9Xjg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA",
|
||||
company: "Stillmark VC",
|
||||
twitter: "@AlyseKilleen"
|
||||
},
|
||||
{
|
||||
name: "Johns Beharry",
|
||||
avatar: "https://s3-alpha-sig.figma.com/img/5e65/c22c/673b8f74ac43f024b036dbc4e6479e0d?Expires=1662940800&Signature=GR54s7FBcLGcPTVclWdmPjzU92tyrYpdUbbDUYKMUkdQbxq2yQlUhZ-AOLDHhOPY4P2G3aW2yT16b1AUbC8RBx1boH25MSrH-jpn6X57IJA-4ZeHP8zCo-yjTLpb8Gn~vudIi9rPfbwJ34stp-VeOAYMuOHlah3YO-B4MBsBv-NqhP7BMY4zz9vGdBLZhOjYQYdLZ2494Ae6L5FpD1ah3WD3U5qUN9dDvYvAtqYfhQeBOnsG6PfYoq8LouCuERC4S26BeooPg8UdGUCf324-SjEihCoL8mQFq80PSsaAZl5~EBOKRUx14FOprizMusaYN0K06E~fjDIDbM2Rmc9Xjg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA",
|
||||
company: "Peak Shift",
|
||||
twitter: "@johnsBeharry"
|
||||
},
|
||||
{
|
||||
name: "Ben Price",
|
||||
avatar: "https://s3-alpha-sig.figma.com/img/5e65/c22c/673b8f74ac43f024b036dbc4e6479e0d?Expires=1662940800&Signature=GR54s7FBcLGcPTVclWdmPjzU92tyrYpdUbbDUYKMUkdQbxq2yQlUhZ-AOLDHhOPY4P2G3aW2yT16b1AUbC8RBx1boH25MSrH-jpn6X57IJA-4ZeHP8zCo-yjTLpb8Gn~vudIi9rPfbwJ34stp-VeOAYMuOHlah3YO-B4MBsBv-NqhP7BMY4zz9vGdBLZhOjYQYdLZ2494Ae6L5FpD1ah3WD3U5qUN9dDvYvAtqYfhQeBOnsG6PfYoq8LouCuERC4S26BeooPg8UdGUCf324-SjEihCoL8mQFq80PSsaAZl5~EBOKRUx14FOprizMusaYN0K06E~fjDIDbM2Rmc9Xjg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA",
|
||||
company: "The Bitcoin Company",
|
||||
twitter: "@abitcoinperson"
|
||||
},
|
||||
],
|
||||
|
||||
faqs: [
|
||||
{
|
||||
question: "When does the tournament start and end?",
|
||||
answer:
|
||||
`The tournament starts when team and project registrations open on 12th October. The tournament will finish with submissions closing on 30th November, 2022. Judges will then score projects and announce the winners on the 12th December.`
|
||||
},
|
||||
{
|
||||
question: "When and how do we register our projects?",
|
||||
answer:
|
||||
`Makers can register their projects anytime between 12th October - 30th November. If a project is added on the tournament page, it is automatically registered and it will be judged at the end of the tournament.`
|
||||
},
|
||||
{
|
||||
question: "How will projects be judged?",
|
||||
answer:
|
||||
`Projects will be judged and scored on the following criteria:
|
||||
|
||||
**1). 🎯 Value Proposition**
|
||||
Does the project have a product market fit? Does it provide value to the bitcoin ecosystem and beyond?
|
||||
|
||||
**2). 🚨 Innovation**
|
||||
Is it something we've seen before or does it bring something new and exciting to bitcoin and beyond?
|
||||
|
||||
**3). 👁️ Transparency (#BuildInPublic)**
|
||||
Encouraging makers to #BuildInPublic. Has the project’s team been transparent throughout their product design and development journey?
|
||||
|
||||
**4). ✅ Execution**
|
||||
Makers should focus on attention to detail. How well has the project been executed?
|
||||
|
||||
**5). 🍒 UIUX Design**
|
||||
Design can separate the good from the bad. Taking into account both UI and UX, how well has the application or feature been designed?
|
||||
|
||||
**6). 🔥 Je ne sais quoi**
|
||||
Does the project have that extra level of pizazz or coolness? Does it raise the bar?`
|
||||
},
|
||||
{
|
||||
question: "Can I submit a project that I hacked on during another event?",
|
||||
answer:
|
||||
`Makers can submit their projects from other hackathons, events, and meetups that are registered as events within The Long Night tournament. This allows makers to take advantage of IRL + online meetups, workshops, hackerspaces, inspirational weekend events, and more.`
|
||||
},
|
||||
{
|
||||
question: "Can I submit multiple projects?",
|
||||
answer:
|
||||
`Yes, makers can submit multiple projects. However we encourage makers to focus on quality rather than quantity.`
|
||||
},
|
||||
{
|
||||
question: "How can I find other makers or projects to team up with?",
|
||||
answer:
|
||||
`You can see a list of makers who are open to connect in the tournament’s Makers tab. You can also search for projects that are looking to recruit members.`
|
||||
},
|
||||
{
|
||||
question: "This is my first time hacking on bitcoin, is there any help?",
|
||||
answer:
|
||||
`We collected some awesome design, development, and project management resources here to get you up and running. You can also watch workshops and tutorials from BOLT🔩FUN’s previous ShockTheWeb⚡hackathons here.`
|
||||
},
|
||||
{
|
||||
question: "Not sure what to hack on?",
|
||||
answer:
|
||||
`Not sure where to get started? Need an idea to hack on? Not to worry, we’ve collected a list of great project ideas for you to look at here.`
|
||||
},
|
||||
{
|
||||
question: "How can I #BuildInPublic?",
|
||||
answer:
|
||||
`Using BOLT🔩FUN Stories ✍️, makers can transparently document their project’s design, development, and management processes. This will help other makers learn from one another, decreasing essential onboarding and learning time, whilst inspiring more great bitcoin apps to be built and innovated on. To see an example of this type of transparent reporting, check out this story here.`
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
module.exports = { tournament };
|
||||
@@ -3,6 +3,7 @@ const { generatePrivateKey, getPublicKey } = require("../../api/utils/nostr-tool
|
||||
const { categories, projects, tags, hackathons, roles, skills } = require("./data");
|
||||
const Chance = require('chance');
|
||||
const { getCoverImage, randomItems, random } = require("./helpers");
|
||||
const { tournament: tournamentMock } = require("./data/tournament.seed");
|
||||
|
||||
const chance = new Chance();
|
||||
|
||||
@@ -64,6 +65,8 @@ async function main() {
|
||||
|
||||
// await createSkills();
|
||||
|
||||
await createTournament();
|
||||
|
||||
}
|
||||
|
||||
async function createCategories() {
|
||||
@@ -194,6 +197,55 @@ async function createSkills() {
|
||||
})
|
||||
}
|
||||
|
||||
async function createTournament() {
|
||||
console.log("Creating Tournament");
|
||||
|
||||
await prisma.tournamentFAQ.createMany({
|
||||
|
||||
data: tournamentMock.faqs.map(i => ({
|
||||
tournament_id: 1,
|
||||
question: i.question,
|
||||
answer: i.answer
|
||||
}))
|
||||
})
|
||||
return
|
||||
const createdTournament = await prisma.tournament.create({
|
||||
data: {
|
||||
title: tournamentMock.title,
|
||||
description: tournamentMock.description,
|
||||
start_date: tournamentMock.start_date,
|
||||
end_date: tournamentMock.end_date,
|
||||
thumbnail_image: tournamentMock.thumbnail_image,
|
||||
cover_image: tournamentMock.cover_image,
|
||||
location: tournamentMock.location,
|
||||
website: tournamentMock.website,
|
||||
|
||||
faqs: {
|
||||
createMany: {
|
||||
data: tournamentMock.faqs.map(f => ({ question: f.question, answer: f.answer }))
|
||||
}
|
||||
},
|
||||
prizes: {
|
||||
createMany: {
|
||||
data: tournamentMock.prizes.map(p => ({ title: p.title, image: p.image, amount: p.amount }))
|
||||
}
|
||||
},
|
||||
judges: {
|
||||
createMany: {
|
||||
data: tournamentMock.judges.map(j => ({ name: j.name, company: j.company, twitter: j.twitter, avatar: j.avatar }))
|
||||
}
|
||||
},
|
||||
events: {
|
||||
createMany: {
|
||||
data: tournamentMock.events.map(e => ({ title: e.title, description: e.description, starts_at: e.starts_at, ends_at: e.ends_at, image: e.image, location: e.location, type: e.type, website: e.website }))
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
|
||||
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.8 KiB |
@@ -25,6 +25,8 @@ const ExplorePage = Loadable(React.lazy(() => import( /* webpackChunkName: "expl
|
||||
|
||||
const HackathonsPage = Loadable(React.lazy(() => import( /* webpackChunkName: "hackathons_page" */ "./features/Hackathons/pages/HackathonsPage/HackathonsPage")))
|
||||
|
||||
const TournamentDetailsPage = Loadable(React.lazy(() => import( /* webpackChunkName: "hackathons_page" */ "./features/Tournaments/pages/TournamentDetailsPage/TournamentDetailsPage")))
|
||||
|
||||
const DonatePage = Loadable(React.lazy(() => import( /* webpackChunkName: "donate_page" */ "./features/Donations/pages/DonatePage/DonatePage")))
|
||||
const LoginPage = Loadable(React.lazy(() => import( /* webpackChunkName: "login_page" */ "./features/Auth/pages/LoginPage/LoginPage")))
|
||||
const LogoutPage = Loadable(React.lazy(() => import( /* webpackChunkName: "logout_page" */ "./features/Auth/pages/LogoutPage/LogoutPage")))
|
||||
@@ -103,6 +105,8 @@ function App() {
|
||||
|
||||
<Route path={PAGES_ROUTES.hackathons.default} element={<HackathonsPage />} />
|
||||
|
||||
<Route path={PAGES_ROUTES.tournament.byId} element={<TournamentDetailsPage />} />
|
||||
|
||||
<Route path={PAGES_ROUTES.donate.default} element={<DonatePage />} />
|
||||
|
||||
<Route path={PAGES_ROUTES.profile.editProfile} element={<EditProfilePage />} />
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Button from 'src/Components/Button/Button'
|
||||
import Logo from './fulgur_logo.svg'
|
||||
import Lines from './lines-h.png'
|
||||
|
||||
|
||||
@@ -8,7 +7,7 @@ export default function Fulgur() {
|
||||
<div className='p-24 pt-48 bg-black rounded-16 relative'>
|
||||
<img src={Lines} alt="" className='w-full absolute top-0 left-0' />
|
||||
<div className="flex flex-col gap-24 relative">
|
||||
<img src={Logo} alt="Fulgur Ventures Logo" className='w-10/12 max-w-[230px] ' />
|
||||
<img src={'/assets/images/logos/fulgur_logo.svg'} alt="Fulgur Ventures Logo" className='w-10/12 max-w-[230px] ' />
|
||||
<div>
|
||||
<h3 className="text-white font-bolder text-body1">Turn your hackathon project into a startup</h3>
|
||||
<p className="text-white font-medium mt-8 text-body4">Schedule an office hour with Fulgur Ventures</p>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { TailSpin } from 'react-loader-spinner';
|
||||
|
||||
type Props = {
|
||||
color?: 'primary' | 'red' | 'white' | 'gray' | "black" | 'none',
|
||||
variant?: 'fill' | 'outline'
|
||||
variant?: 'fill' | 'outline' | 'text'
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
children: ReactNode;
|
||||
href?: string;
|
||||
@@ -40,7 +40,7 @@ const loadingColor: UnionToObjectKeys<Props, 'color'> = {
|
||||
const btnStylesOutline: UnionToObjectKeys<Props, 'color'> = {
|
||||
none: "",
|
||||
primary: "text-primary-600",
|
||||
gray: 'text-gray-700',
|
||||
gray: 'text-gray-600',
|
||||
white: 'text-gray-900',
|
||||
black: 'text-black',
|
||||
red: "text-red-500",
|
||||
@@ -48,7 +48,8 @@ const btnStylesOutline: UnionToObjectKeys<Props, 'color'> = {
|
||||
|
||||
const baseBtnStyles: UnionToObjectKeys<Props, 'variant'> = {
|
||||
fill: "transition-transform active:scale-95",
|
||||
outline: "transition-transform bg-gray-900 bg-opacity-0 hover:bg-opacity-5 active:bg-opacity-10 border border-gray-200 active:scale-95 "
|
||||
outline: "transition-transform bg-gray-900 bg-opacity-0 hover:bg-opacity-5 active:bg-opacity-10 border border-gray-200 active:scale-95 ",
|
||||
text: "transition-transform active:scale-95 hover:bg-gray-100 active:bg-gray-50"
|
||||
}
|
||||
|
||||
|
||||
@@ -88,7 +89,7 @@ const Button = React.forwardRef<any, Props>(({ color = 'white',
|
||||
${variant === 'fill' ? btnStylesFill[color] : btnStylesOutline[color]}
|
||||
${isLoading && disableOnLoading && 'bg-opacity-70 pointer-events-none'}
|
||||
${fullWidth && 'w-full'}
|
||||
${disabled && 'opacity-40 pointer-events-none'}
|
||||
${disabled && 'opacity-90 pointer-events-none'}
|
||||
`;
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ interface Props {
|
||||
className?: string
|
||||
size?: "sm" | 'md' | 'lg'
|
||||
variant?: 'blank' | 'fill'
|
||||
isDisabled?: boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +33,8 @@ const IconButton = React.forwardRef<any, PropsWithChildren<Props>>(({
|
||||
children,
|
||||
onClick = () => { },
|
||||
onKeyDown,
|
||||
variant = 'blank'
|
||||
variant = 'blank',
|
||||
isDisabled,
|
||||
}, ref) => {
|
||||
|
||||
if (href)
|
||||
@@ -57,12 +59,16 @@ const IconButton = React.forwardRef<any, PropsWithChildren<Props>>(({
|
||||
ref={ref}
|
||||
type='button'
|
||||
className={`
|
||||
${className}
|
||||
${sizeToPadding[size]}
|
||||
${baseBtnStyles[variant]}
|
||||
active:scale-95 rounded-full ${className}`}
|
||||
active:scale-95 rounded-full
|
||||
${isDisabled && "opacity-60"}
|
||||
`}
|
||||
style={{ lineHeight: 0 }}
|
||||
onClick={onClick}
|
||||
onKeyDown={onKeyDown}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { PropsWithChildren } from 'react'
|
||||
|
||||
interface Props {
|
||||
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function InfoCard(props: PropsWithChildren<Props>) {
|
||||
return (
|
||||
<div className="bg-gray-50 p-16 rounded-12 border border-gray-200 mt-24">
|
||||
<div className={`bg-gray-50 p-16 rounded-12 border border-gray-200 ${props.className}`}>
|
||||
<p className="text-body5 text-gray-600">
|
||||
{props.children}
|
||||
</p>
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { useWatch } from 'react-hook-form';
|
||||
import { WrapForm } from 'src/utils/storybook/decorators';
|
||||
|
||||
import Autocomplete from './Autocomplete';
|
||||
|
||||
export default {
|
||||
title: 'Shared/Inputs/AutoComplete',
|
||||
component: Autocomplete,
|
||||
decorators: [WrapForm({
|
||||
defaultValues: {
|
||||
autocomplete: null
|
||||
}
|
||||
})],
|
||||
|
||||
} as ComponentMeta<typeof Autocomplete>;
|
||||
|
||||
const options = [
|
||||
{
|
||||
"id": "20f0eb8d-c0cd-4e12-8a08-0d9846fc8704",
|
||||
"name": "Nichole Bailey",
|
||||
"username": "Cassie14",
|
||||
"email": "Daisy_Auer50@hotmail.com",
|
||||
"address": {
|
||||
"street": "Anastasia Tunnel",
|
||||
"suite": 95587,
|
||||
"city": "Port Casperview",
|
||||
"zipcode": "04167-6996",
|
||||
"geo": {
|
||||
"lat": "-73.4727",
|
||||
"lng": "-142.9435"
|
||||
}
|
||||
},
|
||||
"phone": "324-615-9195 x5902",
|
||||
"website": "ron.net",
|
||||
"company": {
|
||||
"name": "Roberts, Tremblay and Christiansen",
|
||||
"catchPhrase": "Vision-oriented actuating access",
|
||||
"bs": "bricks-and-clicks strategize portals"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "62b70f76-85ba-4241-9ffd-07582008c497",
|
||||
"name": "Robert Blick",
|
||||
"username": "Madilyn93",
|
||||
"email": "Ronaldo82@gmail.com",
|
||||
"address": {
|
||||
"street": "Charlie Plain",
|
||||
"suite": 83070,
|
||||
"city": "Lake Bonitaland",
|
||||
"zipcode": "01109",
|
||||
"geo": {
|
||||
"lat": "50.0971",
|
||||
"lng": "-2.3057"
|
||||
}
|
||||
},
|
||||
"phone": "1-541-367-2047 x9006",
|
||||
"website": "jovani.com",
|
||||
"company": {
|
||||
"name": "Parisian - Kling",
|
||||
"catchPhrase": "Multi-tiered tertiary toolset",
|
||||
"bs": "plug-and-play benchmark content"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "d02f74d9-bf99-4e41-b678-15e903abc1b3",
|
||||
"name": "Eli O'Kon",
|
||||
"username": "Rosario.Davis",
|
||||
"email": "Mckayla59@hotmail.com",
|
||||
"address": {
|
||||
"street": "Wilford Drive",
|
||||
"suite": 69742,
|
||||
"city": "North Dianna",
|
||||
"zipcode": "80620",
|
||||
"geo": {
|
||||
"lat": "-61.4191",
|
||||
"lng": "126.7878"
|
||||
}
|
||||
},
|
||||
"phone": "(339) 709-4080",
|
||||
"website": "clay.name",
|
||||
"company": {
|
||||
"name": "Gerlach - Metz",
|
||||
"catchPhrase": "Pre-emptive user-facing service-desk",
|
||||
"bs": "frictionless monetize markets"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "21077fa6-6a53-4b84-8407-6cd949718945",
|
||||
"name": "Marilie Feil",
|
||||
"username": "Antwon.Carter92",
|
||||
"email": "Demario.Hyatt20@yahoo.com",
|
||||
"address": {
|
||||
"street": "Kenton Spurs",
|
||||
"suite": 20079,
|
||||
"city": "Beahanberg",
|
||||
"zipcode": "79385",
|
||||
"geo": {
|
||||
"lat": "-70.7199",
|
||||
"lng": "4.6977"
|
||||
}
|
||||
},
|
||||
"phone": "608.750.4947",
|
||||
"website": "jacynthe.org",
|
||||
"company": {
|
||||
"name": "Kuhn and Sons",
|
||||
"catchPhrase": "Total eco-centric matrices",
|
||||
"bs": "out-of-the-box target communities"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "e07cf1b4-ff43-4c4a-a670-fd7417d6bbaf",
|
||||
"name": "Ella Pagac",
|
||||
"username": "Damien.Jaskolski",
|
||||
"email": "Delmer1@gmail.com",
|
||||
"address": {
|
||||
"street": "VonRueden Shoals",
|
||||
"suite": 14035,
|
||||
"city": "Starkmouth",
|
||||
"zipcode": "72448-1915",
|
||||
"geo": {
|
||||
"lat": "55.2157",
|
||||
"lng": "98.0822"
|
||||
}
|
||||
},
|
||||
"phone": "(165) 247-5332 x71067",
|
||||
"website": "chad.info",
|
||||
"company": {
|
||||
"name": "Nicolas, Doyle and Rempel",
|
||||
"catchPhrase": "Adaptive real-time strategy",
|
||||
"bs": "innovative whiteboard supply-chains"
|
||||
}
|
||||
}
|
||||
]
|
||||
const Template: ComponentStory<typeof Autocomplete> = (args) => {
|
||||
|
||||
const value = useWatch({ name: 'autocomplete' })
|
||||
|
||||
console.log(value);
|
||||
|
||||
|
||||
return <Autocomplete
|
||||
options={options}
|
||||
labelField='name'
|
||||
valueField='name'
|
||||
{...args as any}
|
||||
/>
|
||||
}
|
||||
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
onChange: console.log
|
||||
}
|
||||
|
||||
|
||||
export const Lodaing = Template.bind({});
|
||||
Lodaing.args = {
|
||||
isLoading: true
|
||||
}
|
||||
|
||||
export const Clearable = Template.bind({});
|
||||
Clearable.args = {
|
||||
isClearable: true
|
||||
}
|
||||
|
||||
export const MultipleAllowed = Template.bind({});
|
||||
MultipleAllowed.args = {
|
||||
isMulti: true
|
||||
}
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
|
||||
import { useMemo } from "react";
|
||||
import Select, { StylesConfig } from "react-select";
|
||||
import { ControlledStateHandler } from "src/utils/interfaces";
|
||||
|
||||
|
||||
type Props<T extends object | string, IsMulti extends boolean = false> = {
|
||||
options: T[];
|
||||
labelField?: keyof T
|
||||
valueField?: keyof T
|
||||
placeholder?: string
|
||||
disabled?: boolean
|
||||
isLoading?: boolean;
|
||||
isClearable?: boolean;
|
||||
control?: any,
|
||||
name?: string,
|
||||
className?: string,
|
||||
onBlur?: () => void;
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
} & ControlledStateHandler<T, IsMulti>
|
||||
|
||||
|
||||
|
||||
|
||||
export default function AutoComplete<T extends object, IsMulti extends boolean>({
|
||||
options,
|
||||
labelField,
|
||||
valueField,
|
||||
placeholder = "Select Option...",
|
||||
isMulti,
|
||||
isClearable,
|
||||
disabled,
|
||||
className,
|
||||
value,
|
||||
onChange,
|
||||
onBlur,
|
||||
size = 'md',
|
||||
...props
|
||||
}: Props<T, IsMulti>) {
|
||||
|
||||
|
||||
const colourStyles: StylesConfig = useMemo(() => ({
|
||||
control: (styles, state) => ({
|
||||
...styles,
|
||||
padding: size === 'md' ? '1px 4px' : '8px 12px',
|
||||
borderRadius: size === 'md' ? 8 : 12,
|
||||
}),
|
||||
indicatorSeparator: (styles, state) => ({
|
||||
...styles,
|
||||
display: "none"
|
||||
}),
|
||||
input: (styles, state) => ({
|
||||
...styles,
|
||||
" input": {
|
||||
boxShadow: 'none !important'
|
||||
},
|
||||
}),
|
||||
}), [size])
|
||||
|
||||
return (
|
||||
<div className='w-full'>
|
||||
<Select
|
||||
options={options}
|
||||
|
||||
placeholder={placeholder}
|
||||
className={className}
|
||||
isMulti={isMulti}
|
||||
isClearable={isClearable}
|
||||
isLoading={props.isLoading}
|
||||
|
||||
getOptionLabel={o => o[labelField]}
|
||||
getOptionValue={o => o[valueField]}
|
||||
|
||||
value={value as any}
|
||||
onChange={v => onChange?.(v as any)}
|
||||
onBlur={onBlur}
|
||||
|
||||
|
||||
styles={colourStyles}
|
||||
theme={(theme) => ({
|
||||
...theme,
|
||||
borderRadius: 8,
|
||||
colors: {
|
||||
...theme.colors,
|
||||
primary: 'var(--primary)',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { MdOutlineKingBed } from 'react-icons/md';
|
||||
|
||||
import BasicSelectInput from './BasicSelectInput';
|
||||
|
||||
export default {
|
||||
title: 'Shared/Inputs/Select/Basic',
|
||||
component: BasicSelectInput,
|
||||
|
||||
} as ComponentMeta<typeof BasicSelectInput>;
|
||||
|
||||
const Template: ComponentStory<typeof BasicSelectInput> = (args) => <BasicSelectInput {...args} />
|
||||
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
// defaultValue: 4,
|
||||
options: [
|
||||
{ value: 1, label: "Option 1" },
|
||||
{ value: 2, label: 'Option 2' },
|
||||
{ value: 3, label: 'Option 3' },
|
||||
{ value: 4, label: 'Option 4' },
|
||||
],
|
||||
onChange: (nv) => alert("New value is: " + nv),
|
||||
labelField: 'label',
|
||||
valueField: "value"
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
|
||||
import { useCallback } from "react";
|
||||
import Select, { OnChangeValue, StylesConfig, components, OptionProps } from "react-select";
|
||||
import { ControlledStateHandler } from "src/utils/interfaces";
|
||||
|
||||
|
||||
|
||||
type Props<T extends Record<string, any>, IsMulti extends boolean = boolean> = {
|
||||
options: T[];
|
||||
labelField: keyof T
|
||||
valueField: keyof T
|
||||
placeholder?: string
|
||||
disabled?: boolean
|
||||
isLoading?: boolean;
|
||||
isClearable?: boolean;
|
||||
control?: any,
|
||||
name?: string,
|
||||
className?: string,
|
||||
renderOption?: (option: OptionProps<T>) => JSX.Element
|
||||
} & ControlledStateHandler<T, IsMulti>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const selectCustomStyle: StylesConfig = ({
|
||||
control: (styles, state) => ({
|
||||
...styles,
|
||||
padding: '5px 12px',
|
||||
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'
|
||||
}
|
||||
}),
|
||||
indicatorSeparator: (styles, state) => ({
|
||||
...styles,
|
||||
display: "none"
|
||||
}),
|
||||
input: (styles, state) => ({
|
||||
...styles,
|
||||
" input": {
|
||||
boxShadow: 'none !important'
|
||||
},
|
||||
}),
|
||||
menu: (styles, state) => ({
|
||||
...styles,
|
||||
padding: 8,
|
||||
borderRadius: "16px !important"
|
||||
}),
|
||||
})
|
||||
|
||||
|
||||
|
||||
export default function BasicSelectInput<T extends Record<string, any>, IsMulti extends boolean>({
|
||||
options,
|
||||
labelField,
|
||||
valueField,
|
||||
placeholder = "Select Option...",
|
||||
isMulti,
|
||||
isClearable,
|
||||
disabled,
|
||||
className,
|
||||
value,
|
||||
onChange,
|
||||
onBlur,
|
||||
renderOption,
|
||||
...props
|
||||
}: Props<T, IsMulti>) {
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className='w-full'>
|
||||
<Select
|
||||
options={options}
|
||||
placeholder={placeholder}
|
||||
className={className}
|
||||
isMulti={isMulti}
|
||||
isClearable={isClearable}
|
||||
isLoading={props.isLoading}
|
||||
|
||||
getOptionLabel={o => o[labelField]}
|
||||
getOptionValue={o => o[valueField]}
|
||||
|
||||
value={value as any}
|
||||
onChange={v => onChange?.(v as any)}
|
||||
onBlur={onBlur}
|
||||
components={{
|
||||
Option: getOptionComponent(renderOption, labelField),
|
||||
// ValueContainer: CustomValueContainer
|
||||
}}
|
||||
|
||||
styles={selectCustomStyle as any}
|
||||
theme={(theme) => ({
|
||||
...theme,
|
||||
borderRadius: 8,
|
||||
colors: {
|
||||
...theme.colors,
|
||||
primary: 'var(--primary)',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
function getOptionComponent<T extends Record<string, any>>(renderOption: Props<T>['renderOption'], labelField: Props<T>['labelField']) {
|
||||
const _render = renderOption ?? ((option) => <div
|
||||
className={`
|
||||
flex gap-16 my-4 px-16 py-12 rounded-12 text-gray-800 cursor-pointer
|
||||
${!(option.isSelected || option.isFocused) ?
|
||||
"hover:bg-gray-50"
|
||||
:
|
||||
option.isSelected ? "bg-gray-100 text-gray-800" : "bg-gray-50"
|
||||
}
|
||||
`}>
|
||||
{option.data[labelField]}
|
||||
</div>)
|
||||
|
||||
return function OptionComponent(props: OptionProps<T>) {
|
||||
return (
|
||||
<components.Option {...props} className='!p-0 !bg-transparent hover:!bg-transparent'>
|
||||
{_render(props)}
|
||||
</components.Option>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -17,7 +17,11 @@ ReactModal.setAppElement('#root');
|
||||
|
||||
export default function Modal({ onClose, children, ...props }: Props) {
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const onAfterClose = () => {
|
||||
dispatch(removeClosedModal(props.id))
|
||||
}
|
||||
|
||||
return <ReactModal
|
||||
isOpen={props.isOpen}
|
||||
@@ -25,7 +29,7 @@ export default function Modal({ onClose, children, ...props }: Props) {
|
||||
overlayClassName='fixed w-full inset-0 overflow-x-hidden z-[2020] no-scrollbar'
|
||||
className=' '
|
||||
closeTimeoutMS={1000}
|
||||
onAfterClose={() => dispatch(removeClosedModal(props.id))}
|
||||
onAfterClose={onAfterClose}
|
||||
contentElement={(_props, children) => <div {..._props} className={`${_props.className} w-screen min-h-screen relative flex flex-col justify-center items-center md:py-64 md:px-16 inset-0`}>
|
||||
<div
|
||||
onClick={onClose}
|
||||
|
||||
@@ -71,10 +71,9 @@ export default function ModalsContainer() {
|
||||
<div className="z-[2020]">
|
||||
{openModals.map((modal, idx) => {
|
||||
const Child = ALL_MODALS[modal.Modal];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
key={idx}
|
||||
key={modal.id}
|
||||
isOpen={modal.isOpen}
|
||||
onClose={onClose}
|
||||
direction={direction}
|
||||
|
||||
@@ -237,6 +237,7 @@ export default function NavDesktop() {
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -18,7 +18,7 @@ interface Particle {
|
||||
offsetY: number,
|
||||
color: string
|
||||
animation: 'fly-spark-1' | 'fly-spark-2',
|
||||
animationSpeed: 1 | 2 | 3,
|
||||
animationSpeed: number,
|
||||
scale: number
|
||||
}
|
||||
|
||||
@@ -108,11 +108,11 @@ export default function VoteButton({
|
||||
id: (Math.random() + 1).toString(),
|
||||
offsetX: random(-10, 99),
|
||||
offsetY: random(10, 90),
|
||||
animation: randomItem(styles.fly_spark_1, styles.fly_spark_1),
|
||||
animation: randomItem(styles.fly_spark_1, styles.fly_spark_1) as any,
|
||||
animationSpeed: randomItem(1, 1.5, 2),
|
||||
color: `hsl(0deg 86% ${random(50, 63)}%)`,
|
||||
scale: random(1, 1.5)
|
||||
}))
|
||||
} as const))
|
||||
|
||||
// if on mobile screen, reduce number of sparks particles to 60%
|
||||
setSparks(oldSparks => [...oldSparks, ...newSparks])
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import { useCallback, useEffect, useRef, useState } from "react"
|
||||
import { Helmet } from "react-helmet";
|
||||
import { Grid } from "react-loader-spinner";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
@@ -21,7 +21,7 @@ const fetchLnurlAuth = async () => {
|
||||
return data;
|
||||
}
|
||||
|
||||
const useLnurlQuery = () => {
|
||||
export const useLnurlQuery = () => {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<any>(null);
|
||||
const [data, setData] = useState<{ lnurl: string, session_token: string }>({ lnurl: '', session_token: '' })
|
||||
@@ -62,6 +62,7 @@ export default function LoginPage() {
|
||||
const location = useLocation();
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const canFetchIsLogged = useRef(true)
|
||||
const { loadingLnurl, data: { lnurl, session_token }, error } = useLnurlQuery();
|
||||
const clipboard = useCopyToClipboard()
|
||||
|
||||
@@ -96,18 +97,26 @@ export default function LoginPage() {
|
||||
const startPolling = useCallback(
|
||||
() => {
|
||||
const interval = setInterval(() => {
|
||||
if (canFetchIsLogged.current === false) return;
|
||||
|
||||
canFetchIsLogged.current = false;
|
||||
fetch(CONSTS.apiEndpoint + '/is-logged-in', {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
session_token
|
||||
}
|
||||
}).then(data => data.json())
|
||||
})
|
||||
.then(data => data.json())
|
||||
.then(data => {
|
||||
if (data.logged_in) {
|
||||
clearInterval(interval)
|
||||
refetch();
|
||||
}
|
||||
})
|
||||
.catch()
|
||||
.finally(() => {
|
||||
canFetchIsLogged.current = true;
|
||||
})
|
||||
}, 2000);
|
||||
|
||||
return interval;
|
||||
@@ -123,6 +132,7 @@ export default function LoginPage() {
|
||||
interval = startPolling();
|
||||
|
||||
return () => {
|
||||
canFetchIsLogged.current = true;
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, [lnurl, startPolling])
|
||||
|
||||
@@ -19,6 +19,7 @@ export default function HackathonsList(props: Props) {
|
||||
<HackathonCardSkeleton />
|
||||
<HackathonCardSkeleton />
|
||||
<HackathonCardSkeleton />
|
||||
<HackathonCardSkeleton />
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useState } from 'react'
|
||||
import AutoComplete from 'src/Components/Inputs/Autocomplete/Autocomplete';
|
||||
import { useMediaQuery } from 'src/utils/hooks';
|
||||
import { MEDIA_QUERIES } from 'src/utils/theme';
|
||||
|
||||
|
||||
@@ -7,6 +7,10 @@ import SortByFilter from '../../Components/SortByFilter/SortByFilter'
|
||||
import styles from './styles.module.scss'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { Fulgur } from 'src/Components/Ads/Fulgur'
|
||||
import { IoLocationOutline } from 'react-icons/io5'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { createRoute } from 'src/utils/routing'
|
||||
import { bannerData } from 'src/features/Projects/pages/ExplorePage/Header/Header'
|
||||
|
||||
|
||||
export default function HackathonsPage() {
|
||||
@@ -30,17 +34,36 @@ export default function HackathonsPage() {
|
||||
<div
|
||||
className={`page-container`}
|
||||
>
|
||||
<div className={`pt-16 w-full ${styles.grid}`}>
|
||||
<aside className='no-scrollbar'>
|
||||
<div className={`w-full`}>
|
||||
<div className="rounded-16 min-h-[280px] relative overflow-hidden p-16 md:p-24 flex flex-col items-start justify-end">
|
||||
<img
|
||||
className="w-full h-full object-cover object-top absolute top-0 left-0 z-[-2]"
|
||||
src={bannerData.img}
|
||||
alt=""
|
||||
/>
|
||||
<div className="w-full h-full object-cover bg-black bg-opacity-60 absolute top-0 left-0 z-[-1]"></div>
|
||||
<div className="max-w-[90%]">
|
||||
{bannerData.title}
|
||||
</div>
|
||||
|
||||
<Button href={bannerData.link.url} color="white" className="mt-24">
|
||||
{bannerData.link.content}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex gap-16 flex-wrap my-24 justify-between">
|
||||
<h1 id='title' className="text-body1 lg:text-h2 font-bolder">Hackathons 🏆</h1>
|
||||
<div className="self-center">
|
||||
<SortByFilter
|
||||
filterChanged={setSortByFilter}
|
||||
/></div>
|
||||
</div>
|
||||
{/* <aside className='no-scrollbar'>
|
||||
<div className="flex flex-col gap-24 md:overflow-y-scroll sticky-side-element">
|
||||
<h1 id='title' className="text-body1 lg:text-h2 font-bolder">Hackathons 🏆</h1>
|
||||
<SortByFilter
|
||||
filterChanged={setSortByFilter}
|
||||
/>
|
||||
{/* <TopicsFilter
|
||||
filterChanged={setTopicsFilter}
|
||||
/> */}
|
||||
<Button
|
||||
/>
|
||||
<Button
|
||||
href='https://airtable.com/shrgXKynON8YWeyyE'
|
||||
newTab
|
||||
color='primary'
|
||||
@@ -52,7 +75,7 @@ export default function HackathonsPage() {
|
||||
<Fulgur />
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</aside> */}
|
||||
<main className="self-start">
|
||||
<HackathonsList
|
||||
currentFilter={sortByFilter}
|
||||
|
||||
@@ -32,7 +32,6 @@ export default function PopularTagsFilter({ value, onChange }: Props) {
|
||||
<div className='overflow-hidden'>
|
||||
{isMdScreen ?
|
||||
<Card>
|
||||
|
||||
<p className="text-body2 font-bolder text-black mb-16">Popular Tags</p>
|
||||
<ul className=' flex flex-col gap-16'>
|
||||
{tagsQuery.loading ?
|
||||
|
||||
@@ -28,7 +28,7 @@ export default function StoryPageContent({ story }: Props) {
|
||||
return (
|
||||
<>
|
||||
<div id="content" className="bg-white md:p-32 md:border-2 border-gray-200 rounded-16 relative"> </div>
|
||||
<Card id="content" onlyMd className="relative">
|
||||
<Card id="content" onlyMd className="relative max">
|
||||
{story.cover_image &&
|
||||
<img src={story.cover_image}
|
||||
className='w-full object-cover rounded-12 md:rounded-16 mb-16'
|
||||
|
||||
@@ -26,32 +26,34 @@ export default function PostDetailsPageSkeleton() {
|
||||
|
||||
return (
|
||||
|
||||
<div
|
||||
className={`page-container grid pt-16 w-full gap-32 ${styles.grid}`}
|
||||
>
|
||||
<aside id='actions' className='no-scrollbar'>
|
||||
<div className="sticky"
|
||||
style={{
|
||||
top: `${navHeight + 16}px`,
|
||||
maxHeight: `calc(100vh - ${navHeight}px - 16px)`,
|
||||
}}>
|
||||
<PostActionsSkeleton />
|
||||
</div>
|
||||
</aside>
|
||||
<div className="page-container max-md:bg-white">
|
||||
<div
|
||||
className={`grid pt-16 w-full gap-32 ${styles.grid}`}
|
||||
>
|
||||
<aside id='actions' className='no-scrollbar'>
|
||||
<div className="sticky"
|
||||
style={{
|
||||
top: `${navHeight + 16}px`,
|
||||
maxHeight: `calc(100vh - ${navHeight}px - 16px)`,
|
||||
}}>
|
||||
<PostActionsSkeleton />
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
|
||||
<PageContentSkeleton />
|
||||
<aside id='author' className='no-scrollbar min-w-0'>
|
||||
<div className="flex flex-col gap-24"
|
||||
style={{
|
||||
top: `${navHeight + 16}px`,
|
||||
maxHeight: `calc(100vh - ${navHeight}px - 16px)`,
|
||||
overflowY: "scroll",
|
||||
}}>
|
||||
<AuthorCardSkeleton />
|
||||
<TrendingCard />
|
||||
</div>
|
||||
</aside>
|
||||
<PageContentSkeleton />
|
||||
<aside id='author' className='no-scrollbar min-w-0'>
|
||||
<div className="flex flex-col gap-24"
|
||||
style={{
|
||||
top: `${navHeight + 16}px`,
|
||||
maxHeight: `calc(100vh - ${navHeight}px - 16px)`,
|
||||
overflowY: "scroll",
|
||||
}}>
|
||||
<AuthorCardSkeleton />
|
||||
<TrendingCard />
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -45,10 +45,10 @@ export default function PostDetailsPage() {
|
||||
</Helmet>
|
||||
<ScrollToTop />
|
||||
<div
|
||||
className={`page-container`}
|
||||
className={`page-container max-md:bg-white`}
|
||||
>
|
||||
<div className={`grid w-full gap-32 ${styles.grid}`}>
|
||||
<aside id='actions' className='no-scrollbar'>
|
||||
<aside id='actions' className='no-scrollbar fill-container'>
|
||||
<div className="sticky-side-element">
|
||||
<PostActions post={post} />
|
||||
</div>
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
interface Props {
|
||||
src: string;
|
||||
alt?: string;
|
||||
width?: number;
|
||||
width?: number | string;
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function Avatar({ src, alt, width = 40 }: Props) {
|
||||
export default function Avatar({ src, alt, className, width = 40 }: Props) {
|
||||
return (
|
||||
<img src={src} className='shrink-0 rounded-full object-cover border-2 bg-white border-gray-100' style={{
|
||||
<img src={src} className={`shrink-0 rounded-full object-cover border-2 bg-white border-gray-100 ${className}`} style={{
|
||||
width: width,
|
||||
height: width,
|
||||
aspectRatio: '1/1'
|
||||
}} alt={alt ?? "avatar"} />
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ const schema: yup.SchemaOf<IFormInputs> = yup.object({
|
||||
bio: yup.string().ensure(),
|
||||
email: yup.string().email().ensure(),
|
||||
github: yup.string().ensure(),
|
||||
discord: yup.string().ensure(),
|
||||
jobTitle: yup.string().ensure(),
|
||||
lightning_address: yup
|
||||
.string()
|
||||
@@ -94,6 +95,7 @@ export default function BasicProfileInfoTab() {
|
||||
jobTitle: data.jobTitle,
|
||||
bio: data.bio,
|
||||
email: data.email,
|
||||
discord: data.discord,
|
||||
github: data.github,
|
||||
linkedin: data.linkedin,
|
||||
lightning_address: data.lightning_address,
|
||||
@@ -196,7 +198,6 @@ export default function BasicProfileInfoTab() {
|
||||
</p>
|
||||
<div className="input-wrapper mt-8 relative">
|
||||
<input
|
||||
|
||||
type='text'
|
||||
className="input-text"
|
||||
placeholder="UK, London"
|
||||
@@ -211,31 +212,19 @@ export default function BasicProfileInfoTab() {
|
||||
</p>
|
||||
<div className="input-wrapper mt-8 relative">
|
||||
<input
|
||||
|
||||
type='text'
|
||||
className="input-text"
|
||||
placeholder="johndoe@gmail.com"
|
||||
{...register("email")}
|
||||
/>
|
||||
</div>
|
||||
{errors.website && <p className="input-error">
|
||||
{errors.website.message}
|
||||
{errors.email && <p className="input-error">
|
||||
{errors.email.message}
|
||||
</p>}
|
||||
<p className="text-body5 mt-16 font-medium">
|
||||
Website
|
||||
<p className="text-body6 text-gray-400 mt-8 max-w-[70ch]">
|
||||
Your email is visible only to you, we will only use it to send you important updates or notices. No spam!
|
||||
</p>
|
||||
<div className="input-wrapper mt-8 relative">
|
||||
<input
|
||||
|
||||
type='text'
|
||||
className="input-text"
|
||||
placeholder="www.website.io"
|
||||
{...register("website")}
|
||||
/>
|
||||
</div>
|
||||
{errors.website && <p className="input-error">
|
||||
{errors.website.message}
|
||||
</p>}
|
||||
<p className="text-body5 mt-16 font-medium">
|
||||
Twitter handle
|
||||
</p>
|
||||
@@ -251,6 +240,21 @@ export default function BasicProfileInfoTab() {
|
||||
{errors.twitter && <p className="input-error">
|
||||
{errors.twitter.message}
|
||||
</p>}
|
||||
<p className="text-body5 mt-16 font-medium">
|
||||
Discord username
|
||||
</p>
|
||||
<div className="input-wrapper mt-8 relative">
|
||||
<input
|
||||
|
||||
type='text'
|
||||
className="input-text"
|
||||
placeholder="Satoshi#2121"
|
||||
{...register("discord")}
|
||||
/>
|
||||
</div>
|
||||
{errors.discord && <p className="input-error">
|
||||
{errors.discord.message}
|
||||
</p>}
|
||||
<p className="text-body5 mt-16 font-medium">
|
||||
Github username
|
||||
</p>
|
||||
@@ -281,6 +285,20 @@ export default function BasicProfileInfoTab() {
|
||||
{errors.linkedin && <p className="input-error">
|
||||
{errors.linkedin.message}
|
||||
</p>}
|
||||
<p className="text-body5 mt-16 font-medium">
|
||||
Your website
|
||||
</p>
|
||||
<div className="input-wrapper mt-8 relative">
|
||||
<input
|
||||
type='text'
|
||||
className="input-text"
|
||||
placeholder="www.website.io"
|
||||
{...register("website")}
|
||||
/>
|
||||
</div>
|
||||
{errors.website && <p className="input-error">
|
||||
{errors.website.message}
|
||||
</p>}
|
||||
<p className="text-body5 mt-16 font-medium">
|
||||
Lightning address
|
||||
</p>
|
||||
|
||||
@@ -4,11 +4,11 @@ fragment UserBasicInfo on BaseUser {
|
||||
avatar
|
||||
join_date
|
||||
role
|
||||
email
|
||||
jobTitle
|
||||
lightning_address
|
||||
website
|
||||
twitter
|
||||
discord
|
||||
github
|
||||
linkedin
|
||||
bio
|
||||
@@ -17,12 +17,14 @@ fragment UserBasicInfo on BaseUser {
|
||||
|
||||
query MyProfileAbout {
|
||||
me {
|
||||
email
|
||||
...UserBasicInfo
|
||||
}
|
||||
}
|
||||
|
||||
mutation updateProfileAbout($data: ProfileDetailsInput) {
|
||||
updateProfileDetails(data: $data) {
|
||||
email
|
||||
...UserBasicInfo
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,28 +67,30 @@ export default function EditProfilePage() {
|
||||
</ul>
|
||||
</Card>
|
||||
:
|
||||
<div className="border-b-2 border-gray-200">
|
||||
<Slider>
|
||||
{links.map((link, idx) =>
|
||||
<NavLink
|
||||
to={link.path}
|
||||
key={idx}
|
||||
className={`flex items-start cursor-pointer font-bold py-12
|
||||
<div className="overflow-hidden">
|
||||
<div className="border-b-2 border-gray-200">
|
||||
<Slider>
|
||||
{links.map((link, idx) =>
|
||||
<NavLink
|
||||
to={link.path}
|
||||
key={idx}
|
||||
className={`flex items-start cursor-pointer font-bold py-12
|
||||
active:scale-95 transition-transform`}
|
||||
style={({ isActive }) => ({
|
||||
boxShadow: isActive ? '0px 2px var(--primary)' : 'none'
|
||||
})}
|
||||
>
|
||||
{link.text}
|
||||
</NavLink>
|
||||
)}
|
||||
</Slider>
|
||||
style={({ isActive }) => ({
|
||||
boxShadow: isActive ? '0px 3px 1px -1px var(--primary)' : 'none'
|
||||
})}
|
||||
>
|
||||
{link.text}
|
||||
</NavLink>
|
||||
)}
|
||||
</Slider>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</aside>
|
||||
<main className="md:col-span-3">
|
||||
<Routes>
|
||||
<Route index element={<Navigate to='basic-info' />} />
|
||||
<Route index element={<Navigate to='basic-info' replace />} />
|
||||
<Route path='basic-info' element={<BasicProfileInfoTab />} />
|
||||
<Route path='roles-skills' element={<RolesSkillsTab />} />
|
||||
<Route path='preferences' element={<PreferencesTab />
|
||||
|
||||
@@ -64,7 +64,7 @@ export default function LinkedAccountsCard({ value, onChange }: Props) {
|
||||
<Button color='none' size='sm' className='mt-16 text-gray-600 hover:bg-gray-50' onClick={connectNewWallet}>
|
||||
+ Add another wallet
|
||||
</Button>}
|
||||
<InfoCard>
|
||||
<InfoCard className='mt-24'>
|
||||
<span className="font-bold">💡 Note:</span> if you link a wallet that was used to create another account previously, you won't be able to login to that account until you remove it from here.
|
||||
</InfoCard>
|
||||
</Card>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Controller, SubmitHandler, useForm } from 'react-hook-form';
|
||||
import SaveChangesCard from '../SaveChangesCard/SaveChangesCard';
|
||||
import { toast } from 'react-toastify';
|
||||
import { NotificationsService } from 'src/services';
|
||||
import { gql, NetworkStatus, useApolloClient } from '@apollo/client';
|
||||
import { NetworkStatus, useApolloClient } from '@apollo/client';
|
||||
import { usePrompt } from 'src/utils/hooks';
|
||||
import { UpdateUserRolesSkillsMutationVariables, useMyProfileRolesSkillsQuery, useUpdateUserRolesSkillsMutation, UserRolesSkillsFragmentDoc } from 'src/graphql'
|
||||
import UpdateRolesCard from "./UpdateRolesCard/UpdateRolesCard";
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import React from 'react'
|
||||
import { Control, useFieldArray } from 'react-hook-form'
|
||||
import Card from 'src/Components/Card/Card'
|
||||
import { GenericMakerRole, MakerRole, RoleLevelEnum } from 'src/graphql'
|
||||
import { IRolesSkillsForm } from '../RolesSkillsTab'
|
||||
|
||||
type Value = Pick<MakerRole, 'id' | 'level'>
|
||||
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
|
||||
|
||||
import Select from 'react-select';
|
||||
import { OnChangeValue, StylesConfig, components, OptionProps } from "react-select";
|
||||
import Avatar from "src/features/Profiles/Components/Avatar/Avatar";
|
||||
import { FiSearch } from 'react-icons/fi';
|
||||
import { useState } from 'react';
|
||||
import { OnChangeValue, StylesConfig, } from "react-select";
|
||||
import { MyProfileRolesSkillsQuery } from 'src/graphql';
|
||||
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ export default function UpdateSkillsCard(props: Props) {
|
||||
</li>)}
|
||||
</ul>}
|
||||
|
||||
<InfoCard>
|
||||
<InfoCard className='mt-24'>
|
||||
<span className="font-bold">ℹ️ Can't find a specific skill?</span> You can suggest it to be added <a href="https://github.com/peakshift/makers.bolt.fun/discussions/143" target='_blank' rel="noreferrer" className='font-bold underline'>here</a>
|
||||
</InfoCard>
|
||||
</Card>
|
||||
|
||||
@@ -4,16 +4,19 @@ import { trimText, withHttp } from "src/utils/helperFunctions"
|
||||
import { FiGithub, FiGlobe, FiLinkedin, FiMail, FiTwitter } from 'react-icons/fi'
|
||||
import Button from "src/Components/Button/Button";
|
||||
import Card from "src/Components/Card/Card";
|
||||
import { FaDiscord } from "react-icons/fa";
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||
import { NotificationsService } from 'src/services/notifications.service'
|
||||
|
||||
interface Props {
|
||||
isOwner?: boolean;
|
||||
user: Pick<User,
|
||||
| 'name'
|
||||
| 'email'
|
||||
| 'lightning_address'
|
||||
| 'jobTitle'
|
||||
| 'avatar'
|
||||
| 'website'
|
||||
| 'discord'
|
||||
| 'github'
|
||||
| 'twitter'
|
||||
| 'linkedin'
|
||||
@@ -24,37 +27,37 @@ interface Props {
|
||||
|
||||
export default function AboutCard({ user, isOwner }: Props) {
|
||||
|
||||
|
||||
const links = [
|
||||
{
|
||||
hasValue: user.email,
|
||||
text: user.email,
|
||||
icon: FiMail,
|
||||
value: user.discord,
|
||||
text: user.discord,
|
||||
icon: FaDiscord,
|
||||
colors: "bg-violet-100 text-violet-900",
|
||||
url: user.email && `mailto:${user.email}`
|
||||
},
|
||||
{
|
||||
hasValue: user.website,
|
||||
value: user.website,
|
||||
text: user.website?.replace(/(^\w+:|^)\/\//, '').replace(/\/$/, ""),
|
||||
icon: FiGlobe,
|
||||
colors: "bg-gray-100 text-gray-900",
|
||||
url: user.website && withHttp(user.website)
|
||||
},
|
||||
{
|
||||
hasValue: user.twitter,
|
||||
value: user.twitter,
|
||||
text: user.twitter,
|
||||
icon: FiTwitter,
|
||||
colors: "bg-blue-100 text-blue-500",
|
||||
url: `https://twitter.com/@${user.twitter}`
|
||||
url: `https://twitter.com/${user.twitter}`
|
||||
},
|
||||
{
|
||||
hasValue: user.github,
|
||||
value: user.github,
|
||||
text: user.github,
|
||||
icon: FiGithub,
|
||||
colors: "bg-pink-100 text-pink-600",
|
||||
url: `https://github.com/${user.github}`
|
||||
},
|
||||
{
|
||||
hasValue: user.linkedin,
|
||||
value: user.linkedin,
|
||||
text: "LinkedIn",
|
||||
icon: FiLinkedin,
|
||||
colors: "bg-sky-100 text-cyan-600",
|
||||
@@ -65,7 +68,6 @@ export default function AboutCard({ user, isOwner }: Props) {
|
||||
|
||||
return (
|
||||
<Card defaultPadding={false}>
|
||||
|
||||
<div className="bg-gray-600 relative h-[160px] rounded-t-16">
|
||||
<div className="absolute left-24 bottom-0 translate-y-1/2">
|
||||
<Avatar src={user.avatar} width={120} />
|
||||
@@ -77,7 +79,7 @@ export default function AboutCard({ user, isOwner }: Props) {
|
||||
<div className="p-24 pt-0">
|
||||
<div className="flex flex-col gap-16">
|
||||
<div>
|
||||
<h1 className="text-h2 font-bolder break-words">
|
||||
<h1 className="text-h2 font-bolder overflow-hidden text-ellipsis">
|
||||
{user.name}
|
||||
</h1>
|
||||
|
||||
@@ -86,8 +88,9 @@ export default function AboutCard({ user, isOwner }: Props) {
|
||||
</p>}
|
||||
</div>
|
||||
{<div className="flex flex-wrap gap-16">
|
||||
{links.filter(link => link.hasValue || isOwner).map((link, idx) => link.hasValue ?
|
||||
<a
|
||||
{links.filter(link => !!link.value || isOwner).map((link, idx) => !!link.value ?
|
||||
|
||||
(link.url ? <a
|
||||
key={idx}
|
||||
href={link.url!}
|
||||
className={`w-40 aspect-square rounded-full flex justify-center items-center ${link.colors}`}
|
||||
@@ -95,6 +98,20 @@ export default function AboutCard({ user, isOwner }: Props) {
|
||||
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>
|
||||
)
|
||||
:
|
||||
(isOwner &&
|
||||
<p
|
||||
|
||||
@@ -50,7 +50,7 @@ export default function ProfilePage() {
|
||||
</Helmet>
|
||||
<div className={`page-container`}
|
||||
>
|
||||
<div className={`${styles.grid}`}
|
||||
<div className={` ${styles.grid}`}
|
||||
>{isMediumScreen ?
|
||||
<>
|
||||
<aside>
|
||||
@@ -59,7 +59,7 @@ export default function ProfilePage() {
|
||||
<SkillsCard skills={profileQuery.data.profile.skills} isOwner={isOwner} />
|
||||
<TournamentsCard tournaments={profileQuery.data.profile.tournaments} isOwner={isOwner} />
|
||||
</aside>
|
||||
<main>
|
||||
<main className="min-w-0">
|
||||
|
||||
<AboutCard user={profileQuery.data.profile} isOwner={isOwner} />
|
||||
<StoriesCard stories={profileQuery.data.profile.stories} isOwner={isOwner} />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Card from 'src/Components/Card/Card'
|
||||
import Button from 'src/Components/Button/Button'
|
||||
import { RoleLevelEnum, User } from 'src/graphql';
|
||||
import { User } from 'src/graphql';
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
|
||||
|
||||
@@ -27,15 +27,22 @@ export default function TournamentsCard({ tournaments, isOwner }: Props) {
|
||||
<ul className=' flex flex-wrap gap-x-8 gap-y-20'>
|
||||
{
|
||||
tournaments.map((tournament) => {
|
||||
|
||||
const isLive = ((new Date() < new Date(tournament.end_date)) && (new Date() > new Date(tournament.start_date)));
|
||||
|
||||
return <li key={tournament.id} className="flex gap-16 items-center">
|
||||
<img src={tournament.thumbnail_image} className='w-48 border-2 border-gray-100 aspect-square rounded-16 object-cover' alt="" />
|
||||
<div>
|
||||
<p className="text-gray-900 font-medium">{tournament.title}</p>
|
||||
<p className={`${isLive ? "text-green-500" : "text-warning-500"} text-body5 font-medium`}>• {isLive ? "Live" : "Completed"}</p>
|
||||
</div>
|
||||
const status = getDateStatus(tournament.start_date, tournament.end_date)
|
||||
return <li key={tournament.id}>
|
||||
<Link to={'/tournaments/' + tournament.id} className="flex gap-16 items-center">
|
||||
<img src={tournament.thumbnail_image} className='w-48 border-2 border-gray-100 aspect-square rounded-16 object-cover' alt="" />
|
||||
<div>
|
||||
<p className="text-gray-900 font-medium">{tournament.title}</p>
|
||||
<p className={`
|
||||
text-body5 font-medium
|
||||
${status === 'live' && 'text-green-500'}
|
||||
${status === 'upcoming' && 'text-violet-500'}
|
||||
${status === 'finished' && 'text-warning-500'}
|
||||
`}>• {status === 'live' && "Running"}
|
||||
{status === 'upcoming' && "Upcoming"}
|
||||
{status === 'finished' && "Completed"} </p>
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
})}
|
||||
</ul>
|
||||
@@ -43,3 +50,15 @@ export default function TournamentsCard({ tournaments, isOwner }: Props) {
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
function getDateStatus(start: string, end: string) {
|
||||
|
||||
const start_date = new Date(start);
|
||||
const now_date = new Date();
|
||||
const end_date = new Date(end);
|
||||
|
||||
if (now_date < start_date) return 'upcoming'
|
||||
if (now_date >= start_date && now_date <= end_date) return 'live'
|
||||
return 'finished'
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
import ProjectCardMini from "src/features/Projects/Components/ProjectCardMini/ProjectCardMini";
|
||||
import ProjectCardMiniSkeleton from 'src/features/Projects/Components/ProjectCardMini/ProjectCardMini.Skeleton';
|
||||
import { openModal } from 'src/redux/features/modals.slice';
|
||||
import { openProject } from 'src/redux/features/project.slice';
|
||||
import { useAppDispatch } from 'src/utils/hooks';
|
||||
import { ProjectCard } from 'src/utils/interfaces';
|
||||
|
||||
@@ -17,7 +16,9 @@ export default function ProjectsGrid({ isLoading, projects }: Props) {
|
||||
|
||||
const handleClick = (projectId: number) => {
|
||||
dispatch(openModal({
|
||||
Modal: "ProjectDetailsCard", props: {
|
||||
Modal: "ProjectDetailsCard",
|
||||
isPageModal: true,
|
||||
props: {
|
||||
projectId
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -6,7 +6,7 @@ import Categories from "./Categories/Categories";
|
||||
|
||||
export default function ExplorePage() {
|
||||
return (
|
||||
<>
|
||||
<div className="bg-white">
|
||||
<Helmet>
|
||||
<title>{`Explore Lightning Products`}</title>
|
||||
<meta property="og:title" content={`Explore Lightning Products`} />
|
||||
@@ -20,6 +20,6 @@ export default function ExplorePage() {
|
||||
<ProjectsSection />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,8 +5,34 @@ import { MEDIA_QUERIES } from "src/utils/theme/media_queries";
|
||||
import CustomDot from "./CustomDot/CustomDot";
|
||||
import useEmblaCarousel from 'embla-carousel-react'
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { createRoute } from "src/utils/routing";
|
||||
|
||||
export const bannerData = {
|
||||
title:
|
||||
<>
|
||||
<p className="text-body1 font-bolder text-white">Legends of Lightning ⚡️ Tournament</p>
|
||||
<p className="text-body3 font-medium text-white mt-8">1st Oct - 31st Nov, 2022</p>
|
||||
</>,
|
||||
img: "https://imagedelivery.net/wyrwp3c-j0gDDUWgnE7lig/1d5d2c86-fe46-4478-6909-bb3c425c0d00/public",
|
||||
link: {
|
||||
content: "Register Now",
|
||||
url: createRoute({ type: "tournament", id: 1, tab: 'overview' }),
|
||||
},
|
||||
}
|
||||
|
||||
const headerLinks = [
|
||||
{
|
||||
title:
|
||||
<>
|
||||
<p className="text-body1 font-bolder text-white">Legends of Lightning ⚡️ Tournament</p>
|
||||
<p className="text-body3 font-medium text-white mt-8">1st Oct - 31st Nov, 2022</p>
|
||||
</>,
|
||||
img: "https://imagedelivery.net/wyrwp3c-j0gDDUWgnE7lig/1d5d2c86-fe46-4478-6909-bb3c425c0d00/public",
|
||||
link: {
|
||||
content: "Register Now",
|
||||
url: createRoute({ type: "tournament", id: 1, tab: 'overview' }),
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <p className="text-body1 font-bolder text-white">Explore a fun directory of lightning web apps</p>,
|
||||
img: Assets.Images_ExploreHeader1,
|
||||
@@ -15,18 +41,6 @@ const headerLinks = [
|
||||
url: "https://form.jotform.com/220301236112030",
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
<>
|
||||
<p className="text-body1 font-bolder text-white">Take part in BOLT🔩FUN’s Shock the Web 2 ⚡️</p>
|
||||
<p className="text-body3 font-medium text-white mt-8">16th - 19th June, 2022</p>
|
||||
</>,
|
||||
img: Assets.Images_ExploreHeader2,
|
||||
link: {
|
||||
content: "Register Now",
|
||||
url: "https://bolt.fun/hackathons/shock-the-web-2/",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -61,22 +75,22 @@ export default function Header() {
|
||||
<div className="relative group">
|
||||
<div className="overflow-hidden" ref={emblaRef}>
|
||||
<div className="w-full flex gap-16">
|
||||
<div className="flex-[0_0_100%] md:flex-[0_0_calc(50%-8px)] rounded-20 h-[280px] relative overflow-hidden p-24 flex flex-col items-start justify-end">
|
||||
<div className="flex-[0_0_100%] rounded-20 min-h-[280px] relative overflow-hidden p-24 flex flex-col items-start justify-end">
|
||||
<img
|
||||
className="w-full h-full object-cover absolute top-0 left-0 z-[-2]"
|
||||
src={headerLinks[0].img}
|
||||
className="w-full h-full object-cover object-top absolute top-0 left-0 z-[-2]"
|
||||
src={bannerData.img}
|
||||
alt=""
|
||||
/>
|
||||
<div className="w-full h-full object-cover bg-gradient-to-t from-gray-900 absolute top-0 left-0 z-[-1]"></div>
|
||||
<div className="w-full h-full object-cover bg-gradient-to-tr from-gray-900 absolute top-0 left-0 z-[-1]"></div>
|
||||
<div className="max-w-[90%]">
|
||||
{headerLinks[0].title}
|
||||
{bannerData.title}
|
||||
</div>
|
||||
|
||||
<Button href={headerLinks[0].link.url} newTab color="white" className="mt-24">
|
||||
{headerLinks[0].link.content}
|
||||
<Button href={bannerData.link.url} color="white" className="mt-24">
|
||||
{bannerData.link.content}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-[0_0_100%] md:flex-[0_0_calc(50%-8px)] rounded-20 h-[280px] relative overflow-hidden p-24 flex flex-col items-start justify-end">
|
||||
{/* <div className="flex-[0_0_100%] md:flex-[0_0_calc(50%-8px)] rounded-20 h-[280px] relative overflow-hidden p-24 flex flex-col items-start justify-end">
|
||||
<img
|
||||
className="w-full h-full object-cover absolute top-0 left-0 z-[-2]"
|
||||
src={headerLinks[1].img}
|
||||
@@ -89,7 +103,7 @@ export default function Header() {
|
||||
<Button color="white" href={headerLinks[1].link.url} newTab className="mt-24">
|
||||
{headerLinks[1].link.content}
|
||||
</Button>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute inset-x-0 bottom-8 flex justify-center gap-4 md:hidden">
|
||||
|
||||
@@ -51,7 +51,7 @@ export default function ProjectsRow({ title, link, projects }: Props) {
|
||||
|
||||
const handleClick = (projectId: number) => {
|
||||
if (isClickAllowed()) {
|
||||
dispatch(openModal({ Modal: "ProjectDetailsCard", props: { projectId } }))
|
||||
dispatch(openModal({ Modal: "ProjectDetailsCard", isPageModal: true, props: { projectId } }))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@ import { MdClose, } from 'react-icons/md';
|
||||
import { ModalCard, modalCardVariants } from 'src/Components/Modals/ModalsContainer/ModalsContainer';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
import Badge from 'src/Components/Badge/Badge';
|
||||
import { useAppSelector } from 'src/utils/hooks';
|
||||
import { useMediaQuery } from 'src/utils/hooks';
|
||||
import { MEDIA_QUERIES } from 'src/utils/theme';
|
||||
|
||||
|
||||
interface Props extends ModalCard {
|
||||
@@ -13,9 +14,7 @@ export default function ProjectDetailsCardSkeleton({ onClose, direction, ...prop
|
||||
|
||||
|
||||
|
||||
const { isMobileScreen } = useAppSelector(state => ({
|
||||
isMobileScreen: state.ui.isMobileScreen
|
||||
}));
|
||||
const isMdScreen = useMediaQuery(MEDIA_QUERIES.isMedium)
|
||||
|
||||
|
||||
|
||||
@@ -27,12 +26,12 @@ export default function ProjectDetailsCardSkeleton({ onClose, direction, ...prop
|
||||
initial='initial'
|
||||
animate="animate"
|
||||
exit='exit'
|
||||
className={`modal-card max-w-[768px] ${props.isPageModal && isMobileScreen && 'rounded-0 w-full min-h-screen'}`}
|
||||
className={`modal-card max-w-[768px] ${props.isPageModal && !isMdScreen && 'rounded-0 w-full min-h-screen'}`}
|
||||
|
||||
>
|
||||
<div className="relative h-[80px] lg:h-[152px]">
|
||||
<Skeleton height='100%' className='!leading-inherit' />
|
||||
<button className="w-[48px] h-[48px] 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-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>
|
||||
</div>
|
||||
<div className="p-24">
|
||||
<div className="flex gap-24 items-center h-[93px]">
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'
|
||||
import { BsJoystick } from 'react-icons/bs'
|
||||
import { MdClose, MdLocalFireDepartment } from 'react-icons/md';
|
||||
import { ModalCard } from 'src/Components/Modals/ModalsContainer/ModalsContainer';
|
||||
import { useAppDispatch, useAppSelector } from 'src/utils/hooks';
|
||||
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';
|
||||
@@ -16,6 +16,7 @@ import linkifyHtml from 'linkify-html';
|
||||
import ErrorMessage from 'src/Components/ErrorMessage/ErrorMessage';
|
||||
import { setVoteAmount } from 'src/redux/features/vote.slice';
|
||||
import { numberFormatter } from 'src/utils/helperFunctions';
|
||||
import { MEDIA_QUERIES } from 'src/utils/theme';
|
||||
|
||||
|
||||
interface Props extends ModalCard {
|
||||
@@ -28,11 +29,11 @@ export default function ProjectDetailsCard({ direction, projectId, ...props }: P
|
||||
const [screenshotsOpen, setScreenshotsOpen] = useState(-1);
|
||||
|
||||
|
||||
const { isWalletConnected, project, isMobileScreen } = useAppSelector(state => ({
|
||||
const { isWalletConnected, project } = useAppSelector(state => ({
|
||||
isWalletConnected: state.wallet.isConnected,
|
||||
project: state.project.project,
|
||||
isMobileScreen: state.ui.isMobileScreen
|
||||
project: state.project.project
|
||||
}));
|
||||
const isMdScreen = useMediaQuery(MEDIA_QUERIES.isMedium)
|
||||
|
||||
const { loading, error } = useProjectDetailsQuery({
|
||||
variables: { projectId: projectId! },
|
||||
@@ -57,7 +58,7 @@ export default function ProjectDetailsCard({ direction, projectId, ...props }: P
|
||||
|
||||
if (error)
|
||||
return <div
|
||||
className={`modal-card max-w-[768px] ${props.isPageModal && isMobileScreen && 'rounded-0 w-full min-h-screen'}`}
|
||||
className={`modal-card max-w-[768px] ${props.isPageModal && !isMdScreen && 'rounded-0 w-full min-h-screen'}`}
|
||||
>
|
||||
<div className="p-64">
|
||||
<ErrorMessage type='fetching' message='Something Wrong happened while fetching project details, please try refreshing the page' />
|
||||
@@ -98,7 +99,7 @@ export default function ProjectDetailsCard({ direction, projectId, ...props }: P
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`modal-card max-w-[768px] ${props.isPageModal && isMobileScreen && '!rounded-0 w-full min-h-screen'}`}
|
||||
className={`modal-card max-w-[768px] ${(props.isPageModal && !isMdScreen) && '!rounded-0 w-full min-h-screen'}`}
|
||||
>
|
||||
<div className="relative h-[80px] lg:h-[152px]">
|
||||
<img className="w-full h-full object-cover" src={project.cover_image} alt="" />
|
||||
|
||||
@@ -12,7 +12,7 @@ interface Particle {
|
||||
offsetX: number,
|
||||
color: '#ff6a00' | '#ff7717' | '#ff6217' | '#ff8217' | '#ff5717'
|
||||
animation: 'fly-spark-1' | 'fly-spark-2',
|
||||
animationSpeed: 1 | 2 | 3,
|
||||
animationSpeed: number,
|
||||
scale: number
|
||||
}
|
||||
|
||||
@@ -40,11 +40,11 @@ export default function VoteButton({ onVote = () => { }, ...props }: Props) {
|
||||
const newSpark = {
|
||||
id: Math.random().toString(),
|
||||
offsetX: random(1, 99),
|
||||
animation: randomItem(styles.fly_spark_1, styles.fly_spark_1),
|
||||
animation: randomItem(styles.fly_spark_1, styles.fly_spark_1) as any,
|
||||
animationSpeed: randomItem(1, 1.5, 2),
|
||||
color: randomItem('#ff6a00', '#ff7717', '#ff6217', '#ff8217', '#ff5717'),
|
||||
scale: random(1.2, 2.2)
|
||||
};
|
||||
} as const;
|
||||
|
||||
// if on mobile screen, reduce number of sparks particles to 60%
|
||||
if (!isMobileScreen || Math.random() > .4) {
|
||||
|
||||
@@ -8,17 +8,19 @@ export default function NotFoundPage() {
|
||||
const goBack = () => navigate(-1);
|
||||
|
||||
return (
|
||||
<div className='page-container min-h-screen flex flex-col gap-36 justify-center items-center relative'>
|
||||
<p className='text-gray-100 absolute top-1/3 left-1/2 -translate-x-1/2 -translate-y-1/2 text-[50vmin] z-[-1]'>404</p>
|
||||
<h1 className="text-h1 font-bold">
|
||||
Not Found...
|
||||
</h1>
|
||||
<p className="text-body4 text-gray-500 font-medium text-center">
|
||||
The resource you are looking for isn't here anymore, it may have been removed or the url may be invalid.
|
||||
</p>
|
||||
<Button color='primary' onClick={goBack}>
|
||||
Go back
|
||||
</Button>
|
||||
<div className="page-container">
|
||||
<div className='min-h-screen flex flex-col gap-36 justify-center items-center relative'>
|
||||
<p className='text-gray-100 absolute top-1/3 left-1/2 -translate-x-1/2 -translate-y-1/2 text-[50vmin] z-[-1]'>404</p>
|
||||
<h1 className="text-h1 font-bold">
|
||||
Not Found...
|
||||
</h1>
|
||||
<p className="text-body4 text-gray-500 font-medium text-center">
|
||||
The resource you are looking for isn't here anymore, it may have been removed or the url may be invalid.
|
||||
</p>
|
||||
<Button color='primary' onClick={goBack}>
|
||||
Go back
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { IoLocationOutline } from 'react-icons/io5'
|
||||
import { trimText } from "src/utils/helperFunctions";
|
||||
import { Tournament, TournamentEventTypeEnum } from "src/graphql";
|
||||
import { UnionToObjectKeys } from 'src/utils/types/utils';
|
||||
import { useAppDispatch, } from "src/utils/hooks";
|
||||
import { openModal } from "src/redux/features/modals.slice";
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
|
||||
interface Props {
|
||||
event: Pick<Tournament['events'][number],
|
||||
| 'id'
|
||||
| 'title'
|
||||
| 'image'
|
||||
| 'starts_at'
|
||||
| 'ends_at'
|
||||
| 'location'
|
||||
| 'description'
|
||||
| 'website'
|
||||
| 'type'
|
||||
>
|
||||
}
|
||||
|
||||
export default function EventCard({ event }: Props) {
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const openEventModal = () => {
|
||||
dispatch(openModal({
|
||||
Modal: "EventModal",
|
||||
isPageModal: true,
|
||||
props: {
|
||||
event
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
role='button'
|
||||
className="rounded-16 bg-white overflow-hidden outline outline-2 outline-gray-200 flex flex-col group"
|
||||
onClick={openEventModal}
|
||||
>
|
||||
<img className="w-full h-[160px] object-cover rounded-t-16" src={event.image} alt="" />
|
||||
<div className="p-16 grow flex flex-col">
|
||||
<div className="flex flex-col gap-8">
|
||||
<h3 className="text-body2 font-bold text-gray-900 group-hover:underline">
|
||||
{event.title}
|
||||
</h3>
|
||||
<p className="text-body4 font-medium text-gray-900">
|
||||
|
||||
{`${dayjs(event.starts_at).format('H:mm')} - ${dayjs(event.ends_at).format('H:mm, Do MMM')}`}
|
||||
|
||||
</p>
|
||||
<p className="text-body4 font-medium text-gray-600">
|
||||
<IoLocationOutline className="mr-4" /> <span className="align-middle">{event.location}</span>
|
||||
</p>
|
||||
<p className="text-body4 text-gray-600 line-clamp-2">
|
||||
{trimText(event.description, 90)}
|
||||
</p>
|
||||
<span className={`mt-8 text-body5 self-start px-8 py-4 rounded-20 ${mapTypeToBadge[event.type].color}`}>
|
||||
{mapTypeToBadge[event.type].text}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const mapTypeToBadge: UnionToObjectKeys<Props['event'], 'type', { text: string, color: string }> = {
|
||||
[TournamentEventTypeEnum.TwitterSpace]: {
|
||||
text: "🐦 Twitter space",
|
||||
color: "bg-blue-50 text-blue-500"
|
||||
},
|
||||
[TournamentEventTypeEnum.Workshop]: {
|
||||
text: "🛠️ Workshop",
|
||||
color: "bg-green-50 text-green-500"
|
||||
},
|
||||
[TournamentEventTypeEnum.IrlMeetup]: {
|
||||
text: "🤝 IRL meetup",
|
||||
color: "bg-red-50 text-red-500"
|
||||
},
|
||||
[TournamentEventTypeEnum.OnlineMeetup]: {
|
||||
text: "🤖 Online meetup",
|
||||
color: "bg-violet-50 text-violet-500"
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
|
||||
import { MdClose, } from 'react-icons/md';
|
||||
import { ModalCard } from 'src/Components/Modals/ModalsContainer/ModalsContainer';
|
||||
import { useMediaQuery } from 'src/utils/hooks';
|
||||
import { Tournament, } from 'src/graphql';
|
||||
import { MEDIA_QUERIES } from 'src/utils/theme';
|
||||
import { IoGlobe, IoLocationOutline } from 'react-icons/io5';
|
||||
import { mapTypeToBadge } from '../EventCard/EventCard';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
|
||||
interface Props extends ModalCard {
|
||||
event: Pick<Tournament['events'][number],
|
||||
| "id"
|
||||
| "title"
|
||||
| "image"
|
||||
| "description"
|
||||
| "starts_at"
|
||||
| "ends_at"
|
||||
| "location"
|
||||
| "type"
|
||||
| "website">
|
||||
}
|
||||
|
||||
export default function ProjectDetailsCard({ direction, event, ...props }: Props) {
|
||||
|
||||
const closeModal = () => {
|
||||
props.onClose?.();
|
||||
}
|
||||
|
||||
|
||||
const isMdScreen = useMediaQuery(MEDIA_QUERIES.isMedium)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`modal-card max-w-[768px] ${(props.isPageModal && !isMdScreen) && '!rounded-0 w-full min-h-screen'}`}
|
||||
>
|
||||
<div className="relative h-[160px]">
|
||||
<img className="w-full h-full object-cover" src={event.image} alt="" />
|
||||
<button className="w-32 h-32 bg-gray-700 text-white absolute top-16 right-16 rounded-full flex flex-col justify-center items-center" onClick={closeModal}><MdClose className=' inline-block text-body2 lg:text-body1' /></button>
|
||||
<span className={`absolute top-16 left-16 text-body5 self-start px-8 py-4 rounded-20 bg-gray-700 text-white `}>
|
||||
{mapTypeToBadge[event.type].text}
|
||||
</span>
|
||||
</div>
|
||||
<div className="p-16 md:p-24">
|
||||
<h1 className="text-body1 font-bold">{event.title}</h1>
|
||||
<p className="text-body4 font-medium text-gray-900 mt-8">
|
||||
|
||||
{`${dayjs(event.starts_at).format('H:mm')} - ${dayjs(event.starts_at).format('H:mm, Do MMM')}`}
|
||||
</p>
|
||||
<div className="flex gap-16 mt-8">
|
||||
<p className="text-body4 font-medium text-primary-600 shrink-0">
|
||||
<IoLocationOutline className="mr-4" /> <span className="align-middle">{event.location}</span>
|
||||
</p>
|
||||
<p className="text-body4 font-medium text-primary-600 overflow-hidden whitespace-nowrap text-ellipsis">
|
||||
<IoGlobe className="mr-4" />
|
||||
<a href={event.website} target="_blank" rel="noreferrer" > <span className="align-middle ">{event.website}</span></a>
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-body4 text-gray-600 mt-24">
|
||||
{event.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
|
||||
import { lazyModal } from 'src/utils/helperFunctions';
|
||||
|
||||
|
||||
|
||||
export const { LazyComponent: EventModal, } = lazyModal(() => import('./EventModal'))
|
||||
@@ -0,0 +1,45 @@
|
||||
import { FiSearch } from 'react-icons/fi'
|
||||
import BasicSelectInput from 'src/Components/Inputs/Selects/BasicSelectInput/BasicSelectInput';
|
||||
import { TournamentEventTypeEnum } from 'src/graphql';
|
||||
import { mapTypeToBadge } from '../EventCard/EventCard';
|
||||
|
||||
interface Props {
|
||||
searchValue: string;
|
||||
onSearchChange: (new_value: string) => void;
|
||||
|
||||
eventValue: TournamentEventTypeEnum | null
|
||||
onEventChange: (new_value: TournamentEventTypeEnum | null) => void;
|
||||
}
|
||||
|
||||
export default function EventsFilters(props: Props) {
|
||||
|
||||
const options = Object.values(TournamentEventTypeEnum).map((v: TournamentEventTypeEnum) => ({ label: mapTypeToBadge[v].text, value: v }))
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="input-wrapper relative lg:col-span-2">
|
||||
<FiSearch className="self-center ml-16 flex-shrink-0 w-[20px] text-gray-400" />
|
||||
<input
|
||||
type='text'
|
||||
className="input-text"
|
||||
placeholder="Search"
|
||||
value={props.searchValue}
|
||||
onChange={e => props.onSearchChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<BasicSelectInput
|
||||
isMulti={false}
|
||||
labelField='label'
|
||||
valueField='value'
|
||||
placeholder='All events'
|
||||
isClearable
|
||||
value={props.eventValue ? { label: mapTypeToBadge[props.eventValue].text, value: props.eventValue } : null}
|
||||
onChange={(v) => props.onEventChange(v ? v.value : null)}
|
||||
options={options}
|
||||
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// const x = Object.values(TournamentEventTypeEnum)
|
||||
42
src/features/Tournaments/pages/EventsPage/EventsPage.tsx
Normal file
42
src/features/Tournaments/pages/EventsPage/EventsPage.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { useState } from 'react'
|
||||
import Button from 'src/Components/Button/Button';
|
||||
import { TournamentEventTypeEnum } from 'src/graphql'
|
||||
import { useTournament } from '../TournamentDetailsPage/TournamentDetailsContext';
|
||||
import EventCard from './EventCard/EventCard';
|
||||
import EventsFilters from './EventsFilters/EventsFilters';
|
||||
|
||||
export default function EventsPage() {
|
||||
|
||||
const [searchFilter, setSearchFilter] = useState("")
|
||||
const [eventFilter, setEventFilter] = useState<TournamentEventTypeEnum | null>(null)
|
||||
const { tournamentDetails: { events, events_count } } = useTournament()
|
||||
|
||||
return (
|
||||
<div className='pb-42'>
|
||||
<div className="flex gap-24 justify-between">
|
||||
<h2 className='text-body1 font-bolder text-gray-900 mb-24'>Events 📆 ({events_count})</h2>
|
||||
<Button size='sm' variant='text' href='https://airtable.com/shrjVx8MjLfl8zyXD' color='gray' newTab className='ml-auto'>List an event</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-16 lg:gap-24">
|
||||
<EventsFilters
|
||||
searchValue={searchFilter}
|
||||
onSearchChange={setSearchFilter}
|
||||
eventValue={eventFilter}
|
||||
onEventChange={setEventFilter}
|
||||
/>
|
||||
{
|
||||
events
|
||||
.filter(event => {
|
||||
if (!searchFilter) return true;
|
||||
return event.title.search(new RegExp(searchFilter, 'i')) !== -1 || event.description.search(new RegExp(searchFilter, 'i')) !== -1
|
||||
})
|
||||
.filter(event => {
|
||||
if (!eventFilter) return true;
|
||||
return event.type === eventFilter;
|
||||
})
|
||||
.map(event => <EventCard key={event.id} event={event} />)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { ModalCard, modalCardVariants } from 'src/Components/Modals/ModalsContainer/ModalsContainer'
|
||||
import { FiGithub, FiLinkedin, FiTwitter } from "react-icons/fi";
|
||||
import { FaDiscord } from 'react-icons/fa'
|
||||
import { IoClose } from 'react-icons/io5';
|
||||
import { GetMakersInTournamentQuery } from 'src/graphql';
|
||||
import Avatar from 'src/features/Profiles/Components/Avatar/Avatar';
|
||||
import { withHttp } from 'src/utils/helperFunctions';
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||
import { NotificationsService } from 'src/services';
|
||||
|
||||
|
||||
interface Props extends ModalCard {
|
||||
maker: GetMakersInTournamentQuery['getMakersInTournament']['makers'][number]
|
||||
}
|
||||
|
||||
export default function LinkingAccountModal({ onClose, direction, maker, ...props }: Props) {
|
||||
|
||||
|
||||
const links = [
|
||||
{
|
||||
value: maker.user.discord,
|
||||
text: maker.user.discord,
|
||||
icon: FaDiscord,
|
||||
colors: "bg-violet-100 text-violet-900",
|
||||
},
|
||||
{
|
||||
value: maker.user.twitter,
|
||||
text: maker.user.twitter,
|
||||
icon: FiTwitter,
|
||||
colors: "bg-blue-100 text-blue-500",
|
||||
url: `https://twitter.com/${maker.user.twitter}`
|
||||
},
|
||||
{
|
||||
value: maker.user.github,
|
||||
text: maker.user.github,
|
||||
icon: FiGithub,
|
||||
colors: "bg-pink-100 text-pink-600",
|
||||
url: `https://github.com/${maker.user.github}`
|
||||
},
|
||||
{
|
||||
value: maker.user.linkedin,
|
||||
text: "LinkedIn",
|
||||
icon: FiLinkedin,
|
||||
colors: "bg-sky-100 text-cyan-600",
|
||||
url: maker.user.linkedin && withHttp(maker.user.linkedin),
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
custom={direction}
|
||||
variants={modalCardVariants}
|
||||
initial='initial'
|
||||
animate="animate"
|
||||
exit='exit'
|
||||
className="modal-card max-w-[442px] rounded-xl relative"
|
||||
>
|
||||
<div className="p-24">
|
||||
<IoClose className='absolute text-body2 top-24 right-24 hover:cursor-pointer' onClick={onClose} />
|
||||
<h2 className='text-h5 font-bold text-center'>Send team request 🤝</h2>
|
||||
</div>
|
||||
<hr className="bg-gray-200" />
|
||||
<div className='flex flex-col justify-center gap-24 items-center text-center p-24'>
|
||||
<Avatar src={maker.user.avatar} width={80} />
|
||||
<div className="flex flex-col gap-4 overflow-hidden max-w-full">
|
||||
<p className="text-body3 text-gray-900 text-ellipsis overflow-hidden">{maker.user.name}</p>
|
||||
<p className="text-body4 text-gray-600">{maker.user.jobTitle}</p>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-600">Team up with this maker by sending them a message on one of the following platforms.</p>
|
||||
<div className="flex gap-24 justify-center">
|
||||
{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 bg-primary-100 text-primary-900`}
|
||||
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>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { lazyModal } from 'src/utils/helperFunctions';
|
||||
|
||||
export const { LazyComponent: ConnectToMakerModal } = lazyModal(() => import('./ConnectToMakerModal'))
|
||||
@@ -0,0 +1,39 @@
|
||||
import Card from 'src/Components/Card/Card';
|
||||
import Badge from 'src/Components/Badge/Badge';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
|
||||
export default function MakerCardSkeleton() {
|
||||
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div className="flex flex-wrap gap-24 items-start">
|
||||
<div className="shrink-0 w-64 md:w-80 aspect-square">
|
||||
<Skeleton circle width={"100%"} height={'100%'} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 flex-1">
|
||||
<p className="text-body2 text-gray-900 font-bold"><Skeleton width={"15ch"} /> </p>
|
||||
<p className="text-body4 text-gray-600 font-medium"><Skeleton width={"25ch"} /> </p>
|
||||
<ul className="hidden md:flex flex-wrap gap-8 mt-4">
|
||||
{Array(3).fill(0).map((_, idx) => <li key={idx}><Badge size='sm' className='!text-body5'> <span className="opacity-0">Loading role</span> </Badge> </li>)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<hr className="hidden md:block bg-gray-200 mt-24"></hr>
|
||||
|
||||
<div className="md:hidden mt-24">
|
||||
<p className="text-body5 text-gray-900 font-medium"><Skeleton width={"7ch"} /></p>
|
||||
<ul className="flex flex-wrap gap-8 mt-4">
|
||||
{Array(3).fill(0).map((_, idx) => <li key={idx}><Badge size='sm' className='!text-body5'> <span className="opacity-0">Loading role</span> </Badge> </li>)}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="mt-24">
|
||||
<p className="text-body5 text-gray-900 font-medium"><Skeleton width={"7ch"} /></p>
|
||||
<ul className="flex flex-wrap gap-8 mt-12">
|
||||
{Array(3).fill(0).map((_, idx) => <li key={idx}><Badge size='sm' className='!text-body5'> <span className="opacity-0">Loading role</span> </Badge> </li>)} </ul>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import Button from "src/Components/Button/Button"
|
||||
import { GetMakersInTournamentQuery, TournamentMakerHackingStatusEnum, useUpdateTournamentRegistrationMutation } from "src/graphql";
|
||||
import { useAppDispatch, } from "src/utils/hooks";
|
||||
import Card from 'src/Components/Card/Card';
|
||||
import Avatar from 'src/features/Profiles/Components/Avatar/Avatar';
|
||||
import Badge from 'src/Components/Badge/Badge';
|
||||
import { createRoute } from 'src/utils/routing';
|
||||
import { openModal } from "src/redux/features/modals.slice";
|
||||
import InfoCard from "src/Components/InfoCard/InfoCard";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useState } from "react";
|
||||
import { NotificationsService } from "src/services";
|
||||
import { useTournament } from "../../TournamentDetailsPage/TournamentDetailsContext";
|
||||
|
||||
type MakerType = GetMakersInTournamentQuery['getMakersInTournament']['makers'][number]
|
||||
|
||||
interface Props {
|
||||
maker: MakerType,
|
||||
isMe?: boolean;
|
||||
}
|
||||
|
||||
export default function MakerCard({ maker, isMe }: Props) {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const [hackingStatus, setHackingStatus] = useState(maker.hacking_status)
|
||||
|
||||
const { tournamentDetails: { id: tournamentId } } = useTournament()
|
||||
|
||||
|
||||
const contactLinksAvailable = maker.user.github || maker.user.linkedin || maker.user.twitter;
|
||||
const [udpateInfo, updateInfoMutation] = useUpdateTournamentRegistrationMutation()
|
||||
|
||||
let actionBtn = <></>
|
||||
|
||||
if (isMe)
|
||||
actionBtn = <Button fullWidth color='white' href={createRoute({ type: 'edit-profile' })} size='sm' className='ml-auto'>Edit Profile</Button>;
|
||||
else if (maker.hacking_status === TournamentMakerHackingStatusEnum.OpenToConnect && contactLinksAvailable)
|
||||
actionBtn = <Button fullWidth color='white' size='sm' className='ml-auto' onClick={() => dispatch(openModal({ Modal: "ConnectToMakerModal", props: { maker } }))}>🤝 Team Up</Button>
|
||||
else if (maker.hacking_status === TournamentMakerHackingStatusEnum.Solo)
|
||||
actionBtn = <Button fullWidth color='white' disabled size='sm' className='ml-auto'>Hacking solo</Button>
|
||||
|
||||
|
||||
const missingFields = isMe && getMissingFields(maker);
|
||||
|
||||
const changeHacktingStatus = (value: typeof hackingStatus) => {
|
||||
setHackingStatus(value);
|
||||
udpateInfo({
|
||||
variables: {
|
||||
tournamentId,
|
||||
data: {
|
||||
hacking_status: value
|
||||
}
|
||||
},
|
||||
})
|
||||
.catch(() => {
|
||||
setHackingStatus(maker.hacking_status)
|
||||
NotificationsService.error("A network error happened")
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div className="flex flex-wrap gap-24 items-start">
|
||||
<div className="shrink-0 w-64 md:w-80">
|
||||
<Avatar src={maker.user.avatar} width={'100%'}></Avatar>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 flex-1 overflow-hidden">
|
||||
<p className="text-body2 text-gray-900 font-bold overflow-hidden text-ellipsis">{maker.user.name}</p>
|
||||
{maker.user.jobTitle ? <p className="text-body4 text-gray-600 font-medium">{maker.user.jobTitle}</p>
|
||||
:
|
||||
<p className="text-body4 text-gray-400 font-medium">No job title</p>}
|
||||
{maker.user.roles.length ? <ul className="hidden md:flex flex-wrap gap-8 mt-4">
|
||||
{maker.user.roles.map(role => <li key={role.id}><Badge size='sm' className='!text-body5'>{role.icon} {role.title}</Badge> </li>)}
|
||||
</ul>
|
||||
:
|
||||
<p className="hidden md:block text-body4 text-gray-400">No roles added</p>
|
||||
}
|
||||
</div>
|
||||
<span className="ml-auto hidden md:inline-block">{actionBtn}</span>
|
||||
</div>
|
||||
<hr className="hidden md:block bg-gray-200 mt-24"></hr>
|
||||
|
||||
<div className="md:hidden mt-24">
|
||||
<p className="text-body5 text-gray-900 font-medium mb-12">🌈 Roles</p>
|
||||
|
||||
{maker.user.roles.length ? <ul className="flex flex-wrap gap-8">
|
||||
{maker.user.roles.map(role => <li key={role.id}><Badge size='sm' className='!text-body5'>{role.icon} {role.title}</Badge> </li>)}
|
||||
</ul>
|
||||
:
|
||||
<p className="text-body4 text-gray-400">No roles added</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className="mt-24">
|
||||
<p className="text-body5 text-gray-900 font-medium mb-12">🛠️ Skills</p>
|
||||
{maker.user.skills.length ? <ul className="flex flex-wrap gap-8">
|
||||
{maker.user.skills.map(skill => <li key={skill.id}><Badge size='sm' className='!text-body5'>{skill.title}</Badge> </li>)}
|
||||
</ul>
|
||||
:
|
||||
<p className="text-body4 text-gray-400">No skills added</p>
|
||||
}
|
||||
</div>
|
||||
{isMe && <div className="mt-24">
|
||||
<p className="text-body5 text-gray-900 font-medium mb-12">🚦 Hacking status</p>
|
||||
<div className="flex flex-wrap gap-8">
|
||||
<button
|
||||
className={`py-8 px-16 text-body5 rounded-10 border ${hackingStatus === TournamentMakerHackingStatusEnum.OpenToConnect ? "bg-primary-100 text-primary-600 border-primary-200" : "bg-gray-50 hover:bg-gray-100 border-gray-200"}`}
|
||||
onClick={() => changeHacktingStatus(TournamentMakerHackingStatusEnum.OpenToConnect)}
|
||||
>👋 Open to connect</button>
|
||||
<button
|
||||
className={`py-8 px-16 text-body5 rounded-10 border ${hackingStatus === TournamentMakerHackingStatusEnum.Solo ? "bg-primary-100 text-primary-600 border-primary-200" : "bg-gray-50 hover:bg-gray-100 border-gray-200"}`}
|
||||
onClick={() => changeHacktingStatus(TournamentMakerHackingStatusEnum.Solo)}
|
||||
>👻 Hacking han solo</button>
|
||||
</div>
|
||||
</div>}
|
||||
<div className="md:hidden w-full mt-24">{actionBtn}</div>
|
||||
{missingFields && <InfoCard className="!bg-warning-50 !border-warning-200 mt-24">
|
||||
<span className="font-bold">👾 Complete your profile:</span> make it easy for other makers to find you by adding your <span className="font-bold">{missingFields}</span>. You can add this information in your profile’s <Link to={createRoute({ type: "edit-profile" })} className='underline text-blue-500'>Settings ⚙️ menu.</Link>
|
||||
</InfoCard>}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
function getMissingFields(maker: Props['maker']) {
|
||||
let res: string[] = [];
|
||||
|
||||
if (!maker.user.jobTitle) res.push("job title")
|
||||
|
||||
if (maker.user.roles.length === 0) res.push('roles')
|
||||
|
||||
if (maker.user.skills.length === 0) res.push('skills')
|
||||
|
||||
if (!maker.user.linkedin && !maker.user.twitter) res.push('contacts')
|
||||
|
||||
return res.join(', ');
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { FiSearch } from 'react-icons/fi'
|
||||
import BasicSelectInput from 'src/Components/Inputs/Selects/BasicSelectInput/BasicSelectInput';
|
||||
import { GenericMakerRole, useGetAllRolesQuery } from 'src/graphql';
|
||||
|
||||
interface Props {
|
||||
searchValue: string;
|
||||
onSearchChange: (new_value: string) => void;
|
||||
|
||||
roleValue: GenericMakerRole | null
|
||||
onRoleChange: (new_value: GenericMakerRole | null) => void;
|
||||
}
|
||||
|
||||
export default function MakersFilters(props: Props) {
|
||||
|
||||
const allRolesQuery = useGetAllRolesQuery();
|
||||
|
||||
const options = allRolesQuery.data?.getAllMakersRoles
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="input-wrapper relative lg:col-span-2">
|
||||
<FiSearch className="self-center ml-16 flex-shrink-0 w-[20px] text-gray-400" />
|
||||
<input
|
||||
type='text'
|
||||
className="input-text"
|
||||
placeholder="Search"
|
||||
value={props.searchValue}
|
||||
onChange={e => props.onSearchChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<BasicSelectInput
|
||||
isMulti={false}
|
||||
isLoading={allRolesQuery.loading}
|
||||
labelField='title'
|
||||
valueField='id'
|
||||
placeholder='Any role'
|
||||
isClearable
|
||||
value={props.roleValue}
|
||||
onChange={props.onRoleChange}
|
||||
options={options ?? []}
|
||||
renderOption={option => <div
|
||||
className={`
|
||||
flex gap-16 my-4 px-16 py-12 rounded-12 text-gray-800 cursor-pointer
|
||||
${!(option.isSelected || option.isFocused) ?
|
||||
"hover:bg-gray-50"
|
||||
:
|
||||
option.isSelected ? "bg-gray-100 text-gray-800" : "bg-gray-50"
|
||||
}
|
||||
`}>
|
||||
{option.data.icon} {option.data.title}
|
||||
</div>}
|
||||
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// const x = Object.values(TournamentEventTypeEnum)
|
||||
32
src/features/Tournaments/pages/MakersPage/MakersPage.tsx
Normal file
32
src/features/Tournaments/pages/MakersPage/MakersPage.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { useMeTournamentQuery, User, } from 'src/graphql'
|
||||
import { useTournament } from '../TournamentDetailsPage/TournamentDetailsContext';
|
||||
import MakerCard from './MakerCard/MakerCard';
|
||||
import MakerCardSkeleton from './MakerCard/MakerCard.Skeleton';
|
||||
import ParticipantsSection from './ParticipantsSection/ParticipantsSection';
|
||||
|
||||
|
||||
|
||||
export default function MakersPage() {
|
||||
|
||||
const { tournamentDetails: { id } } = useTournament()
|
||||
|
||||
const query = useMeTournamentQuery({
|
||||
variables: { id }
|
||||
});
|
||||
|
||||
return (
|
||||
<div className='pb-42'>
|
||||
<div className="flex flex-col gap-16 lg:gap-24">
|
||||
{query.loading ?
|
||||
<MakerCardSkeleton />
|
||||
:
|
||||
query.data?.me ?
|
||||
<MakerCard isMe maker={{ user: query.data.me as User, hacking_status: query.data.tournamentParticipationInfo?.hacking_status! }} />
|
||||
: null
|
||||
}
|
||||
<ParticipantsSection tournamentId={id} />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { FaChevronLeft, FaChevronRight } from 'react-icons/fa';
|
||||
import IconButton from 'src/Components/IconButton/IconButton';
|
||||
import { GetMakersInTournamentQueryVariables, useGetMakersInTournamentQuery } from 'src/graphql';
|
||||
import MakerCard from '../MakerCard/MakerCard';
|
||||
import MakerCardSkeleton from '../MakerCard/MakerCard.Skeleton';
|
||||
|
||||
interface Props {
|
||||
tournamentId: number
|
||||
searchFilter: string,
|
||||
roleFilter: number | null
|
||||
onlyLookingToTeam?: boolean
|
||||
}
|
||||
|
||||
const ITEMS_PER_PAGE = 15;
|
||||
|
||||
export default function MakersList(props: Props) {
|
||||
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const topContainerRef = useRef<HTMLDivElement>(null)
|
||||
const [scrollToTop, setScrollToTop] = useState(false)
|
||||
|
||||
const [queryFilter, setQueryFilter] = useState<GetMakersInTournamentQueryVariables>({
|
||||
tournamentId: props.tournamentId,
|
||||
roleId: props.roleFilter ?? null,
|
||||
search: props.searchFilter ?? null,
|
||||
skip: ITEMS_PER_PAGE * page,
|
||||
take: ITEMS_PER_PAGE,
|
||||
openToConnect: props.onlyLookingToTeam ?? null
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const query = useGetMakersInTournamentQuery({
|
||||
variables: queryFilter,
|
||||
});
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setPage(0);
|
||||
setQueryFilter(f => ({ ...f, search: props.searchFilter, roleId: props.roleFilter, openToConnect: props.onlyLookingToTeam ?? null, skip: 0 }))
|
||||
}, [props.onlyLookingToTeam, props.roleFilter, props.searchFilter]);
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollToTop && topContainerRef.current) {
|
||||
topContainerRef.current.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: "center"
|
||||
})
|
||||
setScrollToTop(false)
|
||||
}
|
||||
}, [scrollToTop])
|
||||
|
||||
|
||||
|
||||
const nextPage = () => {
|
||||
setPage(p => p + 1)
|
||||
setQueryFilter(f => ({ ...f, skip: (f.skip ?? 0) + ITEMS_PER_PAGE }))
|
||||
setScrollToTop(true)
|
||||
}
|
||||
const prevPage = () => {
|
||||
if (page === 0) return
|
||||
setPage(p => p - 1)
|
||||
setQueryFilter(f => ({ ...f, skip: (f.skip ?? 0) - ITEMS_PER_PAGE }))
|
||||
setScrollToTop(true)
|
||||
}
|
||||
|
||||
|
||||
const itemsCount = query.data?.getMakersInTournament && query.data.getMakersInTournament.makers.length;
|
||||
|
||||
return (
|
||||
<div >
|
||||
<div ref={topContainerRef}></div>
|
||||
<div className='flex flex-col gap-16 lg:gap-24'>
|
||||
{
|
||||
query.loading ?
|
||||
<>
|
||||
<div >
|
||||
<MakerCardSkeleton />
|
||||
</div>
|
||||
<MakerCardSkeleton />
|
||||
<MakerCardSkeleton />
|
||||
</>
|
||||
:
|
||||
(itemsCount !== 0 ?
|
||||
query.data?.getMakersInTournament.makers.map(maker => <MakerCard key={maker.user.id} maker={maker} />) :
|
||||
<div className="py-80 text-center text-body2">
|
||||
<p className="text-gray-400">No makers found here...</p>
|
||||
</div>)
|
||||
}
|
||||
< div className='flex justify-center gap-36 text-gray-400' >
|
||||
<IconButton isDisabled={!query.data?.getMakersInTournament.hasPrev} onClick={prevPage}>
|
||||
<FaChevronLeft />
|
||||
</IconButton>
|
||||
<IconButton isDisabled={!query.data?.getMakersInTournament.hasNext} onClick={nextPage} >
|
||||
<FaChevronRight />
|
||||
</IconButton>
|
||||
</div >
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { useDebouncedState } from '@react-hookz/web';
|
||||
import { useState } from 'react'
|
||||
import { GenericMakerRole } from 'src/graphql'
|
||||
import { useCarousel } from 'src/utils/hooks';
|
||||
import MakersFilters from '../MakersFilters/MakersFilters';
|
||||
import MakersList from './MakersList';
|
||||
import ProjectsList from './ProjectsList';
|
||||
|
||||
interface Props {
|
||||
tournamentId: number
|
||||
}
|
||||
export default function ParticipantsSection({ tournamentId }: Props) {
|
||||
|
||||
const [searchFilter, setSearchFilter] = useState("");
|
||||
const [debouncedsearchFilter, setDebouncedSearchFilter] = useDebouncedState("", 500);
|
||||
const [roleFilter, setRoleFilter] = useState<GenericMakerRole | null>(null);
|
||||
const [curTab, setCurTab] = useState<'all-makers' | 'makers-to-team' | 'projects'>('all-makers')
|
||||
|
||||
|
||||
const { viewportRef, } = useCarousel({
|
||||
align: 'start', slidesToScroll: 1,
|
||||
containScroll: "trimSnaps",
|
||||
})
|
||||
|
||||
const changeSearchFilter = (new_value: string) => {
|
||||
setSearchFilter(new_value);
|
||||
setDebouncedSearchFilter(new_value);
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (<>
|
||||
<div className="flex flex-col gap-16">
|
||||
<h3 className="text-body1 text-gray-900 font-bold mt-24">Makers 👾</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-16 lg:gap-24">
|
||||
<MakersFilters
|
||||
searchValue={searchFilter}
|
||||
onSearchChange={changeSearchFilter}
|
||||
roleValue={roleFilter}
|
||||
onRoleChange={setRoleFilter}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-hidden" ref={viewportRef}>
|
||||
<div className="select-none w-full flex gap-8">
|
||||
<button
|
||||
className={`
|
||||
min-w-max rounded-48 px-16 py-8 cursor-pointer font-medium text-body5
|
||||
active:scale-95 transition-transform
|
||||
${curTab === 'all-makers' ? 'bg-primary-100 text-primary-600' : 'bg-gray-100 hover:bg-gray-200 text-gray-600'}
|
||||
`}
|
||||
onClick={() => setCurTab('all-makers')}
|
||||
>
|
||||
All makers
|
||||
</button>
|
||||
<button
|
||||
className={`
|
||||
min-w-max rounded-48 px-16 py-8 cursor-pointer font-medium text-body5
|
||||
active:scale-95 transition-transform
|
||||
${curTab === 'makers-to-team' ? 'bg-primary-100 text-primary-600' : 'bg-gray-100 hover:bg-gray-200 text-gray-600'}
|
||||
`}
|
||||
onClick={() => setCurTab('makers-to-team')}
|
||||
>
|
||||
Makers looking for a team
|
||||
</button>
|
||||
{/* <button
|
||||
className={`
|
||||
min-w-max rounded-48 px-16 py-8 cursor-pointer font-medium text-body5
|
||||
active:scale-95 transition-transform
|
||||
${curTab === 'projects' ? 'bg-primary-100 text-primary-600' : 'bg-gray-100 hover:bg-gray-200 text-gray-600'}
|
||||
`}
|
||||
onClick={() => setCurTab('projects')}
|
||||
>
|
||||
Projects looking for makers
|
||||
</button> */}
|
||||
</div>
|
||||
</div>
|
||||
{curTab === 'projects' && <ProjectsList searchFilter={debouncedsearchFilter} roleFilter={roleFilter?.id ?? null} tournamentId={tournamentId} />}
|
||||
{curTab !== 'projects' && <MakersList onlyLookingToTeam={curTab === 'makers-to-team'} searchFilter={debouncedsearchFilter} roleFilter={roleFilter?.id ?? null} tournamentId={tournamentId} />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { FaChevronLeft, FaChevronRight } from 'react-icons/fa';
|
||||
import IconButton from 'src/Components/IconButton/IconButton';
|
||||
import { GetProjectsInTournamentQueryVariables, useGetProjectsInTournamentQuery } from 'src/graphql';
|
||||
import ProjectCard from '../ProjectCard/ProjectCard';
|
||||
import ProjectCardSkeleton from '../ProjectCard/ProjectCard.Skeleton';
|
||||
|
||||
interface Props {
|
||||
tournamentId: number
|
||||
searchFilter: string,
|
||||
roleFilter: number | null
|
||||
}
|
||||
|
||||
const ITEMS_PER_PAGE = 15;
|
||||
|
||||
export default function ProjectsList(props: Props) {
|
||||
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const topContainerRef = useRef<HTMLDivElement>(null)
|
||||
const [scrollToTop, setScrollToTop] = useState(false)
|
||||
|
||||
const [queryFilter, setQueryFilter] = useState<GetProjectsInTournamentQueryVariables>({
|
||||
tournamentId: props.tournamentId,
|
||||
roleId: props.roleFilter ?? null,
|
||||
search: props.searchFilter ?? null,
|
||||
skip: ITEMS_PER_PAGE * page,
|
||||
take: ITEMS_PER_PAGE,
|
||||
});
|
||||
|
||||
const query = useGetProjectsInTournamentQuery({
|
||||
variables: queryFilter,
|
||||
});
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setPage(0);
|
||||
setQueryFilter(f => ({ ...f, search: props.searchFilter, roleId: props.roleFilter, skip: 0 }))
|
||||
}, [props.roleFilter, props.searchFilter]);
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollToTop && topContainerRef.current) {
|
||||
topContainerRef.current.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: "center"
|
||||
})
|
||||
setScrollToTop(false)
|
||||
}
|
||||
}, [scrollToTop])
|
||||
|
||||
|
||||
|
||||
const nextPage = () => {
|
||||
setPage(p => p + 1)
|
||||
setQueryFilter(f => ({ ...f, skip: (f.skip ?? 0) + ITEMS_PER_PAGE }))
|
||||
setScrollToTop(true)
|
||||
}
|
||||
const prevPage = () => {
|
||||
if (page === 0) return
|
||||
setPage(p => p - 1)
|
||||
setQueryFilter(f => ({ ...f, skip: (f.skip ?? 0) - ITEMS_PER_PAGE }))
|
||||
setScrollToTop(true)
|
||||
}
|
||||
|
||||
|
||||
const itemsCount = query.data?.getProjectsInTournament && query.data.getProjectsInTournament.projects.length;
|
||||
|
||||
return (
|
||||
<div >
|
||||
<div ref={topContainerRef}></div>
|
||||
<div className='flex flex-col gap-16 lg:gap-24'>
|
||||
{
|
||||
query.loading ?
|
||||
<>
|
||||
<ProjectCardSkeleton />
|
||||
|
||||
<ProjectCardSkeleton />
|
||||
<ProjectCardSkeleton />
|
||||
</>
|
||||
:
|
||||
(itemsCount !== 0 ?
|
||||
query.data?.getProjectsInTournament.projects.map(project => <ProjectCard key={project.id} project={project} />) :
|
||||
<div className="py-80 text-center text-body2">
|
||||
<p className="text-gray-400">No projects found here...</p>
|
||||
</div>)
|
||||
}
|
||||
< div className='flex justify-center gap-36 text-gray-400' >
|
||||
<IconButton isDisabled={!query.data?.getProjectsInTournament.hasPrev} onClick={prevPage}>
|
||||
<FaChevronLeft />
|
||||
</IconButton>
|
||||
<IconButton isDisabled={!query.data?.getProjectsInTournament.hasNext} onClick={nextPage} >
|
||||
<FaChevronRight />
|
||||
</IconButton>
|
||||
</div >
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import Card from 'src/Components/Card/Card';
|
||||
import Badge from 'src/Components/Badge/Badge';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
|
||||
export default function ProjectCardSkeleton() {
|
||||
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div className="flex flex-wrap gap-24 items-start">
|
||||
<div className="shrink-0 w-64 md:w-80 aspect-square">
|
||||
<Skeleton borderRadius={16} width={"100%"} height={'100%'} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 flex-1">
|
||||
<p className="text-body2 text-gray-900 font-bold"><Skeleton width={"15ch"} /> </p>
|
||||
<p className="text-body4 text-gray-600 font-medium"><Skeleton width={"8ch"} /> </p>
|
||||
<p className="text-body5 text-gray-600 font-medium"><Skeleton width={"35ch"} /> </p>
|
||||
</div>
|
||||
</div>
|
||||
<hr className="hidden md:block bg-gray-200 mt-24"></hr>
|
||||
|
||||
<div className="md:hidden mt-24">
|
||||
<p className="text-body5 text-gray-900 font-medium"><Skeleton width={"7ch"} /></p>
|
||||
<ul className="flex flex-wrap gap-8 mt-4">
|
||||
{Array(3).fill(0).map((_, idx) => <li key={idx}><Badge size='sm' className='!text-body5'> <span className="opacity-0">Loading role</span> </Badge> </li>)}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="mt-24">
|
||||
<p className="text-body5 text-gray-900 font-medium"><Skeleton width={"7ch"} /></p>
|
||||
<ul className="flex flex-wrap gap-8 mt-12">
|
||||
{Array(3).fill(0).map((_, idx) => <li key={idx}><Badge size='sm' className='!text-body5'> <span className="opacity-0">Loading role</span> </Badge> </li>)} </ul>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { GetProjectsInTournamentQuery, } from "src/graphql";
|
||||
import Card from 'src/Components/Card/Card';
|
||||
import Badge from 'src/Components/Badge/Badge';
|
||||
import Button from "src/Components/Button/Button"
|
||||
import { createRoute } from "src/utils/routing";
|
||||
import { useAppDispatch } from "src/utils/hooks";
|
||||
import { openModal } from "src/redux/features/modals.slice";
|
||||
|
||||
type ProjectType = GetProjectsInTournamentQuery['getProjectsInTournament']['projects'][number]
|
||||
|
||||
interface Props {
|
||||
project: ProjectType,
|
||||
}
|
||||
|
||||
export default function ProjectCard({ project }: Props) {
|
||||
|
||||
const showLookingFor = project.recruit_roles.length > 0;
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const openProject = () => {
|
||||
dispatch(openModal({
|
||||
Modal: "ProjectDetailsCard",
|
||||
isPageModal: true,
|
||||
props: {
|
||||
projectId: project.id
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div className="flex flex-wrap gap-24 items-start">
|
||||
<img src={project.thumbnail_image} className='shrink-0 w-64 md:w-80 aspect-square rounded-16 outline outline-2 outline-gray-200' alt="" />
|
||||
<div className="flex flex-col gap-4 flex-1 overflow-hidden">
|
||||
<p className="text-body2 text-gray-900 font-bold">{project.title}</p>
|
||||
<p className="text-body4 text-gray-600 font-medium">{project.category.icon} {project.category.title}</p>
|
||||
<div className="hidden md:block">
|
||||
<p className="text-body5 text-gray-400 line-clamp-2 max-w-[60ch]">{project.description} </p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="ml-auto hidden md:inline-block"><Button color='white' onClick={openProject} size='sm' className='ml-auto'>View Details</Button></span>
|
||||
|
||||
</div>
|
||||
{showLookingFor && <hr className="hidden md:block bg-gray-200 mt-24"></hr>}
|
||||
<p className="md:hidden mt-24 text-body5 text-gray-400 line-clamp-2 max-w-[60ch]">{project.description} </p>
|
||||
{showLookingFor && <div className="mt-24">
|
||||
<p className="text-body5 text-gray-900 font-medium mb-12">👀 Looking for</p>
|
||||
{project.recruit_roles.length ? <ul className="flex flex-wrap gap-8">
|
||||
{project.recruit_roles.map(role => <li key={role.id}><Badge size='sm' className='!text-body5'>{role.icon} {role.title}</Badge> </li>)}
|
||||
</ul>
|
||||
:
|
||||
null
|
||||
}
|
||||
</div>}
|
||||
<Button fullWidth color='white' onClick={openProject} size='sm' className='mt-32 md:hidden'>View Details</Button>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
query GetAllRoles {
|
||||
getAllMakersRoles {
|
||||
id
|
||||
title
|
||||
icon
|
||||
}
|
||||
}
|
||||
|
||||
query GetMakersInTournament(
|
||||
$tournamentId: Int!
|
||||
$take: Int
|
||||
$skip: Int
|
||||
$search: String
|
||||
$roleId: Int
|
||||
$openToConnect: Boolean
|
||||
) {
|
||||
getMakersInTournament(
|
||||
tournamentId: $tournamentId
|
||||
take: $take
|
||||
skip: $skip
|
||||
search: $search
|
||||
roleId: $roleId
|
||||
openToConnect: $openToConnect
|
||||
) {
|
||||
hasNext
|
||||
hasPrev
|
||||
makers {
|
||||
hacking_status
|
||||
user {
|
||||
id
|
||||
name
|
||||
avatar
|
||||
jobTitle
|
||||
discord
|
||||
twitter
|
||||
linkedin
|
||||
github
|
||||
roles {
|
||||
id
|
||||
icon
|
||||
title
|
||||
}
|
||||
skills {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query GetProjectsInTournament(
|
||||
$tournamentId: Int!
|
||||
$take: Int
|
||||
$skip: Int
|
||||
$roleId: Int
|
||||
$search: String
|
||||
) {
|
||||
getProjectsInTournament(
|
||||
tournamentId: $tournamentId
|
||||
take: $take
|
||||
skip: $skip
|
||||
roleId: $roleId
|
||||
search: $search
|
||||
) {
|
||||
hasNext
|
||||
hasPrev
|
||||
projects {
|
||||
id
|
||||
title
|
||||
description
|
||||
thumbnail_image
|
||||
|
||||
category {
|
||||
id
|
||||
title
|
||||
icon
|
||||
}
|
||||
|
||||
recruit_roles {
|
||||
id
|
||||
title
|
||||
icon
|
||||
level
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mutation UpdateTournamentRegistration(
|
||||
$tournamentId: Int!
|
||||
$data: UpdateTournamentRegistrationInput
|
||||
) {
|
||||
updateTournamentRegistration(tournament_id: $tournamentId, data: $data) {
|
||||
createdAt
|
||||
email
|
||||
hacking_status
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import DOMPurify from 'dompurify';
|
||||
import { marked } from 'marked';
|
||||
import React, { useMemo } from 'react'
|
||||
import Accordion from 'src/Components/Accordion/Accordion';
|
||||
import { Tournament } from 'src/graphql'
|
||||
|
||||
interface Props {
|
||||
faqs: Tournament['faqs']
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default function FAQsSection({ faqs }: Props) {
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className='text-body1 font-bolder text-gray-900 mb-4'>FAQs</h2>
|
||||
<Accordion
|
||||
classes={{
|
||||
heading: "!text-body3"
|
||||
}}
|
||||
items={faqs.map(faq => ({
|
||||
heading: faq.question, content: <div
|
||||
className={`text-gray-600 prose `}
|
||||
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(marked.parse(faq.answer)) }}
|
||||
>
|
||||
</div>
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import { Tournament } from 'src/graphql'
|
||||
|
||||
interface Props {
|
||||
judges: Tournament['judges']
|
||||
}
|
||||
|
||||
const bgColors = ['#FDE68A', '#FECACA', '#BFDBFE', '#BBF7D0', '#DDD6FE', '#FBCFE8', '#FED7AA'];
|
||||
|
||||
export default function JudgesSection({ judges }: Props) {
|
||||
|
||||
const colors = useMemo(() => {
|
||||
return judges.map((_, i) => bgColors[i % bgColors.length])
|
||||
}, [judges])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className='text-body1 font-bolder text-gray-900 mb-16'>Judges</h2>
|
||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(167px,1fr))] gap-8 md:gap-24">
|
||||
{judges.map((judge, idx) => <div
|
||||
key={idx}
|
||||
className="p-16 rounded-16 flex flex-col justify-center items-center gap-16 md:gap-24"
|
||||
style={{ backgroundColor: colors[idx] }}
|
||||
>
|
||||
<img src={judge.avatar} className='w-[100px] md:w-[128px] aspect-square object-contain' alt="" />
|
||||
<div className='text-center'>
|
||||
<p className='text-body4 font-medium'>{judge.name}</p>
|
||||
<p className='text-body4 mt-4'>{judge.company}</p>
|
||||
</div>
|
||||
</div>)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
34
src/features/Tournaments/pages/OverviewPage/OverviewPage.tsx
Normal file
34
src/features/Tournaments/pages/OverviewPage/OverviewPage.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import DOMPurify from 'dompurify'
|
||||
import { marked } from 'marked'
|
||||
import Card from 'src/Components/Card/Card'
|
||||
import { Tournament } from 'src/graphql'
|
||||
import { useTournament } from '../TournamentDetailsPage/TournamentDetailsContext'
|
||||
import FAQsSection from './FAQsSection/FAQsSection'
|
||||
import JudgesSection from './JudgesSection/JudgesSection'
|
||||
import PrizesSection from './PrizesSection/PrizesSection'
|
||||
import RegisterCard from './RegisterCard/RegisterCard'
|
||||
|
||||
|
||||
|
||||
export default function OverviewPage() {
|
||||
|
||||
const { tournamentDetails, makers, myParticipationInfo } = useTournament()
|
||||
|
||||
return (
|
||||
<Card onlyMd className='flex flex-col gap-42 bg-white max-md:-mx-16 max-md:-mt-24 px-16'>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-24 items-start">
|
||||
<div className='md:col-span-2'>
|
||||
<div
|
||||
className={`text-gray-600 mt-16 prose `}
|
||||
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(marked.parse(tournamentDetails.description)) }}
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<RegisterCard makers_count={tournamentDetails.makers_count} start_date={tournamentDetails.start_date} avatars={makers.map(m => m.user.avatar)} isRegistered={!!myParticipationInfo} />
|
||||
</div>
|
||||
<PrizesSection prizes={tournamentDetails.prizes} />
|
||||
<JudgesSection judges={tournamentDetails.judges} />
|
||||
<FAQsSection faqs={tournamentDetails.faqs} />
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react'
|
||||
import { Tournament } from 'src/graphql'
|
||||
import styles from './styles.module.scss'
|
||||
|
||||
interface Props {
|
||||
prizes: Tournament['prizes']
|
||||
}
|
||||
|
||||
export default function PrizesSection({ prizes }: Props) {
|
||||
return (
|
||||
<div>
|
||||
<h2 className='text-body1 font-bolder text-gray-900 mb-16'>Prizes</h2>
|
||||
<div className={styles.grid}>
|
||||
{prizes.map((prize, idx) => <div
|
||||
key={idx}
|
||||
className='bg-gray-50 rounded-16 py-24 px-32'>
|
||||
<img src={prize.image} className=' max-w-[64px]' alt="" />
|
||||
<div>
|
||||
<h3 className="text-h2">{prize.title}</h3>
|
||||
<p className="text-h1 text-green-500">{prize.amount}</p>
|
||||
</div>
|
||||
</div>)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
@import "/src/styles/mixins";
|
||||
|
||||
@import url("https://fonts.googleapis.com/css2?family=Luckiest+Guy&display=swap");
|
||||
|
||||
.grid {
|
||||
font-family: "Luckiest Guy", cursive;
|
||||
display: grid;
|
||||
gap: 24px;
|
||||
> div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
h3 {
|
||||
font-size: 20px;
|
||||
color: white;
|
||||
-webkit-text-stroke: 1px black;
|
||||
}
|
||||
p {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
|
||||
img {
|
||||
max-width: 100px;
|
||||
}
|
||||
h3 {
|
||||
font-size: 24px;
|
||||
margin: 40px auto 0 auto;
|
||||
}
|
||||
p {
|
||||
font-size: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:first-child) {
|
||||
justify-content: space-between;
|
||||
text-align: right;
|
||||
img {
|
||||
max-width: 42px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
grid-auto-rows: 120px;
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
> div:first-child {
|
||||
grid-row: span 3;
|
||||
}
|
||||
|
||||
@include gt-md {
|
||||
grid-auto-rows: 150px;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
> div {
|
||||
h3 {
|
||||
font-size: 32px;
|
||||
-webkit-text-stroke: 2px black;
|
||||
}
|
||||
p {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
grid-row: 1/4;
|
||||
grid-column: 1/2;
|
||||
img {
|
||||
max-width: 160px;
|
||||
}
|
||||
h3 {
|
||||
font-size: 32px;
|
||||
margin: 24px auto 24px auto;
|
||||
}
|
||||
p {
|
||||
font-size: 56px;
|
||||
}
|
||||
}
|
||||
&:not(:first-child) {
|
||||
img {
|
||||
max-width: 64px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import React from 'react'
|
||||
import { FaUsers } from 'react-icons/fa'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import Button from 'src/Components/Button/Button'
|
||||
import Card from 'src/Components/Card/Card'
|
||||
import Avatar from 'src/features/Profiles/Components/Avatar/Avatar'
|
||||
import { openModal } from 'src/redux/features/modals.slice'
|
||||
import { useCountdown } from 'src/utils/hooks'
|
||||
import { useAppDispatch, useAppSelector } from "src/utils/hooks";
|
||||
|
||||
interface Props {
|
||||
start_date: string;
|
||||
makers_count: number
|
||||
avatars: string[]
|
||||
isRegistered: boolean;
|
||||
}
|
||||
|
||||
export default function RegisterCard({ makers_count, start_date, avatars, isRegistered }: Props) {
|
||||
|
||||
const counter = useCountdown(start_date)
|
||||
const { id: tournamentId } = useParams()
|
||||
|
||||
|
||||
const isLoggedIn = useAppSelector(state => !!state.user.me)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const onRegister = () => {
|
||||
if (!tournamentId) return;
|
||||
|
||||
if (isLoggedIn)
|
||||
dispatch(openModal({
|
||||
Modal: "RegisterTournamet_ConfrimAccount",
|
||||
props: {
|
||||
tournamentId: Number(tournamentId)
|
||||
}
|
||||
}))
|
||||
else
|
||||
dispatch(openModal({
|
||||
Modal: "RegisterTournamet_Login",
|
||||
props: {
|
||||
tournamentId: Number(tournamentId)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Card onlyMd className='flex flex-col gap-24 !border'>
|
||||
<div>
|
||||
{makers_count > 2 && <p className="text-body5 text-gray-600 flex">
|
||||
{avatars.map((img, idx) => <div className='w-[16px] h-32 relative'><Avatar key={idx} src={img} width={32} className='absolute top-0 left-0 min-w-[32px] !border-white' /></div>)}
|
||||
<span className='self-center ml-24 font-medium '>+ {makers_count} makers</span>
|
||||
</p>}
|
||||
<Button color={isRegistered ? 'gray' : "primary"} disabled={isRegistered} fullWidth className='mt-16' onClick={onRegister}>{isRegistered ? "Registered!" : "Register Now"}</Button>
|
||||
</div>
|
||||
<div>
|
||||
{counter.isExpired ?
|
||||
<p className="text-body3 text-gray-600 text-center">Tournament running!</p>
|
||||
:
|
||||
<>
|
||||
<p className="text-body5 text-gray-900 font-medium">
|
||||
Tournament starts in
|
||||
</p>
|
||||
<div className="grid grid-cols-3 gap-10 mt-16">
|
||||
<div className="border border-gray-200 rounded-10 flex flex-col py-10 justify-center items-center text-primary-600 text-body3 font-medium">
|
||||
{counter.days}d
|
||||
</div>
|
||||
<div className="border border-gray-200 rounded-10 flex flex-col py-10 justify-center items-center text-primary-600 text-body3 font-medium">
|
||||
{counter.hours}h
|
||||
</div>
|
||||
<div className="border border-gray-200 rounded-10 flex flex-col py-10 justify-center items-center text-primary-600 text-body3 font-medium">
|
||||
{counter.minutes}m
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-body5 text-gray-900 font-medium">
|
||||
Sponsored by
|
||||
</p>
|
||||
<img src={'/assets/images/logos/fulgur_logo.svg'} alt="Fulgur Ventures Logo" className='max-h-48 mt-16 ' />
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { ModalCard, modalCardVariants } from 'src/Components/Modals/ModalsContainer/ModalsContainer'
|
||||
import { IoClose } from 'react-icons/io5';
|
||||
import Avatar from 'src/features/Profiles/Components/Avatar/Avatar';
|
||||
import { useAppDispatch, useAppSelector } from "src/utils/hooks";
|
||||
import Button from 'src/Components/Button/Button';
|
||||
import { Direction, replaceModal } from 'src/redux/features/modals.slice';
|
||||
|
||||
|
||||
interface Props extends ModalCard {
|
||||
tournamentId: number
|
||||
}
|
||||
|
||||
export default function ConfirmAccount({ onClose, direction, tournamentId, ...props }: Props) {
|
||||
|
||||
const me = useAppSelector(state => state.user.me)
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
if (!me)
|
||||
return null;
|
||||
|
||||
const onCancel = () => onClose?.();
|
||||
const onContinue = () => {
|
||||
dispatch(replaceModal({
|
||||
Modal: "RegisterTournamet_RegistrationDetails",
|
||||
direction: Direction.NEXT,
|
||||
props: {
|
||||
tournamentId
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
custom={direction}
|
||||
variants={modalCardVariants}
|
||||
initial='initial'
|
||||
animate="animate"
|
||||
exit='exit'
|
||||
className="modal-card max-w-[442px] rounded-xl relative"
|
||||
>
|
||||
<div className="p-16 md:p-24">
|
||||
<IoClose className='absolute text-body2 top-16 right-16 hover:cursor-pointer' onClick={onClose} />
|
||||
<h2 className='text-h5 font-bold text-center'>Register for tournament</h2>
|
||||
</div>
|
||||
<hr className="bg-gray-200" />
|
||||
<div className='flex flex-col justify-center gap-16 items-center text-center p-16 md:p-24'>
|
||||
<Avatar src={me.avatar} width={80} />
|
||||
<div className="flex flex-col gap-4 overflow-hidden max-w-full">
|
||||
<p className="text-body3 text-gray-900 text-ellipsis overflow-hidden">{me.name}</p>
|
||||
<p className="text-body4 text-gray-600">{me.jobTitle}</p>
|
||||
</div>
|
||||
|
||||
|
||||
<p className="text-body4 text-gray-600">You are currently signed in using this profile. Would you like to continue with your ⚔️ Tournament registration?</p>
|
||||
|
||||
<div className="grid grid-cols-2 gap-16 w-full">
|
||||
<Button color='gray' onClick={onCancel}>Cancel</Button>
|
||||
<Button color='primary' onClick={onContinue}>Continue</Button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { lazyModal } from 'src/utils/helperFunctions';
|
||||
|
||||
export const { LazyComponent: ConfirmAccount } = lazyModal(() => import('./ConfirmAccount'))
|
||||
@@ -0,0 +1,186 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { ModalCard, modalCardVariants } from 'src/Components/Modals/ModalsContainer/ModalsContainer'
|
||||
import { FiCopy } from "react-icons/fi";
|
||||
import { IoClose, IoRocketOutline } from 'react-icons/io5';
|
||||
import { useMeTournamentQuery } from 'src/graphql';
|
||||
import Button from 'src/Components/Button/Button';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { Grid } from 'react-loader-spinner';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { CONSTS } from 'src/utils';
|
||||
import useCopyToClipboard from 'src/utils/hooks/useCopyToClipboard';
|
||||
import { useLnurlQuery } from 'src/features/Auth/pages/LoginPage/LoginPage';
|
||||
import { useAppDispatch } from 'src/utils/hooks';
|
||||
import { Direction, replaceModal } from 'src/redux/features/modals.slice';
|
||||
import { NotificationsService } from 'src/services';
|
||||
|
||||
|
||||
interface Props extends ModalCard {
|
||||
tournamentId: number
|
||||
}
|
||||
|
||||
export default function LinkingAccountModal({ onClose, direction, tournamentId, ...props }: Props) {
|
||||
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const { loadingLnurl, data: { lnurl, session_token }, error } = useLnurlQuery();
|
||||
const clipboard = useCopyToClipboard();
|
||||
|
||||
const canFetchIsLogged = useRef(true)
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setCopied(false);
|
||||
}, [lnurl])
|
||||
|
||||
const meQuery = useMeTournamentQuery({
|
||||
variables: {
|
||||
id: tournamentId
|
||||
},
|
||||
onCompleted: (data) => {
|
||||
if (data.me) {
|
||||
const already_registerd = !!data.tournamentParticipationInfo;
|
||||
if (already_registerd) {
|
||||
onClose?.();
|
||||
NotificationsService.info("You are already registered")
|
||||
}
|
||||
else dispatch(replaceModal({
|
||||
Modal: "RegisterTournamet_RegistrationDetails",
|
||||
direction: Direction.NEXT,
|
||||
props: { tournamentId }
|
||||
}))
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
const copyToClipboard = () => {
|
||||
setCopied(true);
|
||||
clipboard(lnurl);
|
||||
}
|
||||
|
||||
const refetch = meQuery.refetch;
|
||||
const startPolling = useCallback(
|
||||
() => {
|
||||
const interval = setInterval(() => {
|
||||
if (canFetchIsLogged.current === false) return;
|
||||
|
||||
canFetchIsLogged.current = false;
|
||||
fetch(CONSTS.apiEndpoint + '/is-logged-in', {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
session_token
|
||||
}
|
||||
})
|
||||
.then(data => data.json())
|
||||
.then(data => {
|
||||
if (data.logged_in) {
|
||||
clearInterval(interval)
|
||||
refetch();
|
||||
}
|
||||
})
|
||||
.catch()
|
||||
.finally(() => {
|
||||
canFetchIsLogged.current = true;
|
||||
})
|
||||
}, 2000);
|
||||
|
||||
return interval;
|
||||
}
|
||||
, [refetch, session_token],
|
||||
)
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
let interval: NodeJS.Timer;
|
||||
if (lnurl)
|
||||
interval = startPolling();
|
||||
|
||||
return () => {
|
||||
canFetchIsLogged.current = true;
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, [lnurl, startPolling])
|
||||
|
||||
|
||||
|
||||
let content = <></>
|
||||
|
||||
if (error)
|
||||
content = <div className="flex flex-col gap-24 items-center">
|
||||
<p className="text-body3 text-red-500 font-bold">Something wrong happened...</p>
|
||||
<a href='/login' className="text body4 text-gray-500 hover:underline">Please try again</a>
|
||||
</div>
|
||||
|
||||
else if (loadingLnurl)
|
||||
content = <div className="flex flex-col gap-24 py-48 items-center">
|
||||
<Grid color="var(--primary)" width="150" />
|
||||
<p className="text-body3 font-bold">Fetching Lnurl-Auth link</p>
|
||||
</div>
|
||||
|
||||
|
||||
else
|
||||
content = <div className="flex flex-col justify-center gap-24 items-center text-center" >
|
||||
<a href={`lightning:${lnurl}`} >
|
||||
<QRCodeSVG
|
||||
width={280}
|
||||
height={280}
|
||||
value={lnurl}
|
||||
bgColor='transparent'
|
||||
imageSettings={{
|
||||
src: '/assets/images/nut_3d.png',
|
||||
width: 16,
|
||||
height: 16,
|
||||
excavate: true,
|
||||
|
||||
}}
|
||||
/>
|
||||
</a>
|
||||
<p className="text-gray-600 text-body4 text-left">
|
||||
To register for this tournament, you need a maker profile. Luckily, this is very easy!
|
||||
<br />
|
||||
To sign in or create an account, just scan this QR, or click to connect using any lightning wallet like <a href="https://getalby.com" className='underline' target='_blank' rel="noreferrer">Alby</a> or <a href="https://breez.technology/" className='underline' target='_blank' rel="noreferrer">Breez</a>.
|
||||
</p>
|
||||
<div className="w-full grid grid-cols-2 gap-16">
|
||||
<a href={`lightning:${lnurl}`}
|
||||
className='block text-body4 text-center text-white bg-primary-500 hover:bg-primary-600 rounded-10 px-16 py-12 active:scale-90 transition-transform'
|
||||
>Click to connect <IoRocketOutline /></a>
|
||||
<Button
|
||||
color='gray'
|
||||
onClick={copyToClipboard}
|
||||
>{copied ? "Copied" : "Copy"} <FiCopy /></Button>
|
||||
<a href={`https://makers.bolt.fun/blog/post/story/99/sign-in-with-lightning`} target='_blank' rel="noreferrer"
|
||||
className='col-span-2 block text-body4 text-center text-gray-900 border border-gray-200 rounded-10 px-16 py-12 active:scale-90 transition-transform'
|
||||
>What is a lightning wallet?</a>
|
||||
</div>
|
||||
|
||||
</div>;
|
||||
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
custom={direction}
|
||||
variants={modalCardVariants}
|
||||
initial='initial'
|
||||
animate="animate"
|
||||
exit='exit'
|
||||
className="modal-card max-w-[442px] rounded-xl relative"
|
||||
>
|
||||
<div className="p-16 md:p-24">
|
||||
<IoClose className='absolute text-body2 top-16 right-16 hover:cursor-pointer' onClick={onClose} />
|
||||
<h2 className='text-h5 font-bold text-center'>Connect ⚡️ your maker profile</h2>
|
||||
</div>
|
||||
<hr className="bg-gray-200" />
|
||||
<div className=' p-16 md:p-24'>
|
||||
{content}
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { lazyModal } from 'src/utils/helperFunctions';
|
||||
|
||||
export const { LazyComponent: LoginModal } = lazyModal(() => import('./LoginModal'))
|
||||
@@ -0,0 +1,184 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { ModalCard, modalCardVariants } from 'src/Components/Modals/ModalsContainer/ModalsContainer'
|
||||
import { IoClose } from 'react-icons/io5';
|
||||
import { useAppDispatch, useAppSelector } from "src/utils/hooks";
|
||||
import Button from 'src/Components/Button/Button';
|
||||
import { Direction, replaceModal } from 'src/redux/features/modals.slice';
|
||||
import BasicSelectInput from 'src/Components/Inputs/Selects/BasicSelectInput/BasicSelectInput';
|
||||
import { GetTournamentByIdDocument, TournamentMakerHackingStatusEnum, useRegisterInTournamentMutation } from 'src/graphql';
|
||||
import InfoCard from 'src/Components/InfoCard/InfoCard';
|
||||
import * as yup from "yup";
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { Controller, SubmitHandler, useForm } from 'react-hook-form';
|
||||
import { NotificationsService } from "src/services/notifications.service";
|
||||
|
||||
|
||||
interface Props extends ModalCard {
|
||||
tournamentId: number
|
||||
}
|
||||
|
||||
const hackingStatusOptions = [
|
||||
{
|
||||
label: "Hacking han solo 👻",
|
||||
value: TournamentMakerHackingStatusEnum.Solo
|
||||
}, {
|
||||
label: "Open to connect 👋",
|
||||
value: TournamentMakerHackingStatusEnum.OpenToConnect
|
||||
},
|
||||
]
|
||||
|
||||
interface IFormInputs {
|
||||
email: string;
|
||||
agreement: boolean;
|
||||
hacking_status: typeof hackingStatusOptions[number];
|
||||
}
|
||||
|
||||
|
||||
const schema: yup.SchemaOf<IFormInputs> = yup.object({
|
||||
email: yup.string().required().email(),
|
||||
hacking_status: yup.object().shape({
|
||||
label: yup.string().required(),
|
||||
value: yup.string().required()
|
||||
}).required(),
|
||||
agreement: yup.boolean().required().isTrue("You won't be able to follow the updates/events of the tournament if you don't allow this"),
|
||||
}).required();
|
||||
|
||||
export default function RegistrationDetails({ onClose, direction, ...props }: Props) {
|
||||
|
||||
|
||||
const me = useAppSelector(state => state.user.me)
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [mutate, mutationStatus] = useRegisterInTournamentMutation()
|
||||
|
||||
|
||||
const { handleSubmit, control, register, formState: { errors }, } = useForm<IFormInputs>({
|
||||
mode: "onChange",
|
||||
resolver: yupResolver(schema),
|
||||
defaultValues: {
|
||||
email: "",
|
||||
hacking_status: hackingStatusOptions[0]
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
if (!me)
|
||||
return null;
|
||||
|
||||
|
||||
const onCancel = () => onClose?.();
|
||||
|
||||
const onSubmit: SubmitHandler<IFormInputs> = data => {
|
||||
mutate({
|
||||
variables: {
|
||||
data: {
|
||||
email: data.email,
|
||||
hacking_status: data.hacking_status.value,
|
||||
},
|
||||
tournamentId: Number(props.tournamentId)
|
||||
},
|
||||
onCompleted: (data) => {
|
||||
if (data.registerInTournament?.in_tournament) {
|
||||
dispatch(replaceModal({
|
||||
Modal: "RegisterTournamet_RegistrationSuccess",
|
||||
direction: Direction.NEXT,
|
||||
props: {
|
||||
tournamentId: Number(props.tournamentId)
|
||||
}
|
||||
}))
|
||||
|
||||
}
|
||||
},
|
||||
refetchQueries: [{
|
||||
query: GetTournamentByIdDocument,
|
||||
variables: {
|
||||
id: props.tournamentId
|
||||
}
|
||||
}]
|
||||
})
|
||||
.catch(() => {
|
||||
NotificationsService.error("A network error happned...")
|
||||
mutationStatus.reset()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
custom={direction}
|
||||
variants={modalCardVariants}
|
||||
initial='initial'
|
||||
animate="animate"
|
||||
exit='exit'
|
||||
className="modal-card max-w-[442px] rounded-xl relative "
|
||||
>
|
||||
<div className="p-16 md:p-24">
|
||||
<IoClose className='absolute text-body2 top-16 right-16 hover:cursor-pointer' onClick={onClose} />
|
||||
<h2 className='text-h5 font-bold text-center'>Register for tournament</h2>
|
||||
</div>
|
||||
<hr className="bg-gray-200" />
|
||||
<form onSubmit={handleSubmit(onSubmit)} className='flex flex-col gap-24 p-16 md:p-24'>
|
||||
<p className="text-body4 text-gray-600">Please provide us with some additional details below.</p>
|
||||
|
||||
<div className='flex flex-col gap-8'>
|
||||
<label className="text-body5 text-gray-600 font-medium">Hacking status</label>
|
||||
<Controller
|
||||
name="hacking_status"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => <BasicSelectInput
|
||||
isMulti={false}
|
||||
labelField='label'
|
||||
valueField='value'
|
||||
placeholder='Your hacking status'
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
options={hackingStatusOptions}
|
||||
/>}
|
||||
/>
|
||||
|
||||
<InfoCard >
|
||||
<span className="font-bold">👋 Details:</span> other makers will be able to see your hacker card and send you Team Up requests.
|
||||
</InfoCard>
|
||||
</div>
|
||||
<div className='flex flex-col gap-8'>
|
||||
<label className="text-body5 text-gray-600 font-medium">Email address*</label>
|
||||
<div className="input-wrapper relative">
|
||||
<input
|
||||
type='text'
|
||||
className="input-text"
|
||||
placeholder="johndoe@gmail.com"
|
||||
{...register("email")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{errors.email && <p className="input-error">
|
||||
{errors.email.message}
|
||||
</p>}
|
||||
<div className="mt-12 flex gap-12">
|
||||
<input
|
||||
className='input-checkbox self-center cursor-pointer'
|
||||
type="checkbox"
|
||||
{...register('agreement', {})} />
|
||||
<label className="text-body5 text-gray-600" >
|
||||
Send me news and updates about the tournament.
|
||||
<br />
|
||||
No spam!
|
||||
</label>
|
||||
</div>
|
||||
{errors.agreement && <p className="input-error">
|
||||
{errors.agreement.message}
|
||||
</p>}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-16">
|
||||
<Button color='gray' onClick={onCancel}>Cancel</Button>
|
||||
<Button type='submit' color='primary'>Continue</Button>
|
||||
</div>
|
||||
</form>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { lazyModal } from 'src/utils/helperFunctions';
|
||||
|
||||
export const { LazyComponent: RegistrationDetails } = lazyModal(() => import('./RegistrationDetails'))
|
||||
@@ -0,0 +1,9 @@
|
||||
mutation RegisterInTournament(
|
||||
$tournamentId: Int!
|
||||
$data: RegisterInTournamentInput
|
||||
) {
|
||||
registerInTournament(tournament_id: $tournamentId, data: $data) {
|
||||
id
|
||||
in_tournament(id: $tournamentId)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { ModalCard, modalCardVariants } from 'src/Components/Modals/ModalsContainer/ModalsContainer'
|
||||
import { IoClose } from 'react-icons/io5';
|
||||
import Avatar from 'src/features/Profiles/Components/Avatar/Avatar';
|
||||
import { useAppSelector } from "src/utils/hooks";
|
||||
import Button from 'src/Components/Button/Button';
|
||||
import Confetti from "react-confetti";
|
||||
import { Portal } from 'src/Components/Portal/Portal';
|
||||
import { createRoute } from 'src/utils/routing';
|
||||
|
||||
|
||||
interface Props extends ModalCard {
|
||||
tournamentId: number
|
||||
}
|
||||
|
||||
export default function RegistrationSuccess({ onClose, direction, ...props }: Props) {
|
||||
|
||||
const me = useAppSelector(state => state.user.me)
|
||||
|
||||
if (!me)
|
||||
throw new Error("User not defined");
|
||||
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
custom={direction}
|
||||
variants={modalCardVariants}
|
||||
initial='initial'
|
||||
animate="animate"
|
||||
exit='exit'
|
||||
className="modal-card max-w-[442px] rounded-xl relative"
|
||||
>
|
||||
<div className="p-16 md:p-24">
|
||||
<IoClose className='absolute text-body2 top-16 right-16 hover:cursor-pointer' onClick={onClose} />
|
||||
<h2 className='text-h5 font-bold text-center'>Registration succeeded!! ✅</h2>
|
||||
</div>
|
||||
<hr className="bg-gray-200" />
|
||||
<div className='flex flex-col justify-center gap-16 items-center text-center p-16 md:p-24'>
|
||||
<Avatar src={me.avatar} width={80} />
|
||||
<div className="flex flex-col gap-4 max-w-full">
|
||||
<p className="text-body3 text-gray-900 font-medium overflow-hidden text-ellipsis">{me.name}</p>
|
||||
<p className="text-body4 text-gray-600">{me.jobTitle}</p>
|
||||
</div>
|
||||
|
||||
<p className="text-body4 text-gray-600">Nice work! You’ve successfully registered for the tournament. You can get started with some of the options below!</p>
|
||||
|
||||
|
||||
<div className="flex w-full gap-8 items-center">
|
||||
<div className={`shrink-0 flex flex-col justify-center items-center bg-gray-50 rounded-8 w-48 h-48`}>👾</div>
|
||||
<div className="self-center px-16 text-left">
|
||||
<p className="text-body4 text-gray-900 font-medium">Complete your maker profile</p>
|
||||
<p className="text-body5 text-gray-400">Add details to your maker profile so you stand out.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full gap-8 items-center">
|
||||
<div className={`shrink-0 flex flex-col justify-center items-center bg-gray-50 rounded-8 w-48 h-48`}>🤝️️️</div>
|
||||
<div className="self-center px-16 text-left">
|
||||
<p className="text-body4 text-gray-900 font-medium">Find makers to team up with</p>
|
||||
<p className="text-body5 text-gray-400">Recruit or find makers to team up with.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-16 w-full mt-24">
|
||||
<Button fullWidth href={createRoute({ type: "tournament", tab: "makers", id: props.tournamentId })} onClick={onClose} color='primary'>🤝 Team up with other makers</Button>
|
||||
<Button fullWidth href={createRoute({ type: "edit-profile" })} onClick={onClose} color='gray'>👾 Complete maker profile</Button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { lazyModal } from 'src/utils/helperFunctions';
|
||||
|
||||
export const { LazyComponent: RegistrationSuccess } = lazyModal(() => import('./RegistrationSuccess'))
|
||||
@@ -0,0 +1,11 @@
|
||||
import { LoginModal } from './LoginModal'
|
||||
import { ConfirmAccount } from './ConfirmAccount'
|
||||
import { RegistrationDetails } from './RegistrationDetails'
|
||||
import { RegistrationSuccess } from './RegistrationSuccess'
|
||||
|
||||
export const RegistrationModals = {
|
||||
LoginModal,
|
||||
RegistrationDetails,
|
||||
ConfirmAccount,
|
||||
RegistrationSuccess,
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import Card from 'src/Components/Card/Card';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
import Button from 'src/Components/Button/Button';
|
||||
|
||||
|
||||
export default function ProjectCardSkeleton() {
|
||||
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div className="flex flex-wrap gap-24 items-start">
|
||||
<div className='bg-gray-100 shrink-0 w-64 md:w-80 aspect-square rounded-full outline outline-2 outline-gray-200' />
|
||||
<div className="flex flex-col gap-4 flex-1 overflow-hidden">
|
||||
<p className="text-body2 text-gray-900 font-bold"><Skeleton width={'13ch'} /></p>
|
||||
<p className="text-body4 text-gray-600 font-medium"><Skeleton width={'8ch'} /></p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-24 text-body5 text-gray-400 line-clamp-2 max-w-[60ch]"><Skeleton width={'100%'} /><Skeleton width={'70%'} /> </p>
|
||||
<div className="mt-24">
|
||||
<p className="text-body5 text-gray-900 font-medium mb-12"><Skeleton width={'7ch'} /> </p>
|
||||
<span className='align-middle'><Skeleton width={'12ch'} /> </span>
|
||||
|
||||
</div>
|
||||
<Button fullWidth color='primary' size='sm' className='mt-24 invisible' hidden>View Details</Button>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { GetProjectsInTournamentQuery, } from "src/graphql";
|
||||
import Card from 'src/Components/Card/Card';
|
||||
import Badge from 'src/Components/Badge/Badge';
|
||||
import Button from "src/Components/Button/Button"
|
||||
import { createRoute } from "src/utils/routing";
|
||||
import { useAppDispatch } from "src/utils/hooks";
|
||||
import { openModal } from "src/redux/features/modals.slice";
|
||||
import { FaUsers } from "react-icons/fa";
|
||||
|
||||
type ProjectType = GetProjectsInTournamentQuery['getProjectsInTournament']['projects'][number]
|
||||
|
||||
interface Props {
|
||||
project: ProjectType,
|
||||
}
|
||||
|
||||
export default function ProjectCard({ project }: Props) {
|
||||
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const openProject = () => {
|
||||
dispatch(openModal({
|
||||
Modal: "ProjectDetailsCard",
|
||||
isPageModal: true,
|
||||
props: {
|
||||
projectId: project.id
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="flex flex-col gap-24">
|
||||
<div className="flex flex-wrap gap-24 items-start">
|
||||
<img src={project.thumbnail_image} className='shrink-0 w-64 aspect-square rounded-full outline outline-2 outline-gray-200' alt="" />
|
||||
<div className="flex flex-col gap-4 flex-1 overflow-hidden">
|
||||
<p className="text-body2 text-gray-900 font-bold">{project.title}</p>
|
||||
<p className="text-body4 text-gray-600 font-medium">{project.category.icon} {project.category.title}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className=" text-body5 text-gray-400 line-clamp-2 max-w-[60ch]">{project.description} </p>
|
||||
<div className="mt-auto">
|
||||
{/* <p className="text-body5 text-gray-900 font-medium mb-12">👾 Makers</p> */}
|
||||
<p className="text-body5 text-gray-600 font-medium">
|
||||
<FaUsers className='text-body2 mr-4' /> <span className='align-middle'>6 makers</span>
|
||||
</p>
|
||||
</div>
|
||||
<Button fullWidth color='white' onClick={openProject} className=''>View Details</Button>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
66
src/features/Tournaments/pages/ProjectsPage/ProjectsPage.tsx
Normal file
66
src/features/Tournaments/pages/ProjectsPage/ProjectsPage.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { useDebouncedState } from '@react-hookz/web';
|
||||
import { useState } from 'react'
|
||||
import { FiSearch } from 'react-icons/fi';
|
||||
import { useGetProjectsInTournamentQuery } from 'src/graphql'
|
||||
import { useTournament } from '../TournamentDetailsPage/TournamentDetailsContext';
|
||||
import ProjectCard from './ProjectCard/ProjectCard';
|
||||
import ProjectCardSkeleton from './ProjectCard/ProjectCard.Skeleton';
|
||||
|
||||
|
||||
export default function ProjectsPage() {
|
||||
|
||||
|
||||
const { tournamentDetails: { id } } = useTournament()
|
||||
|
||||
const [searchFilter, setSearchFilter] = useState("");
|
||||
const [debouncedsearchFilter, setDebouncedSearchFilter] = useDebouncedState("", 500);
|
||||
|
||||
|
||||
|
||||
const query = useGetProjectsInTournamentQuery({
|
||||
variables: {
|
||||
tournamentId: id,
|
||||
roleId: null,
|
||||
search: debouncedsearchFilter,
|
||||
skip: 0,
|
||||
take: 200,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const changeSearchFilter = (new_value: string) => {
|
||||
setSearchFilter(new_value);
|
||||
setDebouncedSearchFilter(new_value);
|
||||
}
|
||||
|
||||
const projectsCount = !!query.data?.getProjectsInTournament.projects && query.data.getProjectsInTournament.projects.length;
|
||||
|
||||
|
||||
return (
|
||||
<div className='pb-42 flex flex-col gap-24'>
|
||||
<h2 className='text-body1 font-bolder text-gray-900'>Projects {projectsCount && `(${projectsCount})`}</h2>
|
||||
|
||||
<div className="input-wrapper relative">
|
||||
<FiSearch className="self-center ml-16 flex-shrink-0 w-[20px] text-gray-400" />
|
||||
<input
|
||||
type='text'
|
||||
className="input-text"
|
||||
placeholder="Search"
|
||||
value={searchFilter}
|
||||
onChange={e => changeSearchFilter(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-16 lg:gap-24">
|
||||
{query.loading ?
|
||||
Array(9).fill(0).map((_, idx) => <ProjectCardSkeleton key={idx} />)
|
||||
:
|
||||
query.data?.getProjectsInTournament.projects.map(project =>
|
||||
<ProjectCard
|
||||
key={project.id}
|
||||
project={project}
|
||||
/>)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import dayjs from 'dayjs'
|
||||
import advancedFormat from 'dayjs/plugin/advancedFormat'
|
||||
import React from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { IoLocationOutline } from 'react-icons/io5'
|
||||
import { useTournament } from '../TournamentDetailsContext'
|
||||
dayjs.extend(advancedFormat)
|
||||
|
||||
|
||||
|
||||
export default function Header() {
|
||||
const { tournamentDetails } = useTournament()
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{tournamentDetails.title} Tournament</title>
|
||||
</Helmet>
|
||||
<div className="w-full p-16 md:p-24 flex flex-col h-[280px] relative mb-[-1px]">
|
||||
<img src={tournamentDetails.cover_image} className='absolute inset-0 h-full w-full object-cover object-top' alt="" />
|
||||
<div className='absolute inset-0 h-full w-full bg-black bg-opacity-50 ' />
|
||||
<div className="content-container mt-auto">
|
||||
<div className=" text-white flex flex-col md:flex-row gap-16 md:gap-32 relative" style={{ marginTop: 'auto' }}>
|
||||
<img src={tournamentDetails.thumbnail_image} className={'w-64 md:w-[128px] aspect-square rounded-16 md:rounded-24 border-2 border-gray-100'} alt="" />
|
||||
<div className='flex flex-col gap-4'>
|
||||
<p className="text-body6">TOURNAMENT 🏆</p>
|
||||
<p className="text-body1 md:text-h2 font-bold">{tournamentDetails.title}</p>
|
||||
<p className="text-body3"> {`${dayjs(tournamentDetails.start_date).format('Do')} - ${dayjs(tournamentDetails.end_date).format('Do MMMM, YYYY')}`}</p>
|
||||
|
||||
<p className='text-body5'><IoLocationOutline className="mr-8" /> {tournamentDetails.location}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { useMemo } from 'react'
|
||||
import { NavLink } from 'react-router-dom'
|
||||
import { useCarousel } from 'src/utils/hooks'
|
||||
import { useTournament } from '../TournamentDetailsContext'
|
||||
|
||||
|
||||
export default function Navigation() {
|
||||
|
||||
const { viewportRef, } = useCarousel({
|
||||
align: 'start', slidesToScroll: 2,
|
||||
containScroll: "trimSnaps",
|
||||
})
|
||||
|
||||
const { tournamentDetails } = useTournament()
|
||||
|
||||
const links = useMemo(() => [
|
||||
{
|
||||
text: "Overview",
|
||||
path: "overview",
|
||||
},
|
||||
{
|
||||
text: `Events (${tournamentDetails.events_count})`,
|
||||
path: "events",
|
||||
},
|
||||
{
|
||||
text: `Makers (${tournamentDetails.makers_count})`,
|
||||
path: "makers",
|
||||
},
|
||||
{
|
||||
text: `Projects 🔒`,
|
||||
path: "projects",
|
||||
isDisabled: true,
|
||||
},
|
||||
// {
|
||||
// text: "???? 🚧",
|
||||
// path: "ideas",
|
||||
// isDisabled: true,
|
||||
// },
|
||||
// {
|
||||
// text: "?????????? 🚧",
|
||||
// path: "resources",
|
||||
// isDisabled: true,
|
||||
// },
|
||||
], [tournamentDetails.events_count, tournamentDetails.makers_count])
|
||||
|
||||
return (
|
||||
<div className="w-full bg-white py-16 border-y border-gray-200 sticky-top-element z-10">
|
||||
<div className="content-container">
|
||||
<div className="relative group">
|
||||
<div className="overflow-hidden" ref={viewportRef}>
|
||||
<div className="select-none w-full flex gap-8 md:gap-16">
|
||||
{links.map((link) => <NavLink
|
||||
key={link.path}
|
||||
to={link.path}
|
||||
className={({ isActive }) => `
|
||||
min-w-max rounded-48 px-16 py-8 cursor-pointer font-medium text-body5
|
||||
active:scale-95 transition-transform
|
||||
${isActive ? 'bg-primary-100 text-primary-600' : 'bg-gray-100 hover:bg-gray-200 text-gray-600'}
|
||||
${link.isDisabled && "pointer-events-none opacity-60"}
|
||||
`}
|
||||
|
||||
role='button'
|
||||
>
|
||||
{link.text}
|
||||
</NavLink>)}
|
||||
</div>
|
||||
</div>
|
||||
{/* <button className={`absolute text-body6 w-[28px] aspect-square flex justify-center items-center left-0 -translate-x-1/2 top-1/2 -translate-y-1/2 rounded-full bg-white text-gray-400 opacity-0 ${canScrollPrev && 'group-hover:opacity-100'} active:scale-90 transition-opacity border border-gray-200 shadow-md`} onClick={() => scrollSlides(-1)}>
|
||||
{"<"}
|
||||
</button>
|
||||
<button className={`absolute text-body6 w-[28px] aspect-square flex justify-center items-center right-0 translate-x-1/2 top-1/2 -translate-y-1/2 rounded-full bg-white text-gray-400 opacity-0 ${canScrollNext && 'group-hover:opacity-100'} active:scale-90 transition-opacity border border-gray-200 shadow-md`} onClick={() => scrollSlides(1)}>
|
||||
{">"}
|
||||
</button> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
|
||||
|
||||
import React, { createContext, PropsWithChildren, useContext } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import LoadingPage from 'src/Components/LoadingPage/LoadingPage'
|
||||
import NotFoundPage from 'src/features/Shared/pages/NotFoundPage/NotFoundPage'
|
||||
import { GetTournamentByIdQuery, useGetTournamentByIdQuery } from 'src/graphql'
|
||||
|
||||
interface ITournamentDetails {
|
||||
makers: GetTournamentByIdQuery['getMakersInTournament']['makers']
|
||||
me: GetTournamentByIdQuery['me']
|
||||
tournamentDetails: GetTournamentByIdQuery['getTournamentById']
|
||||
myParticipationInfo: GetTournamentByIdQuery['tournamentParticipationInfo']
|
||||
}
|
||||
|
||||
const Ctx = createContext<ITournamentDetails>(null!)
|
||||
|
||||
|
||||
export default function TournamentDetailsContext({ children }: PropsWithChildren<{}>) {
|
||||
const { id } = useParams()
|
||||
|
||||
const tournaemntQuery = useGetTournamentByIdQuery({
|
||||
variables: {
|
||||
id: Number(id)!,
|
||||
},
|
||||
skip: !id
|
||||
})
|
||||
|
||||
|
||||
|
||||
if (tournaemntQuery.loading)
|
||||
return <LoadingPage />
|
||||
|
||||
if (!tournaemntQuery.data?.getTournamentById)
|
||||
return <NotFoundPage />
|
||||
|
||||
const { getMakersInTournament: makers, me, getTournamentById: tournamentDetails, tournamentParticipationInfo: myParticipationInfo } = tournaemntQuery.data
|
||||
|
||||
return (
|
||||
<Ctx.Provider value={{ makers: makers.makers, me, tournamentDetails, myParticipationInfo }}>{children}</Ctx.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useTournament = () => {
|
||||
return useContext(Ctx)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
|
||||
import Header from './Header/Header'
|
||||
import { Navigate, Route, Routes, useParams } from 'react-router-dom'
|
||||
import OverviewPage from '../OverviewPage/OverviewPage'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import Navigation from './Navigation/Navigation'
|
||||
import EventsPage from '../EventsPage/EventsPage'
|
||||
import MakersPage from '../MakersPage/MakersPage'
|
||||
import ProjectsPage from '../ProjectsPage/ProjectsPage'
|
||||
import { GetTournamentByIdQuery } from 'src/graphql'
|
||||
import TournamentDetailsContext from './TournamentDetailsContext'
|
||||
|
||||
|
||||
|
||||
export type MeTournament = GetTournamentByIdQuery['me']
|
||||
|
||||
export default function TournamentDetailsPage() {
|
||||
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
"--maxPageWidth": "910px"
|
||||
} as any}>
|
||||
|
||||
<TournamentDetailsContext>
|
||||
<Header />
|
||||
<Navigation />
|
||||
|
||||
<div className="content-container !mt-24">
|
||||
<Routes >
|
||||
<Route index element={<Navigate to='overview' replace />} />
|
||||
<Route path='overview' element={<OverviewPage />} />
|
||||
<Route path='events' element={<EventsPage />} />
|
||||
<Route path='makers' element={<MakersPage />} />
|
||||
<Route path='projects' element={<ProjectsPage />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</TournamentDetailsContext>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export const description =
|
||||
`## Tournament Details
|
||||
Lorem ipsum dolor sit **amet**, consectetur adipiscing elit. Semper turpis est, ac eget nullam. In leo at pharetra morbi ornare eget. Ultrices posuere senectus purus nulla vitae volutpat id id suspendisse. Urna mattis nulla diam semper erat. Mattis gravida ultrices aliquam odio. Praesent viverra egestas sed elementum nisl imperdiet a, non.
|
||||
|
||||
#### Subtitle1
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Semper turpis est, ac eget nullam. In leo at pharetra morbi ornare eget. Ultrices posuere senectus purus nulla vitae volutpat id id suspendisse. Urna mattis nulla diam semper erat. Mattis gravida ultrices aliquam odio. Praesent viverra egestas sed elementum nisl imperdiet a, non.
|
||||
|
||||
|
||||
#### Subtitle2
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Semper turpis est, ac eget nullam. In leo at pharetra morbi ornare eget. Ultrices posuere senectus purus nulla vitae volutpat id id suspendisse. Urna mattis nulla diam semper erat. Mattis gravida ultrices aliquam odio. Praesent viverra egestas sed elementum nisl imperdiet a, non.
|
||||
`
|
||||
@@ -0,0 +1,86 @@
|
||||
import { Tournament, TournamentEventTypeEnum } from "src/graphql";
|
||||
import { getCoverImage } from "src/mocks/data/utils";
|
||||
|
||||
|
||||
export const events: Tournament['events'] = [
|
||||
{
|
||||
id: 12,
|
||||
title: "STW3 Round Table #1",
|
||||
|
||||
starts_at: "2022-09-30T21:00:00.000Z",
|
||||
ends_at: "2022-10-30T22:00:00.000Z",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Diam morbi pellentesque velit congue. Aliquet rutrum a, augue vitae tincidunt ac egestas. Mauris nec fringilla diam eget fusce malesuada cum parturient. Nulla pretium purus odio odio.",
|
||||
image: getCoverImage(),
|
||||
links: [],
|
||||
location: "Online",
|
||||
type: TournamentEventTypeEnum.TwitterSpace,
|
||||
website: "https://event.name"
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
title: "STW3 Round Table #2",
|
||||
|
||||
starts_at: "2022-09-30T21:00:00.000Z",
|
||||
ends_at: "2022-10-30T22:00:00.000Z",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Diam morbi pellentesque velit congue. Aliquet rutrum a, augue vitae tincidunt ac egestas. Mauris nec fringilla diam eget fusce malesuada cum parturient. Nulla pretium purus odio odio.",
|
||||
image: getCoverImage(),
|
||||
links: [],
|
||||
location: "Online",
|
||||
type: TournamentEventTypeEnum.Workshop,
|
||||
website: "https://event.name"
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
title: "STW3 Round Table #3",
|
||||
|
||||
starts_at: "2022-09-30T21:00:00.000Z",
|
||||
ends_at: "2022-10-30T22:00:00.000Z",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Diam morbi pellentesque velit congue. Aliquet rutrum a, augue vitae tincidunt ac egestas. Mauris nec fringilla diam eget fusce malesuada cum parturient. Nulla pretium purus odio odio.",
|
||||
image: getCoverImage(),
|
||||
links: [],
|
||||
location: "Online",
|
||||
type: TournamentEventTypeEnum.IrlMeetup,
|
||||
website: "https://event.name"
|
||||
},
|
||||
{
|
||||
id: 44,
|
||||
title: "Lightning Login",
|
||||
|
||||
starts_at: "2022-09-30T21:00:00.000Z",
|
||||
ends_at: "2022-10-30T22:00:00.000Z",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Diam morbi pellentesque velit congue. Aliquet rutrum a, augue vitae tincidunt ac egestas. Mauris nec fringilla diam eget fusce malesuada cum parturient. Nulla pretium purus odio odio.",
|
||||
image: getCoverImage(),
|
||||
links: [],
|
||||
location: "Online",
|
||||
type: TournamentEventTypeEnum.Workshop,
|
||||
website: "https://event.name"
|
||||
},
|
||||
|
||||
{
|
||||
id: 46,
|
||||
title: "Escrow contracts",
|
||||
|
||||
starts_at: "2022-09-30T21:00:00.000Z",
|
||||
ends_at: "2022-10-30T22:00:00.000Z",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Diam morbi pellentesque velit congue. Aliquet rutrum a, augue vitae tincidunt ac egestas. Mauris nec fringilla diam eget fusce malesuada cum parturient. Nulla pretium purus odio odio.",
|
||||
image: getCoverImage(),
|
||||
links: [],
|
||||
location: "Online",
|
||||
type: TournamentEventTypeEnum.Workshop,
|
||||
website: "https://event.name"
|
||||
},
|
||||
|
||||
{
|
||||
id: 444,
|
||||
title: "Lsats - What & Why",
|
||||
|
||||
starts_at: "2022-09-30T21:00:00.000Z",
|
||||
ends_at: "2022-10-30T22:00:00.000Z",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Diam morbi pellentesque velit congue. Aliquet rutrum a, augue vitae tincidunt ac egestas. Mauris nec fringilla diam eget fusce malesuada cum parturient. Nulla pretium purus odio odio.",
|
||||
image: getCoverImage(),
|
||||
links: [],
|
||||
location: "Online",
|
||||
type: TournamentEventTypeEnum.OnlineMeetup,
|
||||
website: "https://event.name"
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Tournament } from "src/graphql";
|
||||
|
||||
|
||||
export const faqs: Tournament['faqs'] = [
|
||||
{
|
||||
question: "What is Shock the Web?",
|
||||
answer:
|
||||
`Shock the Web is a virtual hackathon to promote, explore, build and design web applications that can interact with WebLN enabled wallets and browsers. We want to make building on bitcoin more accessible to the masses of web developers out there.
|
||||
|
||||
Bitcoin development can seem scary for new developers coming in, but it doesn't have to be. With the lightning network's toolkit and libraries a bunch of new opportunities are waiting to be explored. We hope these hackathons can be a chance for you to preview what is possible on bitcoin and the lightning network by fostering collaboration, hopefully shortening (or easing) any developer onboarding time, and helping you connect with other bitcoiners in a fun and friendly space.`
|
||||
},
|
||||
{
|
||||
question: "When and where will it take place?",
|
||||
answer:
|
||||
`Shock the Web is a virtual hackathon to promote, explore, build and design web applications that can interact with WebLN enabled wallets and browsers. We want to make building on bitcoin more accessible to the masses of web developers out there.
|
||||
|
||||
Bitcoin development can seem scary for new developers coming in, but it doesn't have to be. With the lightning network's toolkit and libraries a bunch of new opportunities are waiting to be explored. We hope these hackathons can be a chance for you to preview what is possible on bitcoin and the lightning network by fostering collaboration, hopefully shortening (or easing) any developer onboarding time, and helping you connect with other bitcoiners in a fun and friendly space.`
|
||||
},
|
||||
{
|
||||
question: "What will we be doing?",
|
||||
answer:
|
||||
`Shock the Web is a virtual hackathon to promote, explore, build and design web applications that can interact with WebLN enabled wallets and browsers. We want to make building on bitcoin more accessible to the masses of web developers out there.
|
||||
|
||||
Bitcoin development can seem scary for new developers coming in, but it doesn't have to be. With the lightning network's toolkit and libraries a bunch of new opportunities are waiting to be explored. We hope these hackathons can be a chance for you to preview what is possible on bitcoin and the lightning network by fostering collaboration, hopefully shortening (or easing) any developer onboarding time, and helping you connect with other bitcoiners in a fun and friendly space.`
|
||||
},
|
||||
{
|
||||
question: "This is my first time hacking on lightning, will there be help?",
|
||||
answer:
|
||||
`Shock the Web is a virtual hackathon to promote, explore, build and design web applications that can interact with WebLN enabled wallets and browsers. We want to make building on bitcoin more accessible to the masses of web developers out there.
|
||||
|
||||
Bitcoin development can seem scary for new developers coming in, but it doesn't have to be. With the lightning network's toolkit and libraries a bunch of new opportunities are waiting to be explored. We hope these hackathons can be a chance for you to preview what is possible on bitcoin and the lightning network by fostering collaboration, hopefully shortening (or easing) any developer onboarding time, and helping you connect with other bitcoiners in a fun and friendly space.`
|
||||
},
|
||||
{
|
||||
question: "This is my first time hacking on lightning, will there be help?",
|
||||
answer:
|
||||
`Shock the Web is a virtual hackathon to promote, explore, build and design web applications that can interact with WebLN enabled wallets and browsers. We want to make building on bitcoin more accessible to the masses of web developers out there.
|
||||
|
||||
Bitcoin development can seem scary for new developers coming in, but it doesn't have to be. With the lightning network's toolkit and libraries a bunch of new opportunities are waiting to be explored. We hope these hackathons can be a chance for you to preview what is possible on bitcoin and the lightning network by fostering collaboration, hopefully shortening (or easing) any developer onboarding time, and helping you connect with other bitcoiners in a fun and friendly space.`
|
||||
},
|
||||
{
|
||||
question: "How many members can I have on my team?",
|
||||
answer:
|
||||
`Shock the Web is a virtual hackathon to promote, explore, build and design web applications that can interact with WebLN enabled wallets and browsers. We want to make building on bitcoin more accessible to the masses of web developers out there.
|
||||
|
||||
Bitcoin development can seem scary for new developers coming in, but it doesn't have to be. With the lightning network's toolkit and libraries a bunch of new opportunities are waiting to be explored. We hope these hackathons can be a chance for you to preview what is possible on bitcoin and the lightning network by fostering collaboration, hopefully shortening (or easing) any developer onboarding time, and helping you connect with other bitcoiners in a fun and friendly space.`
|
||||
},
|
||||
{
|
||||
question: "Who will choose the winners?",
|
||||
answer:
|
||||
`Shock the Web is a virtual hackathon to promote, explore, build and design web applications that can interact with WebLN enabled wallets and browsers. We want to make building on bitcoin more accessible to the masses of web developers out there.
|
||||
|
||||
Bitcoin development can seem scary for new developers coming in, but it doesn't have to be. With the lightning network's toolkit and libraries a bunch of new opportunities are waiting to be explored. We hope these hackathons can be a chance for you to preview what is possible on bitcoin and the lightning network by fostering collaboration, hopefully shortening (or easing) any developer onboarding time, and helping you connect with other bitcoiners in a fun and friendly space.`
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Tournament, } from "src/graphql";
|
||||
import { description } from "./description";
|
||||
import { events } from "./events";
|
||||
import { faqs } from "./faqs";
|
||||
import { judges } from "./judeges";
|
||||
import { prizes } from "./prizes";
|
||||
|
||||
export const tournamentData: Tournament = {
|
||||
__typename: "Tournament",
|
||||
id: 12,
|
||||
title: "The Long Night",
|
||||
start_date: "2022-09-30T21:00:00.000Z",
|
||||
end_date: "2022-10-30T22:00:00.000Z",
|
||||
cover_image: "https://imagedelivery.net/wyrwp3c-j0gDDUWgnE7lig/1d5d2c86-fe46-4478-6909-bb3c425c0d00/public",
|
||||
thumbnail_image: "https://imagedelivery.net/wyrwp3c-j0gDDUWgnE7lig/37fb9cd6-e4f1-43f9-c3fe-7c3e119d5600/public",
|
||||
location: "Online",
|
||||
website: "#",
|
||||
description: description,
|
||||
prizes: prizes,
|
||||
events_count: events.length,
|
||||
makers_count: 668,
|
||||
projects_count: 21,
|
||||
|
||||
events: events,
|
||||
judges: judges,
|
||||
faqs: faqs,
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Tournament } from "src/graphql";
|
||||
|
||||
|
||||
export const judges: Tournament['judges'] = [
|
||||
{
|
||||
name: "Ben Arc",
|
||||
avatar: "https://s3-alpha-sig.figma.com/img/5e65/c22c/673b8f74ac43f024b036dbc4e6479e0d?Expires=1662940800&Signature=GR54s7FBcLGcPTVclWdmPjzU92tyrYpdUbbDUYKMUkdQbxq2yQlUhZ-AOLDHhOPY4P2G3aW2yT16b1AUbC8RBx1boH25MSrH-jpn6X57IJA-4ZeHP8zCo-yjTLpb8Gn~vudIi9rPfbwJ34stp-VeOAYMuOHlah3YO-B4MBsBv-NqhP7BMY4zz9vGdBLZhOjYQYdLZ2494Ae6L5FpD1ah3WD3U5qUN9dDvYvAtqYfhQeBOnsG6PfYoq8LouCuERC4S26BeooPg8UdGUCf324-SjEihCoL8mQFq80PSsaAZl5~EBOKRUx14FOprizMusaYN0K06E~fjDIDbM2Rmc9Xjg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA",
|
||||
company: "Company"
|
||||
},
|
||||
{
|
||||
name: "Ben Arc",
|
||||
avatar: "https://s3-alpha-sig.figma.com/img/5e65/c22c/673b8f74ac43f024b036dbc4e6479e0d?Expires=1662940800&Signature=GR54s7FBcLGcPTVclWdmPjzU92tyrYpdUbbDUYKMUkdQbxq2yQlUhZ-AOLDHhOPY4P2G3aW2yT16b1AUbC8RBx1boH25MSrH-jpn6X57IJA-4ZeHP8zCo-yjTLpb8Gn~vudIi9rPfbwJ34stp-VeOAYMuOHlah3YO-B4MBsBv-NqhP7BMY4zz9vGdBLZhOjYQYdLZ2494Ae6L5FpD1ah3WD3U5qUN9dDvYvAtqYfhQeBOnsG6PfYoq8LouCuERC4S26BeooPg8UdGUCf324-SjEihCoL8mQFq80PSsaAZl5~EBOKRUx14FOprizMusaYN0K06E~fjDIDbM2Rmc9Xjg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA",
|
||||
company: "Company"
|
||||
},
|
||||
{
|
||||
name: "Ben Arc",
|
||||
avatar: "https://s3-alpha-sig.figma.com/img/5e65/c22c/673b8f74ac43f024b036dbc4e6479e0d?Expires=1662940800&Signature=GR54s7FBcLGcPTVclWdmPjzU92tyrYpdUbbDUYKMUkdQbxq2yQlUhZ-AOLDHhOPY4P2G3aW2yT16b1AUbC8RBx1boH25MSrH-jpn6X57IJA-4ZeHP8zCo-yjTLpb8Gn~vudIi9rPfbwJ34stp-VeOAYMuOHlah3YO-B4MBsBv-NqhP7BMY4zz9vGdBLZhOjYQYdLZ2494Ae6L5FpD1ah3WD3U5qUN9dDvYvAtqYfhQeBOnsG6PfYoq8LouCuERC4S26BeooPg8UdGUCf324-SjEihCoL8mQFq80PSsaAZl5~EBOKRUx14FOprizMusaYN0K06E~fjDIDbM2Rmc9Xjg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA",
|
||||
company: "Company"
|
||||
},
|
||||
{
|
||||
name: "Ben Arc",
|
||||
avatar: "https://s3-alpha-sig.figma.com/img/5e65/c22c/673b8f74ac43f024b036dbc4e6479e0d?Expires=1662940800&Signature=GR54s7FBcLGcPTVclWdmPjzU92tyrYpdUbbDUYKMUkdQbxq2yQlUhZ-AOLDHhOPY4P2G3aW2yT16b1AUbC8RBx1boH25MSrH-jpn6X57IJA-4ZeHP8zCo-yjTLpb8Gn~vudIi9rPfbwJ34stp-VeOAYMuOHlah3YO-B4MBsBv-NqhP7BMY4zz9vGdBLZhOjYQYdLZ2494Ae6L5FpD1ah3WD3U5qUN9dDvYvAtqYfhQeBOnsG6PfYoq8LouCuERC4S26BeooPg8UdGUCf324-SjEihCoL8mQFq80PSsaAZl5~EBOKRUx14FOprizMusaYN0K06E~fjDIDbM2Rmc9Xjg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA",
|
||||
company: "Company"
|
||||
},
|
||||
{
|
||||
name: "Ben Arc",
|
||||
avatar: "https://s3-alpha-sig.figma.com/img/5e65/c22c/673b8f74ac43f024b036dbc4e6479e0d?Expires=1662940800&Signature=GR54s7FBcLGcPTVclWdmPjzU92tyrYpdUbbDUYKMUkdQbxq2yQlUhZ-AOLDHhOPY4P2G3aW2yT16b1AUbC8RBx1boH25MSrH-jpn6X57IJA-4ZeHP8zCo-yjTLpb8Gn~vudIi9rPfbwJ34stp-VeOAYMuOHlah3YO-B4MBsBv-NqhP7BMY4zz9vGdBLZhOjYQYdLZ2494Ae6L5FpD1ah3WD3U5qUN9dDvYvAtqYfhQeBOnsG6PfYoq8LouCuERC4S26BeooPg8UdGUCf324-SjEihCoL8mQFq80PSsaAZl5~EBOKRUx14FOprizMusaYN0K06E~fjDIDbM2Rmc9Xjg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA",
|
||||
company: "Company"
|
||||
},
|
||||
{
|
||||
name: "Ben Arc",
|
||||
avatar: "https://s3-alpha-sig.figma.com/img/5e65/c22c/673b8f74ac43f024b036dbc4e6479e0d?Expires=1662940800&Signature=GR54s7FBcLGcPTVclWdmPjzU92tyrYpdUbbDUYKMUkdQbxq2yQlUhZ-AOLDHhOPY4P2G3aW2yT16b1AUbC8RBx1boH25MSrH-jpn6X57IJA-4ZeHP8zCo-yjTLpb8Gn~vudIi9rPfbwJ34stp-VeOAYMuOHlah3YO-B4MBsBv-NqhP7BMY4zz9vGdBLZhOjYQYdLZ2494Ae6L5FpD1ah3WD3U5qUN9dDvYvAtqYfhQeBOnsG6PfYoq8LouCuERC4S26BeooPg8UdGUCf324-SjEihCoL8mQFq80PSsaAZl5~EBOKRUx14FOprizMusaYN0K06E~fjDIDbM2Rmc9Xjg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA",
|
||||
company: "Company"
|
||||
},
|
||||
{
|
||||
name: "Ben Arc",
|
||||
avatar: "https://s3-alpha-sig.figma.com/img/5e65/c22c/673b8f74ac43f024b036dbc4e6479e0d?Expires=1662940800&Signature=GR54s7FBcLGcPTVclWdmPjzU92tyrYpdUbbDUYKMUkdQbxq2yQlUhZ-AOLDHhOPY4P2G3aW2yT16b1AUbC8RBx1boH25MSrH-jpn6X57IJA-4ZeHP8zCo-yjTLpb8Gn~vudIi9rPfbwJ34stp-VeOAYMuOHlah3YO-B4MBsBv-NqhP7BMY4zz9vGdBLZhOjYQYdLZ2494Ae6L5FpD1ah3WD3U5qUN9dDvYvAtqYfhQeBOnsG6PfYoq8LouCuERC4S26BeooPg8UdGUCf324-SjEihCoL8mQFq80PSsaAZl5~EBOKRUx14FOprizMusaYN0K06E~fjDIDbM2Rmc9Xjg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA",
|
||||
company: "Company"
|
||||
},
|
||||
{
|
||||
name: "Ben Arc",
|
||||
avatar: "https://s3-alpha-sig.figma.com/img/5e65/c22c/673b8f74ac43f024b036dbc4e6479e0d?Expires=1662940800&Signature=GR54s7FBcLGcPTVclWdmPjzU92tyrYpdUbbDUYKMUkdQbxq2yQlUhZ-AOLDHhOPY4P2G3aW2yT16b1AUbC8RBx1boH25MSrH-jpn6X57IJA-4ZeHP8zCo-yjTLpb8Gn~vudIi9rPfbwJ34stp-VeOAYMuOHlah3YO-B4MBsBv-NqhP7BMY4zz9vGdBLZhOjYQYdLZ2494Ae6L5FpD1ah3WD3U5qUN9dDvYvAtqYfhQeBOnsG6PfYoq8LouCuERC4S26BeooPg8UdGUCf324-SjEihCoL8mQFq80PSsaAZl5~EBOKRUx14FOprizMusaYN0K06E~fjDIDbM2Rmc9Xjg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA",
|
||||
company: "Company"
|
||||
},
|
||||
{
|
||||
name: "Ben Arc",
|
||||
avatar: "https://s3-alpha-sig.figma.com/img/5e65/c22c/673b8f74ac43f024b036dbc4e6479e0d?Expires=1662940800&Signature=GR54s7FBcLGcPTVclWdmPjzU92tyrYpdUbbDUYKMUkdQbxq2yQlUhZ-AOLDHhOPY4P2G3aW2yT16b1AUbC8RBx1boH25MSrH-jpn6X57IJA-4ZeHP8zCo-yjTLpb8Gn~vudIi9rPfbwJ34stp-VeOAYMuOHlah3YO-B4MBsBv-NqhP7BMY4zz9vGdBLZhOjYQYdLZ2494Ae6L5FpD1ah3WD3U5qUN9dDvYvAtqYfhQeBOnsG6PfYoq8LouCuERC4S26BeooPg8UdGUCf324-SjEihCoL8mQFq80PSsaAZl5~EBOKRUx14FOprizMusaYN0K06E~fjDIDbM2Rmc9Xjg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA",
|
||||
company: "Company"
|
||||
},
|
||||
{
|
||||
name: "Ben Arc",
|
||||
avatar: "https://s3-alpha-sig.figma.com/img/5e65/c22c/673b8f74ac43f024b036dbc4e6479e0d?Expires=1662940800&Signature=GR54s7FBcLGcPTVclWdmPjzU92tyrYpdUbbDUYKMUkdQbxq2yQlUhZ-AOLDHhOPY4P2G3aW2yT16b1AUbC8RBx1boH25MSrH-jpn6X57IJA-4ZeHP8zCo-yjTLpb8Gn~vudIi9rPfbwJ34stp-VeOAYMuOHlah3YO-B4MBsBv-NqhP7BMY4zz9vGdBLZhOjYQYdLZ2494Ae6L5FpD1ah3WD3U5qUN9dDvYvAtqYfhQeBOnsG6PfYoq8LouCuERC4S26BeooPg8UdGUCf324-SjEihCoL8mQFq80PSsaAZl5~EBOKRUx14FOprizMusaYN0K06E~fjDIDbM2Rmc9Xjg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA",
|
||||
company: "Company"
|
||||
},
|
||||
{
|
||||
name: "Ben Arc",
|
||||
avatar: "https://s3-alpha-sig.figma.com/img/5e65/c22c/673b8f74ac43f024b036dbc4e6479e0d?Expires=1662940800&Signature=GR54s7FBcLGcPTVclWdmPjzU92tyrYpdUbbDUYKMUkdQbxq2yQlUhZ-AOLDHhOPY4P2G3aW2yT16b1AUbC8RBx1boH25MSrH-jpn6X57IJA-4ZeHP8zCo-yjTLpb8Gn~vudIi9rPfbwJ34stp-VeOAYMuOHlah3YO-B4MBsBv-NqhP7BMY4zz9vGdBLZhOjYQYdLZ2494Ae6L5FpD1ah3WD3U5qUN9dDvYvAtqYfhQeBOnsG6PfYoq8LouCuERC4S26BeooPg8UdGUCf324-SjEihCoL8mQFq80PSsaAZl5~EBOKRUx14FOprizMusaYN0K06E~fjDIDbM2Rmc9Xjg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA",
|
||||
company: "Company"
|
||||
},
|
||||
{
|
||||
name: "Ben Arc",
|
||||
avatar: "https://s3-alpha-sig.figma.com/img/5e65/c22c/673b8f74ac43f024b036dbc4e6479e0d?Expires=1662940800&Signature=GR54s7FBcLGcPTVclWdmPjzU92tyrYpdUbbDUYKMUkdQbxq2yQlUhZ-AOLDHhOPY4P2G3aW2yT16b1AUbC8RBx1boH25MSrH-jpn6X57IJA-4ZeHP8zCo-yjTLpb8Gn~vudIi9rPfbwJ34stp-VeOAYMuOHlah3YO-B4MBsBv-NqhP7BMY4zz9vGdBLZhOjYQYdLZ2494Ae6L5FpD1ah3WD3U5qUN9dDvYvAtqYfhQeBOnsG6PfYoq8LouCuERC4S26BeooPg8UdGUCf324-SjEihCoL8mQFq80PSsaAZl5~EBOKRUx14FOprizMusaYN0K06E~fjDIDbM2Rmc9Xjg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA",
|
||||
company: "Company"
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Tournament } from "src/graphql";
|
||||
|
||||
|
||||
export const prizes: Tournament['prizes'] = [{
|
||||
title: "stw3 champion",
|
||||
amount: "$ 20k",
|
||||
image: "https://imagedelivery.net/wyrwp3c-j0gDDUWgnE7lig/39217dcf-c900-46be-153f-169e3a1f0400/public",
|
||||
},
|
||||
{
|
||||
title: "2nd place",
|
||||
amount: "$ 5k",
|
||||
image: "https://imagedelivery.net/wyrwp3c-j0gDDUWgnE7lig/39cdb7c8-5fbf-49ff-32cf-fdabc3aa2d00/public",
|
||||
},
|
||||
{
|
||||
title: "3rd place ",
|
||||
amount: "$ 2k",
|
||||
image: "https://imagedelivery.net/wyrwp3c-j0gDDUWgnE7lig/75958797-73b2-4a62-52df-9f0f98c53900/public",
|
||||
},
|
||||
{
|
||||
title: "best design ",
|
||||
amount: "$ 1k",
|
||||
image: "https://imagedelivery.net/wyrwp3c-j0gDDUWgnE7lig/fa7b7cdd-7c06-4ebe-1a2d-94af9d2dae00/public",
|
||||
}]
|
||||
@@ -0,0 +1,17 @@
|
||||
query MeTournament($id: Int!) {
|
||||
tournamentParticipationInfo(tournamentId: $id) {
|
||||
createdAt
|
||||
hacking_status
|
||||
}
|
||||
me {
|
||||
id
|
||||
name
|
||||
avatar
|
||||
jobTitle
|
||||
twitter
|
||||
linkedin
|
||||
github
|
||||
|
||||
...UserRolesSkills
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user