diff --git a/api/functions/graphql/nexus-typegen.ts b/api/functions/graphql/nexus-typegen.ts
index 0137828..d405ede 100644
--- a/api/functions/graphql/nexus-typegen.ts
+++ b/api/functions/graphql/nexus-typegen.ts
@@ -28,6 +28,11 @@ declare global {
}
export interface NexusGenInputs {
+ ImageInput: { // input type
+ id?: string | null; // String
+ name?: string | null; // String
+ url: string; // String!
+ }
MakerRoleInput: { // input type
id: number; // Int!
level: NexusGenEnums['RoleLevelEnum']; // RoleLevelEnum!
@@ -36,8 +41,9 @@ export interface NexusGenInputs {
id: number; // Int!
}
ProfileDetailsInput: { // input type
- avatar?: string | null; // String
+ avatar?: NexusGenInputs['ImageInput'] | null; // ImageInput
bio?: string | null; // String
+ discord?: string | null; // String
email?: string | null; // String
github?: string | null; // String
jobTitle?: string | null; // String
@@ -52,14 +58,22 @@ 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
+ cover_image?: NexusGenInputs['ImageInput'] | null; // ImageInput
id?: number | null; // Int
is_published?: boolean | null; // Boolean
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 +83,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"
}
@@ -83,7 +99,6 @@ export interface NexusGenScalars {
export interface NexusGenObjects {
Author: { // root type
- avatar: string; // String!
id: number; // Int!
join_date: NexusGenScalars['Date']; // Date!
lightning_address?: string | null; // String
@@ -117,7 +132,6 @@ export interface NexusGenObjects {
workplan: string; // String!
}
Category: { // root type
- cover_image?: string | null; // String
icon?: string | null; // String
id: number; // Int!
title: string; // String!
@@ -142,7 +156,6 @@ export interface NexusGenObjects {
title: string; // String!
}
Hackathon: { // root type
- cover_image: string; // String!
description: string; // String!
end_date: NexusGenScalars['Date']; // Date!
id: number; // Int!
@@ -169,8 +182,8 @@ export interface NexusGenObjects {
}
Mutation: {};
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 +199,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!
@@ -195,13 +213,10 @@ export interface NexusGenObjects {
votes_count: number; // Int!
}
Project: { // root type
- cover_image: string; // String!
description: string; // String!
id: number; // Int!
lightning_address?: string | null; // String
lnurl_callback_url?: string | null; // String
- screenshots: string[]; // [String!]!
- thumbnail_image: string; // String!
title: string; // String!
votes_count: number; // Int!
website: string; // String!
@@ -219,7 +234,6 @@ export interface NexusGenObjects {
}
Story: { // root type
body: string; // String!
- cover_image?: string | null; // String
createdAt: NexusGenScalars['Date']; // Date!
excerpt: string; // String!
id: number; // Int!
@@ -236,19 +250,54 @@ export interface NexusGenObjects {
title: string; // String!
}
Tournament: { // root type
- cover_image: string; // String!
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!
+ 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
+ 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!
+ 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 +320,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 +441,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 +474,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!
@@ -436,6 +495,7 @@ export interface NexusGenFieldTypes {
id: number; // Int!
lightning_address: string | null; // String
lnurl_callback_url: string | null; // String
+ recruit_roles: NexusGenRootTypes['MakerRole'][]; // [MakerRole!]!
screenshots: string[]; // [String!]!
tags: NexusGenRootTypes['Tag'][]; // [Tag!]!
thumbnail_image: string; // String!
@@ -453,9 +513,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
@@ -466,6 +529,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!
@@ -507,19 +571,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
@@ -545,6 +658,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!
@@ -552,9 +666,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
@@ -683,17 +798,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'
@@ -712,6 +831,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'
@@ -728,6 +852,7 @@ export interface NexusGenFieldTypeNames {
id: 'Int'
lightning_address: 'String'
lnurl_callback_url: 'String'
+ recruit_roles: 'MakerRole'
screenshots: 'String'
tags: 'Tag'
thumbnail_image: 'String'
@@ -745,9 +870,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'
@@ -758,6 +886,7 @@ export interface NexusGenFieldTypeNames {
projectsByCategory: 'Project'
searchProjects: 'Project'
similarMakers: 'User'
+ tournamentParticipationInfo: 'ParticipationInfo'
}
Question: { // field return type name
author: 'Author'
@@ -799,19 +928,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'
@@ -837,6 +1015,7 @@ export interface NexusGenFieldTypeNames {
payment_request: 'String'
}
WalletKey: { // field return type name
+ createdAt: 'Date'
is_current: 'Boolean'
key: 'String'
name: 'String'
@@ -844,9 +1023,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'
@@ -893,12 +1073,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!]
}
@@ -908,6 +1096,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
@@ -929,6 +1122,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!
}
@@ -939,6 +1140,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
@@ -963,6 +1174,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!
+ }
}
}
diff --git a/api/functions/graphql/schema.graphql b/api/functions/graphql/schema.graphql
index 75e6573..ebfa9b4 100644
--- a/api/functions/graphql/schema.graphql
+++ b/api/functions/graphql/schema.graphql
@@ -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
@@ -114,6 +115,12 @@ type Hackathon {
website: String!
}
+input ImageInput {
+ id: String
+ name: String
+ url: String!
+}
+
type LnurlDetails {
commentAllowed: Int
maxSendable: Int
@@ -148,8 +155,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 +166,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 +196,12 @@ enum POST_TYPE {
Story
}
+type ParticipationInfo {
+ createdAt: Date!
+ email: String!
+ hacking_status: TournamentMakerHackingStatusEnum!
+}
+
union Post = Bounty | Question | Story
interface PostBase {
@@ -208,8 +225,9 @@ type PostComment {
}
input ProfileDetailsInput {
- avatar: String
+ avatar: ImageInput
bio: String
+ discord: String
email: String
github: String
jobTitle: String
@@ -234,6 +252,7 @@ type Project {
id: Int!
lightning_address: String
lnurl_callback_url: String
+ recruit_roles: [MakerRole!]!
screenshots: [String!]!
tags: [Tag!]!
thumbnail_image: String!
@@ -252,9 +271,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
@@ -265,6 +287,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 {
@@ -281,6 +304,11 @@ type Question implements PostBase {
votes_count: Int!
}
+input RegisterInTournamentInput {
+ email: String!
+ hacking_status: TournamentMakerHackingStatusEnum!
+}
+
enum RoleLevelEnum {
Advanced
Beginner
@@ -308,7 +336,7 @@ type Story implements PostBase {
input StoryInputType {
body: String!
- cover_image: String
+ cover_image: ImageInput
id: Int
is_published: Boolean
tags: [String!]!
@@ -327,20 +355,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
@@ -382,6 +483,7 @@ type Vote {
}
type WalletKey {
+ createdAt: Date!
is_current: Boolean!
key: String!
name: String!
diff --git a/api/functions/graphql/types/category.js b/api/functions/graphql/types/category.js
index 0e7b205..4f3f5b8 100644
--- a/api/functions/graphql/types/category.js
+++ b/api/functions/graphql/types/category.js
@@ -4,7 +4,8 @@ const {
extendType,
nonNull,
} = require('nexus');
-const { prisma } = require('../../../prisma')
+const { prisma } = require('../../../prisma');
+const { resolveImgObjectToUrl } = require('../../../utils/resolveImageUrl');
const Category = objectType({
@@ -12,7 +13,11 @@ const Category = objectType({
definition(t) {
t.nonNull.int('id');
t.nonNull.string('title');
- t.string('cover_image');
+ t.string('cover_image', {
+ async resolve(parent) {
+ return prisma.category.findUnique({ where: { id: parent.id } }).cover_image_rel().then(resolveImgObjectToUrl)
+ }
+ });
t.string('icon');
diff --git a/api/functions/graphql/types/hackathon.js b/api/functions/graphql/types/hackathon.js
index c0b139a..6212b92 100644
--- a/api/functions/graphql/types/hackathon.js
+++ b/api/functions/graphql/types/hackathon.js
@@ -6,6 +6,7 @@ const {
nonNull,
} = require('nexus');
const { prisma } = require('../../../prisma');
+const { resolveImgObjectToUrl } = require('../../../utils/resolveImageUrl');
@@ -15,7 +16,11 @@ const Hackathon = objectType({
t.nonNull.int('id');
t.nonNull.string('title');
t.nonNull.string('description');
- t.nonNull.string('cover_image');
+ t.nonNull.string('cover_image', {
+ async resolve(parent) {
+ return prisma.hackathon.findUnique({ where: { id: parent.id } }).cover_image_rel().then(resolveImgObjectToUrl)
+ }
+ });
t.nonNull.date('start_date');
t.nonNull.date('end_date');
t.nonNull.string('location');
diff --git a/api/functions/graphql/types/index.js b/api/functions/graphql/types/index.js
index 75e2db0..2c7ad75 100644
--- a/api/functions/graphql/types/index.js
+++ b/api/functions/graphql/types/index.js
@@ -1,14 +1,17 @@
const scalars = require('./_scalars')
+const misc = require('./misc')
const category = require('./category')
const project = require('./project')
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')
module.exports = {
+ ...misc,
...tag,
...scalars,
...category,
@@ -17,5 +20,6 @@ module.exports = {
...post,
...users,
...hackathon,
+ ...tournament,
...donation,
}
\ No newline at end of file
diff --git a/api/functions/graphql/types/misc.js b/api/functions/graphql/types/misc.js
new file mode 100644
index 0000000..2fde16f
--- /dev/null
+++ b/api/functions/graphql/types/misc.js
@@ -0,0 +1,19 @@
+const { objectType, extendType, inputObjectType } = require("nexus");
+const { prisma } = require('../../../prisma');
+
+const ImageInput = inputObjectType({
+ name: 'ImageInput',
+ definition(t) {
+ t.string('id');
+ t.string('name');
+ t.nonNull.string('url');
+ }
+});
+
+
+module.exports = {
+ // Types
+ ImageInput,
+
+ // Queries
+}
\ No newline at end of file
diff --git a/api/functions/graphql/types/post.js b/api/functions/graphql/types/post.js
index 76881c6..1bf7999 100644
--- a/api/functions/graphql/types/post.js
+++ b/api/functions/graphql/types/post.js
@@ -15,6 +15,10 @@ const { prisma } = require('../../../prisma');
const { getUserByPubKey } = require('../../../auth/utils/helperFuncs');
const { ApolloError } = require('apollo-server-lambda');
const { marked } = require('marked');
+const { resolveImgObjectToUrl } = require('../../../utils/resolveImageUrl');
+const { ImageInput } = require('./misc');
+const { deleteImage } = require('../../../services/imageUpload.service');
+const { logError } = require('../../../utils/logger');
const POST_TYPE = enumType({
@@ -37,7 +41,11 @@ const Author = objectType({
definition(t) {
t.nonNull.int('id');
t.nonNull.string('name');
- t.nonNull.string('avatar');
+ t.nonNull.string('avatar', {
+ async resolve(parent) {
+ return prisma.user.findUnique({ where: { id: parent.id } }).avatar_rel().then(resolveImgObjectToUrl)
+ }
+ });
t.nonNull.date('join_date');
t.string('lightning_address');
@@ -71,7 +79,11 @@ const Story = objectType({
t.nonNull.string('type', {
resolve: () => t.typeName
});
- t.string('cover_image');
+ t.string('cover_image', {
+ async resolve(parent) {
+ return prisma.story.findUnique({ where: { id: parent.id } }).cover_image_rel().then(resolveImgObjectToUrl)
+ }
+ });
t.nonNull.list.nonNull.field('comments', {
type: "PostComment",
resolve: (parent) => []
@@ -111,7 +123,9 @@ const StoryInputType = inputObjectType({
t.int('id');
t.nonNull.string('title');
t.nonNull.string('body');
- t.string('cover_image');
+ t.field('cover_image', {
+ type: ImageInput
+ })
t.nonNull.list.nonNull.string('tags');
t.boolean('is_published')
}
@@ -342,6 +356,64 @@ const getPostById = extendType({
}
})
+const addCoverImage = async (providerImageId) => {
+ const newCoverImage = await prisma.hostedImage.findFirst({
+ where: {
+ provider_image_id: providerImageId
+ }
+ })
+
+ if (!newCoverImage) throw new ApolloError("New cover image not found")
+
+ await prisma.hostedImage.update({
+ where: {
+ id: newCoverImage.id
+ },
+ data: {
+ is_used: true
+ }
+ })
+
+ return newCoverImage
+}
+
+const getHostedImageIdsFromBody = async (body, oldBodyImagesIds = null) => {
+ let bodyImageIds = []
+
+ const regex = /(?:!\[(.*?)\]\((.*?)\))/g
+ let match;
+ while ((match = regex.exec(body))) {
+ const [, , value] = match
+
+ // Useful for old external images in case of duplicates. We need to be sure we are targeting an image from the good story.
+ const where = oldBodyImagesIds ? {
+ AND: [
+ { url: value },
+ { id: { in: oldBodyImagesIds } }
+ ]
+ } :
+ {
+ url: value,
+ }
+
+ const hostedImage = await prisma.hostedImage.findFirst({
+ where
+ })
+ if (hostedImage) {
+ bodyImageIds.push(hostedImage.id)
+ await prisma.hostedImage.update({
+ where: {
+ id: hostedImage.id
+ },
+ data: {
+ is_used: true
+ }
+ })
+ }
+ }
+ return bodyImageIds
+}
+
const createStory = extendType({
type: 'Mutation',
definition(t) {
@@ -358,49 +430,135 @@ const createStory = extendType({
let was_published = false;
- if (id) {
- const oldPost = await prisma.story.findFirst({
- where: { id },
- select: {
- user_id: true,
- is_published: true
- }
- })
- was_published = oldPost.is_published;
- if (user.id !== oldPost.user_id)
- throw new ApolloError("Not post author")
- }
+
// TODO: validate post data
+ let coverImage = null
+ let bodyImageIds = []
- // Preprocess & insert
- const htmlBody = marked.parse(body);
- const excerpt = htmlBody
- .replace(/<[^>]+>/g, '')
- .slice(0, 120)
- .replace(/&/g, "&")
- .replace(/'/g, "'")
- .replace(/"/g, '"')
- ;
- if (id) {
- await prisma.story.update({
- where: { id },
- data: {
- tags: {
- set: []
- },
+ try {
+ if (id) {
+ const oldPost = await prisma.story.findFirst({
+ where: { id },
+ select: {
+ user_id: true,
+ is_published: true,
+ cover_image_id: true,
+ body_image_ids: true
+ }
+ })
+ was_published = oldPost.is_published;
+ if (user.id !== oldPost.user_id) throw new ApolloError("Not post author")
+
+ // Body images
+ bodyImageIds = await getHostedImageIdsFromBody(body, oldPost.body_image_ids)
+
+ // Old cover image is found
+ if (oldPost.cover_image_id) {
+ const oldCoverImage = await prisma.hostedImage.findFirst({
+ where: {
+ id: oldPost.cover_image_id
+ }
+ })
+
+ // New cover image
+ if (cover_image?.id && cover_image.id !== oldCoverImage?.provider_image_id) {
+ await deleteImage(oldCoverImage.id)
+ coverImage = await addCoverImage(cover_image.id)
+ } else {
+ coverImage = oldCoverImage
+ }
+ } else {
+ // No old image found and new cover image
+ if (cover_image?.id) {
+ coverImage = await addCoverImage(cover_image.id)
+ }
}
- });
- return prisma.story.update({
- where: { id },
+ // Remove unused body images
+ const unusedImagesIds = oldPost.body_image_ids.filter(x => !bodyImageIds.includes(x));
+ unusedImagesIds.map(async i => await deleteImage(i))
+
+ } else {
+ // Body images
+ bodyImageIds = await getHostedImageIdsFromBody(body)
+
+ // New story and new cover image
+ if (cover_image?.id) {
+ coverImage = await addCoverImage(cover_image.id)
+ }
+ }
+
+ // Preprocess & insert
+ const htmlBody = marked.parse(body);
+ const excerpt = htmlBody
+ .replace(/<[^>]+>/g, '')
+ .slice(0, 120)
+ .replace(/&/g, "&")
+ .replace(/'/g, "'")
+ .replace(/"/g, '"')
+ ;
+
+
+ const coverImageRel = coverImage ? {
+ cover_image_rel: {
+ connect:
+ {
+ id: coverImage ? coverImage.id : null
+ }
+ }
+ } : {}
+
+ if (id) {
+ await prisma.story.update({
+ where: { id },
+ data: {
+ tags: {
+ set: []
+ },
+ }
+ });
+
+ return prisma.story.update({
+ where: { id },
+ data: {
+ title,
+ body,
+ cover_image: '',
+ excerpt,
+ is_published: was_published || is_published,
+ tags: {
+ connectOrCreate:
+ tags.map(tag => {
+ tag = tag.toLowerCase().trim();
+ return {
+ where: {
+ title: tag,
+ },
+ create: {
+ title: tag
+ }
+ }
+ })
+ },
+ body_image_ids: bodyImageIds,
+ ...coverImageRel
+ }
+ })
+ .catch(error => {
+ logError(error)
+ throw new ApolloError("Unexpected error happened...")
+ })
+ }
+
+ return prisma.story.create({
data: {
title,
body,
- cover_image,
+ cover_image: '',
excerpt,
- is_published: was_published || is_published,
+ is_published,
tags: {
connectOrCreate:
tags.map(tag => {
@@ -415,39 +573,24 @@ const createStory = extendType({
}
})
},
+ user: {
+ connect: {
+ id: user.id,
+ }
+ },
+ body_image_ids: bodyImageIds,
+ ...coverImageRel
}
+ }).catch(error => {
+ logError(error)
+ throw new ApolloError("Unexpected error happened...")
})
+
+
+ } catch (error) {
+ logError(error)
+ throw new ApolloError("Unexpected error happened...")
}
-
-
- return prisma.story.create({
- data: {
- title,
- body,
- cover_image,
- excerpt,
- is_published,
- tags: {
- connectOrCreate:
- tags.map(tag => {
- tag = tag.toLowerCase().trim();
- return {
- where: {
- title: tag,
- },
- create: {
- title: tag
- }
- }
- })
- },
- user: {
- connect: {
- id: user.id,
- }
- }
- }
- })
}
})
},
@@ -470,17 +613,39 @@ const deleteStory = extendType({
const oldPost = await prisma.story.findFirst({
where: { id },
select: {
- user_id: true
+ user_id: true,
+ body_image_ids: true,
+ cover_image_id: true
}
})
if (user.id !== oldPost.user_id)
throw new ApolloError("Not post author")
- return prisma.story.delete({
+ const deletedPost = await prisma.story.delete({
where: {
id
}
})
+
+ const coverImage = await prisma.hostedImage.findMany({
+ where: {
+ OR: [
+ { id: oldPost.cover_image_id },
+ {
+ id: {
+ in: oldPost.body_image_ids
+ }
+ }
+ ]
+ },
+ select: {
+ id: true,
+ provider_image_id: true
+ }
+ })
+ coverImage.map(async i => await deleteImage(i.id))
+
+ return deletedPost
}
})
},
diff --git a/api/functions/graphql/types/project.js b/api/functions/graphql/types/project.js
index a9fed49..6a360c6 100644
--- a/api/functions/graphql/types/project.js
+++ b/api/functions/graphql/types/project.js
@@ -6,8 +6,10 @@ const {
nonNull,
} = require('nexus')
const { prisma } = require('../../../prisma');
+const { resolveImgObjectToUrl } = require('../../../utils/resolveImageUrl');
const { paginationArgs, getLnurlDetails, lightningAddressToLnurl } = require('./helpers');
+const { MakerRole } = require('./users');
const Project = objectType({
@@ -16,9 +18,30 @@ const Project = objectType({
t.nonNull.int('id');
t.nonNull.string('title');
t.nonNull.string('description');
- t.nonNull.string('cover_image');
- t.nonNull.string('thumbnail_image');
- t.nonNull.list.nonNull.string('screenshots');
+ t.nonNull.string('cover_image', {
+ async resolve(parent) {
+ return prisma.project.findUnique({ where: { id: parent.id } }).cover_image_rel().then(resolveImgObjectToUrl)
+ }
+ });
+ t.nonNull.string('thumbnail_image', {
+ async resolve(parent) {
+ return prisma.project.findUnique({ where: { id: parent.id } }).thumbnail_image_rel().then(resolveImgObjectToUrl)
+ }
+ });
+ t.nonNull.list.nonNull.string('screenshots', {
+ async resolve(parent) {
+ if (!parent.screenshots_ids) return null
+ const imgObject = await prisma.hostedImage.findMany({
+ where: {
+ id: { in: parent.screenshots_ids }
+ }
+ });
+
+ return imgObject.map(img => {
+ return resolveImgObjectToUrl(img);
+ });
+ }
+ });
t.nonNull.string('website');
t.string('lightning_address');
t.string('lnurl_callback_url');
@@ -44,6 +67,28 @@ const Project = objectType({
return prisma.project.findUnique({ where: { id: parent.id } }).tags();
}
})
+
+ t.nonNull.list.nonNull.field('recruit_roles', {
+ type: MakerRole,
+ resolve: async (parent) => {
+ const data = await prisma.project.findUnique({
+ where: {
+ id: parent.id
+ },
+ select: {
+ recruit_roles: {
+ select: {
+ role: true,
+ level: true
+ }
+ },
+ }
+ })
+ return data.recruit_roles.map(data => {
+ return ({ ...data.role, level: data.level })
+ })
+ }
+ })
}
})
diff --git a/api/functions/graphql/types/tournament.js b/api/functions/graphql/types/tournament.js
new file mode 100644
index 0000000..7e05b78
--- /dev/null
+++ b/api/functions/graphql/types/tournament.js
@@ -0,0 +1,545 @@
+const {
+ intArg,
+ objectType,
+ stringArg,
+ extendType,
+ nonNull,
+ enumType,
+ inputObjectType,
+ booleanArg,
+} = require('nexus');
+const { getUserByPubKey } = require('../../../auth/utils/helperFuncs');
+const { resolveImgObjectToUrl } = require('../../../utils/resolveImageUrl');
+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', {
+ async resolve(parent) {
+ return prisma.tournamentPrize.findUnique({ where: { id: parent.id } }).image_rel().then(resolveImgObjectToUrl)
+ }
+ });
+ }
+})
+
+const TournamentJudge = objectType({
+ name: 'TournamentJudge',
+ definition(t) {
+ t.nonNull.string('name');
+ t.nonNull.string('company');
+ t.nonNull.string('avatar', {
+ async resolve(parent) {
+ return prisma.tournamentJudge.findUnique({ where: { id: parent.id } }).avatar_rel().then(resolveImgObjectToUrl)
+ }
+ });
+ }
+})
+
+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', {
+ async resolve(parent) {
+ return prisma.tournamentEvent.findUnique({ where: { id: parent.id } }).image_rel().then(resolveImgObjectToUrl)
+ }
+ });
+ 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', {
+ async resolve(parent) {
+ return prisma.tournament.findUnique({ where: { id: parent.id } }).thumbnail_image_rel().then(resolveImgObjectToUrl)
+ }
+ });
+ t.nonNull.string('cover_image', {
+ async resolve(parent) {
+ return prisma.tournament.findUnique({ where: { id: parent.id } }).cover_image_rel().then(resolveImgObjectToUrl)
+ }
+ });
+ 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,
+}
diff --git a/api/functions/graphql/types/tournaments.js b/api/functions/graphql/types/tournaments.js
index 9f04081..e69de29 100644
--- a/api/functions/graphql/types/tournaments.js
+++ b/api/functions/graphql/types/tournaments.js
@@ -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,
-}
\ No newline at end of file
diff --git a/api/functions/graphql/types/users.js b/api/functions/graphql/types/users.js
index 8b9cd1c..dfd3afd 100644
--- a/api/functions/graphql/types/users.js
+++ b/api/functions/graphql/types/users.js
@@ -3,7 +3,10 @@ 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 { ImageInput } = require('./misc');
+const { Tournament } = require('./tournament');
+const { resolveImgObjectToUrl } = require('../../../utils/resolveImageUrl');
+const { deleteImage } = require('../../../services/imageUpload.service');
@@ -13,14 +16,18 @@ const BaseUser = interfaceType({
definition(t) {
t.nonNull.int('id');
t.nonNull.string('name');
- t.nonNull.string('avatar');
+ t.nonNull.string('avatar', {
+ async resolve(parent) {
+ return prisma.user.findUnique({ where: { id: parent.id } }).avatar_rel().then(resolveImgObjectToUrl)
+ }
+ });
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 +61,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 +96,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,14 +189,23 @@ const MyProfile = objectType({
name: 'MyProfile',
definition(t) {
t.implements('BaseUser')
+
+ t.string('email')
t.string('nostr_prv_key')
t.string('nostr_pub_key')
t.nonNull.list.nonNull.field('walletsKeys', {
type: "WalletKey",
- resolve: async (parent, _, context) => {
- const userKeys = await prisma.user.findUnique({ where: { id: parent.id } }).userKeys();
- return userKeys.map(k => ({ ...k, is_current: k.key === context.userPubKey }))
+ resolve: (parent, _, context) => {
+ return prisma.userKey.findMany({
+ where: {
+ user_id: parent.id,
+ },
+ orderBy: {
+ createdAt: "asc"
+ }
+ })
+ .then(keys => keys.map(k => ({ ...k, is_current: k.key === context.userPubKey })))
}
});
}
@@ -202,6 +234,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 } })
}
})
@@ -236,12 +273,15 @@ const ProfileDetailsInput = inputObjectType({
name: 'ProfileDetailsInput',
definition(t) {
t.string('name');
- t.string('avatar');
+ t.field('avatar', {
+ type: ImageInput
+ })
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')
@@ -263,14 +303,48 @@ const updateProfileDetails = extendType({
throw new Error("You have to login");
// TODO: validate new data
+ // ----------------
+ // Check if the user uploaded a new image, and if so,
+ // remove the old one from the hosting service, then replace it with this one
+ // ----------------
+ let avatarId = user.avatar_id;
+ if (args.data.avatar.id) {
+ const newAvatarProviderId = args.data.avatar.id;
+ const newAvatar = await prisma.hostedImage.findFirst({
+ where: {
+ provider_image_id: newAvatarProviderId
+ }
+ })
+
+ if (newAvatar && newAvatar.id !== user.avatar_id) {
+ avatarId = newAvatar.id;
+
+ await prisma.hostedImage.update({
+ where: {
+ id: newAvatar.id
+ },
+ data: {
+ is_used: true
+ }
+ });
+
+ await deleteImage(user.avatar_id)
+ }
+ }
// Preprocess & insert
-
return prisma.user.update({
where: {
id: user.id,
},
- data: removeNulls(args.data)
+ data: removeNulls({
+ ...args.data,
+ avatar_id: avatarId,
+
+ //hack to remove avatar from args.data
+ // can be removed later with a schema data validator
+ avatar: '',
+ })
})
}
})
@@ -283,6 +357,7 @@ const WalletKey = objectType({
definition(t) {
t.nonNull.string('key');
t.nonNull.string('name');
+ t.nonNull.date('createdAt');
t.nonNull.boolean('is_current')
}
})
@@ -451,6 +526,7 @@ module.exports = {
User,
MyProfile,
WalletKey,
+ MakerRole,
// Queries
me,
profile,
diff --git a/api/functions/login/login.js b/api/functions/login/login.js
index 782f195..9dbf7e8 100644
--- a/api/functions/login/login.js
+++ b/api/functions/login/login.js
@@ -9,6 +9,7 @@ const jose = require('jose');
const { JWT_SECRET } = require('../../utils/consts');
const { generatePrivateKey, getPublicKey } = require('../../utils/nostr-tools');
const { getUserByPubKey } = require('../../auth/utils/helperFuncs');
+const { logError } = require('../../utils/logger');
@@ -28,10 +29,18 @@ const loginHandler = async (req, res) => {
if (action === 'link' && user_token) {
try {
- const { payload } = await jose.jwtVerify(user_token, Buffer.from(JWT_SECRET), {
- algorithms: ['HS256'],
- })
- const user_id = payload.user_id;
+
+ let user_id;
+
+ try {
+ const { payload } = await jose.jwtVerify(user_token, Buffer.from(JWT_SECRET), {
+ algorithms: ['HS256'],
+ })
+ user_id = payload.user_id;
+ } catch (error) {
+ return res.status(400).json({ status: 'ERROR', reason: "Invalid user_token" })
+ }
+
const existingKeys = await prisma.userKey.findMany({ where: { user_id }, select: { key: true } });
if (existingKeys.length >= 3)
@@ -55,7 +64,8 @@ const loginHandler = async (req, res) => {
.json({ status: "OK" })
} catch (error) {
- return res.status(400).json({ status: 'ERROR', reason: 'Invalid User Token' })
+ logError(error)
+ return res.status(500).json({ status: 'ERROR', reason: 'Unexpected error happened' })
}
}
@@ -65,8 +75,7 @@ const loginHandler = async (req, res) => {
const user = await getUserByPubKey(key)
if (user === null) {
- // Check if user had a previous account using this wallet
-
+ // Check if user had a previous account using this wallet
const oldAccount = await prisma.user.findFirst({
where: {
pubKey: key
@@ -85,11 +94,21 @@ const loginHandler = async (req, res) => {
const nostr_prv_key = generatePrivateKey();
const nostr_pub_key = getPublicKey(nostr_prv_key);
+ const avatar = await prisma.hostedImage.create({
+ data: {
+ filename: 'avatar.svg',
+ provider: 'external',
+ is_used: true,
+ url: `https://avatars.dicebear.com/api/bottts/${key}.svg`,
+ provider_image_id: ''
+ }
+ })
+
const createdUser = await prisma.user.create({
data: {
pubKey: key,
name: key,
- avatar: `https://avatars.dicebear.com/api/bottts/${key}.svg`,
+ avatar_id: avatar.id,
nostr_prv_key,
nostr_pub_key,
},
@@ -126,7 +145,8 @@ const loginHandler = async (req, res) => {
return res.status(200).json({ status: "OK" })
} catch (error) {
- return res.status(400).json({ status: 'ERROR', reason: 'Unexpected error happened, please try again' })
+ logError(error)
+ return res.status(500).json({ status: 'ERROR', reason: 'Unexpected error happened, please try again' })
}
}
diff --git a/api/functions/upload-image-url/upload-image-url.js b/api/functions/upload-image-url/upload-image-url.js
index b40682a..91f0eb7 100644
--- a/api/functions/upload-image-url/upload-image-url.js
+++ b/api/functions/upload-image-url/upload-image-url.js
@@ -5,11 +5,10 @@ const extractKeyFromCookie = require('../../utils/extractKeyFromCookie')
const { getUserByPubKey } = require('../../auth/utils/helperFuncs')
const { getDirectUploadUrl } = require('../../services/imageUpload.service')
const { prisma } = require('../../prisma')
+const { getUrlFromProvider } = require('../../utils/resolveImageUrl')
const postUploadImageUrl = async (req, res) => {
- return res.status(404).send("This api is in progress");
-
const userPubKey = await extractKeyFromCookie(req.headers.cookie ?? req.headers.Cookie)
const user = await getUserByPubKey(userPubKey)
@@ -22,11 +21,16 @@ const postUploadImageUrl = async (req, res) => {
try {
const uploadUrl = await getDirectUploadUrl()
- await prisma.hostedImage.create({
- data: { id: uploadUrl.id, filename },
+ const hostedImage = await prisma.hostedImage.create({
+ data: {
+ filename,
+ url: getUrlFromProvider(uploadUrl.provider, uploadUrl.id),
+ provider_image_id: uploadUrl.id,
+ provider: uploadUrl.provider
+ },
})
- return res.status(200).json(uploadUrl)
+ return res.status(200).json({ id: hostedImage.id, uploadURL: uploadUrl.uploadURL })
} catch (error) {
res.status(500).send('Unexpected error happened, please try again')
}
diff --git a/api/services/imageUpload.service.js b/api/services/imageUpload.service.js
index 642c0de..ef63701 100644
--- a/api/services/imageUpload.service.js
+++ b/api/services/imageUpload.service.js
@@ -1,11 +1,21 @@
const { CONSTS } = require('../utils')
const axios = require('axios')
const FormData = require('form-data')
+const { prisma } = require('../prisma')
const BASE_URL = 'https://api.cloudflare.com/client/v4'
const operationUrls = {
'image.uploadUrl': `${BASE_URL}/accounts/${CONSTS.CLOUDFLARE_IMAGE_ACCOUNT_ID}/images/v2/direct_upload`,
+ 'image.delete': `${BASE_URL}/accounts/${CONSTS.CLOUDFLARE_IMAGE_ACCOUNT_ID}/images/v1/`,
+}
+
+function getAxiosConfig() {
+ return {
+ headers: {
+ Authorization: `Bearer ${CONSTS.CLOUDFLARE_IMAGE_API_KEY}`,
+ },
+ }
}
async function getDirectUploadUrl() {
@@ -27,9 +37,62 @@ async function getDirectUploadUrl() {
throw new Error(result.data, { cause: result.data.errors })
}
- return result.data.result
+ const data = result.data.result
+
+ return { id: data.id, uploadURL: data.uploadURL, provider: 'cloudflare' }
+}
+
+async function deleteImageFromProvider(providerImageId) {
+ try {
+ const url = operationUrls['image.delete'] + providerImageId
+ const result = await axios.delete(url, getAxiosConfig())
+
+ if (!result.data.success) {
+ throw new Error(result.data, { cause: result.data.errors })
+ }
+ } catch (error) {
+ throw error
+ }
+}
+
+async function deleteImage(hostedImageId) {
+ if (!hostedImageId) throw new Error("argument 'hostedImageId' must be provider")
+
+ const hostedImage = await prisma.hostedImage.findFirst({
+ where: {
+ id: hostedImageId,
+ },
+ })
+
+ if (!hostedImage) throw new Error(`No HostedImage row found for HostedImage.id=${hostedImageId}`)
+ if (hostedImage.provider_image_id && hostedImage.provider_image_id === '')
+ throw new Error(`Field 'provider_image_id' for HostedImage.id=${hostedImageId} must not be empty. Current value '${hostedImage.provider_image_id}'`)
+
+ // Set is_used to false in case of deletion fail from the hosting image provider. The scheduled job will try to delete the HostedImage row
+ await prisma.hostedImage.update({
+ where: {
+ id: hostedImage.id,
+ },
+ data: {
+ is_used: false,
+ },
+ })
+
+ if (hostedImage.provider_image_id && hostedImage.provider_image_id !== '') {
+ deleteImageFromProvider(hostedImage.provider_image_id)
+ .then(async () => {
+ await prisma.hostedImage.delete({
+ where: {
+ id: hostedImageId,
+ },
+ })
+ })
+ .catch((error) => console.error(error))
+ }
}
module.exports = {
getDirectUploadUrl,
+ deleteImage,
+ deleteImageFromProvider,
}
diff --git a/api/utils/consts.js b/api/utils/consts.js
index 8f2083b..bc62418 100644
--- a/api/utils/consts.js
+++ b/api/utils/consts.js
@@ -3,6 +3,7 @@ const JWT_SECRET = process.env.JWT_SECRET
const LNURL_AUTH_HOST = process.env.LNURL_AUTH_HOST
const CLOUDFLARE_IMAGE_ACCOUNT_ID = process.env.CLOUDFLARE_IMAGE_ACCOUNT_ID
const CLOUDFLARE_IMAGE_API_KEY = process.env.CLOUDFLARE_IMAGE_API_KEY
+const CLOUDFLARE_IMAGE_ACCOUNT_HASH = process.env.CLOUDFLARE_IMAGE_ACCOUNT_HASH
const CONSTS = {
JWT_SECRET,
@@ -10,6 +11,7 @@ const CONSTS = {
LNURL_AUTH_HOST,
CLOUDFLARE_IMAGE_ACCOUNT_ID,
CLOUDFLARE_IMAGE_API_KEY,
+ CLOUDFLARE_IMAGE_ACCOUNT_HASH
}
module.exports = CONSTS
diff --git a/api/utils/logger.js b/api/utils/logger.js
new file mode 100644
index 0000000..5bc72ab
--- /dev/null
+++ b/api/utils/logger.js
@@ -0,0 +1,9 @@
+
+function logError(error) {
+ console.log("Unexpected Error: ");
+ console.log(error);
+}
+
+module.exports = {
+ logError
+}
\ No newline at end of file
diff --git a/api/utils/resolveImageUrl.js b/api/utils/resolveImageUrl.js
new file mode 100644
index 0000000..40931ed
--- /dev/null
+++ b/api/utils/resolveImageUrl.js
@@ -0,0 +1,45 @@
+const { CLOUDFLARE_IMAGE_ACCOUNT_HASH } = require('./consts')
+
+const PROVIDERS = [
+ {
+ name: 'cloudflare',
+ prefixUrl: `https://imagedelivery.net/${CLOUDFLARE_IMAGE_ACCOUNT_HASH}/`,
+ variants: [
+ {
+ default: true,
+ name: 'public',
+ },
+ ],
+ },
+]
+
+/**
+ * resolveImgObjectToUrl
+ * @param {object} imgObject
+ * @param {string} variant - List to be defined. DEFAULT TO 'public'
+ * @returns {string} image url
+ */
+function resolveImgObjectToUrl(imgObject, variant = null) {
+ if (!imgObject) return null;
+
+ if (imgObject.provider === 'external') {
+ return imgObject.url
+ }
+
+ return getUrlFromProvider(imgObject.provider, imgObject.provider_image_id, variant)
+}
+
+function getUrlFromProvider(provider, providerImageId, variant = null) {
+ const p = PROVIDERS.find((p) => p.name === provider)
+
+ if (p) {
+ if (p && p.name === 'cloudflare') {
+ const variantName = variant ?? p.variants.find((v) => v.default).name
+ return p.prefixUrl + providerImageId + '/' + variantName
+ }
+ }
+
+ throw new Error('Hosting images provider not supported')
+}
+
+module.exports = { resolveImgObjectToUrl, getUrlFromProvider }
diff --git a/codegen.yml b/codegen.yml
index 28cd2dd..b2c805d 100644
--- a/codegen.yml
+++ b/codegen.yml
@@ -9,4 +9,8 @@ generates:
- "typescript-react-apollo"
config:
withHooks: true
- avoidOptionals: true
+ avoidOptionals:
+ field: true
+ inputValue: false
+ object: true
+ defaultValue: true
diff --git a/package-lock.json b/package-lock.json
index 1638ed0..19b851c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
@@ -17,6 +17,11 @@
"@reduxjs/toolkit": "^1.8.1",
"@remirror/pm": "^1.0.16",
"@remirror/react": "^1.0.34",
+ "@rpldy/mock-sender": "^1.0.1",
+ "@rpldy/upload-button": "^1.0.1",
+ "@rpldy/upload-drop-zone": "^1.0.1",
+ "@rpldy/upload-preview": "^1.0.1",
+ "@rpldy/uploady": "^1.0.1",
"@shopify/react-web-worker": "^5.0.1",
"@szhsin/react-menu": "^3.0.2",
"@testing-library/jest-dom": "^5.16.4",
@@ -71,6 +76,7 @@
"react-copy-to-clipboard": "^5.1.0",
"react-datepicker": "^4.7.0",
"react-dom": "^18.0.0",
+ "react-error-boundary": "^3.1.4",
"react-file-drop": "^3.1.4",
"react-helmet": "^6.1.0",
"react-hook-form": "^7.30.0",
@@ -113,6 +119,8 @@
"@storybook/react": "^6.4.22",
"@storybook/testing-library": "^0.0.10",
"@tailwindcss/forms": "^0.5.0",
+ "@tailwindcss/line-clamp": "^0.4.2",
+ "@tailwindcss/typography": "^0.5.7",
"@types/chance": "^1.1.3",
"@types/dompurify": "^2.3.3",
"@types/fslightbox-react": "^1.4.2",
@@ -6935,6 +6943,170 @@
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz",
"integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw=="
},
+ "node_modules/@rpldy/life-events": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rpldy/life-events/-/life-events-1.0.1.tgz",
+ "integrity": "sha512-z9b8Yi1jq4/Um0BQJVsKwwBps8jB+X6UMJXU3dG7Q4rHe7rIiAQw4fCjN5W3L4b9JF9jTs9yX+X7ouWvBLkPVw==",
+ "dependencies": {
+ "@rpldy/shared": "^1.0.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-uploady"
+ }
+ },
+ "node_modules/@rpldy/mock-sender": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rpldy/mock-sender/-/mock-sender-1.0.1.tgz",
+ "integrity": "sha512-87UT/az8J2AD6NIYX/uVp6GUqM/M4vVpZsXod14GkNkAOOilGow6R1W/3PW7lxx8bjfM51Moraipq1KNwlIODA==",
+ "dependencies": {
+ "@rpldy/sender": "^1.0.1",
+ "@rpldy/shared": "^1.0.1",
+ "@rpldy/uploader": "^1.0.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-uploady"
+ }
+ },
+ "node_modules/@rpldy/sender": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rpldy/sender/-/sender-1.0.1.tgz",
+ "integrity": "sha512-5lGB2uPP22xESYhXdqzKKqZTtS03e0Gi9xx+1mu3XLEpUH7uu55dUMa8CT1cOzLM94JWwaS3fEQG+yB3n9Q7HQ==",
+ "dependencies": {
+ "@rpldy/shared": "^1.0.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-uploady"
+ }
+ },
+ "node_modules/@rpldy/shared": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rpldy/shared/-/shared-1.0.1.tgz",
+ "integrity": "sha512-22R1ZI+J4vvD6JhHlevxYwn6PxSZ2eXmP1mHxkW/7MHgTWgcKD2xBhl2khfjub64rqnhhiU1KDumhAWEO3GoAw==",
+ "dependencies": {
+ "invariant": "^2.2.4",
+ "just-throttle": "^1.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-uploady"
+ }
+ },
+ "node_modules/@rpldy/shared-ui": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rpldy/shared-ui/-/shared-ui-1.0.1.tgz",
+ "integrity": "sha512-pEKKifp4srk8vWyV7TAmpMRz5Dx66YcjOggP40ZZ7TV+CQfjY+b8TY1zx48ptakHqdpbG6Qzz0+OaA+VrOd3mA==",
+ "dependencies": {
+ "@rpldy/shared": "^1.0.1",
+ "@rpldy/uploader": "^1.0.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-uploady"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
+ "node_modules/@rpldy/simple-state": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rpldy/simple-state/-/simple-state-1.0.1.tgz",
+ "integrity": "sha512-ku/WBl2RFCqMORGEL9/qklMTVN92mqaLbQAY7JtS+IhpG3hfIzwMEXh1eVAs8bpSU2nrsp2SeR1ykyxU8aFWiw==",
+ "dependencies": {
+ "@rpldy/shared": "^1.0.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-uploady"
+ }
+ },
+ "node_modules/@rpldy/upload-button": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rpldy/upload-button/-/upload-button-1.0.1.tgz",
+ "integrity": "sha512-HuJuWlI9xljD7rQAmL5kEf40sOLZs8KF4VQCaj0y8+ErnEblmvjh/pSuRp+QgXVPT5NYrCq2YXSqOK9UYTBdsA==",
+ "dependencies": {
+ "@rpldy/shared-ui": "^1.0.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-uploady"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
+ "node_modules/@rpldy/upload-drop-zone": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rpldy/upload-drop-zone/-/upload-drop-zone-1.0.1.tgz",
+ "integrity": "sha512-B6fhOuIrzCQhHKal+hoGkiubEVxjzFPja9RoYd5dOtaZ7aMwmTX5dtFgVyaL1hjit7H0npHkD6v27b4K6snCXw==",
+ "dependencies": {
+ "@rpldy/shared-ui": "^1.0.1",
+ "html-dir-content": "^0.3.2"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-uploady"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
+ "node_modules/@rpldy/upload-preview": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rpldy/upload-preview/-/upload-preview-1.0.1.tgz",
+ "integrity": "sha512-Cq4+9fdgV2VRG4MsD9gnQ6AdRC4hMgPM4o88Iq7ifS5cDKNZa3hPVauSN39VpcoOZca1rDrdVslp9eStqmDTBg==",
+ "dependencies": {
+ "@rpldy/shared": "^1.0.1",
+ "@rpldy/shared-ui": "^1.0.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-uploady"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
+ "node_modules/@rpldy/uploader": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rpldy/uploader/-/uploader-1.0.1.tgz",
+ "integrity": "sha512-QuFvKu/xdCtiQU8Szx/pH/3MIJvnTMtDlmN5P62GuHrgUJw9SypZCCv4hCLuq1LuHs7ZENplToPjvj3pscYFdA==",
+ "dependencies": {
+ "@rpldy/life-events": "^1.0.1",
+ "@rpldy/sender": "^1.0.1",
+ "@rpldy/shared": "^1.0.1",
+ "@rpldy/simple-state": "^1.0.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-uploady"
+ }
+ },
+ "node_modules/@rpldy/uploady": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rpldy/uploady/-/uploady-1.0.1.tgz",
+ "integrity": "sha512-dwyNdYVy/wtioFfMUDsuAuHKKMi2/zMzKaiuOFGgPpx+3a9cVnbORNW/RcAMO6fVYLQgS4VxnqNSdnjS2m6buw==",
+ "dependencies": {
+ "@rpldy/life-events": "^1.0.1",
+ "@rpldy/shared": "^1.0.1",
+ "@rpldy/shared-ui": "^1.0.1",
+ "@rpldy/uploader": "^1.0.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-uploady"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
"node_modules/@rushstack/eslint-patch": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.1.3.tgz",
@@ -15322,6 +15494,30 @@
"tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1"
}
},
+ "node_modules/@tailwindcss/line-clamp": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/line-clamp/-/line-clamp-0.4.2.tgz",
+ "integrity": "sha512-HFzAQuqYCjyy/SX9sLGB1lroPzmcnWv1FHkIpmypte10hptf4oPUfucryMKovZh2u0uiS9U5Ty3GghWfEJGwVw==",
+ "dev": true,
+ "peerDependencies": {
+ "tailwindcss": ">=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1"
+ }
+ },
+ "node_modules/@tailwindcss/typography": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.7.tgz",
+ "integrity": "sha512-JTTSTrgZfp6Ki4svhPA4mkd9nmQ/j9EfE7SbHJ1cLtthKkpW2OxsFXzSmxbhYbEkfNIyAyhle5p4SYyKRbz/jg==",
+ "dev": true,
+ "dependencies": {
+ "lodash.castarray": "^4.4.0",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.merge": "^4.6.2",
+ "postcss-selector-parser": "6.0.10"
+ },
+ "peerDependencies": {
+ "tailwindcss": ">=3.0.0 || insiders"
+ }
+ },
"node_modules/@testing-library/dom": {
"version": "8.13.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.13.0.tgz",
@@ -27139,6 +27335,11 @@
"safe-buffer": "~5.1.0"
}
},
+ "node_modules/html-dir-content": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/html-dir-content/-/html-dir-content-0.3.2.tgz",
+ "integrity": "sha512-a1EJZbvBGmmFwk9VxFhEgaHkyXUXKTkw0jr0FCvXKCqgzO1H0wbFQbbzRA6FhR3twxAyjqVc80bzGHEmKrYsSw=="
+ },
"node_modules/html-encoding-sniffer": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
@@ -27753,7 +27954,6 @@
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
- "dev": true,
"dependencies": {
"loose-envify": "^1.0.0"
}
@@ -30912,6 +31112,11 @@
"node": ">=8"
}
},
+ "node_modules/just-throttle": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/just-throttle/-/just-throttle-1.1.0.tgz",
+ "integrity": "sha512-iePC/13XYX1Tyn9C6jY+DG3UEejkDvrKsw5xxgGhtGUwYWmoJm4CoKexscBKELOu3FTyCDzjr21ZJ67AXnz+bg=="
+ },
"node_modules/jwa": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
@@ -31588,6 +31793,12 @@
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
},
+ "node_modules/lodash.castarray": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
+ "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==",
+ "dev": true
+ },
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@@ -62210,6 +62421,21 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-error-boundary": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz",
+ "integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5"
+ },
+ "engines": {
+ "node": ">=10",
+ "npm": ">=6"
+ },
+ "peerDependencies": {
+ "react": ">=16.13.1"
+ }
+ },
"node_modules/react-error-overlay": {
"version": "6.0.11",
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz",
@@ -75883,6 +76109,106 @@
}
}
},
+ "@rpldy/life-events": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rpldy/life-events/-/life-events-1.0.1.tgz",
+ "integrity": "sha512-z9b8Yi1jq4/Um0BQJVsKwwBps8jB+X6UMJXU3dG7Q4rHe7rIiAQw4fCjN5W3L4b9JF9jTs9yX+X7ouWvBLkPVw==",
+ "requires": {
+ "@rpldy/shared": "^1.0.1"
+ }
+ },
+ "@rpldy/mock-sender": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rpldy/mock-sender/-/mock-sender-1.0.1.tgz",
+ "integrity": "sha512-87UT/az8J2AD6NIYX/uVp6GUqM/M4vVpZsXod14GkNkAOOilGow6R1W/3PW7lxx8bjfM51Moraipq1KNwlIODA==",
+ "requires": {
+ "@rpldy/sender": "^1.0.1",
+ "@rpldy/shared": "^1.0.1",
+ "@rpldy/uploader": "^1.0.1"
+ }
+ },
+ "@rpldy/sender": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rpldy/sender/-/sender-1.0.1.tgz",
+ "integrity": "sha512-5lGB2uPP22xESYhXdqzKKqZTtS03e0Gi9xx+1mu3XLEpUH7uu55dUMa8CT1cOzLM94JWwaS3fEQG+yB3n9Q7HQ==",
+ "requires": {
+ "@rpldy/shared": "^1.0.1"
+ }
+ },
+ "@rpldy/shared": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rpldy/shared/-/shared-1.0.1.tgz",
+ "integrity": "sha512-22R1ZI+J4vvD6JhHlevxYwn6PxSZ2eXmP1mHxkW/7MHgTWgcKD2xBhl2khfjub64rqnhhiU1KDumhAWEO3GoAw==",
+ "requires": {
+ "invariant": "^2.2.4",
+ "just-throttle": "^1.1.0"
+ }
+ },
+ "@rpldy/shared-ui": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rpldy/shared-ui/-/shared-ui-1.0.1.tgz",
+ "integrity": "sha512-pEKKifp4srk8vWyV7TAmpMRz5Dx66YcjOggP40ZZ7TV+CQfjY+b8TY1zx48ptakHqdpbG6Qzz0+OaA+VrOd3mA==",
+ "requires": {
+ "@rpldy/shared": "^1.0.1",
+ "@rpldy/uploader": "^1.0.1"
+ }
+ },
+ "@rpldy/simple-state": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rpldy/simple-state/-/simple-state-1.0.1.tgz",
+ "integrity": "sha512-ku/WBl2RFCqMORGEL9/qklMTVN92mqaLbQAY7JtS+IhpG3hfIzwMEXh1eVAs8bpSU2nrsp2SeR1ykyxU8aFWiw==",
+ "requires": {
+ "@rpldy/shared": "^1.0.1"
+ }
+ },
+ "@rpldy/upload-button": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rpldy/upload-button/-/upload-button-1.0.1.tgz",
+ "integrity": "sha512-HuJuWlI9xljD7rQAmL5kEf40sOLZs8KF4VQCaj0y8+ErnEblmvjh/pSuRp+QgXVPT5NYrCq2YXSqOK9UYTBdsA==",
+ "requires": {
+ "@rpldy/shared-ui": "^1.0.1"
+ }
+ },
+ "@rpldy/upload-drop-zone": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rpldy/upload-drop-zone/-/upload-drop-zone-1.0.1.tgz",
+ "integrity": "sha512-B6fhOuIrzCQhHKal+hoGkiubEVxjzFPja9RoYd5dOtaZ7aMwmTX5dtFgVyaL1hjit7H0npHkD6v27b4K6snCXw==",
+ "requires": {
+ "@rpldy/shared-ui": "^1.0.1",
+ "html-dir-content": "^0.3.2"
+ }
+ },
+ "@rpldy/upload-preview": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rpldy/upload-preview/-/upload-preview-1.0.1.tgz",
+ "integrity": "sha512-Cq4+9fdgV2VRG4MsD9gnQ6AdRC4hMgPM4o88Iq7ifS5cDKNZa3hPVauSN39VpcoOZca1rDrdVslp9eStqmDTBg==",
+ "requires": {
+ "@rpldy/shared": "^1.0.1",
+ "@rpldy/shared-ui": "^1.0.1"
+ }
+ },
+ "@rpldy/uploader": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rpldy/uploader/-/uploader-1.0.1.tgz",
+ "integrity": "sha512-QuFvKu/xdCtiQU8Szx/pH/3MIJvnTMtDlmN5P62GuHrgUJw9SypZCCv4hCLuq1LuHs7ZENplToPjvj3pscYFdA==",
+ "requires": {
+ "@rpldy/life-events": "^1.0.1",
+ "@rpldy/sender": "^1.0.1",
+ "@rpldy/shared": "^1.0.1",
+ "@rpldy/simple-state": "^1.0.1"
+ }
+ },
+ "@rpldy/uploady": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rpldy/uploady/-/uploady-1.0.1.tgz",
+ "integrity": "sha512-dwyNdYVy/wtioFfMUDsuAuHKKMi2/zMzKaiuOFGgPpx+3a9cVnbORNW/RcAMO6fVYLQgS4VxnqNSdnjS2m6buw==",
+ "requires": {
+ "@rpldy/life-events": "^1.0.1",
+ "@rpldy/shared": "^1.0.1",
+ "@rpldy/shared-ui": "^1.0.1",
+ "@rpldy/uploader": "^1.0.1"
+ }
+ },
"@rushstack/eslint-patch": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.1.3.tgz",
@@ -82371,6 +82697,25 @@
"mini-svg-data-uri": "^1.2.3"
}
},
+ "@tailwindcss/line-clamp": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/line-clamp/-/line-clamp-0.4.2.tgz",
+ "integrity": "sha512-HFzAQuqYCjyy/SX9sLGB1lroPzmcnWv1FHkIpmypte10hptf4oPUfucryMKovZh2u0uiS9U5Ty3GghWfEJGwVw==",
+ "dev": true,
+ "requires": {}
+ },
+ "@tailwindcss/typography": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.7.tgz",
+ "integrity": "sha512-JTTSTrgZfp6Ki4svhPA4mkd9nmQ/j9EfE7SbHJ1cLtthKkpW2OxsFXzSmxbhYbEkfNIyAyhle5p4SYyKRbz/jg==",
+ "dev": true,
+ "requires": {
+ "lodash.castarray": "^4.4.0",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.merge": "^4.6.2",
+ "postcss-selector-parser": "6.0.10"
+ }
+ },
"@testing-library/dom": {
"version": "8.13.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.13.0.tgz",
@@ -91831,6 +92176,11 @@
}
}
},
+ "html-dir-content": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/html-dir-content/-/html-dir-content-0.3.2.tgz",
+ "integrity": "sha512-a1EJZbvBGmmFwk9VxFhEgaHkyXUXKTkw0jr0FCvXKCqgzO1H0wbFQbbzRA6FhR3twxAyjqVc80bzGHEmKrYsSw=="
+ },
"html-encoding-sniffer": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
@@ -92280,7 +92630,6 @@
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
- "dev": true,
"requires": {
"loose-envify": "^1.0.0"
}
@@ -94574,6 +94923,11 @@
"integrity": "sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==",
"dev": true
},
+ "just-throttle": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/just-throttle/-/just-throttle-1.1.0.tgz",
+ "integrity": "sha512-iePC/13XYX1Tyn9C6jY+DG3UEejkDvrKsw5xxgGhtGUwYWmoJm4CoKexscBKELOu3FTyCDzjr21ZJ67AXnz+bg=="
+ },
"jwa": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
@@ -95121,6 +95475,12 @@
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
},
+ "lodash.castarray": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
+ "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==",
+ "dev": true
+ },
"lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@@ -118373,6 +118733,14 @@
}
}
},
+ "react-error-boundary": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz",
+ "integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==",
+ "requires": {
+ "@babel/runtime": "^7.12.5"
+ }
+ },
"react-error-overlay": {
"version": "6.0.11",
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz",
diff --git a/package.json b/package.json
index f78d0a4..a3a3b6b 100644
--- a/package.json
+++ b/package.json
@@ -1,5 +1,5 @@
{
- "name": "my-app",
+ "name": "makers-bolt-fun",
"version": "0.1.0",
"private": true,
"dependencies": {
@@ -12,6 +12,11 @@
"@reduxjs/toolkit": "^1.8.1",
"@remirror/pm": "^1.0.16",
"@remirror/react": "^1.0.34",
+ "@rpldy/mock-sender": "^1.0.1",
+ "@rpldy/upload-button": "^1.0.1",
+ "@rpldy/upload-drop-zone": "^1.0.1",
+ "@rpldy/upload-preview": "^1.0.1",
+ "@rpldy/uploady": "^1.0.1",
"@shopify/react-web-worker": "^5.0.1",
"@szhsin/react-menu": "^3.0.2",
"@testing-library/jest-dom": "^5.16.4",
@@ -66,6 +71,7 @@
"react-copy-to-clipboard": "^5.1.0",
"react-datepicker": "^4.7.0",
"react-dom": "^18.0.0",
+ "react-error-boundary": "^3.1.4",
"react-file-drop": "^3.1.4",
"react-helmet": "^6.1.0",
"react-hook-form": "^7.30.0",
@@ -165,6 +171,8 @@
"@storybook/react": "^6.4.22",
"@storybook/testing-library": "^0.0.10",
"@tailwindcss/forms": "^0.5.0",
+ "@tailwindcss/line-clamp": "^0.4.2",
+ "@tailwindcss/typography": "^0.5.7",
"@types/chance": "^1.1.3",
"@types/dompurify": "^2.3.3",
"@types/fslightbox-react": "^1.4.2",
diff --git a/prisma/migrations/20220831121710_update_hosted_image_table/migration.sql b/prisma/migrations/20220831121710_update_hosted_image_table/migration.sql
new file mode 100644
index 0000000..e25ab98
--- /dev/null
+++ b/prisma/migrations/20220831121710_update_hosted_image_table/migration.sql
@@ -0,0 +1,29 @@
+/*
+ Warnings:
+
+ - The primary key for the `HostedImage` table will be changed. If it partially fails, the table could be left without primary key constraint.
+ - The `id` column on the `HostedImage` table would be dropped and recreated. This will lead to data loss if there is data in the column.
+ - Added the required column `provider` to the `HostedImage` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `provider_image_id` to the `HostedImage` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `url` to the `HostedImage` table without a default value. This is not possible if the table is not empty.
+
+*/
+
+-- START Custom SQL
+-- Because of the breaking changes, we have to apply some custom SQL.
+-- By chance, the previous HostedImage migration is not used it production, so we can remove all data from this table
+DELETE FROM "HostedImage";
+-- END Custom SQL
+
+
+-- AlterTable
+ALTER TABLE "HostedImage" DROP CONSTRAINT "HostedImage_pkey",
+ADD COLUMN "provider" TEXT NOT NULL,
+ADD COLUMN "provider_image_id" TEXT NOT NULL,
+ADD COLUMN "url" TEXT NOT NULL,
+DROP COLUMN "id",
+ADD COLUMN "id" SERIAL NOT NULL,
+ADD CONSTRAINT "HostedImage_pkey" PRIMARY KEY ("id");
+
+-- AlterTable
+ALTER TABLE "UserKey" ALTER COLUMN "name" SET DEFAULT E'My new wallet key';
diff --git a/prisma/migrations/20220906083327_add_project_recruit_roles_tabel/migration.sql b/prisma/migrations/20220906083327_add_project_recruit_roles_tabel/migration.sql
new file mode 100644
index 0000000..54a0f2f
--- /dev/null
+++ b/prisma/migrations/20220906083327_add_project_recruit_roles_tabel/migration.sql
@@ -0,0 +1,14 @@
+-- CreateTable
+CREATE TABLE "ProjectRecruitRoles" (
+ "projectId" INTEGER NOT NULL,
+ "roleId" INTEGER NOT NULL,
+ "level" INTEGER NOT NULL,
+
+ CONSTRAINT "ProjectRecruitRoles_pkey" PRIMARY KEY ("projectId","roleId")
+);
+
+-- AddForeignKey
+ALTER TABLE "ProjectRecruitRoles" ADD CONSTRAINT "ProjectRecruitRoles_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "WorkRole"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "ProjectRecruitRoles" ADD CONSTRAINT "ProjectRecruitRoles_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
diff --git a/prisma/migrations/20220907081831_create_tournament_tables/migration.sql b/prisma/migrations/20220907081831_create_tournament_tables/migration.sql
new file mode 100644
index 0000000..9b417f2
--- /dev/null
+++ b/prisma/migrations/20220907081831_create_tournament_tables/migration.sql
@@ -0,0 +1,102 @@
+-- CreateTable
+CREATE TABLE "Tournament" (
+ "id" SERIAL NOT NULL,
+ "title" TEXT NOT NULL,
+ "description" TEXT NOT NULL,
+ "thumbnail_image" TEXT NOT NULL,
+ "cover_image" TEXT NOT NULL,
+ "start_date" DATE NOT NULL,
+ "end_date" DATE NOT NULL,
+ "location" TEXT NOT NULL,
+ "website" TEXT NOT NULL,
+ "votes_count" INTEGER NOT NULL DEFAULT 0,
+
+ CONSTRAINT "Tournament_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "TournamentPrize" (
+ "id" SERIAL NOT NULL,
+ "title" TEXT NOT NULL,
+ "amount" TEXT NOT NULL,
+ "image" TEXT NOT NULL,
+ "tournament_id" INTEGER NOT NULL,
+
+ CONSTRAINT "TournamentPrize_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "TournamentJudge" (
+ "id" SERIAL NOT NULL,
+ "name" TEXT NOT NULL,
+ "company" TEXT NOT NULL,
+ "twitter" TEXT,
+ "tournament_id" INTEGER NOT NULL,
+
+ CONSTRAINT "TournamentJudge_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "TournamentFAQ" (
+ "id" SERIAL NOT NULL,
+ "question" TEXT NOT NULL,
+ "answer" TEXT NOT NULL,
+ "tournament_id" INTEGER NOT NULL,
+
+ CONSTRAINT "TournamentFAQ_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "TournamentEvent" (
+ "id" SERIAL NOT NULL,
+ "title" TEXT NOT NULL,
+ "image" TEXT NOT NULL,
+ "description" TEXT NOT NULL,
+ "date" DATE NOT NULL,
+ "location" TEXT NOT NULL,
+ "website" TEXT NOT NULL,
+ "type" INTEGER NOT NULL,
+ "tournament_id" INTEGER NOT NULL,
+
+ CONSTRAINT "TournamentEvent_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "TournamentParticipant" (
+ "tournament_id" INTEGER NOT NULL,
+ "user_id" INTEGER NOT NULL,
+
+ CONSTRAINT "TournamentParticipant_pkey" PRIMARY KEY ("tournament_id","user_id")
+);
+
+-- CreateTable
+CREATE TABLE "TournamentProject" (
+ "tournament_id" INTEGER NOT NULL,
+ "project_id" INTEGER NOT NULL,
+
+ CONSTRAINT "TournamentProject_pkey" PRIMARY KEY ("tournament_id","project_id")
+);
+
+-- AddForeignKey
+ALTER TABLE "TournamentPrize" ADD CONSTRAINT "TournamentPrize_tournament_id_fkey" FOREIGN KEY ("tournament_id") REFERENCES "Tournament"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "TournamentJudge" ADD CONSTRAINT "TournamentJudge_tournament_id_fkey" FOREIGN KEY ("tournament_id") REFERENCES "Tournament"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "TournamentFAQ" ADD CONSTRAINT "TournamentFAQ_tournament_id_fkey" FOREIGN KEY ("tournament_id") REFERENCES "Tournament"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "TournamentEvent" ADD CONSTRAINT "TournamentEvent_tournament_id_fkey" FOREIGN KEY ("tournament_id") REFERENCES "Tournament"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "TournamentParticipant" ADD CONSTRAINT "TournamentParticipant_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "TournamentParticipant" ADD CONSTRAINT "TournamentParticipant_tournament_id_fkey" FOREIGN KEY ("tournament_id") REFERENCES "Tournament"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "TournamentProject" ADD CONSTRAINT "TournamentProject_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "TournamentProject" ADD CONSTRAINT "TournamentProject_tournament_id_fkey" FOREIGN KEY ("tournament_id") REFERENCES "Tournament"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
diff --git a/prisma/migrations/20220907083457_update_tournament_event_date_field/migration.sql b/prisma/migrations/20220907083457_update_tournament_event_date_field/migration.sql
new file mode 100644
index 0000000..777faca
--- /dev/null
+++ b/prisma/migrations/20220907083457_update_tournament_event_date_field/migration.sql
@@ -0,0 +1,12 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `date` on the `TournamentEvent` table. All the data in the column will be lost.
+ - Added the required column `ends_at` to the `TournamentEvent` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `starts_at` to the `TournamentEvent` table without a default value. This is not possible if the table is not empty.
+
+*/
+-- AlterTable
+ALTER TABLE "TournamentEvent" DROP COLUMN "date",
+ADD COLUMN "ends_at" DATE NOT NULL,
+ADD COLUMN "starts_at" DATE NOT NULL;
diff --git a/prisma/migrations/20220907085319_add_avatar_to_tournament_judge/migration.sql b/prisma/migrations/20220907085319_add_avatar_to_tournament_judge/migration.sql
new file mode 100644
index 0000000..64ce7a1
--- /dev/null
+++ b/prisma/migrations/20220907085319_add_avatar_to_tournament_judge/migration.sql
@@ -0,0 +1,8 @@
+/*
+ Warnings:
+
+ - Added the required column `avatar` to the `TournamentJudge` table without a default value. This is not possible if the table is not empty.
+
+*/
+-- AlterTable
+ALTER TABLE "TournamentJudge" ADD COLUMN "avatar" TEXT NOT NULL;
diff --git a/prisma/migrations/20220908065418_add_email_and_hackingstatus_to_tournament_participant/migration.sql b/prisma/migrations/20220908065418_add_email_and_hackingstatus_to_tournament_participant/migration.sql
new file mode 100644
index 0000000..2b563a1
--- /dev/null
+++ b/prisma/migrations/20220908065418_add_email_and_hackingstatus_to_tournament_participant/migration.sql
@@ -0,0 +1,10 @@
+/*
+ Warnings:
+
+ - Added the required column `email` to the `TournamentParticipant` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `hacking_status` to the `TournamentParticipant` table without a default value. This is not possible if the table is not empty.
+
+*/
+-- AlterTable
+ALTER TABLE "TournamentParticipant" ADD COLUMN "email" TEXT NOT NULL,
+ADD COLUMN "hacking_status" INTEGER NOT NULL;
diff --git a/prisma/migrations/20220909043627_add_created_at_to_keys_and_participations/migration.sql b/prisma/migrations/20220909043627_add_created_at_to_keys_and_participations/migration.sql
new file mode 100644
index 0000000..4bb777b
--- /dev/null
+++ b/prisma/migrations/20220909043627_add_created_at_to_keys_and_participations/migration.sql
@@ -0,0 +1,5 @@
+-- AlterTable
+ALTER TABLE "TournamentParticipant" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
+
+-- AlterTable
+ALTER TABLE "UserKey" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
diff --git a/prisma/migrations/20220909064845_add_discord_to_user_table/migration.sql b/prisma/migrations/20220909064845_add_discord_to_user_table/migration.sql
new file mode 100644
index 0000000..3834a6f
--- /dev/null
+++ b/prisma/migrations/20220909064845_add_discord_to_user_table/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "User" ADD COLUMN "discord" TEXT;
diff --git a/prisma/migrations/20220911174549_remove_only_date_constraints/migration.sql b/prisma/migrations/20220911174549_remove_only_date_constraints/migration.sql
new file mode 100644
index 0000000..b5dd52e
--- /dev/null
+++ b/prisma/migrations/20220911174549_remove_only_date_constraints/migration.sql
@@ -0,0 +1,10 @@
+-- AlterTable
+ALTER TABLE "Donation" ALTER COLUMN "createdAt" SET DATA TYPE TIMESTAMP(3);
+
+-- AlterTable
+ALTER TABLE "Tournament" ALTER COLUMN "start_date" SET DATA TYPE TIMESTAMP(3),
+ALTER COLUMN "end_date" SET DATA TYPE TIMESTAMP(3);
+
+-- AlterTable
+ALTER TABLE "TournamentEvent" ALTER COLUMN "ends_at" SET DATA TYPE TIMESTAMP(3),
+ALTER COLUMN "starts_at" SET DATA TYPE TIMESTAMP(3);
diff --git a/prisma/migrations/20220912140653_update_hosted_image_rel_table/migration.sql b/prisma/migrations/20220912140653_update_hosted_image_rel_table/migration.sql
new file mode 100644
index 0000000..4e5eb9a
--- /dev/null
+++ b/prisma/migrations/20220912140653_update_hosted_image_rel_table/migration.sql
@@ -0,0 +1,74 @@
+/*
+ Warnings:
+
+ - A unique constraint covering the columns `[image_id]` on the table `Award` will be added. If there are existing duplicate values, this will fail.
+ - A unique constraint covering the columns `[cover_image_id]` on the table `Category` will be added. If there are existing duplicate values, this will fail.
+ - A unique constraint covering the columns `[cover_image_id]` on the table `Hackathon` will be added. If there are existing duplicate values, this will fail.
+ - A unique constraint covering the columns `[thumbnail_image_id]` on the table `Project` will be added. If there are existing duplicate values, this will fail.
+ - A unique constraint covering the columns `[cover_image_id]` on the table `Project` will be added. If there are existing duplicate values, this will fail.
+ - A unique constraint covering the columns `[cover_image_id]` on the table `Story` will be added. If there are existing duplicate values, this will fail.
+ - A unique constraint covering the columns `[avatar_id]` on the table `User` will be added. If there are existing duplicate values, this will fail.
+
+*/
+-- AlterTable
+ALTER TABLE "Award" ADD COLUMN "image_id" INTEGER;
+
+-- AlterTable
+ALTER TABLE "Category" ADD COLUMN "cover_image_id" INTEGER;
+
+-- AlterTable
+ALTER TABLE "Hackathon" ADD COLUMN "cover_image_id" INTEGER;
+
+-- AlterTable
+ALTER TABLE "Project" ADD COLUMN "cover_image_id" INTEGER,
+ADD COLUMN "screenshots_ids" INTEGER[],
+ADD COLUMN "thumbnail_image_id" INTEGER;
+
+-- AlterTable
+ALTER TABLE "Story" ADD COLUMN "body_image_ids" INTEGER[],
+ADD COLUMN "cover_image_id" INTEGER;
+
+-- AlterTable
+ALTER TABLE "User" ADD COLUMN "avatar_id" INTEGER;
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Award_image_id_key" ON "Award"("image_id");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Category_cover_image_id_key" ON "Category"("cover_image_id");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Hackathon_cover_image_id_key" ON "Hackathon"("cover_image_id");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Project_thumbnail_image_id_key" ON "Project"("thumbnail_image_id");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Project_cover_image_id_key" ON "Project"("cover_image_id");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Story_cover_image_id_key" ON "Story"("cover_image_id");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_avatar_id_key" ON "User"("avatar_id");
+
+-- AddForeignKey
+ALTER TABLE "User" ADD CONSTRAINT "User_avatar_id_fkey" FOREIGN KEY ("avatar_id") REFERENCES "HostedImage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Category" ADD CONSTRAINT "Category_cover_image_id_fkey" FOREIGN KEY ("cover_image_id") REFERENCES "HostedImage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Project" ADD CONSTRAINT "Project_thumbnail_image_id_fkey" FOREIGN KEY ("thumbnail_image_id") REFERENCES "HostedImage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Project" ADD CONSTRAINT "Project_cover_image_id_fkey" FOREIGN KEY ("cover_image_id") REFERENCES "HostedImage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Award" ADD CONSTRAINT "Award_image_id_fkey" FOREIGN KEY ("image_id") REFERENCES "HostedImage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Story" ADD CONSTRAINT "Story_cover_image_id_fkey" FOREIGN KEY ("cover_image_id") REFERENCES "HostedImage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Hackathon" ADD CONSTRAINT "Hackathon_cover_image_id_fkey" FOREIGN KEY ("cover_image_id") REFERENCES "HostedImage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
diff --git a/prisma/migrations/20220913134026_update_tournament_image_rel/migration.sql b/prisma/migrations/20220913134026_update_tournament_image_rel/migration.sql
new file mode 100644
index 0000000..038339d
--- /dev/null
+++ b/prisma/migrations/20220913134026_update_tournament_image_rel/migration.sql
@@ -0,0 +1,52 @@
+/*
+ Warnings:
+
+ - A unique constraint covering the columns `[thumbnail_image_id]` on the table `Tournament` will be added. If there are existing duplicate values, this will fail.
+ - A unique constraint covering the columns `[cover_image_id]` on the table `Tournament` will be added. If there are existing duplicate values, this will fail.
+ - A unique constraint covering the columns `[image_id]` on the table `TournamentEvent` will be added. If there are existing duplicate values, this will fail.
+ - A unique constraint covering the columns `[avatar_id]` on the table `TournamentJudge` will be added. If there are existing duplicate values, this will fail.
+ - A unique constraint covering the columns `[image_id]` on the table `TournamentPrize` will be added. If there are existing duplicate values, this will fail.
+
+*/
+-- AlterTable
+ALTER TABLE "Tournament" ADD COLUMN "cover_image_id" INTEGER,
+ADD COLUMN "thumbnail_image_id" INTEGER;
+
+-- AlterTable
+ALTER TABLE "TournamentEvent" ADD COLUMN "image_id" INTEGER;
+
+-- AlterTable
+ALTER TABLE "TournamentJudge" ADD COLUMN "avatar_id" INTEGER;
+
+-- AlterTable
+ALTER TABLE "TournamentPrize" ADD COLUMN "image_id" INTEGER;
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Tournament_thumbnail_image_id_key" ON "Tournament"("thumbnail_image_id");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Tournament_cover_image_id_key" ON "Tournament"("cover_image_id");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "TournamentEvent_image_id_key" ON "TournamentEvent"("image_id");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "TournamentJudge_avatar_id_key" ON "TournamentJudge"("avatar_id");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "TournamentPrize_image_id_key" ON "TournamentPrize"("image_id");
+
+-- AddForeignKey
+ALTER TABLE "Tournament" ADD CONSTRAINT "Tournament_thumbnail_image_id_fkey" FOREIGN KEY ("thumbnail_image_id") REFERENCES "HostedImage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Tournament" ADD CONSTRAINT "Tournament_cover_image_id_fkey" FOREIGN KEY ("cover_image_id") REFERENCES "HostedImage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "TournamentPrize" ADD CONSTRAINT "TournamentPrize_image_id_fkey" FOREIGN KEY ("image_id") REFERENCES "HostedImage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "TournamentJudge" ADD CONSTRAINT "TournamentJudge_avatar_id_fkey" FOREIGN KEY ("avatar_id") REFERENCES "HostedImage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "TournamentEvent" ADD CONSTRAINT "TournamentEvent_image_id_fkey" FOREIGN KEY ("image_id") REFERENCES "HostedImage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
diff --git a/prisma/migrations/20220916130117_add_capabilities_project/migration.sql b/prisma/migrations/20220916130117_add_capabilities_project/migration.sql
new file mode 100644
index 0000000..dd8f200
--- /dev/null
+++ b/prisma/migrations/20220916130117_add_capabilities_project/migration.sql
@@ -0,0 +1,38 @@
+-- AlterTable
+ALTER TABLE "Project" ADD COLUMN "discord" TEXT NOT NULL DEFAULT E'',
+ADD COLUMN "github" TEXT NOT NULL DEFAULT E'',
+ADD COLUMN "hashtag" TEXT NOT NULL DEFAULT E'',
+ADD COLUMN "launch_status" TEXT NOT NULL DEFAULT E'',
+ADD COLUMN "tagline" TEXT NOT NULL DEFAULT E'',
+ADD COLUMN "twitter" TEXT NOT NULL DEFAULT E'';
+
+-- CreateTable
+CREATE TABLE "Capability" (
+ "id" SERIAL NOT NULL,
+ "title" TEXT NOT NULL,
+ "icon" TEXT,
+ "is_official" BOOLEAN NOT NULL DEFAULT false,
+
+ CONSTRAINT "Capability_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "_CapabilityToProject" (
+ "A" INTEGER NOT NULL,
+ "B" INTEGER NOT NULL
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Capability_title_key" ON "Capability"("title");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "_CapabilityToProject_AB_unique" ON "_CapabilityToProject"("A", "B");
+
+-- CreateIndex
+CREATE INDEX "_CapabilityToProject_B_index" ON "_CapabilityToProject"("B");
+
+-- AddForeignKey
+ALTER TABLE "_CapabilityToProject" ADD FOREIGN KEY ("A") REFERENCES "Capability"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "_CapabilityToProject" ADD FOREIGN KEY ("B") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/prisma/migrations/20220918072708_update_project_links_fields/migration.sql b/prisma/migrations/20220918072708_update_project_links_fields/migration.sql
new file mode 100644
index 0000000..3b23f83
--- /dev/null
+++ b/prisma/migrations/20220918072708_update_project_links_fields/migration.sql
@@ -0,0 +1,18 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `is_official` on the `Capability` table. All the data in the column will be lost.
+
+*/
+-- AlterTable
+ALTER TABLE "Capability" DROP COLUMN "is_official";
+
+-- AlterTable
+ALTER TABLE "Project" ADD COLUMN "slack" TEXT,
+ADD COLUMN "telegram" TEXT,
+ALTER COLUMN "discord" DROP NOT NULL,
+ALTER COLUMN "discord" DROP DEFAULT,
+ALTER COLUMN "github" DROP NOT NULL,
+ALTER COLUMN "github" DROP DEFAULT,
+ALTER COLUMN "twitter" DROP NOT NULL,
+ALTER COLUMN "twitter" DROP DEFAULT;
diff --git a/prisma/migrations/20220918123547_add_project_makers_table/migration.sql b/prisma/migrations/20220918123547_add_project_makers_table/migration.sql
new file mode 100644
index 0000000..42fc30a
--- /dev/null
+++ b/prisma/migrations/20220918123547_add_project_makers_table/migration.sql
@@ -0,0 +1,14 @@
+-- CreateTable
+CREATE TABLE "ProjectMember" (
+ "projectId" INTEGER NOT NULL,
+ "userId" INTEGER NOT NULL,
+ "level" TEXT NOT NULL,
+
+ CONSTRAINT "ProjectMember_pkey" PRIMARY KEY ("projectId","userId")
+);
+
+-- AddForeignKey
+ALTER TABLE "ProjectMember" ADD CONSTRAINT "ProjectMember_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "ProjectMember" ADD CONSTRAINT "ProjectMember_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
diff --git a/prisma/migrations/20220918125433_rename_projectmember_level_to_role/migration.sql b/prisma/migrations/20220918125433_rename_projectmember_level_to_role/migration.sql
new file mode 100644
index 0000000..9fd5ea2
--- /dev/null
+++ b/prisma/migrations/20220918125433_rename_projectmember_level_to_role/migration.sql
@@ -0,0 +1,10 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `level` on the `ProjectMember` table. All the data in the column will be lost.
+ - Added the required column `role` to the `ProjectMember` table without a default value. This is not possible if the table is not empty.
+
+*/
+-- AlterTable
+ALTER TABLE "ProjectMember" DROP COLUMN "level",
+ADD COLUMN "role" TEXT NOT NULL;
diff --git a/prisma/migrations/20220918130530_change_default_value_for_launch_status_project/migration.sql b/prisma/migrations/20220918130530_change_default_value_for_launch_status_project/migration.sql
new file mode 100644
index 0000000..11cc0f5
--- /dev/null
+++ b/prisma/migrations/20220918130530_change_default_value_for_launch_status_project/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "Project" ALTER COLUMN "launch_status" SET DEFAULT E'Launched';
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 1a5b903..e66a5cd 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -40,11 +40,13 @@ model Vote {
// -----------------
model User {
- id Int @id @default(autoincrement())
- pubKey String? @unique
- name String?
- avatar String?
- role String @default("user")
+ id Int @id @default(autoincrement())
+ pubKey String? @unique
+ name String?
+ avatar String?
+ avatar_id Int? @unique
+ avatar_rel HostedImage? @relation("User_Avatar", fields: [avatar_id], references: [id])
+ role String @default("user")
email String?
jobTitle String?
@@ -52,6 +54,7 @@ model User {
website String?
twitter String?
github String?
+ discord String?
linkedin String?
bio String?
location String?
@@ -67,11 +70,15 @@ model User {
userKeys UserKey[]
skills Skill[]
roles UsersOnWorkRoles[]
+ projects ProjectMember[]
+
+ tournaments TournamentParticipant[]
}
model UserKey {
- key String @id
- name String @default("My new wallet key")
+ key String @id
+ name String @default("My new wallet key")
+ createdAt DateTime @default(now())
user User? @relation(fields: [user_id], references: [id])
user_id Int?
@@ -89,10 +96,11 @@ model UsersOnWorkRoles {
}
model WorkRole {
- id Int @id @default(autoincrement())
- title String @unique
- icon String
- users UsersOnWorkRoles[]
+ id Int @id @default(autoincrement())
+ title String @unique
+ icon String
+ users UsersOnWorkRoles[]
+ projects ProjectRecruitRoles[]
}
model Skill {
@@ -107,39 +115,83 @@ model Skill {
// -----------------
model Category {
- id Int @id @default(autoincrement())
- title String
- cover_image String?
- icon String?
+ id Int @id @default(autoincrement())
+ title String
+ cover_image String?
+ cover_image_id Int? @unique
+ cover_image_rel HostedImage? @relation("Category_CoverImage", fields: [cover_image_id], references: [id])
+ icon String?
project Project[]
}
model Project {
- id Int @id @default(autoincrement())
- title String
- description String
- screenshots String[]
- website String
- thumbnail_image String?
- cover_image String?
- lightning_address String?
- lnurl_callback_url String?
+ id Int @id @default(autoincrement())
+ title String
+ description String
+ screenshots String[]
+ screenshots_ids Int[]
+ website String
+ discord String?
+ twitter String?
+ github String?
+ telegram String?
+ slack String?
+ thumbnail_image String?
+ thumbnail_image_id Int? @unique
+ thumbnail_image_rel HostedImage? @relation("Project_Thumbnail", fields: [thumbnail_image_id], references: [id])
+ cover_image String?
+ cover_image_id Int? @unique
+ cover_image_rel HostedImage? @relation("Project_CoverImage", fields: [cover_image_id], references: [id])
+ lightning_address String?
+ lnurl_callback_url String?
+ tagline String @default("")
+ launch_status String @default("Launched")
+ hashtag String @default("")
category Category @relation(fields: [category_id], references: [id])
category_id Int
votes_count Int @default(0)
createdAt DateTime @default(now())
- awards Award[]
- tags Tag[]
+ awards Award[]
+ tags Tag[]
+ capabilities Capability[]
+
+ members ProjectMember[]
+ recruit_roles ProjectRecruitRoles[]
+ tournaments TournamentProject[]
+}
+
+model ProjectRecruitRoles {
+ project Project @relation(fields: [projectId], references: [id])
+ projectId Int
+ role WorkRole @relation(fields: [roleId], references: [id])
+ roleId Int
+
+ level Int
+
+ @@id([projectId, roleId])
+}
+
+model ProjectMember {
+ project Project @relation(fields: [projectId], references: [id])
+ projectId Int
+ user User @relation(fields: [userId], references: [id])
+ userId Int
+
+ role String // Admin | Maker | (new_roles_later)
+
+ @@id([projectId, userId])
}
model Award {
- id Int @id @default(autoincrement())
- title String
- image String
- url String
+ id Int @id @default(autoincrement())
+ title String
+ image String
+ image_id Int? @unique
+ image_rel HostedImage? @relation("Award_Image", fields: [image_id], references: [id])
+ url String
project Project @relation(fields: [project_id], references: [id])
project_id Int
@@ -150,15 +202,18 @@ model Award {
// -----------------
model Story {
- id Int @id @default(autoincrement())
- title String
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
- body String
- excerpt String
- cover_image String?
- votes_count Int @default(0)
- is_published Boolean @default(true)
+ id Int @id @default(autoincrement())
+ title String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ body String
+ body_image_ids Int[]
+ excerpt String
+ cover_image String?
+ cover_image_id Int? @unique
+ cover_image_rel HostedImage? @relation("Story_CoverImage", fields: [cover_image_id], references: [id])
+ votes_count Int @default(0)
+ is_published Boolean @default(true)
tags Tag[]
@@ -211,15 +266,17 @@ model PostComment {
// Hackathons
// -----------------
model Hackathon {
- id Int @id @default(autoincrement())
- title String
- start_date DateTime @db.Date
- end_date DateTime @db.Date
- cover_image String
- description String
- location String
- website String
- votes_count Int @default(0)
+ id Int @id @default(autoincrement())
+ title String
+ start_date DateTime @db.Date
+ end_date DateTime @db.Date
+ cover_image String
+ cover_image_id Int? @unique
+ cover_image_rel HostedImage? @relation("Hackathon_CoverImage", fields: [cover_image_id], references: [id])
+ description String
+ location String
+ website String
+ votes_count Int @default(0)
tags Tag[]
}
@@ -230,7 +287,7 @@ model Hackathon {
model Donation {
id Int @id @default(autoincrement())
amount Int
- createdAt DateTime @default(now()) @db.Date
+ createdAt DateTime @default(now())
payment_request String?
payment_hash String?
preimage String?
@@ -253,8 +310,134 @@ model GeneratedK1 {
// Hosted Image
// -----------------
model HostedImage {
- id String @id
- filename String
- createdAt DateTime @default(now())
- is_used Boolean @default(false)
+ id Int @id @default(autoincrement())
+ filename String
+ provider_image_id String
+ provider String
+ url String
+ createdAt DateTime @default(now())
+ is_used Boolean @default(false)
+
+ ProjectThumbnail Project? @relation("Project_Thumbnail")
+ ProjectCoverImage Project? @relation("Project_CoverImage")
+ CategoryCoverImage Category? @relation("Category_CoverImage")
+ AwardImage Award? @relation("Award_Image")
+ HackathonCoverImage Hackathon? @relation("Hackathon_CoverImage")
+ StoryCoverImage Story? @relation("Story_CoverImage")
+ User User? @relation("User_Avatar")
+ TournamentThumbnail Tournament? @relation("Tournament_ThumbnailImage")
+ Tournament_CoverImage Tournament? @relation("Tournament_CoverImage")
+ TournamentPrize_Image TournamentPrize? @relation("TournamentPrize_Image")
+ TournamentJudge_Avatar TournamentJudge? @relation("TournamentJudge_Avatar")
+ TournamentEvent_Image TournamentEvent? @relation("TournamentEvent_Image")
+}
+
+// -----------------
+// Tournament
+// -----------------
+model Tournament {
+ id Int @id @default(autoincrement())
+ title String
+ description String
+ thumbnail_image String
+ thumbnail_image_id Int? @unique
+ thumbnail_image_rel HostedImage? @relation("Tournament_ThumbnailImage", fields: [thumbnail_image_id], references: [id])
+ cover_image String
+ cover_image_id Int? @unique
+ cover_image_rel HostedImage? @relation("Tournament_CoverImage", fields: [cover_image_id], references: [id])
+ start_date DateTime
+ end_date DateTime
+ location String
+ website String
+ votes_count Int @default(0)
+
+ prizes TournamentPrize[]
+ judges TournamentJudge[]
+ faqs TournamentFAQ[]
+ events TournamentEvent[]
+ participants TournamentParticipant[]
+ projects TournamentProject[]
+}
+
+model TournamentPrize {
+ id Int @id @default(autoincrement())
+ title String
+ amount String
+ image String
+ image_id Int? @unique
+ image_rel HostedImage? @relation("TournamentPrize_Image", fields: [image_id], references: [id])
+
+ tournament Tournament @relation(fields: [tournament_id], references: [id])
+ tournament_id Int
+}
+
+model TournamentJudge {
+ id Int @id @default(autoincrement())
+ name String
+ avatar String
+ avatar_id Int? @unique
+ avatar_rel HostedImage? @relation("TournamentJudge_Avatar", fields: [avatar_id], references: [id])
+ company String
+ twitter String?
+
+ tournament Tournament @relation(fields: [tournament_id], references: [id])
+ tournament_id Int
+}
+
+model TournamentFAQ {
+ id Int @id @default(autoincrement())
+ question String
+ answer String
+
+ tournament Tournament @relation(fields: [tournament_id], references: [id])
+ tournament_id Int
+}
+
+model TournamentEvent {
+ id Int @id @default(autoincrement())
+ title String
+ image String
+ image_id Int? @unique
+ image_rel HostedImage? @relation("TournamentEvent_Image", fields: [image_id], references: [id])
+ description String
+ starts_at DateTime
+ ends_at DateTime
+ location String
+ website String
+ type Int
+
+ tournament Tournament @relation(fields: [tournament_id], references: [id])
+ tournament_id Int
+}
+
+model TournamentParticipant {
+ tournament Tournament @relation(fields: [tournament_id], references: [id])
+ tournament_id Int
+ user User @relation(fields: [user_id], references: [id])
+ user_id Int
+ createdAt DateTime @default(now())
+
+ email String
+ hacking_status Int
+
+ @@id([tournament_id, user_id])
+}
+
+model TournamentProject {
+ tournament Tournament @relation(fields: [tournament_id], references: [id])
+ tournament_id Int
+ project Project @relation(fields: [project_id], references: [id])
+ project_id Int
+
+ @@id([tournament_id, project_id])
+}
+
+// -----------------
+// Capability
+// -----------------
+model Capability {
+ id Int @id @default(autoincrement())
+ title String @unique
+ icon String?
+ project Project[]
}
diff --git a/prisma/seed/data.js b/prisma/seed/data.js
index cc482df..9ebd9e4 100644
--- a/prisma/seed/data.js
+++ b/prisma/seed/data.js
@@ -510,6 +510,54 @@ const skills = [
},
]
+const capabilities = [
+ {
+ id: 1,
+ title: 'Mobile',
+ icon: '📱'
+ },
+ {
+ id: 2,
+ title: 'Web',
+ icon: '💻'
+ },
+ {
+ id: 3,
+ title: 'WebLN',
+ icon: '🎛️'
+ },
+ {
+ id: 4,
+ title: 'LNURL-auth',
+ icon: '🔑️️'
+ },
+ {
+ id: 5,
+ title: 'LNURL-pay',
+ icon: '💸'
+ },
+ {
+ id: 6,
+ title: 'LNURL-channel',
+ icon: '🕳️️'
+ },
+ {
+ id: 7,
+ title: 'LNURL-withdraw',
+ icon: '🎬️'
+ },
+ {
+ id: 8,
+ title: 'BOLT 11',
+ icon: '⚡'
+ },
+ {
+ id: 9,
+ title: 'BOLT 12',
+ icon: '⚡'
+ },
+]
+
module.exports = {
categories,
projects,
@@ -517,4 +565,5 @@ module.exports = {
hackathons,
roles,
skills,
+ capabilities,
}
\ No newline at end of file
diff --git a/prisma/seed/data/tournament.seed.js b/prisma/seed/data/tournament.seed.js
new file mode 100644
index 0000000..34f7202
--- /dev/null
+++ b/prisma/seed/data/tournament.seed.js
@@ -0,0 +1,232 @@
+
+
+const tournament = {
+ __typename: "Tournament",
+ id: 12,
+ title: "Legends of Lightning ⚡️",
+ start_date: "2022-10-12T21:00:00.000Z",
+ end_date: "2022-11-30T22:00:00.000Z",
+ cover_image: "https://imagedelivery.net/wyrwp3c-j0gDDUWgnE7lig/1d5d2c86-fe46-4478-6909-bb3c425c0d00/public",
+ thumbnail_image: "https://imagedelivery.net/wyrwp3c-j0gDDUWgnE7lig/37fb9cd6-e4f1-43f9-c3fe-7c3e119d5600/public",
+ location: "Online",
+ website: "#",
+ description: // this field accepts markdown
+ `## Tournament Details
+BOLT🔩FUN’s maiden tournament, **Legends of Lightning** ⚡ will be an online global competition for makers to learn, connect, collaborate, and experiment with building innovative applications and tools with bitcoin and lightning.
+
+Spanning a 2-month period, makers can form teams, hack on projects, and show off their progress, activity, and updates as they compete for up to **$10,000 in bitcoin prizes**.
+
+BOLT🔩FUN has partnered with a number of events, meetups, and hackathons to provide makers the opportunity to brainstorm, design, build, and accelerate their tournament projects over the course of a couple of months. At the end of the tournament, a panel of judges will access and score all submitted projects - announcing the winners in the second week of December!
+
+ `, // markdown
+ prizes: [
+ {
+ title: "stw3 champion",
+ amount: "$ 5k",
+ image: "https://imagedelivery.net/wyrwp3c-j0gDDUWgnE7lig/39217dcf-c900-46be-153f-169e3a1f0400/public",
+ },
+ {
+ title: "2nd place",
+ amount: "$ 2.5k",
+ image: "https://imagedelivery.net/wyrwp3c-j0gDDUWgnE7lig/39cdb7c8-5fbf-49ff-32cf-fdabc3aa2d00/public",
+ },
+ {
+ title: "3rd place ",
+ amount: "$ 1.5k",
+ image: "https://imagedelivery.net/wyrwp3c-j0gDDUWgnE7lig/75958797-73b2-4a62-52df-9f0f98c53900/public",
+ },
+ {
+ title: "best design ",
+ amount: "$ 1k",
+ image: "https://imagedelivery.net/wyrwp3c-j0gDDUWgnE7lig/fa7b7cdd-7c06-4ebe-1a2d-94af9d2dae00/public",
+ }
+ ],
+
+ events: [
+ {
+ title: "Tab Conf 22",
+ starts_at: "2022-10-13T21:00:00.000Z",
+ ends_at: "2022-10-15T22:00:00.000Z",
+ description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Diam morbi pellentesque velit congue. Aliquet rutrum a, augue vitae tincidunt ac egestas. Mauris nec fringilla diam eget fusce malesuada cum parturient. Nulla pretium purus odio odio.",
+ image: 'https://picsum.photos/id/10/400/800',
+ links: [],
+ location: "Atlanta, GA",
+ type: 1, /** EVent typs encoding
+ *
+ Twitter Space: 0,
+ Workshop: 1,
+ IRL Meetup: 2,
+ Online Meetup: 3,
+ */
+ website: "https://2022.tabconf.com/"
+ },
+ {
+ title: "Bitcoin Amsterdam",
+ starts_at: "2022-10-12T21:00:00.000Z",
+ ends_at: "2022-10-14T22:00:00.000Z",
+ description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Diam morbi pellentesque velit congue. Aliquet rutrum a, augue vitae tincidunt ac egestas. Mauris nec fringilla diam eget fusce malesuada cum parturient. Nulla pretium purus odio odio.",
+ image: 'https://picsum.photos/id/10/400/800',
+ links: [],
+ location: "Amsterdam, NL",
+ type: 2,
+ website: "https://b.tc/conference/amsterdam"
+ },
+ {
+ title: "Lugano’s Plan ₿",
+ starts_at: "2022-10-28T21:00:00.000Z",
+ ends_at: "2022-11-04T22:00:00.000Z",
+ description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Diam morbi pellentesque velit congue. Aliquet rutrum a, augue vitae tincidunt ac egestas. Mauris nec fringilla diam eget fusce malesuada cum parturient. Nulla pretium purus odio odio.",
+ image: 'https://picsum.photos/id/10/400/800',
+ links: [],
+ location: "Lugano, CH",
+ type: 3,
+ website: "https://planb.lugano.ch/"
+ },
+ {
+ title: "Adopting Bitcoin 22",
+ starts_at: "2022-11-15T21:00:00.000Z",
+ ends_at: "2022-11-17T22:00:00.000Z",
+ description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Diam morbi pellentesque velit congue. Aliquet rutrum a, augue vitae tincidunt ac egestas. Mauris nec fringilla diam eget fusce malesuada cum parturient. Nulla pretium purus odio odio.",
+ image: 'https://picsum.photos/id/10/400/800',
+ links: [],
+ location: "El Salvador",
+ type: 2,
+ website: "https://adoptingbitcoin.org/2022/"
+ },
+
+ {
+ title: "PlebTLV",
+ starts_at: "2022-10-23T21:00:00.000Z",
+ ends_at: "2022-10-23T22:00:00.000Z",
+ description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Diam morbi pellentesque velit congue. Aliquet rutrum a, augue vitae tincidunt ac egestas. Mauris nec fringilla diam eget fusce malesuada cum parturient. Nulla pretium purus odio odio.",
+ image: 'https://picsum.photos/id/10/400/800',
+ links: [],
+ location: "Tel Aviv",
+ type: 2,
+ website: "https://plebtlv.com/"
+ },
+ {
+ title: "Bitcoin Designathon",
+ starts_at: "2022-10-12T21:00:00.000Z",
+ ends_at: "2022-10-16T22:00:00.000Z",
+ description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Diam morbi pellentesque velit congue. Aliquet rutrum a, augue vitae tincidunt ac egestas. Mauris nec fringilla diam eget fusce malesuada cum parturient. Nulla pretium purus odio odio.",
+ image: 'https://picsum.photos/id/10/400/800',
+ links: [],
+ location: "Online",
+ type: 2,
+ website: "https://bitcoin.design"
+ },
+ ],
+ judges: [
+ {
+ name: "Roy Sheinfeld",
+ avatar: "https://s3-alpha-sig.figma.com/img/5e65/c22c/673b8f74ac43f024b036dbc4e6479e0d?Expires=1662940800&Signature=GR54s7FBcLGcPTVclWdmPjzU92tyrYpdUbbDUYKMUkdQbxq2yQlUhZ-AOLDHhOPY4P2G3aW2yT16b1AUbC8RBx1boH25MSrH-jpn6X57IJA-4ZeHP8zCo-yjTLpb8Gn~vudIi9rPfbwJ34stp-VeOAYMuOHlah3YO-B4MBsBv-NqhP7BMY4zz9vGdBLZhOjYQYdLZ2494Ae6L5FpD1ah3WD3U5qUN9dDvYvAtqYfhQeBOnsG6PfYoq8LouCuERC4S26BeooPg8UdGUCf324-SjEihCoL8mQFq80PSsaAZl5~EBOKRUx14FOprizMusaYN0K06E~fjDIDbM2Rmc9Xjg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA",
+ company: "Breez",
+ twitter: "@therealkingonly"
+ },
+ {
+ name: "John Carvalho",
+ avatar: "https://s3-alpha-sig.figma.com/img/5e65/c22c/673b8f74ac43f024b036dbc4e6479e0d?Expires=1662940800&Signature=GR54s7FBcLGcPTVclWdmPjzU92tyrYpdUbbDUYKMUkdQbxq2yQlUhZ-AOLDHhOPY4P2G3aW2yT16b1AUbC8RBx1boH25MSrH-jpn6X57IJA-4ZeHP8zCo-yjTLpb8Gn~vudIi9rPfbwJ34stp-VeOAYMuOHlah3YO-B4MBsBv-NqhP7BMY4zz9vGdBLZhOjYQYdLZ2494Ae6L5FpD1ah3WD3U5qUN9dDvYvAtqYfhQeBOnsG6PfYoq8LouCuERC4S26BeooPg8UdGUCf324-SjEihCoL8mQFq80PSsaAZl5~EBOKRUx14FOprizMusaYN0K06E~fjDIDbM2Rmc9Xjg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA",
+ company: "Synonym",
+ twitter: "@BitcoinErrorLog"
+ },
+ {
+ name: "Nifty Nei",
+ avatar: "https://s3-alpha-sig.figma.com/img/5e65/c22c/673b8f74ac43f024b036dbc4e6479e0d?Expires=1662940800&Signature=GR54s7FBcLGcPTVclWdmPjzU92tyrYpdUbbDUYKMUkdQbxq2yQlUhZ-AOLDHhOPY4P2G3aW2yT16b1AUbC8RBx1boH25MSrH-jpn6X57IJA-4ZeHP8zCo-yjTLpb8Gn~vudIi9rPfbwJ34stp-VeOAYMuOHlah3YO-B4MBsBv-NqhP7BMY4zz9vGdBLZhOjYQYdLZ2494Ae6L5FpD1ah3WD3U5qUN9dDvYvAtqYfhQeBOnsG6PfYoq8LouCuERC4S26BeooPg8UdGUCf324-SjEihCoL8mQFq80PSsaAZl5~EBOKRUx14FOprizMusaYN0K06E~fjDIDbM2Rmc9Xjg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA",
+ company: "Blockstream",
+ twitter: "@niftynei"
+ },
+ {
+ name: "Oleg Mikhalsky",
+ avatar: "https://s3-alpha-sig.figma.com/img/5e65/c22c/673b8f74ac43f024b036dbc4e6479e0d?Expires=1662940800&Signature=GR54s7FBcLGcPTVclWdmPjzU92tyrYpdUbbDUYKMUkdQbxq2yQlUhZ-AOLDHhOPY4P2G3aW2yT16b1AUbC8RBx1boH25MSrH-jpn6X57IJA-4ZeHP8zCo-yjTLpb8Gn~vudIi9rPfbwJ34stp-VeOAYMuOHlah3YO-B4MBsBv-NqhP7BMY4zz9vGdBLZhOjYQYdLZ2494Ae6L5FpD1ah3WD3U5qUN9dDvYvAtqYfhQeBOnsG6PfYoq8LouCuERC4S26BeooPg8UdGUCf324-SjEihCoL8mQFq80PSsaAZl5~EBOKRUx14FOprizMusaYN0K06E~fjDIDbM2Rmc9Xjg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA",
+ company: "Fulgur Ventures",
+ twitter: "@olegmikh1"
+ },
+ {
+ name: "Alyse Kileen",
+ avatar: "https://s3-alpha-sig.figma.com/img/5e65/c22c/673b8f74ac43f024b036dbc4e6479e0d?Expires=1662940800&Signature=GR54s7FBcLGcPTVclWdmPjzU92tyrYpdUbbDUYKMUkdQbxq2yQlUhZ-AOLDHhOPY4P2G3aW2yT16b1AUbC8RBx1boH25MSrH-jpn6X57IJA-4ZeHP8zCo-yjTLpb8Gn~vudIi9rPfbwJ34stp-VeOAYMuOHlah3YO-B4MBsBv-NqhP7BMY4zz9vGdBLZhOjYQYdLZ2494Ae6L5FpD1ah3WD3U5qUN9dDvYvAtqYfhQeBOnsG6PfYoq8LouCuERC4S26BeooPg8UdGUCf324-SjEihCoL8mQFq80PSsaAZl5~EBOKRUx14FOprizMusaYN0K06E~fjDIDbM2Rmc9Xjg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA",
+ company: "Stillmark VC",
+ twitter: "@AlyseKilleen"
+ },
+ {
+ name: "Johns Beharry",
+ avatar: "https://s3-alpha-sig.figma.com/img/5e65/c22c/673b8f74ac43f024b036dbc4e6479e0d?Expires=1662940800&Signature=GR54s7FBcLGcPTVclWdmPjzU92tyrYpdUbbDUYKMUkdQbxq2yQlUhZ-AOLDHhOPY4P2G3aW2yT16b1AUbC8RBx1boH25MSrH-jpn6X57IJA-4ZeHP8zCo-yjTLpb8Gn~vudIi9rPfbwJ34stp-VeOAYMuOHlah3YO-B4MBsBv-NqhP7BMY4zz9vGdBLZhOjYQYdLZ2494Ae6L5FpD1ah3WD3U5qUN9dDvYvAtqYfhQeBOnsG6PfYoq8LouCuERC4S26BeooPg8UdGUCf324-SjEihCoL8mQFq80PSsaAZl5~EBOKRUx14FOprizMusaYN0K06E~fjDIDbM2Rmc9Xjg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA",
+ company: "Peak Shift",
+ twitter: "@johnsBeharry"
+ },
+ {
+ name: "Ben Price",
+ avatar: "https://s3-alpha-sig.figma.com/img/5e65/c22c/673b8f74ac43f024b036dbc4e6479e0d?Expires=1662940800&Signature=GR54s7FBcLGcPTVclWdmPjzU92tyrYpdUbbDUYKMUkdQbxq2yQlUhZ-AOLDHhOPY4P2G3aW2yT16b1AUbC8RBx1boH25MSrH-jpn6X57IJA-4ZeHP8zCo-yjTLpb8Gn~vudIi9rPfbwJ34stp-VeOAYMuOHlah3YO-B4MBsBv-NqhP7BMY4zz9vGdBLZhOjYQYdLZ2494Ae6L5FpD1ah3WD3U5qUN9dDvYvAtqYfhQeBOnsG6PfYoq8LouCuERC4S26BeooPg8UdGUCf324-SjEihCoL8mQFq80PSsaAZl5~EBOKRUx14FOprizMusaYN0K06E~fjDIDbM2Rmc9Xjg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA",
+ company: "The Bitcoin Company",
+ twitter: "@abitcoinperson"
+ },
+ ],
+
+ faqs: [
+ {
+ question: "When does the tournament start and end?",
+ answer:
+ `The tournament starts when team and project registrations open on 12th October. The tournament will finish with submissions closing on 30th November, 2022. Judges will then score projects and announce the winners on the 12th December.`
+ },
+ {
+ question: "When and how do we register our projects?",
+ answer:
+ `Makers can register their projects anytime between 12th October - 30th November. If a project is added on the tournament page, it is automatically registered and it will be judged at the end of the tournament.`
+ },
+ {
+ question: "How will projects be judged?",
+ answer:
+ `Projects will be judged and scored on the following criteria:
+
+**1). 🎯 Value Proposition**
+Does the project have a product market fit? Does it provide value to the bitcoin ecosystem and beyond?
+
+**2). 🚨 Innovation**
+Is it something we've seen before or does it bring something new and exciting to bitcoin and beyond?
+
+**3). 👁️ Transparency (#BuildInPublic)**
+Encouraging makers to #BuildInPublic. Has the project’s team been transparent throughout their product design and development journey?
+
+**4). ✅ Execution**
+Makers should focus on attention to detail. How well has the project been executed?
+
+**5). 🍒 UIUX Design**
+Design can separate the good from the bad. Taking into account both UI and UX, how well has the application or feature been designed?
+
+**6). 🔥 Je ne sais quoi**
+Does the project have that extra level of pizazz or coolness? Does it raise the bar?`
+ },
+ {
+ question: "Can I submit a project that I hacked on during another event?",
+ answer:
+ `Makers can submit their projects from other hackathons, events, and meetups that are registered as events within The Long Night tournament. This allows makers to take advantage of IRL + online meetups, workshops, hackerspaces, inspirational weekend events, and more.`
+ },
+ {
+ question: "Can I submit multiple projects?",
+ answer:
+ `Yes, makers can submit multiple projects. However we encourage makers to focus on quality rather than quantity.`
+ },
+ {
+ question: "How can I find other makers or projects to team up with?",
+ answer:
+ `You can see a list of makers who are open to connect in the tournament’s Makers tab. You can also search for projects that are looking to recruit members.`
+ },
+ {
+ question: "This is my first time hacking on bitcoin, is there any help?",
+ answer:
+ `We collected some awesome design, development, and project management resources here to get you up and running. You can also watch workshops and tutorials from BOLT🔩FUN’s previous ShockTheWeb⚡hackathons here.`
+ },
+ {
+ question: "Not sure what to hack on?",
+ answer:
+ `Not sure where to get started? Need an idea to hack on? Not to worry, we’ve collected a list of great project ideas for you to look at here.`
+ },
+ {
+ question: "How can I #BuildInPublic?",
+ answer:
+ `Using BOLT🔩FUN Stories ✍️, makers can transparently document their project’s design, development, and management processes. This will help other makers learn from one another, decreasing essential onboarding and learning time, whilst inspiring more great bitcoin apps to be built and innovated on. To see an example of this type of transparent reporting, check out this story here.`
+ },
+ ],
+}
+
+module.exports = { tournament };
\ No newline at end of file
diff --git a/prisma/seed/index.js b/prisma/seed/index.js
index abc52d5..2c51ee4 100644
--- a/prisma/seed/index.js
+++ b/prisma/seed/index.js
@@ -1,8 +1,9 @@
const { PrismaClient } = require("@prisma/client");
const { generatePrivateKey, getPublicKey } = require("../../api/utils/nostr-tools");
-const { categories, projects, tags, hackathons, roles, skills } = require("./data");
+const { categories, projects, tags, hackathons, roles, skills, capabilities } = 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,272 @@ async function main() {
// await createSkills();
+ // await createTournament();
+
+ // await migrateOldImages();
+
+ await createCapabilities();
+}
+
+async function migrateOldImages() {
+ console.log('Migrating old images data to HostedImage');
+
+ // Can't use prisma method createMany() for columns like Project.screenshots, because this method doesn't return created IDs.
+
+ /**
+ * Project
+ **/
+ const projects = await prisma.project.findMany({
+ select: {
+ id: true,
+ screenshots: true,
+ cover_image: true,
+ thumbnail_image: true
+ }
+ })
+ for (const project of projects) {
+ /**
+ * Project.screenshots to Project.screenshots_ids
+ **/
+ let projectScreenshotIds = [];
+ for (const screenshot of project.screenshots) {
+ let hostedImageId = await _insertInHostedImage(screenshot)
+ projectScreenshotIds.push(hostedImageId);
+ }
+ if (projectScreenshotIds.length > 0) {
+ await _updateObjectWithHostedImageId(prisma.project, project.id, {
+ screenshots_ids: projectScreenshotIds,
+ })
+ }
+
+ /**
+ * Project.cover_image to Project.cover_image_id
+ **/
+ if (project.cover_image) {
+ let hostedImageId = await _insertInHostedImage(project.cover_image)
+ await _updateObjectWithHostedImageId(prisma.project, project.id, {
+ cover_image_id: hostedImageId,
+ })
+ }
+
+ /**
+ * Project.thumbnail_image to Project.thumbnail_image_id
+ **/
+ if (project.cover_image) {
+ let hostedImageId = await _insertInHostedImage(project.thumbnail_image)
+ await _updateObjectWithHostedImageId(prisma.project, project.id, {
+ thumbnail_image_id: hostedImageId,
+ })
+ }
+ }
+
+ /**
+ * Category
+ **/
+ const categories = await prisma.category.findMany({
+ select: {
+ id: true,
+ cover_image: true,
+ }
+ })
+ for (const category of categories) {
+ if (category.cover_image) {
+ let hostedImageId = await _insertInHostedImage(category.cover_image)
+ await _updateObjectWithHostedImageId(prisma.category, category.id, {
+ cover_image_id: hostedImageId,
+ })
+ }
+ }
+
+ /**
+ * Award
+ **/
+ const awards = await prisma.award.findMany({
+ select: {
+ id: true,
+ image: true,
+ }
+ })
+ for (const award of awards) {
+ if (award.image) {
+ let hostedImageId = await _insertInHostedImage(award.image)
+ await _updateObjectWithHostedImageId(prisma.award, award.id, {
+ image_id: hostedImageId,
+ })
+ }
+ }
+
+ /**
+ * Hackaton
+ **/
+ const hackatons = await prisma.hackathon.findMany({
+ select: {
+ id: true,
+ cover_image: true,
+ }
+ })
+ for (const hackaton of hackatons) {
+ if (hackaton.cover_image) {
+ let hostedImageId = await _insertInHostedImage(hackaton.cover_image)
+ await _updateObjectWithHostedImageId(prisma.hackathon, hackaton.id, {
+ cover_image_id: hostedImageId,
+ })
+ }
+ }
+
+ /**
+ * Story
+ **/
+ const stories = await prisma.story.findMany({
+ select: {
+ id: true,
+ cover_image: true,
+ body: true,
+ }
+ })
+ for (const story of stories) {
+ /**
+ * Story.body to Story.body_image_ids
+ **/
+ let bodyImageIds = [];
+ const regex = /(?:!\[(.*?)\]\((.*?)\))/g
+ let match;
+ while ((match = regex.exec(story.body))) {
+ const [, , value] = match
+ let hostedImageId = await _insertInHostedImage(value)
+ bodyImageIds.push(hostedImageId)
+ }
+ if (bodyImageIds.length > 0) {
+ await _updateObjectWithHostedImageId(prisma.story, story.id, {
+ body_image_ids: bodyImageIds,
+ })
+ }
+
+ /**
+ * Story.cover_image to Story.cover_image_id
+ **/
+ if (story.cover_image) {
+ let hostedImageId = await _insertInHostedImage(story.cover_image)
+ await _updateObjectWithHostedImageId(prisma.story, story.id, {
+ cover_image_id: hostedImageId,
+ })
+ }
+ }
+
+ /**
+ * User
+ **/
+ const users = await prisma.user.findMany({
+ select: {
+ id: true,
+ avatar: true,
+ }
+ })
+ for (const user of users) {
+ if (user.avatar) {
+ let hostedImageId = await _insertInHostedImage(user.avatar)
+ await _updateObjectWithHostedImageId(prisma.user, user.id, {
+ avatar_id: hostedImageId,
+ })
+ }
+ }
+
+ /**
+ * Tournament
+ **/
+ const tournaments = await prisma.tournament.findMany({
+ select: {
+ id: true,
+ thumbnail_image: true,
+ cover_image: true,
+ }
+ })
+ for (const tournament of tournaments) {
+ if (tournament.thumbnail_image) {
+ let hostedImageId = await _insertInHostedImage(tournament.thumbnail_image)
+ await _updateObjectWithHostedImageId(prisma.tournament, tournament.id, {
+ thumbnail_image_id: hostedImageId,
+ })
+ }
+ if (tournament.cover_image) {
+ let hostedImageId = await _insertInHostedImage(tournament.cover_image)
+ await _updateObjectWithHostedImageId(prisma.tournament, tournament.id, {
+ cover_image_id: hostedImageId,
+ })
+ }
+ }
+
+ /**
+ * TournamentPrize
+ **/
+ const tournamentPrizes = await prisma.tournamentPrize.findMany({
+ select: {
+ id: true,
+ image: true,
+ }
+ })
+ for (const tournament of tournamentPrizes) {
+ if (tournament.image) {
+ let hostedImageId = await _insertInHostedImage(tournament.image)
+ await _updateObjectWithHostedImageId(prisma.tournamentPrize, tournament.id, {
+ image_id: hostedImageId,
+ })
+ }
+ }
+
+ /**
+ * TournamentJudge
+ **/
+ const tournamentJudges = await prisma.tournamentJudge.findMany({
+ select: {
+ id: true,
+ avatar: true,
+ }
+ })
+ for (const tournament of tournamentJudges) {
+ if (tournament.avatar) {
+ let hostedImageId = await _insertInHostedImage(tournament.avatar)
+ await _updateObjectWithHostedImageId(prisma.tournamentJudge, tournament.id, {
+ avatar_id: hostedImageId,
+ })
+ }
+ }
+ /**
+ * TournamentEvent
+ **/
+ const tournamentEvents = await prisma.tournamentEvent.findMany({
+ select: {
+ id: true,
+ image: true,
+ }
+ })
+ for (const tournament of tournamentEvents) {
+ if (tournament.image) {
+ let hostedImageId = await _insertInHostedImage(tournament.image)
+ await _updateObjectWithHostedImageId(prisma.tournamentEvent, tournament.id, {
+ image_id: hostedImageId,
+ })
+ }
+ }
+}
+
+async function _insertInHostedImage(url) {
+ const newHostedImage = await prisma.hostedImage.create({
+ data: {
+ filename: "default.png",
+ provider: "external",
+ provider_image_id: "",
+ url,
+ is_used: true
+ }
+ });
+ return newHostedImage.id;
+}
+async function _updateObjectWithHostedImageId(prismaObject, objectId, data) {
+ await prismaObject.update({
+ where: { id: objectId },
+ data,
+ });
}
async function createCategories() {
@@ -194,6 +461,53 @@ async function createSkills() {
})
}
+async function createTournament() {
+ console.log("Creating Tournament");
+
+ 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 }))
+ }
+ },
+
+ }
+ })
+
+}
+
+async function createCapabilities() {
+ console.log("Creating Capabilities");
+ await prisma.capability.createMany({
+ data: capabilities
+ })
+}
+
main()
.catch((e) => {
diff --git a/public/assets/icons/join-discord.svg b/public/assets/icons/join-discord.svg
new file mode 100644
index 0000000..da52ce2
--- /dev/null
+++ b/public/assets/icons/join-discord.svg
@@ -0,0 +1,4 @@
+
diff --git a/public/assets/images/join-discord-card.jpg b/public/assets/images/join-discord-card.jpg
new file mode 100644
index 0000000..f5eabe8
Binary files /dev/null and b/public/assets/images/join-discord-card.jpg differ
diff --git a/src/Components/Ads/Fulgur/fulgur_logo.svg b/public/assets/images/logos/fulgur_logo.svg
similarity index 100%
rename from src/Components/Ads/Fulgur/fulgur_logo.svg
rename to public/assets/images/logos/fulgur_logo.svg
diff --git a/public/mockServiceWorker.js b/public/mockServiceWorker.js
index ba0c013..0966a9d 100644
--- a/public/mockServiceWorker.js
+++ b/public/mockServiceWorker.js
@@ -2,7 +2,7 @@
/* tslint:disable */
/**
- * Mock Service Worker (0.39.1).
+ * Mock Service Worker (0.39.2).
* @see https://github.com/mswjs/msw
* - Please do NOT modify this file.
* - Please do NOT serve this file on production.
diff --git a/src/App.tsx b/src/App.tsx
index 74b7d24..4234abe 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -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")))
@@ -74,7 +76,6 @@ function App() {
}, []);
-
return
makers.bolt.fun
@@ -91,18 +92,21 @@ function App() {
}>
- } />
+ } />
}>
} />
} />
} />
- } />
+ } />
} />
+ } />
} />
+ } />
+
} />
} />
@@ -111,7 +115,7 @@ function App() {
} />
} />
- } />
+ } />
diff --git a/src/Components/Ads/Fulgur/Fulgur.tsx b/src/Components/Ads/Fulgur/Fulgur.tsx
index 10ae079..5328196 100644
--- a/src/Components/Ads/Fulgur/Fulgur.tsx
+++ b/src/Components/Ads/Fulgur/Fulgur.tsx
@@ -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() {
-

+
Turn your hackathon project into a startup
Schedule an office hour with Fulgur Ventures
diff --git a/src/Components/Button/Button.tsx b/src/Components/Button/Button.tsx
index 8d4c378..81fa3c1 100644
--- a/src/Components/Button/Button.tsx
+++ b/src/Components/Button/Button.tsx
@@ -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
= {
const btnStylesOutline: UnionToObjectKeys = {
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 = {
const baseBtnStyles: UnionToObjectKeys = {
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(({ 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'}
`;
diff --git a/src/Components/ErrorBoundary/ErrorBoundary.tsx b/src/Components/ErrorBoundary/ErrorBoundary.tsx
index 7fcfef3..6ddc18b 100644
--- a/src/Components/ErrorBoundary/ErrorBoundary.tsx
+++ b/src/Components/ErrorBoundary/ErrorBoundary.tsx
@@ -1,5 +1,5 @@
import React, { Component, ErrorInfo, ReactNode } from "react";
-import ErrorMessage from "../ErrorMessage/ErrorMessage";
+import ErrorMessage from "../Errors/ErrorMessage/ErrorMessage";
interface Props {
place?: string
diff --git a/src/Components/Errors/ErrorCard/ErrorCard.stories.tsx b/src/Components/Errors/ErrorCard/ErrorCard.stories.tsx
new file mode 100644
index 0000000..a73c3d2
--- /dev/null
+++ b/src/Components/Errors/ErrorCard/ErrorCard.stories.tsx
@@ -0,0 +1,21 @@
+import { ComponentStory, ComponentMeta } from '@storybook/react';
+
+import ErrorCard from './ErrorCard';
+
+export default {
+ title: 'Shared/ErrorCard',
+ component: ErrorCard,
+
+} as ComponentMeta;
+
+const Template: ComponentStory = (args) => ;
+
+export const Default = Template.bind({});
+Default.args = {
+ error: {
+ name: "Error Name",
+ message: "Error Message",
+ }
+}
+
+
diff --git a/src/Components/Errors/ErrorCard/ErrorCard.tsx b/src/Components/Errors/ErrorCard/ErrorCard.tsx
new file mode 100644
index 0000000..d9ee8e6
--- /dev/null
+++ b/src/Components/Errors/ErrorCard/ErrorCard.tsx
@@ -0,0 +1,40 @@
+import { useToggle } from "@react-hookz/web";
+import { FallbackProps } from "react-error-boundary";
+import { RiErrorWarningFill } from "react-icons/ri";
+import Button from "src/Components/Button/Button";
+import Card from "src/Components/Card/Card";
+
+
+
+export default function ErrorCard({ error, resetErrorBoundary }: FallbackProps) {
+
+ const [showDetails, toggleDetails] = useToggle(false)
+
+
+
+ return (
+
+
+
+
Ooops...
Looks like something unexpected went wrong, please check your internet connection & try again.
+
+
+
+
+
+
+
+
+ {showDetails &&
+
+
{error.name}
+
{error.message}
+
{error.stack}
+
+ }
+
+
+
+
+ )
+}
diff --git a/src/Components/ErrorMessage/ErrorMessage.stories.tsx b/src/Components/Errors/ErrorMessage/ErrorMessage.stories.tsx
similarity index 100%
rename from src/Components/ErrorMessage/ErrorMessage.stories.tsx
rename to src/Components/Errors/ErrorMessage/ErrorMessage.stories.tsx
diff --git a/src/Components/ErrorMessage/ErrorMessage.tsx b/src/Components/Errors/ErrorMessage/ErrorMessage.tsx
similarity index 100%
rename from src/Components/ErrorMessage/ErrorMessage.tsx
rename to src/Components/Errors/ErrorMessage/ErrorMessage.tsx
diff --git a/src/Components/Errors/ErrorPage/ErrorPage.tsx b/src/Components/Errors/ErrorPage/ErrorPage.tsx
new file mode 100644
index 0000000..5357f4c
--- /dev/null
+++ b/src/Components/Errors/ErrorPage/ErrorPage.tsx
@@ -0,0 +1,18 @@
+import React from 'react'
+import { Helmet } from 'react-helmet'
+import { FallbackProps } from 'react-error-boundary'
+import ErrorCard from '../ErrorCard/ErrorCard'
+
+
+export default function ErrorPage({ error, resetErrorBoundary }: FallbackProps) {
+ return (
+
+
+ {`Something went wrong...`}
+
+
+
+
+
+ )
+}
diff --git a/src/Components/IconButton/IconButton.tsx b/src/Components/IconButton/IconButton.tsx
index 407052e..7a3ea33 100644
--- a/src/Components/IconButton/IconButton.tsx
+++ b/src/Components/IconButton/IconButton.tsx
@@ -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>(({
children,
onClick = () => { },
onKeyDown,
- variant = 'blank'
+ variant = 'blank',
+ isDisabled,
}, ref) => {
if (href)
@@ -57,12 +59,16 @@ const IconButton = React.forwardRef>(({
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}
diff --git a/src/Components/InfoCard/InfoCard.tsx b/src/Components/InfoCard/InfoCard.tsx
index 6e41f47..c14e977 100644
--- a/src/Components/InfoCard/InfoCard.tsx
+++ b/src/Components/InfoCard/InfoCard.tsx
@@ -1,12 +1,12 @@
import React, { PropsWithChildren } from 'react'
interface Props {
-
+ className?: string
}
export default function InfoCard(props: PropsWithChildren) {
return (
-
+
{props.children}
diff --git a/src/Components/Inputs/Autocomplete/Autocomplete.stories.tsx b/src/Components/Inputs/Autocomplete/Autocomplete.stories.tsx
deleted file mode 100644
index 02df6f4..0000000
--- a/src/Components/Inputs/Autocomplete/Autocomplete.stories.tsx
+++ /dev/null
@@ -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
;
-
-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 = (args) => {
-
- const value = useWatch({ name: 'autocomplete' })
-
- console.log(value);
-
-
- return
-}
-
-
-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
-}
-
diff --git a/src/Components/Inputs/Autocomplete/Autocomplete.tsx b/src/Components/Inputs/Autocomplete/Autocomplete.tsx
deleted file mode 100644
index bd7b873..0000000
--- a/src/Components/Inputs/Autocomplete/Autocomplete.tsx
+++ /dev/null
@@ -1,95 +0,0 @@
-
-import { useMemo } from "react";
-import Select, { StylesConfig } from "react-select";
-import { ControlledStateHandler } from "src/utils/interfaces";
-
-
-type Props = {
- 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
-
-
-
-
-export default function AutoComplete({
- options,
- labelField,
- valueField,
- placeholder = "Select Option...",
- isMulti,
- isClearable,
- disabled,
- className,
- value,
- onChange,
- onBlur,
- size = 'md',
- ...props
-}: Props) {
-
-
- 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 (
-
-
-
- );
-}
-
-
diff --git a/src/Components/Inputs/FilesInput/DropInput.jsx b/src/Components/Inputs/FilesInput/DropInput.jsx
deleted file mode 100644
index 6a2b73c..0000000
--- a/src/Components/Inputs/FilesInput/DropInput.jsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import { useToggle } from "@react-hookz/web";
-import React from "react";
-import { FileDrop } from "react-file-drop";
-
-export default function DropInput({
- value: files,
- onChange,
- emptyContent,
- draggingContent,
- hasFilesContent,
- height,
- multiple = false,
- allowedType = "*",
- classes = {
- base: "",
- idle: "",
- dragging: "",
- },
-}) {
- const [isDragging, toggleDrag] = useToggle(false);
- const fileInputRef = React.useRef(null);
-
- const onAddFiles = (_files) => {
- onChange(_files);
- // do something with your files...
- };
-
- const uploadClick = () => {
- fileInputRef.current.click();
- };
-
- const status = isDragging ? "dragging" : files ? "has-files" : "empty";
-
- return (
-
- onAddFiles(files)}
- onTargetClick={uploadClick}
- onFrameDragEnter={() => toggleDrag(true)}
- onFrameDragLeave={() => toggleDrag(false)}
- onFrameDrop={() => toggleDrag(false)}
- className={`h-full cursor-pointer`}
- targetClassName={`h-full ${classes.base} ${
- status === "empty" && classes.idle
- }`}
- draggingOverFrameClassName={`${classes.dragging}`}
- >
- {status === "dragging" && draggingContent}
- {status === "empty" && emptyContent}
- {status === "has-files" && hasFilesContent}
-
- onAddFiles(e.target.files)}
- ref={fileInputRef}
- type="file"
- className="hidden"
- multiple={multiple}
- accept={allowedType}
- />
-
- );
-}
diff --git a/src/Components/Inputs/FilesInput/FileInput.stories.tsx b/src/Components/Inputs/FilesInput/FileInput.stories.tsx
deleted file mode 100644
index 5b02efa..0000000
--- a/src/Components/Inputs/FilesInput/FileInput.stories.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { ComponentStory, ComponentMeta } from '@storybook/react';
-import { BsImages } from 'react-icons/bs';
-import Button from 'src/Components/Button/Button';
-
-import FilesInput from './FilesInput';
-import FileDropInput from './FilesDropInput';
-
-export default {
- title: 'Shared/Inputs/Files Input',
- component: FilesInput,
-
-} as ComponentMeta;
-
-const Template: ComponentStory = (args) =>
-
-
-export const DefaultButton = Template.bind({});
-DefaultButton.args = {
-}
-
-export const CustomizedButton = Template.bind({});
-CustomizedButton.args = {
- multiple: true,
- uploadBtn:
-}
-
-const DropTemplate: ComponentStory = (args) =>
-export const DropZoneInput = DropTemplate.bind({});
-DropZoneInput.args = {
- onChange: console.log,
-}
\ No newline at end of file
diff --git a/src/Components/Inputs/FilesInput/FileThumbnail.tsx b/src/Components/Inputs/FilesInput/FileThumbnail.tsx
deleted file mode 100644
index b03fb5b..0000000
--- a/src/Components/Inputs/FilesInput/FileThumbnail.tsx
+++ /dev/null
@@ -1,75 +0,0 @@
-import { useMemo } from "react";
-import { MdClose } from "react-icons/md";
-import IconButton from "src/Components/IconButton/IconButton";
-
-interface Props {
- file: File | string,
- onRemove?: () => void
-}
-
-function getFileType(file: File | string) {
- if (typeof file === 'string') {
- if (/^http[^?]*.(jpg|jpeg|gif|png|tiff|bmp)(\?(.*))?$/gmi.test(file))
- return 'image'
- if (/\.(pdf|doc|docx)$/.test(file))
- return 'document';
-
- return 'unknown'
- }
- else {
- if (file['type'].split('/')[0] === 'image')
- return 'image'
-
- return 'unknown'
- }
-}
-
-type ThumbnailFile = {
- name: string;
- src: string;
- type: ReturnType
-}
-
-function processFile(file: Props['file']): ThumbnailFile {
-
- const fileType = getFileType(file);
-
- if (typeof file === 'string') return { name: file, src: file, type: fileType };
-
- return {
- name: file.name,
- src: URL.createObjectURL(file),
- type: fileType
- };
-
-}
-
-
-export default function FileThumbnail({ file: f, onRemove }: Props) {
-
- const file = useMemo(() => processFile(f), [f])
-
- return (
-
- )
-}
diff --git a/src/Components/Inputs/FilesInput/FilesDropInput.tsx b/src/Components/Inputs/FilesInput/FilesDropInput.tsx
deleted file mode 100644
index 6d96622..0000000
--- a/src/Components/Inputs/FilesInput/FilesDropInput.tsx
+++ /dev/null
@@ -1,88 +0,0 @@
-import { FaImage } from "react-icons/fa";
-import { UnionToObjectKeys } from "src/utils/types/utils";
-import DropInput from "./DropInput";
-
-
-type Props = {
- height?: number
- multiple?: boolean;
- value?: File[] | string[] | string;
- max?: number;
- onBlur?: () => void;
- onChange?: (files: (File | string)[] | null) => void
- uploadBtn?: JSX.Element
- uploadText?: string;
- allowedType?: 'images';
- classes?: Partial<{
- base: string,
- idle: string,
- dragging: string,
- hasFiles: string
- }>
-}
-
-const fileAccept: UnionToObjectKeys = {
- images: ".png, .jpg, .jpeg"
-} as const;
-
-const fileUrlToObject = async (url: string, fileName: string = 'filename') => {
- const res = await fetch(url);
- const contentType = res.headers.get('content-type') as string;
- const blob = await res.blob()
- const file = new File([blob], fileName, { contentType } as any)
- return file
-}
-
-export default function FilesInput({
- height = 200,
- multiple,
- value,
- max = 3,
- onBlur,
- onChange,
- allowedType = 'images',
- classes,
- ...props
-}: Props) {
-
-
- const baseClasses = classes?.base ?? 'p-32 rounded-8 text-center flex flex-col justify-center items-center'
- const idleClasses = classes?.idle ?? 'bg-primary-50 hover:bg-primary-25 border border-dashed border-primary-500 text-gray-800'
- const draggingClasses = classes?.dragging ?? 'bg-primary-500 text-white'
-
- return (
-
- )
-}
-
-const defaultEmptyContent = (
- <>
-
- {" "}
- Drop your files here
-
-
- or {" "}
-
- >
-);
-
-const defaultDraggingContent = Drop your files here ⬇⬇⬇
;
-
-const defaultHasFilesContent = (
- Files Uploaded Successfully!!
-);
\ No newline at end of file
diff --git a/src/Components/Inputs/FilesInput/FilesInput.tsx b/src/Components/Inputs/FilesInput/FilesInput.tsx
deleted file mode 100644
index 85e6cdb..0000000
--- a/src/Components/Inputs/FilesInput/FilesInput.tsx
+++ /dev/null
@@ -1,136 +0,0 @@
-import { createAction } from "@reduxjs/toolkit";
-import React, { ChangeEvent, useCallback, useRef } from "react"
-import { BsUpload } from "react-icons/bs";
-import { FaImage } from "react-icons/fa";
-import Button from "src/Components/Button/Button"
-import { openModal } from "src/redux/features/modals.slice";
-import { useAppDispatch } from "src/utils/hooks";
-import { useReduxEffect } from "src/utils/hooks/useReduxEffect";
-import { UnionToObjectKeys } from "src/utils/types/utils";
-import FilesThumbnails from "./FilesThumbnails";
-
-
-type Props = {
- multiple?: boolean;
- value?: File[] | string[] | string;
- max?: number;
- onBlur?: () => void;
- onChange?: (files: (File | string)[] | null) => void
- uploadBtn?: JSX.Element
- uploadText?: string;
- allowedType?: 'images';
-}
-
-const fileAccept: UnionToObjectKeys = {
- images: ".png, .jpg, .jpeg"
-} as const;
-
-const fileUrlToObject = async (url: string, fileName: string = 'filename') => {
- const res = await fetch(url);
- const contentType = res.headers.get('content-type') as string;
- const blob = await res.blob()
- const file = new File([blob], fileName, { contentType } as any)
- return file
-}
-
-const INSERT_IMAGE_ACTION = createAction<{ src: string, alt?: string }>('COVER_IMAGE_INSERTED')({ src: '', alt: "" })
-
-const FilesInput = React.forwardRef(({
- multiple,
- value,
- max = 3,
- onBlur,
- onChange,
- allowedType = 'images',
- uploadText = 'Upload files',
- ...props
-}, ref) => {
-
-
- const dispatch = useAppDispatch();
-
- const handleClick = () => {
- // ref.current.click();
- dispatch(openModal({
- Modal: "InsertImageModal",
- props: {
- callbackAction: {
- type: INSERT_IMAGE_ACTION.type,
- payload: {
- src: "",
- alt: ""
- }
- }
- }
- }))
- }
-
- const onInsertImgUrl = useCallback(({ payload: { src, alt } }: typeof INSERT_IMAGE_ACTION) => {
- if (typeof value === 'string')
- onChange?.([value, src]);
- else
- onChange?.([...(value ?? []), src]);
- }, [onChange, value])
-
- useReduxEffect(onInsertImgUrl, INSERT_IMAGE_ACTION.type)
-
- const handleChange = (e: ChangeEvent) => {
- const files = e.target.files && Array.from(e.target.files).slice(0, max);
- if (typeof value === 'string')
- onChange?.([value, ...(files ?? [])]);
- else
- onChange?.([...(value ?? []), ...(files ?? [])]);
- }
-
- const handleRemove = async (idx: number) => {
- if (!value) return onChange?.([]);
- if (typeof value === 'string')
- onChange?.([]);
- else {
- let files = [...value]
- files.splice(idx, 1);
-
- //change all files urls to file objects
- const filesConverted = await Promise.all(files.map(async file => {
- if (typeof file === 'string') return await fileUrlToObject(file, "")
- else return file;
- }))
-
- onChange?.(filesConverted);
- }
- }
-
- const canUploadMore = multiple ?
- !value || (value && value.length < max)
- :
- !value || value.length === 0
-
-
- const uploadBtn = props.uploadBtn ?
- React.cloneElement(props.uploadBtn, { onClick: handleClick })
- :
-
-
- return (
- <>
-
- {
- canUploadMore &&
- <>
- {uploadBtn}
-
- >
- }
- >
- )
-})
-
-
-export default FilesInput;
\ No newline at end of file
diff --git a/src/Components/Inputs/FilesInput/FilesThumbnails.tsx b/src/Components/Inputs/FilesInput/FilesThumbnails.tsx
deleted file mode 100644
index 362528e..0000000
--- a/src/Components/Inputs/FilesInput/FilesThumbnails.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { useMemo } from 'react'
-import FileThumbnail from './FileThumbnail';
-
-interface Props {
- files?: (File | string)[] | string;
- onRemove?: (idx: number) => void
-}
-
-function processFiles(files: Props['files']) {
-
- if (!files) return [];
- if (typeof files === 'string') return [files];
- return files;
-}
-
-export default function FilesThumbnails({ files, onRemove }: Props) {
- const filesConverted = useMemo(() => processFiles(files), [files])
-
- return (
-
- {
- filesConverted.map((file, idx) => onRemove?.(idx)} />)
- }
-
- )
-}
diff --git a/src/Components/Inputs/FilesInputs/AvatarInput/AvatarInput.stories.tsx b/src/Components/Inputs/FilesInputs/AvatarInput/AvatarInput.stories.tsx
new file mode 100644
index 0000000..73f451e
--- /dev/null
+++ b/src/Components/Inputs/FilesInputs/AvatarInput/AvatarInput.stories.tsx
@@ -0,0 +1,29 @@
+import React from 'react'
+import { ComponentStory, ComponentMeta } from '@storybook/react';
+import AvatarInput from './AvatarInput';
+import { WrapFormController } from 'src/utils/storybook/decorators';
+import { ImageType } from '../SingleImageUploadInput/SingleImageUploadInput';
+
+export default {
+ title: 'Shared/Inputs/Files Inputs/Avatar ',
+ component: AvatarInput,
+ decorators: [
+ WrapFormController<{ avatar: ImageType | null }>({
+ logValues: true,
+ name: "avatar",
+ defaultValues: {
+ avatar: null
+ }
+ })]
+} as ComponentMeta;
+
+const Template: ComponentStory = (args, context) => {
+
+ return
+
+}
+
+
+export const Default = Template.bind({});
+Default.args = {
+}
\ No newline at end of file
diff --git a/src/Components/Inputs/FilesInputs/AvatarInput/AvatarInput.tsx b/src/Components/Inputs/FilesInputs/AvatarInput/AvatarInput.tsx
new file mode 100644
index 0000000..e912eca
--- /dev/null
+++ b/src/Components/Inputs/FilesInputs/AvatarInput/AvatarInput.tsx
@@ -0,0 +1,105 @@
+import { motion } from 'framer-motion';
+import React, { ComponentProps, useRef } from 'react'
+import { AiOutlineCloudUpload } from 'react-icons/ai';
+import { CgArrowsExchangeV } from 'react-icons/cg';
+import { FiCamera } from 'react-icons/fi';
+import { IoMdClose } from 'react-icons/io';
+import { RotatingLines } from 'react-loader-spinner';
+import { Nullable } from 'remirror';
+import { useIsDraggingOnElement } from 'src/utils/hooks';
+import SingleImageUploadInput from '../SingleImageUploadInput/SingleImageUploadInput'
+
+type Value = ComponentProps['value']
+
+interface Props {
+ width?: number;
+ isRemovable?: boolean
+ value: Value;
+ onChange: (new_value: Nullable) => void
+}
+
+export default function AvatarInput(props: Props) {
+
+ const dropAreaRef = useRef(null!)
+ const isDragging = useIsDraggingOnElement({ ref: dropAreaRef });
+
+ return (
+
+
+
+ {!img &&
+
}
+ {img &&
+ <>
+

+ {!isUploading &&
+
+
+ {props.isRemovable && }
+
+ }
+ >}
+ {isUploading &&
+
+
+
+ }
+ {isDraggingOnWindow &&
+
+ }
+
}
+ />
+
+
+ )
+}
diff --git a/src/Components/Inputs/FilesInputs/CoverImageInput/CoverImageInput.stories.tsx b/src/Components/Inputs/FilesInputs/CoverImageInput/CoverImageInput.stories.tsx
new file mode 100644
index 0000000..9bedfe5
--- /dev/null
+++ b/src/Components/Inputs/FilesInputs/CoverImageInput/CoverImageInput.stories.tsx
@@ -0,0 +1,31 @@
+import React from 'react'
+import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { WrapFormController } from 'src/utils/storybook/decorators';
+import { ImageType } from '../SingleImageUploadInput/SingleImageUploadInput';
+import CoverImageInput from './CoverImageInput';
+
+export default {
+ title: 'Shared/Inputs/Files Inputs/Cover Image ',
+ component: CoverImageInput,
+ decorators: [
+ WrapFormController<{ thumbnail: ImageType | null }>({
+ logValues: true,
+ name: "thumbnail",
+ defaultValues: {
+ thumbnail: null
+ }
+ })]
+} as ComponentMeta;
+
+const Template: ComponentStory = (args, context) => {
+
+ return
+
+
+
+}
+
+
+export const Default = Template.bind({});
+Default.args = {
+}
\ No newline at end of file
diff --git a/src/Components/Inputs/FilesInputs/CoverImageInput/CoverImageInput.tsx b/src/Components/Inputs/FilesInputs/CoverImageInput/CoverImageInput.tsx
new file mode 100644
index 0000000..975366d
--- /dev/null
+++ b/src/Components/Inputs/FilesInputs/CoverImageInput/CoverImageInput.tsx
@@ -0,0 +1,103 @@
+import React, { ComponentProps, useEffect, useRef, useState } from 'react'
+import { FaImage } from 'react-icons/fa';
+import { CgArrowsExchangeV } from 'react-icons/cg';
+import { IoMdClose } from 'react-icons/io';
+import { RotatingLines } from 'react-loader-spinner';
+import { Nullable } from 'remirror';
+import SingleImageUploadInput from '../SingleImageUploadInput/SingleImageUploadInput'
+import { motion } from 'framer-motion';
+import { AiOutlineCloudUpload } from 'react-icons/ai';
+import { useIsDraggingOnElement } from 'src/utils/hooks';
+
+type Value = ComponentProps['value']
+
+interface Props {
+ value: Value;
+ rounded?: string;
+ onChange: (new_value: Nullable) => void
+}
+
+export default function CoverImageInput(props: Props) {
+
+ const dropAreaRef = useRef(null!)
+ const isDragging = useIsDraggingOnElement({ ref: dropAreaRef });
+
+
+ return (
+
+
+
+ {!img &&
+
+
+ Drop a COVER IMAGE here or
Click to browse
+
+
}
+ {img && <>
+

+ {!isUploading &&
+
+
+
+
+
+ }
+ >}
+ {isUploading &&
+
+
+
+ }
+ {isDraggingOnWindow &&
+
+
+
+
+
+ Drop here to upload
+
+
+ }
+
}
+ />
+
+
+ )
+}
diff --git a/src/Components/Inputs/FilesInputs/FileUploadInput/FileUploadInput.stories.tsx b/src/Components/Inputs/FilesInputs/FileUploadInput/FileUploadInput.stories.tsx
new file mode 100644
index 0000000..2e5b4dd
--- /dev/null
+++ b/src/Components/Inputs/FilesInputs/FileUploadInput/FileUploadInput.stories.tsx
@@ -0,0 +1,16 @@
+import React from 'react'
+import { ComponentStory, ComponentMeta } from '@storybook/react';
+import FileUploadInput from './FileUploadInput';
+
+export default {
+ title: 'Shared/Inputs/Files Inputs/Basic',
+ component: FileUploadInput,
+
+} as ComponentMeta;
+
+const Template: ComponentStory = (args) =>
+
+
+export const DefaultButton = Template.bind({});
+DefaultButton.args = {
+}
\ No newline at end of file
diff --git a/src/Components/Inputs/FilesInputs/FileUploadInput/FileUploadInput.tsx b/src/Components/Inputs/FilesInputs/FileUploadInput/FileUploadInput.tsx
new file mode 100644
index 0000000..ebfef3c
--- /dev/null
+++ b/src/Components/Inputs/FilesInputs/FileUploadInput/FileUploadInput.tsx
@@ -0,0 +1,141 @@
+import Uploady, { useUploady, useRequestPreSend, UPLOADER_EVENTS, } from "@rpldy/uploady";
+import { asUploadButton } from "@rpldy/upload-button";
+import Button from "src/Components/Button/Button";
+import { fetchUploadUrl } from "../fetch-upload-img-url";
+import ImagePreviews from "./ImagePreviews";
+import { FaImage } from "react-icons/fa";
+import UploadDropZone from "@rpldy/upload-drop-zone";
+import { forwardRef, useCallback } from "react";
+import styles from './styles.module.scss'
+import { MdFileUpload } from "react-icons/md";
+import { AiOutlineCloudUpload } from "react-icons/ai";
+import { motion } from "framer-motion";
+
+interface Props {
+ url: string;
+}
+
+const UploadBtn = asUploadButton((props: any) => {
+
+ useRequestPreSend(async (data) => {
+
+ const filename = data.items?.[0].file.name ?? ''
+
+ const url = await fetchUploadUrl({ filename });
+ return {
+ options: {
+ destination: {
+ url
+ }
+ }
+ }
+ })
+
+ // const handleClick = async () => {
+ // // Make a request to get the url
+ // try {
+ // var bodyFormData = new FormData();
+ // bodyFormData.append('requireSignedURLs', "false");
+ // const res = await axios({
+ // url: 'https://cors-anywhere.herokuapp.com/https://api.cloudflare.com/client/v4/accounts/783da4f06e5fdb9012c0632959a6f5b3/images/v2/direct_upload',
+ // method: 'POST',
+ // data: bodyFormData,
+ // headers: {
+ // "Authorization": "Bearer Xx2-CdsTliYkq6Ayz-1GX4CZubdQVxMwOSDbajP0",
+ // }
+ // })
+ // uploady.upload(res.data.result.uploadUrl, {
+ // destination: res.data.result.uploadUrl
+ // })
+ // } catch (error) {
+ // console.log(error);
+
+ // }
+
+
+ // // make the request with the files
+ // // uploady.upload()
+ // }
+
+ return
+});
+
+
+const DropZone = forwardRef((props, ref) => {
+ const { onClick, ...buttonProps } = props;
+
+ useRequestPreSend(async (data) => {
+
+ const filename = data.items?.[0].file.name ?? ''
+
+ const url = await fetchUploadUrl({ filename });
+ return {
+ options: {
+ destination: {
+ url
+ }
+ }
+ }
+ })
+
+ const onZoneClick = useCallback(
+ (e: any) => {
+ if (onClick) {
+ onClick(e);
+ }
+ },
+ [onClick]
+ );
+
+ return
+
+ Drop your IMAGES here or
+
+
+
+ Drop it to upload
+
+
+})
+
+const DropZoneButton = asUploadButton(DropZone);
+
+
+export default function FileUploadInput(props: Props) {
+ return (
+ {
+ const { id, filename, variants } = item?.uploadResponse?.data?.result ?? {}
+ if (id) {
+ console.log(id, filename, variants);
+ }
+ }
+ }}
+ >
+
+ {/* */}
+
+
+ )
+}
diff --git a/src/Components/Inputs/FilesInputs/FileUploadInput/ImagePreviews.tsx b/src/Components/Inputs/FilesInputs/FileUploadInput/ImagePreviews.tsx
new file mode 100644
index 0000000..e28b375
--- /dev/null
+++ b/src/Components/Inputs/FilesInputs/FileUploadInput/ImagePreviews.tsx
@@ -0,0 +1,92 @@
+import UploadPreview, { PreviewComponentProps, PreviewMethods } from '@rpldy/upload-preview'
+import { useAbortItem, useItemAbortListener, useItemCancelListener, useItemErrorListener, useItemProgressListener } from '@rpldy/uploady';
+import React, { useState } from 'react'
+import { RotatingLines } from 'react-loader-spinner';
+
+export default function ImagePreviews() {
+ return (
+
+
+
+ )
+}
+
+function CustomImagePreview({ id, url }: PreviewComponentProps) {
+
+ const [progress, setProgress] = useState(0);
+ const [itemState, setItemState] = useState(STATES.PROGRESS);
+
+
+ useItemProgressListener(item => {
+ if (item.completed > progress) {
+ setProgress(() => item.completed);
+
+ if (item.completed === 100) {
+ setItemState(STATES.DONE)
+ } else {
+ setItemState(STATES.PROGRESS)
+ }
+ }
+ }, id);
+
+
+
+ useItemAbortListener(item => {
+ setItemState(STATES.CANCELLED);
+ }, id);
+
+
+ useItemCancelListener(item => {
+ setItemState(STATES.CANCELLED);
+ }, id);
+
+ useItemErrorListener(item => {
+ setItemState(STATES.ERROR);
+ }, id);
+
+ return
+

+
+
+ {itemState === STATES.PROGRESS &&
+
+
+
}
+ {itemState === STATES.ERROR &&
+
+ Failed...
+
}
+ {itemState === STATES.CANCELLED &&
+
+ Cancelled
+
}
+
;
+};
+
+const STATES = {
+ PROGRESS: "PROGRESS",
+ DONE: "DONE",
+ CANCELLED: "CANCELLED",
+ ERROR: "ERROR"
+};
+
+const STATE_COLORS = {
+ [STATES.PROGRESS]: "#f4e4a4",
+ [STATES.DONE]: "#a5f7b3",
+ [STATES.CANCELLED]: "#f7cdcd",
+ [STATES.ERROR]: "#ee4c4c"
+};
\ No newline at end of file
diff --git a/src/Components/Inputs/FilesInputs/FileUploadInput/styles.module.scss b/src/Components/Inputs/FilesInputs/FileUploadInput/styles.module.scss
new file mode 100644
index 0000000..217ca39
--- /dev/null
+++ b/src/Components/Inputs/FilesInputs/FileUploadInput/styles.module.scss
@@ -0,0 +1,25 @@
+.zone {
+ background-color: #f2f4f7;
+ border-color: #e4e7ec;
+
+ .active_content {
+ display: none;
+ }
+
+ .idle_content {
+ display: block;
+ }
+
+ &.active {
+ background-color: #b3a0ff;
+ border-color: #9e88ff;
+
+ .active_content {
+ display: block;
+ }
+
+ .idle_content {
+ display: none;
+ }
+ }
+}
diff --git a/src/Components/Inputs/FilesInputs/ScreenshotsInput/ImagePreviews.tsx b/src/Components/Inputs/FilesInputs/ScreenshotsInput/ImagePreviews.tsx
new file mode 100644
index 0000000..74d4d64
--- /dev/null
+++ b/src/Components/Inputs/FilesInputs/ScreenshotsInput/ImagePreviews.tsx
@@ -0,0 +1,97 @@
+import UploadPreview, { PreviewComponentProps, PreviewMethods } from '@rpldy/upload-preview'
+import { useAbortItem, useItemAbortListener, useItemCancelListener, useItemErrorListener, useItemProgressListener } from '@rpldy/uploady';
+import { useState } from 'react'
+import ScreenShotsThumbnail from './ScreenshotThumbnail'
+
+export default function ImagePreviews() {
+ return (
+
+ )
+}
+
+function CustomImagePreview({ id, url }: PreviewComponentProps) {
+
+ const [progress, setProgress] = useState(0);
+ const [itemState, setItemState] = useState(STATES.PROGRESS);
+
+ const abortItem = useAbortItem();
+
+
+ useItemProgressListener(item => {
+ if (item.completed > progress) {
+ setProgress(() => item.completed);
+
+ if (item.completed === 100) {
+ setItemState(STATES.DONE)
+ } else {
+ setItemState(STATES.PROGRESS)
+ }
+ }
+ }, id);
+
+
+
+ useItemAbortListener(item => {
+ setItemState(STATES.CANCELLED);
+ }, id);
+
+
+ useItemCancelListener(item => {
+ setItemState(STATES.CANCELLED);
+ }, id);
+
+ useItemErrorListener(item => {
+ setItemState(STATES.ERROR);
+ }, id);
+
+ if (itemState === STATES.DONE || itemState === STATES.CANCELLED)
+ return null
+
+ return {
+ abortItem(id)
+ }}
+ />
+
+ // return
+ //

+ //
+ //
+ // {itemState === STATES.PROGRESS &&
+ //
+ //
+ //
}
+ // {itemState === STATES.ERROR &&
+ //
+ // Failed...
+ //
}
+ // {itemState === STATES.CANCELLED &&
+ //
+ // Cancelled
+ //
}
+ //
;
+};
+
+const STATES = {
+ PROGRESS: "PROGRESS",
+ DONE: "DONE",
+ CANCELLED: "CANCELLED",
+ ERROR: "ERROR"
+};
diff --git a/src/Components/Inputs/FilesInputs/ScreenshotsInput/ScreenshotThumbnail.tsx b/src/Components/Inputs/FilesInputs/ScreenshotsInput/ScreenshotThumbnail.tsx
new file mode 100644
index 0000000..0003c43
--- /dev/null
+++ b/src/Components/Inputs/FilesInputs/ScreenshotsInput/ScreenshotThumbnail.tsx
@@ -0,0 +1,52 @@
+import React from 'react'
+import { FaTimes } from 'react-icons/fa';
+import { RotatingLines } from 'react-loader-spinner';
+
+interface Props {
+ url?: string,
+ isLoading?: boolean;
+ isError?: boolean;
+ onCancel?: () => void;
+
+}
+
+export default function ScreenshotThumbnail({ url, isLoading, isError, onCancel }: Props) {
+
+ const isEmpty = !url;
+
+ return (
+
+ {!isEmpty &&

}
+
+
+ {isLoading &&
+
+
+
}
+ {isError &&
+
+ Failed...
+
}
+ {!isEmpty &&
+
+ }
+
+ )
+}
diff --git a/src/Components/Inputs/FilesInputs/ScreenshotsInput/ScreenshotsInput.stories.tsx b/src/Components/Inputs/FilesInputs/ScreenshotsInput/ScreenshotsInput.stories.tsx
new file mode 100644
index 0000000..1575707
--- /dev/null
+++ b/src/Components/Inputs/FilesInputs/ScreenshotsInput/ScreenshotsInput.stories.tsx
@@ -0,0 +1,84 @@
+import React from 'react'
+import { ComponentStory, ComponentMeta } from '@storybook/react';
+import ScreenshotsInput, { ScreenshotType } from './ScreenshotsInput';
+import { WrapForm, WrapFormController } from 'src/utils/storybook/decorators';
+
+export default {
+ title: 'Shared/Inputs/Files Inputs/Screenshots',
+ component: ScreenshotsInput,
+ decorators: [
+ WrapFormController<{ screenshots: Array }>({
+ logValues: true,
+ name: "screenshots",
+ defaultValues: {
+ screenshots: []
+ }
+ })]
+} as ComponentMeta;
+
+const Template: ComponentStory = (args, context) => {
+
+ return
+
+}
+
+
+export const Empty = Template.bind({});
+Empty.args = {
+}
+
+export const WithValues = Template.bind({});
+WithValues.decorators = [
+ WrapFormController<{ screenshots: Array }>({
+ logValues: true,
+ name: "screenshots",
+ defaultValues: {
+ screenshots: [{
+ id: '123',
+ name: 'tree',
+ url: "https://picsum.photos/id/1021/800/800.jpg"
+ },
+ {
+ id: '555',
+ name: 'whatever',
+ url: "https://picsum.photos/id/600/800/800.jpg"
+ },]
+ }
+ }) as any
+];
+WithValues.args = {
+}
+
+export const Full = Template.bind({});
+Full.decorators = [
+ WrapFormController<{ screenshots: Array }>({
+ logValues: true,
+ name: "screenshots",
+ defaultValues: {
+ screenshots: [
+ {
+ id: '123',
+ name: 'tree',
+ url: "https://picsum.photos/id/1021/800/800.jpg"
+ },
+ {
+ id: '555',
+ name: 'whatever',
+ url: "https://picsum.photos/id/600/800/800.jpg"
+ },
+ {
+ id: '562',
+ name: 'Moon',
+ url: "https://picsum.photos/id/32/800/800.jpg"
+ },
+ {
+ id: '342',
+ name: 'Sun',
+ url: "https://picsum.photos/id/523/800/800.jpg"
+ },
+ ]
+ }
+ }) as any
+];
+Full.args = {
+}
\ No newline at end of file
diff --git a/src/Components/Inputs/FilesInputs/ScreenshotsInput/ScreenshotsInput.tsx b/src/Components/Inputs/FilesInputs/ScreenshotsInput/ScreenshotsInput.tsx
new file mode 100644
index 0000000..b5bf6c0
--- /dev/null
+++ b/src/Components/Inputs/FilesInputs/ScreenshotsInput/ScreenshotsInput.tsx
@@ -0,0 +1,151 @@
+import Uploady, { useRequestPreSend, UPLOADER_EVENTS } from "@rpldy/uploady";
+import { asUploadButton } from "@rpldy/upload-button";
+// import { fetchUploadUrl } from "./fetch-upload-img-url";
+import ImagePreviews from "./ImagePreviews";
+import UploadDropZone from "@rpldy/upload-drop-zone";
+import { forwardRef, useCallback, useState } from "react";
+import styles from './styles.module.scss'
+import { AiOutlineCloudUpload } from "react-icons/ai";
+import { motion } from "framer-motion";
+import { getMockSenderEnhancer } from "@rpldy/mock-sender";
+import ScreenshotThumbnail from "./ScreenshotThumbnail";
+import { FiCamera } from "react-icons/fi";
+import { Control, Path, useController } from "react-hook-form";
+
+
+
+const mockSenderEnhancer = getMockSenderEnhancer({
+ delay: 1500,
+});
+
+const MAX_UPLOAD_COUNT = 4 as const;
+
+export interface ScreenshotType {
+ id: string,
+ name: string,
+ url: string;
+}
+
+interface Props {
+ value: ScreenshotType[],
+ onChange: (new_value: ScreenshotType[]) => void
+}
+
+
+export default function ScreenshotsInput(props: Props) {
+
+ const { value: uploadedFiles, onChange } = props;
+
+
+ const [uploadingCount, setUploadingCount] = useState(0)
+
+
+ const canUploadMore = uploadingCount + uploadedFiles.length < MAX_UPLOAD_COUNT;
+ const placeholdersCount = (MAX_UPLOAD_COUNT - (uploadingCount + uploadedFiles.length + 1));
+
+
+ return (
+ {
+ setUploadingCount(v => v + batch.items.length)
+ },
+ [UPLOADER_EVENTS.ITEM_FINALIZE]: () => setUploadingCount(v => v - 1),
+ [UPLOADER_EVENTS.ITEM_FINISH]: (item) => {
+
+ // Just for mocking purposes
+ const dataUrl = URL.createObjectURL(item.file);
+
+ const { id, filename, variants } = item?.uploadResponse?.data?.result ?? {
+ id: Math.random().toString(),
+ filename: item.file.name,
+ variants: [
+ "",
+ dataUrl
+ ]
+ }
+ if (id) {
+ onChange([...uploadedFiles, { id, name: filename, url: variants[1] }].slice(-MAX_UPLOAD_COUNT))
+ }
+ }
+ }}
+ >
+
+
+ {canUploadMore && }
+ {uploadedFiles.map(f => {
+ onChange(uploadedFiles.filter(file => file.id !== f.id))
+ }} />)}
+
+ {(placeholdersCount > 0) &&
+ Array(placeholdersCount).fill(0).map((_, idx) => )}
+
+
+ )
+}
+
+const DropZone = forwardRef((props, ref) => {
+ const { onClick, ...buttonProps } = props;
+
+
+ useRequestPreSend(async (data) => {
+
+ const filename = data.items?.[0].file.name ?? ''
+
+ // const url = await fetchUploadUrl({ filename });
+ return {
+ options: {
+ destination: {
+ url: "URL"
+ }
+ }
+ }
+ })
+
+ const onZoneClick = useCallback(
+ (e: any) => {
+ if (onClick) {
+ onClick(e);
+ }
+ },
+ [onClick]
+ );
+
+ return
+
+
+
+ Browse images or
drop
+ them here
+
+
+
+
+ Drop to upload
+
+
+})
+
+const DropZoneButton = asUploadButton(DropZone);
diff --git a/src/Components/Inputs/FilesInputs/ScreenshotsInput/styles.module.scss b/src/Components/Inputs/FilesInputs/ScreenshotsInput/styles.module.scss
new file mode 100644
index 0000000..217ca39
--- /dev/null
+++ b/src/Components/Inputs/FilesInputs/ScreenshotsInput/styles.module.scss
@@ -0,0 +1,25 @@
+.zone {
+ background-color: #f2f4f7;
+ border-color: #e4e7ec;
+
+ .active_content {
+ display: none;
+ }
+
+ .idle_content {
+ display: block;
+ }
+
+ &.active {
+ background-color: #b3a0ff;
+ border-color: #9e88ff;
+
+ .active_content {
+ display: block;
+ }
+
+ .idle_content {
+ display: none;
+ }
+ }
+}
diff --git a/src/Components/Inputs/FilesInputs/SingleImageUploadInput/ImagePreviews.tsx b/src/Components/Inputs/FilesInputs/SingleImageUploadInput/ImagePreviews.tsx
new file mode 100644
index 0000000..74d4d64
--- /dev/null
+++ b/src/Components/Inputs/FilesInputs/SingleImageUploadInput/ImagePreviews.tsx
@@ -0,0 +1,97 @@
+import UploadPreview, { PreviewComponentProps, PreviewMethods } from '@rpldy/upload-preview'
+import { useAbortItem, useItemAbortListener, useItemCancelListener, useItemErrorListener, useItemProgressListener } from '@rpldy/uploady';
+import { useState } from 'react'
+import ScreenShotsThumbnail from './ScreenshotThumbnail'
+
+export default function ImagePreviews() {
+ return (
+
+ )
+}
+
+function CustomImagePreview({ id, url }: PreviewComponentProps) {
+
+ const [progress, setProgress] = useState(0);
+ const [itemState, setItemState] = useState(STATES.PROGRESS);
+
+ const abortItem = useAbortItem();
+
+
+ useItemProgressListener(item => {
+ if (item.completed > progress) {
+ setProgress(() => item.completed);
+
+ if (item.completed === 100) {
+ setItemState(STATES.DONE)
+ } else {
+ setItemState(STATES.PROGRESS)
+ }
+ }
+ }, id);
+
+
+
+ useItemAbortListener(item => {
+ setItemState(STATES.CANCELLED);
+ }, id);
+
+
+ useItemCancelListener(item => {
+ setItemState(STATES.CANCELLED);
+ }, id);
+
+ useItemErrorListener(item => {
+ setItemState(STATES.ERROR);
+ }, id);
+
+ if (itemState === STATES.DONE || itemState === STATES.CANCELLED)
+ return null
+
+ return {
+ abortItem(id)
+ }}
+ />
+
+ // return
+ //

+ //
+ //
+ // {itemState === STATES.PROGRESS &&
+ //
+ //
+ //
}
+ // {itemState === STATES.ERROR &&
+ //
+ // Failed...
+ //
}
+ // {itemState === STATES.CANCELLED &&
+ //
+ // Cancelled
+ //
}
+ //
;
+};
+
+const STATES = {
+ PROGRESS: "PROGRESS",
+ DONE: "DONE",
+ CANCELLED: "CANCELLED",
+ ERROR: "ERROR"
+};
diff --git a/src/Components/Inputs/FilesInputs/SingleImageUploadInput/ScreenshotThumbnail.tsx b/src/Components/Inputs/FilesInputs/SingleImageUploadInput/ScreenshotThumbnail.tsx
new file mode 100644
index 0000000..0003c43
--- /dev/null
+++ b/src/Components/Inputs/FilesInputs/SingleImageUploadInput/ScreenshotThumbnail.tsx
@@ -0,0 +1,52 @@
+import React from 'react'
+import { FaTimes } from 'react-icons/fa';
+import { RotatingLines } from 'react-loader-spinner';
+
+interface Props {
+ url?: string,
+ isLoading?: boolean;
+ isError?: boolean;
+ onCancel?: () => void;
+
+}
+
+export default function ScreenshotThumbnail({ url, isLoading, isError, onCancel }: Props) {
+
+ const isEmpty = !url;
+
+ return (
+
+ {!isEmpty &&

}
+
+
+ {isLoading &&
+
+
+
}
+ {isError &&
+
+ Failed...
+
}
+ {!isEmpty &&
+
+ }
+
+ )
+}
diff --git a/src/Components/Inputs/FilesInputs/SingleImageUploadInput/SingleImageUploadInput.stories.tsx b/src/Components/Inputs/FilesInputs/SingleImageUploadInput/SingleImageUploadInput.stories.tsx
new file mode 100644
index 0000000..06e9fe5
--- /dev/null
+++ b/src/Components/Inputs/FilesInputs/SingleImageUploadInput/SingleImageUploadInput.stories.tsx
@@ -0,0 +1,28 @@
+import React from 'react'
+import { ComponentStory, ComponentMeta } from '@storybook/react';
+import SingleImageUploadInput, { ImageType } from './SingleImageUploadInput';
+import { WrapFormController } from 'src/utils/storybook/decorators';
+import { RotatingLines } from 'react-loader-spinner';
+import { FiCamera, } from 'react-icons/fi';
+import { FaExchangeAlt, FaImage } from 'react-icons/fa';
+
+export default {
+ title: 'Shared/Inputs/Files Inputs/Single Image Upload ',
+ component: SingleImageUploadInput,
+ decorators: [
+ WrapFormController<{ avatar: ImageType | null }>({
+ logValues: true,
+ name: "avatar",
+ defaultValues: {
+ avatar: null
+ }
+ })]
+} as ComponentMeta;
+
+const Template: ComponentStory = (args, context) => {
+
+ return
+
+}
+
+
diff --git a/src/Components/Inputs/FilesInputs/SingleImageUploadInput/SingleImageUploadInput.tsx b/src/Components/Inputs/FilesInputs/SingleImageUploadInput/SingleImageUploadInput.tsx
new file mode 100644
index 0000000..44b7ce0
--- /dev/null
+++ b/src/Components/Inputs/FilesInputs/SingleImageUploadInput/SingleImageUploadInput.tsx
@@ -0,0 +1,141 @@
+import Uploady, { useRequestPreSend, UPLOADER_EVENTS, useAbortAll } from "@rpldy/uploady";
+import { asUploadButton } from "@rpldy/upload-button";
+// import { fetchUploadUrl } from "./fetch-upload-img-url";
+import UploadDropZone from "@rpldy/upload-drop-zone";
+import { forwardRef, ReactElement, useCallback, useState } from "react";
+import styles from './styles.module.scss'
+import { getMockSenderEnhancer } from "@rpldy/mock-sender";
+import { NotificationsService } from "src/services";
+import { useIsDraggingOnElement } from 'src/utils/hooks';
+import { fetchUploadImageUrl } from "src/api/uploading";
+
+
+
+const mockSenderEnhancer = getMockSenderEnhancer({
+ delay: 1500,
+
+});
+
+
+export interface ImageType {
+ id?: string | null,
+ name?: string | null,
+ url: string;
+}
+
+type RenderPropArgs = {
+ isUploading?: boolean;
+ img: ImageType | null,
+ onAbort: () => void,
+ isDraggingOnWindow?: boolean
+}
+
+interface Props {
+ value: ImageType | null | undefined,
+ onChange: (new_value: ImageType | null) => void;
+ wrapperClass?: string;
+ render: (args: RenderPropArgs) => ReactElement;
+}
+
+
+export default function SingleImageUploadInput(props: Props) {
+
+ const { value, onChange, render } = props;
+
+
+ const [currentlyUploadingItem, setCurrentlyUploadingItem] = useState(null)
+
+
+ return (
+ {
+ onChange(null)
+
+ setCurrentlyUploadingItem({
+ id: item.id,
+ url: URL.createObjectURL(item.file),
+ name: item.file.name,
+ })
+ },
+ [UPLOADER_EVENTS.ITEM_ERROR]: (item) => {
+ NotificationsService.error("An error happened while uploading. Please try again.")
+ },
+ [UPLOADER_EVENTS.ITEM_FINALIZE]: () => setCurrentlyUploadingItem(null),
+ [UPLOADER_EVENTS.ITEM_FINISH]: (item) => {
+
+ const { id, filename, variants } = item?.uploadResponse?.data?.result;
+ const url = (variants as string[]).find(v => v.includes('public'));
+
+ if (id && url) {
+ onChange({ id, name: filename, url, })
+ }
+ }
+ }}
+ >
+
+
+ )
+}
+
+
+const DropZone = forwardRef((props, ref) => {
+ const { onClick, children, renderProps, ...buttonProps } = props;
+
+ const isDraggingOnWindow = useIsDraggingOnElement()
+
+ useRequestPreSend(async (data) => {
+
+ const filename = data.items?.[0].file.name ?? ''
+
+ const res = await fetchUploadImageUrl({ filename });
+
+ return {
+ options: {
+ destination: {
+ url: res.uploadURL
+ },
+ }
+ }
+ })
+
+ const onZoneClick = useCallback(
+ (e: any) => {
+ if (onClick) {
+ onClick(e);
+ }
+ },
+ [onClick]
+ );
+
+ return
+
+ {renderProps.render({
+ img: renderProps.img,
+ isUploading: renderProps.isUploading,
+ isDraggingOnWindow,
+ })}
+
+})
+
+const DropZoneButton = asUploadButton(DropZone);
diff --git a/src/Components/Inputs/FilesInputs/SingleImageUploadInput/styles.module.scss b/src/Components/Inputs/FilesInputs/SingleImageUploadInput/styles.module.scss
new file mode 100644
index 0000000..217ca39
--- /dev/null
+++ b/src/Components/Inputs/FilesInputs/SingleImageUploadInput/styles.module.scss
@@ -0,0 +1,25 @@
+.zone {
+ background-color: #f2f4f7;
+ border-color: #e4e7ec;
+
+ .active_content {
+ display: none;
+ }
+
+ .idle_content {
+ display: block;
+ }
+
+ &.active {
+ background-color: #b3a0ff;
+ border-color: #9e88ff;
+
+ .active_content {
+ display: block;
+ }
+
+ .idle_content {
+ display: none;
+ }
+ }
+}
diff --git a/src/Components/Inputs/FilesInputs/ThumbnailInput/ThumbnailInput.stories.tsx b/src/Components/Inputs/FilesInputs/ThumbnailInput/ThumbnailInput.stories.tsx
new file mode 100644
index 0000000..a7ad6b3
--- /dev/null
+++ b/src/Components/Inputs/FilesInputs/ThumbnailInput/ThumbnailInput.stories.tsx
@@ -0,0 +1,29 @@
+import React from 'react'
+import { ComponentStory, ComponentMeta } from '@storybook/react';
+import ThumbnailInput from './ThumbnailInput';
+import { WrapFormController } from 'src/utils/storybook/decorators';
+import { ImageType } from '../SingleImageUploadInput/SingleImageUploadInput';
+
+export default {
+ title: 'Shared/Inputs/Files Inputs/Thumbnail ',
+ component: ThumbnailInput,
+ decorators: [
+ WrapFormController<{ thumbnail: ImageType | null }>({
+ logValues: true,
+ name: "thumbnail",
+ defaultValues: {
+ thumbnail: null
+ }
+ })]
+} as ComponentMeta;
+
+const Template: ComponentStory = (args, context) => {
+
+ return
+
+}
+
+
+export const Default = Template.bind({});
+Default.args = {
+}
\ No newline at end of file
diff --git a/src/Components/Inputs/FilesInputs/ThumbnailInput/ThumbnailInput.tsx b/src/Components/Inputs/FilesInputs/ThumbnailInput/ThumbnailInput.tsx
new file mode 100644
index 0000000..1b677fa
--- /dev/null
+++ b/src/Components/Inputs/FilesInputs/ThumbnailInput/ThumbnailInput.tsx
@@ -0,0 +1,54 @@
+import React, { ComponentProps } from 'react'
+import { FiCamera } from 'react-icons/fi';
+import { RotatingLines } from 'react-loader-spinner';
+import { Nullable } from 'remirror';
+import SingleImageUploadInput from '../SingleImageUploadInput/SingleImageUploadInput'
+
+type Value = ComponentProps['value']
+
+interface Props {
+ width?: number
+ value: Value;
+ onChange: (new_value: Nullable) => void
+}
+
+export default function ThumbnailInput(props: Props) {
+ return (
+
+
+ {img &&

}
+ {!img &&
+ <>
+
+
+ Add Image
+
+ >}
+ {isUploading &&
+
+
+
+ }
+
}
+ />
+
+
+ )
+}
diff --git a/src/Components/Inputs/FilesInputs/fetch-upload-img-url.tsx b/src/Components/Inputs/FilesInputs/fetch-upload-img-url.tsx
new file mode 100644
index 0000000..e2c9b19
--- /dev/null
+++ b/src/Components/Inputs/FilesInputs/fetch-upload-img-url.tsx
@@ -0,0 +1,25 @@
+import axios from "axios";
+import { NotificationsService } from "src/services";
+
+export async function fetchUploadUrl(options?: Partial<{ filename: string }>) {
+
+ const { filename } = options ?? {}
+
+ try {
+ const bodyFormData = new FormData();
+ bodyFormData.append('requireSignedURLs', "false");
+ const res = await axios({
+ url: 'https://cors-anywhere.herokuapp.com/https://api.cloudflare.com/client/v4/accounts/783da4f06e5fdb9012c0632959a6f5b3/images/v2/direct_upload',
+ method: 'POST',
+ data: bodyFormData,
+ headers: {
+ "Authorization": "Bearer XXX",
+ }
+ })
+ return res.data.result.uploadURL as string;
+ } catch (error) {
+ console.log(error);
+ NotificationsService.error("A network error happened.")
+ return "couldnt fetch upload url";
+ }
+}
\ No newline at end of file
diff --git a/src/Components/Inputs/Selects/BasicSelectInput/BasicSelectInput.stories.tsx b/src/Components/Inputs/Selects/BasicSelectInput/BasicSelectInput.stories.tsx
new file mode 100644
index 0000000..72af3cc
--- /dev/null
+++ b/src/Components/Inputs/Selects/BasicSelectInput/BasicSelectInput.stories.tsx
@@ -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;
+
+const Template: ComponentStory = (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"
+}
+
+
+
+
diff --git a/src/Components/Inputs/Selects/BasicSelectInput/BasicSelectInput.tsx b/src/Components/Inputs/Selects/BasicSelectInput/BasicSelectInput.tsx
new file mode 100644
index 0000000..00d3d0a
--- /dev/null
+++ b/src/Components/Inputs/Selects/BasicSelectInput/BasicSelectInput.tsx
@@ -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, 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) => JSX.Element
+} & ControlledStateHandler
+
+
+
+
+
+
+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, IsMulti extends boolean>({
+ options,
+ labelField,
+ valueField,
+ placeholder = "Select Option...",
+ isMulti,
+ isClearable,
+ disabled,
+ className,
+ value,
+ onChange,
+ onBlur,
+ renderOption,
+ ...props
+}: Props) {
+
+
+
+
+ return (
+
+
+
+ );
+}
+
+function getOptionComponent>(renderOption: Props['renderOption'], labelField: Props['labelField']) {
+ const _render = renderOption ?? ((option) =>
+ {option.data[labelField]}
+
)
+
+ return function OptionComponent(props: OptionProps) {
+ return (
+
+ {_render(props)}
+
+ );
+ };
+}
\ No newline at end of file
diff --git a/src/Components/Inputs/TagsInput/TagsInput.tsx b/src/Components/Inputs/TagsInput/TagsInput.tsx
index f36cd0b..26bbfa6 100644
--- a/src/Components/Inputs/TagsInput/TagsInput.tsx
+++ b/src/Components/Inputs/TagsInput/TagsInput.tsx
@@ -13,7 +13,9 @@ interface Option {
readonly description: string | null
}
+
type Tag = Omit
+type Value = { title: Tag['title'] }
interface Props {
classes?: {
@@ -22,11 +24,80 @@ interface Props {
}
placeholder?: string
max?: number;
+ value: Value[];
+ onChange?: (new_value: Value[]) => void;
+ onBlur?: () => void;
[k: string]: any
}
+
+export default function TagsInput({
+ classes,
+ placeholder = 'Write some tags',
+ max = 5,
+ value,
+ onChange,
+ onBlur,
+ ...props }: Props) {
+
+ const officalTags = useOfficialTagsQuery();
+
+
+ const handleChange = (newValue: OnChangeValue