Merge pull request #148 from peakshift/feature/tournament-pages

Draft 0.1: Tournament Page
This commit is contained in:
Mohammed Taher Ghazal
2022-09-12 15:23:39 +03:00
committed by GitHub
118 changed files with 5538 additions and 619 deletions

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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

View File

@@ -1,5 +1,5 @@
{
"name": "my-app",
"name": "makers-bolt-fun",
"version": "0.1.0",
"private": true,
"dependencies": {

View 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🔩FUNs 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: "Luganos 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 projects 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 tournaments 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🔩FUNs 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, weve 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 projects 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 };

View File

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

View File

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -237,6 +237,7 @@ export default function NavDesktop() {
/>
</motion.div>
</div>
</div>
</div>
</nav>

View File

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

View File

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

View File

@@ -19,6 +19,7 @@ export default function HackathonsList(props: Props) {
<HackathonCardSkeleton />
<HackathonCardSkeleton />
<HackathonCardSkeleton />
<HackathonCardSkeleton />
</>
}
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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`}>&#8226; {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'}
`}>&#8226; {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'
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -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 profiles <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(', ');
}

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,9 @@
mutation RegisterInTournament(
$tournamentId: Int!
$data: RegisterInTournamentInput
) {
registerInTournament(tournament_id: $tournamentId, data: $data) {
id
in_tournament(id: $tournamentId)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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.`
},
]

View File

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

View File

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

View File

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

View File

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