Merge pull request #152 from peakshift/dev

Tournament Feature
This commit is contained in:
Ed
2022-09-19 18:00:24 +01:00
committed by GitHub
216 changed files with 10248 additions and 1981 deletions

View File

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

View File

@@ -21,9 +21,10 @@ type Award {
interface BaseUser {
avatar: String!
bio: String
email: String
discord: String
github: String
id: Int!
in_tournament(id: Int!): Boolean!
jobTitle: String
join_date: Date!
lightning_address: String
@@ -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!

View File

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

View File

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

View File

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

View File

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

View File

@@ -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(/&amp;/g, "&")
.replace(/&#39;/g, "'")
.replace(/&quot;/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(/&amp;/g, "&")
.replace(/&#39;/g, "'")
.replace(/&quot;/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
}
})
},

View File

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

View File

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

View File

@@ -1,55 +0,0 @@
const {
intArg,
objectType,
stringArg,
extendType,
nonNull,
} = require('nexus');
const { prisma } = require('../../../prisma');
const Tournament = objectType({
name: 'Tournament',
definition(t) {
t.nonNull.int('id');
t.nonNull.string('title');
t.nonNull.string('description');
t.nonNull.string('thumbnail_image');
t.nonNull.string('cover_image');
t.nonNull.date('start_date');
t.nonNull.date('end_date');
t.nonNull.string('website');
t.nonNull.list.nonNull.field('tags', {
type: "Tag",
resolve: (parent) => {
// return prisma.hackathon.findUnique({ where: { id: parent.id } }).tags();
return [];
}
});
}
})
const getAllTournaments = extendType({
type: "Query",
definition(t) {
t.nonNull.list.nonNull.field('getAllTournaments', {
type: Tournament,
args: {
sortBy: stringArg(),
tag: intArg(),
},
resolve(_, args) {
const { sortBy, tag } = args;
return [];
}
})
}
})
module.exports = {
// Types
Tournament,
// Queries
getAllTournaments,
}

View File

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

View File

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

View File

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

View File

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

View File

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

9
api/utils/logger.js Normal file
View File

@@ -0,0 +1,9 @@
function logError(error) {
console.log("Unexpected Error: ");
console.log(error);
}
module.exports = {
logError
}

View File

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

View File

@@ -9,4 +9,8 @@ generates:
- "typescript-react-apollo"
config:
withHooks: true
avoidOptionals: true
avoidOptionals:
field: true
inputValue: false
object: true
defaultValue: true

376
package-lock.json generated
View File

@@ -1,11 +1,11 @@
{
"name": "my-app",
"name": "makers-bolt-fun",
"version": "0.1.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "my-app",
"name": "makers-bolt-fun",
"version": "0.1.0",
"dependencies": {
"@apollo/client": "^3.6.9",
@@ -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",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "discord" TEXT;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Project" ALTER COLUMN "launch_status" SET DEFAULT E'Launched';

View File

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

View File

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

View File

@@ -0,0 +1,232 @@
const tournament = {
__typename: "Tournament",
id: 12,
title: "Legends of Lightning ⚡️",
start_date: "2022-10-12T21:00:00.000Z",
end_date: "2022-11-30T22:00:00.000Z",
cover_image: "https://imagedelivery.net/wyrwp3c-j0gDDUWgnE7lig/1d5d2c86-fe46-4478-6909-bb3c425c0d00/public",
thumbnail_image: "https://imagedelivery.net/wyrwp3c-j0gDDUWgnE7lig/37fb9cd6-e4f1-43f9-c3fe-7c3e119d5600/public",
location: "Online",
website: "#",
description: // this field accepts markdown
`## Tournament Details
BOLT🔩FUNs maiden tournament, **Legends of Lightning** ⚡ will be an online global competition for makers to learn, connect, collaborate, and experiment with building innovative applications and tools with bitcoin and lightning.
Spanning a 2-month period, makers can form teams, hack on projects, and show off their progress, activity, and updates as they compete for up to **$10,000 in bitcoin prizes**.
BOLT🔩FUN has partnered with a number of events, meetups, and hackathons to provide makers the opportunity to brainstorm, design, build, and accelerate their tournament projects over the course of a couple of months. At the end of the tournament, a panel of judges will access and score all submitted projects - announcing the winners in the second week of December!
`, // markdown
prizes: [
{
title: "stw3 champion",
amount: "$ 5k",
image: "https://imagedelivery.net/wyrwp3c-j0gDDUWgnE7lig/39217dcf-c900-46be-153f-169e3a1f0400/public",
},
{
title: "2nd place",
amount: "$ 2.5k",
image: "https://imagedelivery.net/wyrwp3c-j0gDDUWgnE7lig/39cdb7c8-5fbf-49ff-32cf-fdabc3aa2d00/public",
},
{
title: "3rd place ",
amount: "$ 1.5k",
image: "https://imagedelivery.net/wyrwp3c-j0gDDUWgnE7lig/75958797-73b2-4a62-52df-9f0f98c53900/public",
},
{
title: "best design ",
amount: "$ 1k",
image: "https://imagedelivery.net/wyrwp3c-j0gDDUWgnE7lig/fa7b7cdd-7c06-4ebe-1a2d-94af9d2dae00/public",
}
],
events: [
{
title: "Tab Conf 22",
starts_at: "2022-10-13T21:00:00.000Z",
ends_at: "2022-10-15T22:00:00.000Z",
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Diam morbi pellentesque velit congue. Aliquet rutrum a, augue vitae tincidunt ac egestas. Mauris nec fringilla diam eget fusce malesuada cum parturient. Nulla pretium purus odio odio.",
image: 'https://picsum.photos/id/10/400/800',
links: [],
location: "Atlanta, GA",
type: 1, /** EVent typs encoding
*
Twitter Space: 0,
Workshop: 1,
IRL Meetup: 2,
Online Meetup: 3,
*/
website: "https://2022.tabconf.com/"
},
{
title: "Bitcoin Amsterdam",
starts_at: "2022-10-12T21:00:00.000Z",
ends_at: "2022-10-14T22:00:00.000Z",
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Diam morbi pellentesque velit congue. Aliquet rutrum a, augue vitae tincidunt ac egestas. Mauris nec fringilla diam eget fusce malesuada cum parturient. Nulla pretium purus odio odio.",
image: 'https://picsum.photos/id/10/400/800',
links: [],
location: "Amsterdam, NL",
type: 2,
website: "https://b.tc/conference/amsterdam"
},
{
title: "Luganos Plan ₿",
starts_at: "2022-10-28T21:00:00.000Z",
ends_at: "2022-11-04T22:00:00.000Z",
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Diam morbi pellentesque velit congue. Aliquet rutrum a, augue vitae tincidunt ac egestas. Mauris nec fringilla diam eget fusce malesuada cum parturient. Nulla pretium purus odio odio.",
image: 'https://picsum.photos/id/10/400/800',
links: [],
location: "Lugano, CH",
type: 3,
website: "https://planb.lugano.ch/"
},
{
title: "Adopting Bitcoin 22",
starts_at: "2022-11-15T21:00:00.000Z",
ends_at: "2022-11-17T22:00:00.000Z",
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Diam morbi pellentesque velit congue. Aliquet rutrum a, augue vitae tincidunt ac egestas. Mauris nec fringilla diam eget fusce malesuada cum parturient. Nulla pretium purus odio odio.",
image: 'https://picsum.photos/id/10/400/800',
links: [],
location: "El Salvador",
type: 2,
website: "https://adoptingbitcoin.org/2022/"
},
{
title: "PlebTLV",
starts_at: "2022-10-23T21:00:00.000Z",
ends_at: "2022-10-23T22:00:00.000Z",
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Diam morbi pellentesque velit congue. Aliquet rutrum a, augue vitae tincidunt ac egestas. Mauris nec fringilla diam eget fusce malesuada cum parturient. Nulla pretium purus odio odio.",
image: 'https://picsum.photos/id/10/400/800',
links: [],
location: "Tel Aviv",
type: 2,
website: "https://plebtlv.com/"
},
{
title: "Bitcoin Designathon",
starts_at: "2022-10-12T21:00:00.000Z",
ends_at: "2022-10-16T22:00:00.000Z",
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Diam morbi pellentesque velit congue. Aliquet rutrum a, augue vitae tincidunt ac egestas. Mauris nec fringilla diam eget fusce malesuada cum parturient. Nulla pretium purus odio odio.",
image: 'https://picsum.photos/id/10/400/800',
links: [],
location: "Online",
type: 2,
website: "https://bitcoin.design"
},
],
judges: [
{
name: "Roy Sheinfeld",
avatar: "https://s3-alpha-sig.figma.com/img/5e65/c22c/673b8f74ac43f024b036dbc4e6479e0d?Expires=1662940800&Signature=GR54s7FBcLGcPTVclWdmPjzU92tyrYpdUbbDUYKMUkdQbxq2yQlUhZ-AOLDHhOPY4P2G3aW2yT16b1AUbC8RBx1boH25MSrH-jpn6X57IJA-4ZeHP8zCo-yjTLpb8Gn~vudIi9rPfbwJ34stp-VeOAYMuOHlah3YO-B4MBsBv-NqhP7BMY4zz9vGdBLZhOjYQYdLZ2494Ae6L5FpD1ah3WD3U5qUN9dDvYvAtqYfhQeBOnsG6PfYoq8LouCuERC4S26BeooPg8UdGUCf324-SjEihCoL8mQFq80PSsaAZl5~EBOKRUx14FOprizMusaYN0K06E~fjDIDbM2Rmc9Xjg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA",
company: "Breez",
twitter: "@therealkingonly"
},
{
name: "John Carvalho",
avatar: "https://s3-alpha-sig.figma.com/img/5e65/c22c/673b8f74ac43f024b036dbc4e6479e0d?Expires=1662940800&Signature=GR54s7FBcLGcPTVclWdmPjzU92tyrYpdUbbDUYKMUkdQbxq2yQlUhZ-AOLDHhOPY4P2G3aW2yT16b1AUbC8RBx1boH25MSrH-jpn6X57IJA-4ZeHP8zCo-yjTLpb8Gn~vudIi9rPfbwJ34stp-VeOAYMuOHlah3YO-B4MBsBv-NqhP7BMY4zz9vGdBLZhOjYQYdLZ2494Ae6L5FpD1ah3WD3U5qUN9dDvYvAtqYfhQeBOnsG6PfYoq8LouCuERC4S26BeooPg8UdGUCf324-SjEihCoL8mQFq80PSsaAZl5~EBOKRUx14FOprizMusaYN0K06E~fjDIDbM2Rmc9Xjg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA",
company: "Synonym",
twitter: "@BitcoinErrorLog"
},
{
name: "Nifty Nei",
avatar: "https://s3-alpha-sig.figma.com/img/5e65/c22c/673b8f74ac43f024b036dbc4e6479e0d?Expires=1662940800&Signature=GR54s7FBcLGcPTVclWdmPjzU92tyrYpdUbbDUYKMUkdQbxq2yQlUhZ-AOLDHhOPY4P2G3aW2yT16b1AUbC8RBx1boH25MSrH-jpn6X57IJA-4ZeHP8zCo-yjTLpb8Gn~vudIi9rPfbwJ34stp-VeOAYMuOHlah3YO-B4MBsBv-NqhP7BMY4zz9vGdBLZhOjYQYdLZ2494Ae6L5FpD1ah3WD3U5qUN9dDvYvAtqYfhQeBOnsG6PfYoq8LouCuERC4S26BeooPg8UdGUCf324-SjEihCoL8mQFq80PSsaAZl5~EBOKRUx14FOprizMusaYN0K06E~fjDIDbM2Rmc9Xjg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA",
company: "Blockstream",
twitter: "@niftynei"
},
{
name: "Oleg Mikhalsky",
avatar: "https://s3-alpha-sig.figma.com/img/5e65/c22c/673b8f74ac43f024b036dbc4e6479e0d?Expires=1662940800&Signature=GR54s7FBcLGcPTVclWdmPjzU92tyrYpdUbbDUYKMUkdQbxq2yQlUhZ-AOLDHhOPY4P2G3aW2yT16b1AUbC8RBx1boH25MSrH-jpn6X57IJA-4ZeHP8zCo-yjTLpb8Gn~vudIi9rPfbwJ34stp-VeOAYMuOHlah3YO-B4MBsBv-NqhP7BMY4zz9vGdBLZhOjYQYdLZ2494Ae6L5FpD1ah3WD3U5qUN9dDvYvAtqYfhQeBOnsG6PfYoq8LouCuERC4S26BeooPg8UdGUCf324-SjEihCoL8mQFq80PSsaAZl5~EBOKRUx14FOprizMusaYN0K06E~fjDIDbM2Rmc9Xjg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA",
company: "Fulgur Ventures",
twitter: "@olegmikh1"
},
{
name: "Alyse Kileen",
avatar: "https://s3-alpha-sig.figma.com/img/5e65/c22c/673b8f74ac43f024b036dbc4e6479e0d?Expires=1662940800&Signature=GR54s7FBcLGcPTVclWdmPjzU92tyrYpdUbbDUYKMUkdQbxq2yQlUhZ-AOLDHhOPY4P2G3aW2yT16b1AUbC8RBx1boH25MSrH-jpn6X57IJA-4ZeHP8zCo-yjTLpb8Gn~vudIi9rPfbwJ34stp-VeOAYMuOHlah3YO-B4MBsBv-NqhP7BMY4zz9vGdBLZhOjYQYdLZ2494Ae6L5FpD1ah3WD3U5qUN9dDvYvAtqYfhQeBOnsG6PfYoq8LouCuERC4S26BeooPg8UdGUCf324-SjEihCoL8mQFq80PSsaAZl5~EBOKRUx14FOprizMusaYN0K06E~fjDIDbM2Rmc9Xjg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA",
company: "Stillmark VC",
twitter: "@AlyseKilleen"
},
{
name: "Johns Beharry",
avatar: "https://s3-alpha-sig.figma.com/img/5e65/c22c/673b8f74ac43f024b036dbc4e6479e0d?Expires=1662940800&Signature=GR54s7FBcLGcPTVclWdmPjzU92tyrYpdUbbDUYKMUkdQbxq2yQlUhZ-AOLDHhOPY4P2G3aW2yT16b1AUbC8RBx1boH25MSrH-jpn6X57IJA-4ZeHP8zCo-yjTLpb8Gn~vudIi9rPfbwJ34stp-VeOAYMuOHlah3YO-B4MBsBv-NqhP7BMY4zz9vGdBLZhOjYQYdLZ2494Ae6L5FpD1ah3WD3U5qUN9dDvYvAtqYfhQeBOnsG6PfYoq8LouCuERC4S26BeooPg8UdGUCf324-SjEihCoL8mQFq80PSsaAZl5~EBOKRUx14FOprizMusaYN0K06E~fjDIDbM2Rmc9Xjg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA",
company: "Peak Shift",
twitter: "@johnsBeharry"
},
{
name: "Ben Price",
avatar: "https://s3-alpha-sig.figma.com/img/5e65/c22c/673b8f74ac43f024b036dbc4e6479e0d?Expires=1662940800&Signature=GR54s7FBcLGcPTVclWdmPjzU92tyrYpdUbbDUYKMUkdQbxq2yQlUhZ-AOLDHhOPY4P2G3aW2yT16b1AUbC8RBx1boH25MSrH-jpn6X57IJA-4ZeHP8zCo-yjTLpb8Gn~vudIi9rPfbwJ34stp-VeOAYMuOHlah3YO-B4MBsBv-NqhP7BMY4zz9vGdBLZhOjYQYdLZ2494Ae6L5FpD1ah3WD3U5qUN9dDvYvAtqYfhQeBOnsG6PfYoq8LouCuERC4S26BeooPg8UdGUCf324-SjEihCoL8mQFq80PSsaAZl5~EBOKRUx14FOprizMusaYN0K06E~fjDIDbM2Rmc9Xjg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA",
company: "The Bitcoin Company",
twitter: "@abitcoinperson"
},
],
faqs: [
{
question: "When does the tournament start and end?",
answer:
`The tournament starts when team and project registrations open on 12th October. The tournament will finish with submissions closing on 30th November, 2022. Judges will then score projects and announce the winners on the 12th December.`
},
{
question: "When and how do we register our projects?",
answer:
`Makers can register their projects anytime between 12th October - 30th November. If a project is added on the tournament page, it is automatically registered and it will be judged at the end of the tournament.`
},
{
question: "How will projects be judged?",
answer:
`Projects will be judged and scored on the following criteria:
**1). 🎯 Value Proposition**
Does the project have a product market fit? Does it provide value to the bitcoin ecosystem and beyond?
**2). 🚨 Innovation**
Is it something we've seen before or does it bring something new and exciting to bitcoin and beyond?
**3). 👁️ Transparency (#BuildInPublic)**
Encouraging makers to #BuildInPublic. Has the projects team been transparent throughout their product design and development journey?
**4). ✅ Execution**
Makers should focus on attention to detail. How well has the project been executed?
**5). 🍒 UIUX Design**
Design can separate the good from the bad. Taking into account both UI and UX, how well has the application or feature been designed?
**6). 🔥 Je ne sais quoi**
Does the project have that extra level of pizazz or coolness? Does it raise the bar?`
},
{
question: "Can I submit a project that I hacked on during another event?",
answer:
`Makers can submit their projects from other hackathons, events, and meetups that are registered as events within The Long Night tournament. This allows makers to take advantage of IRL + online meetups, workshops, hackerspaces, inspirational weekend events, and more.`
},
{
question: "Can I submit multiple projects?",
answer:
`Yes, makers can submit multiple projects. However we encourage makers to focus on quality rather than quantity.`
},
{
question: "How can I find other makers or projects to team up with?",
answer:
`You can see a list of makers who are open to connect in the tournaments Makers tab. You can also search for projects that are looking to recruit members.`
},
{
question: "This is my first time hacking on bitcoin, is there any help?",
answer:
`We collected some awesome design, development, and project management resources here to get you up and running. You can also watch workshops and tutorials from BOLT🔩FUNs previous ShockTheWeb⚡hackathons here.`
},
{
question: "Not sure what to hack on?",
answer:
`Not sure where to get started? Need an idea to hack on? Not to worry, weve collected a list of great project ideas for you to look at here.`
},
{
question: "How can I #BuildInPublic?",
answer:
`Using BOLT🔩FUN Stories ✍️, makers can transparently document their projects design, development, and management processes. This will help other makers learn from one another, decreasing essential onboarding and learning time, whilst inspiring more great bitcoin apps to be built and innovated on. To see an example of this type of transparent reporting, check out this story here.`
},
],
}
module.exports = { tournament };

View File

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

View File

@@ -0,0 +1,4 @@
<svg width="175" height="48" viewBox="0 0 175 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M25.5464 20.1655C24.176 20.1655 23.0942 21.3435 23.0942 22.81C23.0942 24.2765 24.2001 25.4545 25.5464 25.4545C26.9167 25.4545 27.9985 24.2765 27.9985 22.81C27.9985 21.3435 26.8927 20.1655 25.5464 20.1655ZM16.7715 20.1655C15.4012 20.1655 14.3193 21.3435 14.3193 22.81C14.3193 24.2765 15.4252 25.4545 16.7715 25.4545C18.1418 25.4545 19.2236 24.2765 19.2236 22.81C19.2477 21.3435 18.1418 20.1655 16.7715 20.1655Z" fill="white"/>
<path d="M37.2302 0.163574H5.01558C2.29897 0.163574 0.0872192 2.37532 0.0872192 5.09193V37.3065C0.0872192 40.0231 2.29897 42.2349 5.01558 42.2349H32.2778L31.0036 37.8354L34.0809 40.6722L36.9898 43.3408L42.1826 47.8364V5.09193C42.1586 2.37532 39.9468 0.163574 37.2302 0.163574ZM27.9505 31.2964C27.9505 31.2964 27.085 30.2626 26.3638 29.3731C29.5131 28.4836 30.7152 26.5363 30.7152 26.5363C29.7295 27.1854 28.7919 27.6422 27.9505 27.9547C26.7484 28.4595 25.5945 28.7721 24.4646 28.9884C22.1566 29.4212 20.0411 29.301 18.238 28.9644C16.8677 28.7 15.6897 28.3393 14.704 27.9306C14.1511 27.7143 13.55 27.4498 12.949 27.1133C12.8769 27.0652 12.8048 27.0411 12.7327 26.9931C12.6846 26.969 12.6605 26.945 12.6365 26.945C12.2038 26.7046 11.9634 26.5363 11.9634 26.5363C11.9634 26.5363 13.1173 28.4355 16.1705 29.3491C15.4493 30.2626 14.5598 31.3204 14.5598 31.3204C9.24675 31.1521 7.22733 27.6902 7.22733 27.6902C7.22733 20.0212 10.6892 13.7947 10.6892 13.7947C14.1511 11.2223 17.4206 11.2944 17.4206 11.2944L17.661 11.5829C13.3337 12.809 11.3623 14.7082 11.3623 14.7082C11.3623 14.7082 11.8912 14.4197 12.7807 14.0351C15.3531 12.9052 17.3966 12.6167 18.238 12.5205C18.3822 12.4965 18.5024 12.4724 18.6467 12.4724C20.1132 12.2801 21.772 12.232 23.5029 12.4244C25.7868 12.6888 28.239 13.3619 30.7392 14.7082C30.7392 14.7082 28.84 12.9052 24.753 11.6791L25.0896 11.2944C25.0896 11.2944 28.3832 11.2223 31.821 13.7947C31.821 13.7947 35.2829 20.0212 35.2829 27.6902C35.2829 27.6662 33.2635 31.1281 27.9505 31.2964ZM64.252 10.453H56.2705V19.4202L61.5835 24.2043V15.5016H64.4203C66.2234 15.5016 67.1129 16.367 67.1129 17.7614V24.4207C67.1129 25.8151 66.2714 26.7526 64.4203 26.7526H56.2465V31.8252H64.228C68.5072 31.8493 72.522 29.7097 72.522 24.8053V17.6412C72.5461 12.6407 68.5313 10.453 64.252 10.453V10.453ZM106.083 24.8053V17.4489C106.083 14.8044 110.843 14.2034 112.285 16.8479L116.685 15.0688C114.954 11.2704 111.805 10.1645 109.184 10.1645C104.905 10.1645 100.674 12.6407 100.674 17.4489V24.8053C100.674 29.6616 104.905 32.0897 109.088 32.0897C111.781 32.0897 115.002 30.7675 116.781 27.3056L112.069 25.1419C110.915 28.0989 106.083 27.3777 106.083 24.8053V24.8053ZM91.5383 18.4586C89.8795 18.098 88.7736 17.497 88.7015 16.4632C88.7976 13.987 92.6201 13.8908 94.8559 16.2709L98.3899 13.5543C96.1782 10.8617 93.6779 10.1405 91.1056 10.1405C87.1869 10.1405 83.3885 12.3522 83.3885 16.5353C83.3885 20.5982 86.5138 22.7859 89.9516 23.3148C91.7066 23.5552 93.6539 24.2524 93.6058 25.4544C93.4616 27.7383 88.7496 27.6181 86.6099 25.0217L83.1962 28.2191C85.1915 30.7915 87.9081 32.0897 90.4565 32.0897C94.3751 32.0897 98.7265 29.8299 98.8948 25.6949C99.1352 20.478 95.3367 19.1558 91.5383 18.4586V18.4586ZM75.431 31.8012H80.8161V10.453H75.431V31.8012ZM166.233 10.453H158.251V19.4202L163.564 24.2043V15.5016H166.401C168.204 15.5016 169.094 16.367 169.094 17.7614V24.4207C169.094 25.8151 168.252 26.7526 166.401 26.7526H158.227V31.8252H166.233C170.512 31.8493 174.527 29.7097 174.527 24.8053V17.6412C174.527 12.6407 170.512 10.453 166.233 10.453V10.453ZM127.071 10.1645C122.647 10.1645 118.248 12.5686 118.248 17.497V24.7813C118.248 29.6616 122.671 32.1137 127.119 32.1137C131.542 32.1137 135.942 29.6616 135.942 24.7813V17.497C135.942 12.5926 131.494 10.1645 127.071 10.1645V10.1645ZM130.532 24.7813C130.532 26.3199 128.801 27.1133 127.095 27.1133C125.364 27.1133 123.633 26.368 123.633 24.7813V17.497C123.633 15.9343 125.316 15.0929 126.998 15.0929C128.753 15.0929 130.532 15.8381 130.532 17.497V24.7813ZM155.27 17.497C155.15 12.4965 151.736 10.4771 147.337 10.4771H138.802V31.8252H144.26V25.0458H145.221L150.174 31.8252H156.905L151.087 24.4928C153.66 23.6754 155.27 21.4396 155.27 17.497V17.497ZM147.433 20.3818H144.26V15.5016H147.433C150.823 15.5016 150.823 20.3818 147.433 20.3818Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

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

View File

@@ -25,6 +25,8 @@ const ExplorePage = Loadable(React.lazy(() => import( /* webpackChunkName: "expl
const HackathonsPage = Loadable(React.lazy(() => import( /* webpackChunkName: "hackathons_page" */ "./features/Hackathons/pages/HackathonsPage/HackathonsPage")))
const TournamentDetailsPage = Loadable(React.lazy(() => import( /* webpackChunkName: "hackathons_page" */ "./features/Tournaments/pages/TournamentDetailsPage/TournamentDetailsPage")))
const DonatePage = Loadable(React.lazy(() => import( /* webpackChunkName: "donate_page" */ "./features/Donations/pages/DonatePage/DonatePage")))
const LoginPage = Loadable(React.lazy(() => import( /* webpackChunkName: "login_page" */ "./features/Auth/pages/LoginPage/LoginPage")))
const LogoutPage = Loadable(React.lazy(() => import( /* webpackChunkName: "logout_page" */ "./features/Auth/pages/LogoutPage/LogoutPage")))
@@ -74,7 +76,6 @@ function App() {
}, []);
return <div id="app" className='w-full'>
<Helmet>
<title >makers.bolt.fun</title>
@@ -91,18 +92,21 @@ function App() {
</Helmet>
<Suspense fallback={<LoadingPage />}>
<Routes>
<Route path={PAGES_ROUTES.blog.createPost} element={<ProtectedRoute><CreatePostPage /></ProtectedRoute>} />
<Route path={PAGES_ROUTES.blog.writeStory} element={<ProtectedRoute><CreatePostPage initType="story" /></ProtectedRoute>} />
<Route element={<NavbarLayout />}>
<Route path={PAGES_ROUTES.projects.hottest} element={<HottestPage />} />
<Route path={PAGES_ROUTES.projects.byCategoryId} element={<CategoryPage />} />
<Route path={PAGES_ROUTES.projects.default} element={<ExplorePage />} />
<Route path={PAGES_ROUTES.blog.postById} element={<PostDetailsPage />} />
<Route path={PAGES_ROUTES.blog.storyById} element={<PostDetailsPage postType='story' />} />
<Route path={PAGES_ROUTES.blog.feed} element={<FeedPage />} />
<Route path={PAGES_ROUTES.blog.catchStory} element={<Navigate replace to={PAGES_ROUTES.blog.feed} />} />
<Route path={PAGES_ROUTES.hackathons.default} element={<HackathonsPage />} />
<Route path={PAGES_ROUTES.tournament.byId} element={<TournamentDetailsPage />} />
<Route path={PAGES_ROUTES.donate.default} element={<DonatePage />} />
<Route path={PAGES_ROUTES.profile.editProfile} element={<EditProfilePage />} />
@@ -111,7 +115,7 @@ function App() {
<Route path={PAGES_ROUTES.auth.login} element={<LoginPage />} />
<Route path={PAGES_ROUTES.auth.logout} element={<LogoutPage />} />
<Route path="/" element={<Navigate to={PAGES_ROUTES.projects.default} />} />
<Route path="/" element={<Navigate replace to={PAGES_ROUTES.blog.feed} />} />
</Route>
</Routes>

View File

@@ -1,5 +1,4 @@
import Button from 'src/Components/Button/Button'
import Logo from './fulgur_logo.svg'
import Lines from './lines-h.png'
@@ -8,7 +7,7 @@ export default function Fulgur() {
<div className='p-24 pt-48 bg-black rounded-16 relative'>
<img src={Lines} alt="" className='w-full absolute top-0 left-0' />
<div className="flex flex-col gap-24 relative">
<img src={Logo} alt="Fulgur Ventures Logo" className='w-10/12 max-w-[230px] ' />
<img src={'/assets/images/logos/fulgur_logo.svg'} alt="Fulgur Ventures Logo" className='w-10/12 max-w-[230px] ' />
<div>
<h3 className="text-white font-bolder text-body1">Turn your hackathon project into a startup</h3>
<p className="text-white font-medium mt-8 text-body4">Schedule an office hour with Fulgur Ventures</p>

View File

@@ -5,7 +5,7 @@ import { TailSpin } from 'react-loader-spinner';
type Props = {
color?: 'primary' | 'red' | 'white' | 'gray' | "black" | 'none',
variant?: 'fill' | 'outline'
variant?: 'fill' | 'outline' | 'text'
size?: 'sm' | 'md' | 'lg'
children: ReactNode;
href?: string;
@@ -40,7 +40,7 @@ const loadingColor: UnionToObjectKeys<Props, 'color'> = {
const btnStylesOutline: UnionToObjectKeys<Props, 'color'> = {
none: "",
primary: "text-primary-600",
gray: 'text-gray-700',
gray: 'text-gray-600',
white: 'text-gray-900',
black: 'text-black',
red: "text-red-500",
@@ -48,7 +48,8 @@ const btnStylesOutline: UnionToObjectKeys<Props, 'color'> = {
const baseBtnStyles: UnionToObjectKeys<Props, 'variant'> = {
fill: "transition-transform active:scale-95",
outline: "transition-transform bg-gray-900 bg-opacity-0 hover:bg-opacity-5 active:bg-opacity-10 border border-gray-200 active:scale-95 "
outline: "transition-transform bg-gray-900 bg-opacity-0 hover:bg-opacity-5 active:bg-opacity-10 border border-gray-200 active:scale-95 ",
text: "transition-transform active:scale-95 hover:bg-gray-100 active:bg-gray-50"
}
@@ -88,7 +89,7 @@ const Button = React.forwardRef<any, Props>(({ color = 'white',
${variant === 'fill' ? btnStylesFill[color] : btnStylesOutline[color]}
${isLoading && disableOnLoading && 'bg-opacity-70 pointer-events-none'}
${fullWidth && 'w-full'}
${disabled && 'opacity-40 pointer-events-none'}
${disabled && 'opacity-90 pointer-events-none'}
`;

View File

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

View File

@@ -0,0 +1,21 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import ErrorCard from './ErrorCard';
export default {
title: 'Shared/ErrorCard',
component: ErrorCard,
} as ComponentMeta<typeof ErrorCard>;
const Template: ComponentStory<typeof ErrorCard> = (args) => <ErrorCard {...args} />;
export const Default = Template.bind({});
Default.args = {
error: {
name: "Error Name",
message: "Error Message",
}
}

View File

@@ -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 (
<Card className="!border-red-500">
<div className="max-w-[60ch] mx-auto flex flex-col justify-center items-center gap-24">
<div className="text-h1 text-red-400 "><RiErrorWarningFill /></div>
<p className="text-body4 text-center text-gray-600">Ooops... <br /> Looks like something unexpected went wrong, please check your internet connection & try again.</p>
<div className="flex flex-col gap-12">
<Button onClick={() => resetErrorBoundary()} color='black'>Try Again</Button>
<Button href="/" color='gray' variant="text" size="sm" className="">Back to homepage</Button>
</div>
<div className="self-start">
<button className="text-gray-400 text-body5 underline" onClick={() => toggleDetails()}>{showDetails ? "Hide" : "Show"} error details</button>
{showDetails &&
<div className="mt-16 text-gray-600">
<p className="text-body3 text-gray-800">{error.name}</p>
<p className="text-body4">{error.message}</p>
<p className="text-body4 whitespace-pre-line">{error.stack}</p>
</div>
}
</div>
</div>
</Card>
)
}

View File

@@ -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 (
<div className="">
<Helmet>
<title>{`Something went wrong...`}</title>
</Helmet>
<div className="page-container">
<ErrorCard error={error} resetErrorBoundary={resetErrorBoundary} />
</div>
</div>
)
}

View File

@@ -9,6 +9,7 @@ interface Props {
className?: string
size?: "sm" | 'md' | 'lg'
variant?: 'blank' | 'fill'
isDisabled?: boolean
}
@@ -32,7 +33,8 @@ const IconButton = React.forwardRef<any, PropsWithChildren<Props>>(({
children,
onClick = () => { },
onKeyDown,
variant = 'blank'
variant = 'blank',
isDisabled,
}, ref) => {
if (href)
@@ -57,12 +59,16 @@ const IconButton = React.forwardRef<any, PropsWithChildren<Props>>(({
ref={ref}
type='button'
className={`
${className}
${sizeToPadding[size]}
${baseBtnStyles[variant]}
active:scale-95 rounded-full ${className}`}
active:scale-95 rounded-full
${isDisabled && "opacity-60"}
`}
style={{ lineHeight: 0 }}
onClick={onClick}
onKeyDown={onKeyDown}
disabled={isDisabled}
>
{children}
</button>

View File

@@ -1,12 +1,12 @@
import React, { PropsWithChildren } from 'react'
interface Props {
className?: string
}
export default function InfoCard(props: PropsWithChildren<Props>) {
return (
<div className="bg-gray-50 p-16 rounded-12 border border-gray-200 mt-24">
<div className={`bg-gray-50 p-16 rounded-12 border border-gray-200 ${props.className}`}>
<p className="text-body5 text-gray-600">
{props.children}
</p>

View File

@@ -1,171 +0,0 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { useWatch } from 'react-hook-form';
import { WrapForm } from 'src/utils/storybook/decorators';
import Autocomplete from './Autocomplete';
export default {
title: 'Shared/Inputs/AutoComplete',
component: Autocomplete,
decorators: [WrapForm({
defaultValues: {
autocomplete: null
}
})],
} as ComponentMeta<typeof Autocomplete>;
const options = [
{
"id": "20f0eb8d-c0cd-4e12-8a08-0d9846fc8704",
"name": "Nichole Bailey",
"username": "Cassie14",
"email": "Daisy_Auer50@hotmail.com",
"address": {
"street": "Anastasia Tunnel",
"suite": 95587,
"city": "Port Casperview",
"zipcode": "04167-6996",
"geo": {
"lat": "-73.4727",
"lng": "-142.9435"
}
},
"phone": "324-615-9195 x5902",
"website": "ron.net",
"company": {
"name": "Roberts, Tremblay and Christiansen",
"catchPhrase": "Vision-oriented actuating access",
"bs": "bricks-and-clicks strategize portals"
}
},
{
"id": "62b70f76-85ba-4241-9ffd-07582008c497",
"name": "Robert Blick",
"username": "Madilyn93",
"email": "Ronaldo82@gmail.com",
"address": {
"street": "Charlie Plain",
"suite": 83070,
"city": "Lake Bonitaland",
"zipcode": "01109",
"geo": {
"lat": "50.0971",
"lng": "-2.3057"
}
},
"phone": "1-541-367-2047 x9006",
"website": "jovani.com",
"company": {
"name": "Parisian - Kling",
"catchPhrase": "Multi-tiered tertiary toolset",
"bs": "plug-and-play benchmark content"
}
},
{
"id": "d02f74d9-bf99-4e41-b678-15e903abc1b3",
"name": "Eli O'Kon",
"username": "Rosario.Davis",
"email": "Mckayla59@hotmail.com",
"address": {
"street": "Wilford Drive",
"suite": 69742,
"city": "North Dianna",
"zipcode": "80620",
"geo": {
"lat": "-61.4191",
"lng": "126.7878"
}
},
"phone": "(339) 709-4080",
"website": "clay.name",
"company": {
"name": "Gerlach - Metz",
"catchPhrase": "Pre-emptive user-facing service-desk",
"bs": "frictionless monetize markets"
}
},
{
"id": "21077fa6-6a53-4b84-8407-6cd949718945",
"name": "Marilie Feil",
"username": "Antwon.Carter92",
"email": "Demario.Hyatt20@yahoo.com",
"address": {
"street": "Kenton Spurs",
"suite": 20079,
"city": "Beahanberg",
"zipcode": "79385",
"geo": {
"lat": "-70.7199",
"lng": "4.6977"
}
},
"phone": "608.750.4947",
"website": "jacynthe.org",
"company": {
"name": "Kuhn and Sons",
"catchPhrase": "Total eco-centric matrices",
"bs": "out-of-the-box target communities"
}
},
{
"id": "e07cf1b4-ff43-4c4a-a670-fd7417d6bbaf",
"name": "Ella Pagac",
"username": "Damien.Jaskolski",
"email": "Delmer1@gmail.com",
"address": {
"street": "VonRueden Shoals",
"suite": 14035,
"city": "Starkmouth",
"zipcode": "72448-1915",
"geo": {
"lat": "55.2157",
"lng": "98.0822"
}
},
"phone": "(165) 247-5332 x71067",
"website": "chad.info",
"company": {
"name": "Nicolas, Doyle and Rempel",
"catchPhrase": "Adaptive real-time strategy",
"bs": "innovative whiteboard supply-chains"
}
}
]
const Template: ComponentStory<typeof Autocomplete> = (args) => {
const value = useWatch({ name: 'autocomplete' })
console.log(value);
return <Autocomplete
options={options}
labelField='name'
valueField='name'
{...args as any}
/>
}
export const Default = Template.bind({});
Default.args = {
onChange: console.log
}
export const Lodaing = Template.bind({});
Lodaing.args = {
isLoading: true
}
export const Clearable = Template.bind({});
Clearable.args = {
isClearable: true
}
export const MultipleAllowed = Template.bind({});
MultipleAllowed.args = {
isMulti: true
}

View File

@@ -1,95 +0,0 @@
import { useMemo } from "react";
import Select, { StylesConfig } from "react-select";
import { ControlledStateHandler } from "src/utils/interfaces";
type Props<T extends object | string, IsMulti extends boolean = false> = {
options: T[];
labelField?: keyof T
valueField?: keyof T
placeholder?: string
disabled?: boolean
isLoading?: boolean;
isClearable?: boolean;
control?: any,
name?: string,
className?: string,
onBlur?: () => void;
size?: 'sm' | 'md' | 'lg'
} & ControlledStateHandler<T, IsMulti>
export default function AutoComplete<T extends object, IsMulti extends boolean>({
options,
labelField,
valueField,
placeholder = "Select Option...",
isMulti,
isClearable,
disabled,
className,
value,
onChange,
onBlur,
size = 'md',
...props
}: Props<T, IsMulti>) {
const colourStyles: StylesConfig = useMemo(() => ({
control: (styles, state) => ({
...styles,
padding: size === 'md' ? '1px 4px' : '8px 12px',
borderRadius: size === 'md' ? 8 : 12,
}),
indicatorSeparator: (styles, state) => ({
...styles,
display: "none"
}),
input: (styles, state) => ({
...styles,
" input": {
boxShadow: 'none !important'
},
}),
}), [size])
return (
<div className='w-full'>
<Select
options={options}
placeholder={placeholder}
className={className}
isMulti={isMulti}
isClearable={isClearable}
isLoading={props.isLoading}
getOptionLabel={o => o[labelField]}
getOptionValue={o => o[valueField]}
value={value as any}
onChange={v => onChange?.(v as any)}
onBlur={onBlur}
styles={colourStyles}
theme={(theme) => ({
...theme,
borderRadius: 8,
colors: {
...theme.colors,
primary: 'var(--primary)',
},
})}
/>
</div>
);
}

View File

@@ -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 (
<div
style={{
height: height + "px",
}}
>
<FileDrop
onDrop={(files) => 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}
</FileDrop>
<input
onChange={(e) => onAddFiles(e.target.files)}
ref={fileInputRef}
type="file"
className="hidden"
multiple={multiple}
accept={allowedType}
/>
</div>
);
}

View File

@@ -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<typeof FilesInput>;
const Template: ComponentStory<typeof FilesInput> = (args) => <FilesInput {...args} />
export const DefaultButton = Template.bind({});
DefaultButton.args = {
}
export const CustomizedButton = Template.bind({});
CustomizedButton.args = {
multiple: true,
uploadBtn: <Button color='primary'><span className="align-middle">Drop Images</span> <BsImages className='ml-12 scale-125' /></Button>
}
const DropTemplate: ComponentStory<typeof FileDropInput> = (args) => <div className="max-w-[500px]"><FileDropInput {...args as any} /></div>
export const DropZoneInput = DropTemplate.bind({});
DropZoneInput.args = {
onChange: console.log,
}

View File

@@ -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<typeof getFileType>
}
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 (
<div className="bg-gray-100 rounded-8 p-12 shrink-0 flex gap-4 overflow-hidden">
<div className="w-[100px]">
<p className="text-body6 overflow-hidden overflow-ellipsis whitespace-nowrap">
{file.name}
</p>
<a
href={file.src}
target='_blank'
rel="noreferrer"
>
{
file.type === 'image' && <img src={file.src} alt={file.name} className="p-4 w-3/4 mx-auto max-h-full object-contain" />
}
</a>
</div>
<div className="w-32 shrink-0 self-start" >
<IconButton size="sm" className="hover:bg-gray-500" onClick={onRemove}>
<MdClose />
</IconButton>
</div>
</div>
)
}

View File

@@ -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<Props, 'allowedType'> = {
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 (
<DropInput
height={height}
emptyContent={defaultEmptyContent}
draggingContent={defaultDraggingContent}
hasFilesContent={defaultHasFilesContent}
value={value}
onChange={onChange}
multiple={multiple}
allowedType={fileAccept[allowedType]}
classes={{
base: baseClasses,
idle: idleClasses,
dragging: draggingClasses
}}
/>
)
}
const defaultEmptyContent = (
<>
<div>
<FaImage className="scale-150 mr-8 text-gray-400" />{" "}
<span className="align-middle">Drop your files here</span>
</div>
<p className="mt-4">
or <button className="hover:underline font-bold">Click to Upload</button>{" "}
</p>
</>
);
const defaultDraggingContent = <p className="font-bold text-body2">Drop your files here </p>;
const defaultHasFilesContent = (
<p className="font-bolder">Files Uploaded Successfully!!</p>
);

View File

@@ -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<Props, 'allowedType'> = {
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<any, Props>(({
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<HTMLInputElement>) => {
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 })
:
<Button type='button' onClick={handleClick} ><span className="align-middle">{uploadText}</span> <FaImage className="ml-12 scale-125" /></Button>
return (
<>
<FilesThumbnails files={value} onRemove={handleRemove} />
{
canUploadMore &&
<>
{uploadBtn}
<input
ref={ref}
type="file"
onBlur={onBlur}
style={{ display: 'none' }}
multiple={multiple}
accept={fileAccept[allowedType]}
onChange={handleChange} />
</>
}
</>
)
})
export default FilesInput;

View File

@@ -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 (
<div className="flex gap-12 mb-12">
{
filesConverted.map((file, idx) => <FileThumbnail
key={idx}
file={file}
onRemove={() => onRemove?.(idx)} />)
}
</div>
)
}

View File

@@ -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<typeof AvatarInput>;
const Template: ComponentStory<typeof AvatarInput> = (args, context) => {
return <AvatarInput {...context.controller} {...args} />
}
export const Default = Template.bind({});
Default.args = {
}

View File

@@ -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<typeof SingleImageUploadInput>['value']
interface Props {
width?: number;
isRemovable?: boolean
value: Value;
onChange: (new_value: Nullable<Value>) => void
}
export default function AvatarInput(props: Props) {
const dropAreaRef = useRef<HTMLDivElement>(null!)
const isDragging = useIsDraggingOnElement({ ref: dropAreaRef });
return (
<div
style={{
width: props.width ?? 120,
}}
ref={dropAreaRef}
className='aspect-square rounded-full outline outline-2 outline-gray-200 overflow-hidden cursor-pointer '
>
<SingleImageUploadInput
value={props.value}
onChange={props.onChange}
wrapperClass='rounded-full bg-white h-full'
render={({ img, isUploading, isDraggingOnWindow }) =>
<div className="w-full h-full rounded-full relative group">
{!img &&
<div className='w-full h-full rounded-full bg-white hover:bg-gray-100 flex flex-col justify-center items-center'>
<p className="text-center text-gray-400 text-body2 mb-8"><FiCamera /></p>
<div className={`text-gray-400 text-center text-body5`}>
Add Image
</div>
</div>}
{img &&
<>
<img src={img.url} className='w-full h-full object-cover rounded-full' alt="" />
{!isUploading &&
<div className="flex flex-wrap gap-16 absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 ">
<button type='button' className='py-8 px-12 rounded-full bg-black bg-opacity-70 opacity-0 group-hover:opacity-100 hover:bg-opacity-90 transition-opacity text-white text-body1'>
<CgArrowsExchangeV />
</button>
{props.isRemovable && <button type='button' className='py-8 px-12 rounded-full bg-black bg-opacity-70 opacity-0 group-hover:opacity-100 hover:bg-opacity-90 transition-opacity text-white text-body1' onClick={(e) => { e.stopPropagation(); props.onChange(null) }}>
<IoMdClose />
</button>}
</div>
}
</>}
{isUploading &&
<div
className="absolute inset-0 bg-gray-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold transition-transform"
>
<RotatingLines
strokeColor="#fff"
strokeWidth="3"
animationDuration="0.75"
width="48"
visible={true}
/>
</div>
}
{isDraggingOnWindow &&
<div
className={
`absolute inset-0 ${isDragging ? 'bg-primary-600' : 'bg-primary-400'} bg-opacity-80 flex flex-col justify-center items-center text-white font-bold transition-transform`
}
>
<motion.div
initial={{ y: 0 }}
animate={
isDragging ? {
y: 5,
transition: {
duration: .4,
repeat: Infinity,
repeatType: 'mirror',
}
} : {
y: 0
}}
className='text-center text-body4'
>
<AiOutlineCloudUpload className="scale-150 text-body2" />
</motion.div>
</div>
}
</div>}
/>
</div>
)
}

View File

@@ -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<typeof CoverImageInput>;
const Template: ComponentStory<typeof CoverImageInput> = (args, context) => {
return <div className="aspect-[5/2] md:aspect-[4/1] rounded-t-16 overflow-hidden">
<CoverImageInput {...context.controller} {...args} />
</div>
}
export const Default = Template.bind({});
Default.args = {
}

View File

@@ -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<typeof SingleImageUploadInput>['value']
interface Props {
value: Value;
rounded?: string;
onChange: (new_value: Nullable<Value>) => void
}
export default function CoverImageInput(props: Props) {
const dropAreaRef = useRef<HTMLDivElement>(null!)
const isDragging = useIsDraggingOnElement({ ref: dropAreaRef });
return (
<div
className='overflow-hidden cursor-pointer w-full h-full'
ref={dropAreaRef}
>
<SingleImageUploadInput
value={props.value}
onChange={props.onChange}
wrapperClass='h-full'
render={({ img, isUploading, isDraggingOnWindow }) =>
<div className="w-full h-full group relative ">
{!img && <div className='w-full h-full flex flex-col justify-center items-center bg-gray-500 outline outline-2 outline-gray-200'>
<p className="text-center text-gray-100 text-body1 md:text-h1 mb-8"><FaImage /></p>
<div className={`text-gray-100 text-center text-body4`}>
Drop a <span className="font-bold">COVER IMAGE</span> here or <br /> <span className="text-blue-300 underline">Click to browse</span>
</div>
</div>}
{img && <>
<img src={img.url} className={`w-full h-full ${props.rounded ?? 'rounded-12'} object-cover`} alt="" />
{!isUploading &&
<div className="flex flex-wrap gap-16 absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 ">
<button type='button' className='py-8 px-16 rounded-12 bg-black bg-opacity-70 opacity-0 group-hover:opacity-100 hover:bg-opacity-90 transition-opacity text-white text-h1'>
<CgArrowsExchangeV />
</button>
<button type='button' className='py-8 px-16 rounded-12 bg-black bg-opacity-70 opacity-0 group-hover:opacity-100 hover:bg-opacity-90 transition-opacity text-white text-h1' onClick={(e) => { e.stopPropagation(); props.onChange(null) }}>
<IoMdClose />
</button>
</div>
}
</>}
{isUploading &&
<div
className="absolute inset-0 bg-gray-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold transition-transform"
>
<RotatingLines
strokeColor="#fff"
strokeWidth="3"
animationDuration="0.75"
width="48"
visible={true}
/>
</div>
}
{isDraggingOnWindow &&
<div
className={
`absolute inset-0 ${isDragging ? 'bg-primary-600' : 'bg-primary-400'} bg-opacity-80 flex flex-col justify-center items-center text-white font-bold transition-transform`
}
>
<motion.div
initial={{ y: 0 }}
animate={
isDragging ? {
y: 5,
transition: {
duration: .4,
repeat: Infinity,
repeatType: 'mirror',
}
} : {
y: 0
}}
className='text-center text-body1'
>
<AiOutlineCloudUpload className="scale-150 text-h1 mb-16" />
<br />
Drop here to upload
</motion.div>
</div>
}
</div>}
/>
</div>
)
}

View File

@@ -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<typeof FileUploadInput>;
const Template: ComponentStory<typeof FileUploadInput> = (args) => <FileUploadInput {...args} />
export const DefaultButton = Template.bind({});
DefaultButton.args = {
}

View File

@@ -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 <Button {...props} color='primary'>
Upload Image <FaImage className="ml-8 scale-125 align-middle" />
</Button>
});
const DropZone = forwardRef<any, any>((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 <UploadDropZone
{...buttonProps}
ref={ref}
onDragOverClassName={styles.active}
extraProps={{ onClick: onZoneClick }}
className={`${styles.zone} border-2 w-full min-h-[200px] max-w-[600px] rounded-16 flex flex-col justify-center items-center text text-body3 border-dashed`}
>
<div className={`${styles.idle_content} text-gray-600`}>
Drop your <span className="font-bold uppercase">IMAGES</span> here or <button className="font-bold text-blue-400 underline">Click to browse</button>
</div>
<motion.div
animate={{
y: 5,
}}
transition={{
duration: .5,
repeat: Infinity,
repeatType: 'mirror'
}}
className={`${styles.active_content} text-white font-bold`}>
Drop it to upload <AiOutlineCloudUpload className="scale-150 text-body1 ml-16" />
</motion.div>
</UploadDropZone>
})
const DropZoneButton = asUploadButton(DropZone);
export default function FileUploadInput(props: Props) {
return (
<Uploady
multiple={true}
inputFieldName='file'
grouped={false}
listeners={{
[UPLOADER_EVENTS.ITEM_FINISH]: (item) => {
const { id, filename, variants } = item?.uploadResponse?.data?.result ?? {}
if (id) {
console.log(id, filename, variants);
}
}
}}
>
<DropZoneButton />
{/* <UploadBtn /> */}
<ImagePreviews />
</Uploady>
)
}

View File

@@ -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 (
<div className="grid grid-cols-[repeat(auto-fill,minmax(200px,1fr))] gap-16 mt-24">
<UploadPreview PreviewComponent={CustomImagePreview} rememberPreviousBatches />
</div>
)
}
function CustomImagePreview({ id, url }: PreviewComponentProps) {
const [progress, setProgress] = useState<number>(0);
const [itemState, setItemState] = useState<string>(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 <div className="aspect-video relative rounded-12 md:rounded-16 overflow-hidden border-2 border-gray-200">
<img src={url}
className={`
w-full h-full object-cover
${itemState === STATES.PROGRESS && 'opacity-50'}
`}
alt="" />
<div className="text-body5 absolute inset-0"
>
</div>
{itemState === STATES.PROGRESS &&
<div
className="absolute inset-0 bg-gray-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold transition-transform"
>
<RotatingLines
strokeColor="#fff"
strokeWidth="3"
animationDuration="0.75"
width="48"
visible={true}
/>
</div>}
{itemState === STATES.ERROR &&
<div className="absolute inset-0 bg-red-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold">
Failed...
</div>}
{itemState === STATES.CANCELLED &&
<div className="absolute inset-0 bg-red-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold">
Cancelled
</div>}
</div>;
};
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"
};

View File

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

View File

@@ -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 (
<UploadPreview PreviewComponent={CustomImagePreview} rememberPreviousBatches />
)
}
function CustomImagePreview({ id, url }: PreviewComponentProps) {
const [progress, setProgress] = useState<number>(0);
const [itemState, setItemState] = useState<string>(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 <ScreenShotsThumbnail
url={url}
isLoading={itemState === STATES.PROGRESS}
isError={itemState === STATES.ERROR}
onCancel={() => {
abortItem(id)
}}
/>
// return <div className="aspect-video relative rounded-12 md:rounded-16 overflow-hidden border-2 border-gray-200">
// <img src={url}
// className={`
// w-full h-full object-cover
// ${itemState === STATES.PROGRESS && 'opacity-50'}
// `}
// alt="" />
// <div className="text-body5 absolute inset-0"
// >
// </div>
// {itemState === STATES.PROGRESS &&
// <div
// className="absolute inset-0 bg-gray-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold transition-transform"
// >
// <RotatingLines
// strokeColor="#fff"
// strokeWidth="3"
// animationDuration="0.75"
// width="48"
// visible={true}
// />
// </div>}
// {itemState === STATES.ERROR &&
// <div className="absolute inset-0 bg-red-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold">
// Failed...
// </div>}
// {itemState === STATES.CANCELLED &&
// <div className="absolute inset-0 bg-red-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold">
// Cancelled
// </div>}
// </div>;
};
const STATES = {
PROGRESS: "PROGRESS",
DONE: "DONE",
CANCELLED: "CANCELLED",
ERROR: "ERROR"
};

View File

@@ -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 (
<div className={`
aspect-video relative rounded-16 md:rounded-14 overflow-hidden border-2 border-gray-200
${isEmpty && "border-dashed"}
`}>
{!isEmpty && <img src={url}
className={`
w-full h-full object-cover
${isLoading && 'opacity-50'}
`}
alt="" />}
<div className="text-body5 absolute inset-0"
>
</div>
{isLoading &&
<div
className="absolute inset-0 bg-gray-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold transition-transform"
>
<RotatingLines
strokeColor="#fff"
strokeWidth="3"
animationDuration="0.75"
width="48"
visible={true}
/>
</div>}
{isError &&
<div className="absolute inset-0 bg-red-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold">
Failed...
</div>}
{!isEmpty &&
<button className="absolute bg-gray-900 hover:bg-opacity-100 bg-opacity-60 text-white rounded-full w-32 h-32 top-8 right-8" onClick={() => onCancel?.()}><FaTimes /></button>
}
</div>
)
}

View File

@@ -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<ScreenshotType> }>({
logValues: true,
name: "screenshots",
defaultValues: {
screenshots: []
}
})]
} as ComponentMeta<typeof ScreenshotsInput>;
const Template: ComponentStory<typeof ScreenshotsInput> = (args, context) => {
return <ScreenshotsInput {...context.controller} {...args} />
}
export const Empty = Template.bind({});
Empty.args = {
}
export const WithValues = Template.bind({});
WithValues.decorators = [
WrapFormController<{ screenshots: Array<ScreenshotType> }>({
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<ScreenshotType> }>({
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 = {
}

View File

@@ -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 (
<Uploady
multiple={true}
inputFieldName='file'
grouped={false}
enhancer={mockSenderEnhancer}
listeners={{
[UPLOADER_EVENTS.BATCH_ADD]: (batch) => {
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))
}
}
}}
>
<div className="grid grid-cols-[repeat(auto-fit,minmax(200px,1fr))] gap-16 mt-24">
{canUploadMore && <DropZoneButton />}
{uploadedFiles.map(f => <ScreenshotThumbnail
key={f.id}
url={f.url}
onCancel={() => {
onChange(uploadedFiles.filter(file => file.id !== f.id))
}} />)}
<ImagePreviews />
{(placeholdersCount > 0) &&
Array(placeholdersCount).fill(0).map((_, idx) => <ScreenshotThumbnail key={idx} />)}
</div>
</Uploady>
)
}
const DropZone = forwardRef<any, any>((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 <UploadDropZone
{...buttonProps}
ref={ref}
onDragOverClassName={styles.active}
extraProps={{ onClick: onZoneClick }}
className={`${styles.zone} aspect-video relative rounded-16 md:rounded-14 overflow-hidden border-2 border-gray-200 flex flex-col justify-center items-center cursor-pointer border-dashed`}
>
<div className={styles.idle_content}>
<p className="text-center text-gray-400 text-body1 mb-8"><FiCamera /></p>
<div className={`text-gray-600 text-center text-body4`}>
<span className="text-blue-500 underline">Browse images</span> or <br /> <span className="text-blue-500">drop </span>
them here
</div>
</div>
<motion.div
animate={{
y: 5,
}}
transition={{
duration: .5,
repeat: Infinity,
repeatType: 'mirror'
}}
className={`${styles.active_content} text-white font-bold text-center`}>
Drop to upload <br /> <AiOutlineCloudUpload className="scale-150 text-body1 mt-16" />
</motion.div>
</UploadDropZone>
})
const DropZoneButton = asUploadButton(DropZone);

View File

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

View File

@@ -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 (
<UploadPreview PreviewComponent={CustomImagePreview} rememberPreviousBatches />
)
}
function CustomImagePreview({ id, url }: PreviewComponentProps) {
const [progress, setProgress] = useState<number>(0);
const [itemState, setItemState] = useState<string>(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 <ScreenShotsThumbnail
url={url}
isLoading={itemState === STATES.PROGRESS}
isError={itemState === STATES.ERROR}
onCancel={() => {
abortItem(id)
}}
/>
// return <div className="aspect-video relative rounded-12 md:rounded-16 overflow-hidden border-2 border-gray-200">
// <img src={url}
// className={`
// w-full h-full object-cover
// ${itemState === STATES.PROGRESS && 'opacity-50'}
// `}
// alt="" />
// <div className="text-body5 absolute inset-0"
// >
// </div>
// {itemState === STATES.PROGRESS &&
// <div
// className="absolute inset-0 bg-gray-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold transition-transform"
// >
// <RotatingLines
// strokeColor="#fff"
// strokeWidth="3"
// animationDuration="0.75"
// width="48"
// visible={true}
// />
// </div>}
// {itemState === STATES.ERROR &&
// <div className="absolute inset-0 bg-red-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold">
// Failed...
// </div>}
// {itemState === STATES.CANCELLED &&
// <div className="absolute inset-0 bg-red-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold">
// Cancelled
// </div>}
// </div>;
};
const STATES = {
PROGRESS: "PROGRESS",
DONE: "DONE",
CANCELLED: "CANCELLED",
ERROR: "ERROR"
};

View File

@@ -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 (
<div className={`
aspect-video relative rounded-16 md:rounded-14 overflow-hidden border-2 border-gray-200
${isEmpty && "border-dashed"}
`}>
{!isEmpty && <img src={url}
className={`
w-full h-full object-cover
${isLoading && 'opacity-50'}
`}
alt="" />}
<div className="text-body5 absolute inset-0"
>
</div>
{isLoading &&
<div
className="absolute inset-0 bg-gray-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold transition-transform"
>
<RotatingLines
strokeColor="#fff"
strokeWidth="3"
animationDuration="0.75"
width="48"
visible={true}
/>
</div>}
{isError &&
<div className="absolute inset-0 bg-red-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold">
Failed...
</div>}
{!isEmpty &&
<button className="absolute bg-gray-900 hover:bg-opacity-100 bg-opacity-60 text-white rounded-full w-32 h-32 top-8 right-8" onClick={() => onCancel?.()}><FaTimes /></button>
}
</div>
)
}

View File

@@ -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<typeof SingleImageUploadInput>;
const Template: ComponentStory<typeof SingleImageUploadInput> = (args, context) => {
return <SingleImageUploadInput {...context.controller} {...args} />
}

View File

@@ -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<ImageType | null>(null)
return (
<Uploady
accept="image/*"
inputFieldName='file'
grouped={false}
listeners={{
[UPLOADER_EVENTS.ITEM_START]: (item) => {
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, })
}
}
}}
>
<DropZoneButton
extraProps={{
renderProps: {
isUploading: !!currentlyUploadingItem,
img: currentlyUploadingItem ?? value ?? null,
render,
wrapperClass: props.wrapperClass
}
}
}
/>
</Uploady>
)
}
const DropZone = forwardRef<any, any>((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 <UploadDropZone
{...buttonProps}
ref={ref}
type='button'
onDragOverClassName={'drag-active'}
extraProps={{ onClick: onZoneClick }}
className={renderProps.wrapperClass}
>
{renderProps.render({
img: renderProps.img,
isUploading: renderProps.isUploading,
isDraggingOnWindow,
})}
</UploadDropZone>
})
const DropZoneButton = asUploadButton(DropZone);

View File

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

View File

@@ -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<typeof ThumbnailInput>;
const Template: ComponentStory<typeof ThumbnailInput> = (args, context) => {
return <ThumbnailInput {...context.controller} {...args} />
}
export const Default = Template.bind({});
Default.args = {
}

View File

@@ -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<typeof SingleImageUploadInput>['value']
interface Props {
width?: number
value: Value;
onChange: (new_value: Nullable<Value>) => void
}
export default function ThumbnailInput(props: Props) {
return (
<div
style={{
width: props.width ?? 120,
}}
className='aspect-square rounded-16 outline outline-2 outline-gray-200 overflow-hidden cursor-pointer bg-white hover:bg-gray-100'
>
<SingleImageUploadInput
value={props.value}
onChange={props.onChange}
wrapperClass='h-full'
render={({ img, isUploading }) => <div className="w-full h-full relative flex flex-col justify-center items-center">
{img && <img src={img.url} className='w-full h-full object-cover rounded-16' alt="" />}
{!img &&
<>
<p className="text-center text-gray-400 text-body2 mb-8"><FiCamera /></p>
<div className={`text-gray-400 text-center text-body5`}>
Add Image
</div>
</>}
{isUploading &&
<div
className="absolute inset-0 bg-gray-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold transition-transform"
>
<RotatingLines
strokeColor="#fff"
strokeWidth="3"
animationDuration="0.75"
width="48"
visible={true}
/>
</div>
}
</div>}
/>
</div>
)
}

View File

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

View File

@@ -0,0 +1,31 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { MdOutlineKingBed } from 'react-icons/md';
import BasicSelectInput from './BasicSelectInput';
export default {
title: 'Shared/Inputs/Select/Basic',
component: BasicSelectInput,
} as ComponentMeta<typeof BasicSelectInput>;
const Template: ComponentStory<typeof BasicSelectInput> = (args) => <BasicSelectInput {...args} />
export const Default = Template.bind({});
Default.args = {
// defaultValue: 4,
options: [
{ value: 1, label: "Option 1" },
{ value: 2, label: 'Option 2' },
{ value: 3, label: 'Option 3' },
{ value: 4, label: 'Option 4' },
],
onChange: (nv) => alert("New value is: " + nv),
labelField: 'label',
valueField: "value"
}

View File

@@ -0,0 +1,144 @@
import { useCallback } from "react";
import Select, { OnChangeValue, StylesConfig, components, OptionProps } from "react-select";
import { ControlledStateHandler } from "src/utils/interfaces";
type Props<T extends Record<string, any>, IsMulti extends boolean = boolean> = {
options: T[];
labelField: keyof T
valueField: keyof T
placeholder?: string
disabled?: boolean
isLoading?: boolean;
isClearable?: boolean;
control?: any,
name?: string,
className?: string,
renderOption?: (option: OptionProps<T>) => JSX.Element
} & ControlledStateHandler<T, IsMulti>
export const selectCustomStyle: StylesConfig = ({
control: (styles, state) => ({
...styles,
padding: '5px 12px',
borderRadius: 12,
// border: 'none',
// boxShadow: 'none',
":hover": {
cursor: "pointer"
},
":focus-within": {
'--tw-border-opacity': '1',
borderColor: 'rgb(179 160 255 / var(--tw-border-opacity))',
outlineColor: '#9E88FF',
'--tw-ring-offset-shadow': 'var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)',
'--tw-ring-shadow': 'var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color)',
boxShadow: 'var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000)',
'--tw-ring-color': 'rgb(179 160 255 / var(--tw-ring-opacity))',
'--tw-ring-opacity': '0.5'
}
}),
indicatorSeparator: (styles, state) => ({
...styles,
display: "none"
}),
input: (styles, state) => ({
...styles,
" input": {
boxShadow: 'none !important'
},
}),
menu: (styles, state) => ({
...styles,
padding: 8,
borderRadius: "16px !important"
}),
})
export default function BasicSelectInput<T extends Record<string, any>, IsMulti extends boolean>({
options,
labelField,
valueField,
placeholder = "Select Option...",
isMulti,
isClearable,
disabled,
className,
value,
onChange,
onBlur,
renderOption,
...props
}: Props<T, IsMulti>) {
return (
<div className='w-full'>
<Select
options={options}
placeholder={placeholder}
className={className}
isMulti={isMulti}
isClearable={isClearable}
isLoading={props.isLoading}
getOptionLabel={o => o[labelField]}
getOptionValue={o => o[valueField]}
value={value as any}
onChange={v => onChange?.(v as any)}
onBlur={onBlur}
components={{
Option: getOptionComponent(renderOption, labelField),
// ValueContainer: CustomValueContainer
}}
styles={selectCustomStyle as any}
theme={(theme) => ({
...theme,
borderRadius: 8,
colors: {
...theme.colors,
primary: 'var(--primary)',
},
})}
/>
</div>
);
}
function getOptionComponent<T extends Record<string, any>>(renderOption: Props<T>['renderOption'], labelField: Props<T>['labelField']) {
const _render = renderOption ?? ((option) => <div
className={`
flex gap-16 my-4 px-16 py-12 rounded-12 text-gray-800 cursor-pointer
${!(option.isSelected || option.isFocused) ?
"hover:bg-gray-50"
:
option.isSelected ? "bg-gray-100 text-gray-800" : "bg-gray-50"
}
`}>
{option.data[labelField]}
</div>)
return function OptionComponent(props: OptionProps<T>) {
return (
<components.Option {...props} className='!p-0 !bg-transparent hover:!bg-transparent'>
{_render(props)}
</components.Option>
);
};
}

View File

@@ -13,7 +13,9 @@ interface Option {
readonly description: string | null
}
type Tag = Omit<OfficialTagsQuery['officialTags'][number], 'id'>
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<Option, true>,) => {
onChange?.([...newValue.map(transformer.optionToTag)]);
onBlur?.();
}
const maxReached = value.length >= max;
const currentPlaceholder = maxReached ? '' : value.length > 0 ? "Add Another..." : placeholder;
const tagsOptions = !maxReached ? (officalTags.data?.officialTags ?? []).filter(t => !value.some((v) => v.title === t.title)).map(transformer.tagToOption) : [];
return (
<div className={`${classes?.container}`}>
<Select
isLoading={officalTags.loading}
options={tagsOptions}
isMulti
isOptionDisabled={() => maxReached}
placeholder={currentPlaceholder}
noOptionsMessage={() => {
return maxReached
? "You've reached the max number of tags."
: "No tags available";
}}
closeMenuOnSelect={false}
value={value.map(transformer.valueToOption)}
onChange={handleChange as any}
onBlur={onBlur}
components={{
Option: OptionComponent,
// ValueContainer: CustomValueContainer
}}
styles={colourStyles as any}
theme={(theme) => ({
...theme,
borderRadius: 8,
colors: {
...theme.colors,
primary: 'var(--primary)',
},
})}
/>
{/* <div className="flex mt-16 gap-8 flex-wrap">
{(value as Tag[]).map((tag, idx) => <Badge color="gray" key={tag.title} onRemove={() => handleRemove(idx)} >{tag.title}</Badge>)}
</div> */}
</div>
)
}
const transformer = {
valueToOption: (tag: Value): Option => ({ label: tag.title, value: tag.title, icon: null, description: null }),
tagToOption: (tag: Tag): Option => ({ label: tag.title, value: tag.title, icon: tag.icon, description: tag.description }),
optionToTag: (o: Option): Tag => ({ title: o.value, icon: o.icon, description: o.description, })
}
@@ -107,75 +178,3 @@ const colourStyles: StylesConfig = {
paddingRight: 0,
})
}
export default function TagsInput({
classes,
placeholder = 'Write some tags',
max = 5,
...props }: Props) {
const officalTags = useOfficialTagsQuery();
const { field: { value, onChange, onBlur } } = useController({
name: props.name ?? "tags",
control: props.control,
})
const handleChange = (newValue: OnChangeValue<Option, true>,) => {
onChange([...newValue.map(transformer.optionToTag)]);
onBlur();
}
const handleRemove = (idx: number) => {
onChange((value as Tag[]).filter((_, i) => idx !== i))
onBlur();
}
const maxReached = value.length >= max;
const currentPlaceholder = maxReached ? '' : value.length > 0 ? "Add Another..." : placeholder;
const tagsOptions = !maxReached ? (officalTags.data?.officialTags ?? []).filter(t => !value.some((v: Tag) => v.title === t.title)).map(transformer.tagToOption) : [];
return (
<div className={`${classes?.container}`}>
<Select
isLoading={officalTags.loading}
options={tagsOptions}
isMulti
isOptionDisabled={() => maxReached}
placeholder={currentPlaceholder}
noOptionsMessage={() => {
return maxReached
? "You've reached the max number of tags."
: "No tags available";
}}
closeMenuOnSelect={false}
value={value.map(transformer.tagToOption)}
onChange={handleChange as any}
onBlur={onBlur}
components={{
Option: OptionComponent,
// ValueContainer: CustomValueContainer
}}
styles={colourStyles as any}
theme={(theme) => ({
...theme,
borderRadius: 8,
colors: {
...theme.colors,
primary: 'var(--primary)',
},
})}
/>
{/* <div className="flex mt-16 gap-8 flex-wrap">
{(value as Tag[]).map((tag, idx) => <Badge color="gray" key={tag.title} onRemove={() => handleRemove(idx)} >{tag.title}</Badge>)}
</div> */}
</div>
)
}

View File

@@ -1,91 +0,0 @@
import React, { FormEvent, useState } from 'react'
import { ModalCard, modalCardVariants } from 'src/Components/Modals/ModalsContainer/ModalsContainer'
import { motion } from 'framer-motion'
import { IoClose } from 'react-icons/io5'
import Button from 'src/Components/Button/Button'
import { useAppDispatch } from 'src/utils/hooks'
import { PayloadAction } from '@reduxjs/toolkit'
interface Props extends ModalCard {
callbackAction: PayloadAction<{ src: string, alt?: string }>
}
export default function InsertImageModal({ onClose, direction, callbackAction, ...props }: Props) {
const [urlInput, setUrlInput] = useState("")
const [altInput, setAltInput] = useState("")
const dispatch = useAppDispatch();
const handleSubmit = (e: FormEvent) => {
e.preventDefault()
if (urlInput.length > 10) {
// onInsert({ src: urlInput, alt: altInput })
const action = Object.assign({}, callbackAction);
action.payload = { src: urlInput, alt: altInput }
dispatch(action)
onClose?.();
}
}
return (
<motion.div
custom={direction}
variants={modalCardVariants}
initial='initial'
animate="animate"
exit='exit'
className="modal-card max-w-[660px] p-24 rounded-xl relative"
>
<IoClose className='absolute text-body2 top-24 right-24 hover:cursor-pointer' onClick={onClose} />
<h2 className='text-h5 font-bold'>Add Image</h2>
<form onSubmit={handleSubmit}>
<div className="grid md:grid-cols-3 gap-16 mt-32">
<div className='md:col-span-2'>
<p className="text-body5">
Image URL
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
value={urlInput}
onChange={e => setUrlInput(e.target.value)}
placeholder='https://images.com/my-image'
/>
</div>
</div>
<div>
<p className="text-body5">
Alt Text
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
value={altInput}
onChange={e => setAltInput(e.target.value)}
placeholder=''
/>
</div>
</div>
</div>
<div className="mt-32 w-1/2 mx-auto aspect-video bg-gray-200 rounded-10">
{urlInput && <img
src={urlInput}
className='w-full h-full object-cover rounded-10'
alt={altInput}
/>}
</div>
<div className="flex gap-16 justify-end mt-32">
<Button onClick={onClose}>
Cancel
</Button>
<Button type='submit' color='primary' >
Add
</Button>
</div>
</form>
</motion.div>
)
}

View File

@@ -5,7 +5,7 @@ import InsertImageModal from './InsertImageModal';
import { ModalsDecorator } from 'src/utils/storybook/decorators';
export default {
title: 'Shared/Inputs/Text Editor/Insert Image Modal',
title: 'Shared/Inputs/Files Inputs/Image Modal',
component: InsertImageModal,
decorators: [ModalsDecorator]
@@ -14,4 +14,13 @@ export default {
const Template: ComponentStory<typeof InsertImageModal> = (args) => <InsertImageModal {...args} />;
export const Default = Template.bind({});
Default.args = {
callbackAction: {
type: "INSERT_IMAGE_IN_STORY",
payload: {
src: "",
alt: "",
}
}
}

View File

@@ -0,0 +1,176 @@
import React, { FormEvent, useRef, useState } from 'react'
import { ModalCard, modalCardVariants } from 'src/Components/Modals/ModalsContainer/ModalsContainer'
import { motion } from 'framer-motion'
import { IoClose } from 'react-icons/io5'
import Button from 'src/Components/Button/Button'
import { useAppDispatch, useIsDraggingOnElement } from 'src/utils/hooks'
import { PayloadAction } from '@reduxjs/toolkit'
import { RotatingLines } from 'react-loader-spinner'
import { FaExchangeAlt, FaImage } from 'react-icons/fa'
import SingleImageUploadInput, { ImageType } from 'src/Components/Inputs/FilesInputs/SingleImageUploadInput/SingleImageUploadInput'
import { AiOutlineCloudUpload } from 'react-icons/ai'
interface Props extends ModalCard {
callbackAction: PayloadAction<{ src: string, alt?: string }>
}
export default function InsertImageModal({ onClose, direction, callbackAction, ...props }: Props) {
const [uploadedImage, setUploadedImage] = useState<ImageType | null>(null)
const [altInput, setAltInput] = useState("")
const dispatch = useAppDispatch();
const dropAreaRef = useRef<HTMLDivElement>(null!)
const isDragging = useIsDraggingOnElement({ ref: dropAreaRef });
const handleSubmit = (e: FormEvent) => {
e.preventDefault()
console.log(uploadedImage?.url);
if (uploadedImage?.url) {
// onInsert({ src: urlInput, alt: altInput })
const action = Object.assign({}, callbackAction);
action.payload = { src: uploadedImage.url, alt: altInput }
dispatch(action)
onClose?.();
}
}
return (
<motion.div
custom={direction}
variants={modalCardVariants}
initial='initial'
animate="animate"
exit='exit'
className="modal-card max-w-[660px] p-24 rounded-xl relative"
>
<IoClose className='absolute text-body2 top-24 right-24 hover:cursor-pointer' onClick={onClose} />
<h2 className='text-h5 font-bold'>Add Image</h2>
<form onSubmit={handleSubmit}>
{/* <div className="grid md:grid-cols-3 gap-16 mt-32">
<div className='md:col-span-2'>
<p className="text-body5">
Image URL
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
value={urlInput}
onChange={e => setUrlInput(e.target.value)}
placeholder='https://images.com/my-image'
/>
</div>
</div>
<div>
<p className="text-body5">
Alt Text
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
value={altInput}
onChange={e => setAltInput(e.target.value)}
placeholder=''
/>
</div>
</div>
</div>
<div className="mt-32 w-1/2 mx-auto aspect-video bg-gray-200 rounded-10">
{urlInput && <img
src={urlInput}
className='w-full h-full object-cover rounded-10'
alt={altInput}
/>}
</div> */}
<SingleImageUploadInput
value={uploadedImage}
onChange={setUploadedImage}
wrapperClass='h-full mt-32'
render={({ img, isUploading, isDraggingOnWindow }) => <div ref={dropAreaRef} className="w-full group aspect-video bg-gray-100 cursor-pointer rounded-16 border-2 border-gray200 overflow-hidden relative flex flex-col justify-center items-center">
{img && <>
<img src={img.url} className='w-full h-full object-cover rounded-16' alt="" />
{!isUploading &&
<button type='button' className='absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 py-16 px-24 rounded-12 bg-black bg-opacity-70 opacity-0 group-hover:opacity-100 hover:bg-opacity-90 transition-opacity text-white text-h3'>
<FaExchangeAlt />
</button>}
</>}
{!img &&
<>
<p className="text-center text-gray-700 text-body1 md:text-h1 mb-8"><FaImage /></p>
<div className={`text-gray-600 text-center text-body4`}>
Drop an <span className="font-bold">IMAGE</span> here or <br /> <span className="text-blue-500 underline">Click to browse</span>
</div>
</>}
{isUploading &&
<div
className="absolute inset-0 bg-gray-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold transition-transform"
>
<RotatingLines
strokeColor="#fff"
strokeWidth="3"
animationDuration="0.75"
width="48"
visible={true}
/>
</div>
}
{isDraggingOnWindow &&
<div
className={
`absolute inset-0 ${isDragging ? 'bg-primary-600' : 'bg-primary-400'} bg-opacity-80 flex flex-col justify-center items-center text-white font-bold transition-transform`
}
>
<motion.div
initial={{ y: 0 }}
animate={
isDragging ? {
y: 5,
transition: {
duration: .4,
repeat: Infinity,
repeatType: 'mirror',
}
} : {
y: 0
}}
className='text-center text-body1'
>
<AiOutlineCloudUpload className="scale-150 text-h1 mb-16" />
<br />
Drop here to upload
</motion.div>
</div>
}
</div>}
/>
<div className='mt-24'>
<p className="text-body5">
Alternative Text
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
value={altInput}
onChange={e => setAltInput(e.target.value)}
placeholder='A description for the content of this image'
/>
</div>
</div>
<div className="flex gap-16 justify-end mt-32">
<Button onClick={onClose}>
Cancel
</Button>
<Button type='submit' color='primary' >
Add
</Button>
</div>
</form>
</motion.div>
)
}

View File

@@ -17,7 +17,11 @@ ReactModal.setAppElement('#root');
export default function Modal({ onClose, children, ...props }: Props) {
const dispatch = useAppDispatch()
const dispatch = useAppDispatch();
const onAfterClose = () => {
dispatch(removeClosedModal(props.id))
}
return <ReactModal
isOpen={props.isOpen}
@@ -25,7 +29,7 @@ export default function Modal({ onClose, children, ...props }: Props) {
overlayClassName='fixed w-full inset-0 overflow-x-hidden z-[2020] no-scrollbar'
className=' '
closeTimeoutMS={1000}
onAfterClose={() => dispatch(removeClosedModal(props.id))}
onAfterClose={onAfterClose}
contentElement={(_props, children) => <div {..._props} className={`${_props.className} w-screen min-h-screen relative flex flex-col justify-center items-center md:py-64 md:px-16 inset-0`}>
<div
onClick={onClose}

View File

@@ -71,10 +71,9 @@ export default function ModalsContainer() {
<div className="z-[2020]">
{openModals.map((modal, idx) => {
const Child = ALL_MODALS[modal.Modal];
return (
<Modal
key={idx}
key={modal.id}
isOpen={modal.isOpen}
onClose={onClose}
direction={direction}

View File

@@ -14,7 +14,7 @@ import {
import '@szhsin/react-menu/dist/index.css';
import { FiChevronDown } from "react-icons/fi";
import Avatar from "src/features/Profiles/Components/Avatar/Avatar";
import { createRoute } from "src/utils/routing";
import { createRoute, PAGES_ROUTES } from "src/utils/routing";
import Button from "../Button/Button";
@@ -38,114 +38,119 @@ export default function NavDesktop() {
return (
<nav className="bg-white py-16 flex items-center w-full min-w-full">
<div className="content-container flex items-center">
<Link to="/">
<h2 className="text-h5 font-bold mr-40 lg:mr-64">
<img className='h-40' src={ASSETS.Logo} alt="Bolt fun logo" />
</h2>
</Link>
<ul className="flex gap-32 xl:gap-64">
<li className="relative">
<Link to={'/projects'} className='text-body4 font-bold hover:text-primary-600'>
Projects
</Link>
</li>
<li>
<Menu
offsetY={28}
menuButton={
<MenuButton
className='text-body4 font-bold hover:text-primary-600'>Community <FiChevronDown className="ml-8" />
</MenuButton>
}
menuClassName='!rounded-12 !p-8 !border-gray-200'
menuStyle={{ border: '1px solid' }}
>
<MenuItem
href="/blog"
onClick={(e) => {
e.syntheticEvent.preventDefault();
navigate("/blog");
}}
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12 '
<nav className="bg-white py-16 w-full min-w-full">
<div className="content-container">
<div className="flex items-center">
<Link to="/">
<h2 className="text-h5 font-bold mr-40 lg:mr-64">
<img className='h-40' src={ASSETS.Logo} alt="Bolt fun logo" />
</h2>
</Link>
<ul className="flex gap-32 xl:gap-64">
<li className="relative">
<Link to={'/projects'} className='text-body4 font-bold hover:text-primary-600'>
Projects
</Link>
</li>
<li className="relative">
<Link to={'/hackathons'} className='text-body4 font-bold hover:text-primary-600'>
Events
</Link>
</li>
{/* <li>
<Menu
offsetY={28}
menuButton={
<MenuButton
className='text-body4 font-bold hover:text-primary-600'>Community <FiChevronDown className="ml-8" />
</MenuButton>
}
menuClassName='!rounded-12 !p-8 !border-gray-200'
menuStyle={{ border: '1px solid' }}
>
<div className="bg-white border border-gray-100 w-48 h-48 rounded-full flex justify-center items-center">
{/* <FiFeather className={`text-body1 inline-block text-primary-600 `} /> */}
<span className="text-body2">🏼</span>
</div>
<div>
<p className="text-body4 text-black font-medium">
Stories
</p>
<p className="text-body5 text-gray-600 mt-4">
Tales from the maker community
</p>
</div>
</MenuItem>
<MenuItem
<MenuItem
href={PAGES_ROUTES.blog.feed}
onClick={(e) => {
e.syntheticEvent.preventDefault();
navigate(PAGES_ROUTES.blog.feed);
}}
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12 '
>
<div className="bg-white border border-gray-100 w-48 h-48 rounded-full flex justify-center items-center">
<span className="text-body2">✍🏼</span>
</div>
<div>
<p className="text-body4 text-black font-medium">
Stories
</p>
<p className="text-body5 text-gray-600 mt-4">
Tales from the maker community
</p>
</div>
</MenuItem>
<MenuItem
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12 opacity-40'
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12 opacity-40'
>
<div className="bg-white border border-gray-100 w-48 h-48 rounded-full flex justify-center items-center">
<span className="text-body2">💬</span>
</div>
<div>
<p className="text-body4 text-black font-medium">
Discussions
</p>
<p className="text-body5 text-gray-600 mt-4">
Coming soon
</p>
</div>
</MenuItem>
<MenuItem
href="/hackathons"
onClick={(e) => {
e.syntheticEvent.preventDefault();
navigate("/hackathons");
}}
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12'
>
<div className="bg-white border border-gray-100 w-48 h-48 rounded-full flex justify-center items-center">
<span className="text-body2">🏆</span>
</div>
<div>
<p className="text-body4 text-black font-medium">
Hackathons
</p>
<p className="text-body5 text-gray-600 mt-4">
Take part in hackathons & tournaments
</p>
</div>
</MenuItem>
</Menu>
</li> */}
<li className="relative">
<a
href={'https://bolt.fun/guide/'}
target="_blank"
rel="noreferrer"
className='text-body4 font-bold hover:text-primary-600'
>
<div className="bg-white border border-gray-100 w-48 h-48 rounded-full flex justify-center items-center">
<span className="text-body2">💬</span>
</div>
<div>
<p className="text-body4 text-black font-medium">
Discussions
</p>
<p className="text-body5 text-gray-600 mt-4">
Coming soon
</p>
</div>
</MenuItem>
<MenuItem
href="/hackathons"
onClick={(e) => {
e.syntheticEvent.preventDefault();
navigate("/hackathons");
}}
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12'
>
<div className="bg-white border border-gray-100 w-48 h-48 rounded-full flex justify-center items-center">
<span className="text-body2">🏆</span>
</div>
<div>
<p className="text-body4 text-black font-medium">
Hackathons
</p>
<p className="text-body5 text-gray-600 mt-4">
Take part in hackathons & tournaments
</p>
</div>
</MenuItem>
</Menu>
</li>
<li className="relative">
<a
href={'https://bolt.fun/guide/'}
target="_blank"
rel="noreferrer"
className='text-body4 font-bold hover:text-primary-600'
>
Guide
</a>
</li>
<li className="relative">
<Link to={'/donate'} className='text-body4 font-bold hover:text-primary-600'>
Donate
</Link>
</li>
</ul>
Guide
</a>
</li>
<li className="relative">
<Link to={'/donate'} className='text-body4 font-bold hover:text-primary-600'>
Donate
</Link>
</li>
</ul>
<div className="flex-1"></div>
<div className="flex-1"></div>
<motion.div
animate={searchOpen ? { opacity: 0 } : { opacity: 1 }}
className="flex"
>
<motion.div
animate={searchOpen ? { opacity: 0 } : { opacity: 1 }}
className="flex"
>
{/* <Button
{/* <Button
color="primary"
size="md"
className="lg:px-40"
@@ -154,87 +159,89 @@ export default function NavDesktop() {
>
Submit App
</Button> */}
{/* {isWalletConnected ?
{/* {isWalletConnected ?
<Button className="ml-16 py-12 px-16 lg:px-20">Connected <AiFillThunderbolt className='inline-block text-thunder transform scale-125' /></Button>
: <Button className="ml-16 py-12 px-16 lg:px-20" onClick={onConnectWallet}><AiFillThunderbolt className='inline-block text-thunder transform scale-125' /> Connect Wallet </Button>
} */}
{currentSection === 'apps' && <IconButton className='mr-16 self-center' onClick={openSearch}>
<BsSearch className='scale-125 text-gray-400' />
</IconButton>}
</motion.div>
{curUser !== undefined &&
(curUser ?
<Menu
align="end"
offsetY={4}
menuClassName='!p-8 !rounded-12'
menuButton={<MenuButton ><Avatar src={curUser.avatar} width={40} /> </MenuButton>}>
<MenuItem
href={createRoute({ type: 'profile', id: curUser.id, username: curUser.name })}
onClick={(e) => {
e.syntheticEvent.preventDefault();
navigate(createRoute({ type: 'profile', id: curUser.id, username: curUser.name }));
}}
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12'
>
👾 Profile
</MenuItem>
<MenuItem
href="/edit-profile"
onClick={(e) => {
e.syntheticEvent.preventDefault();
navigate("/edit-profile");
}}
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12'
>
Settings
</MenuItem>
<MenuItem
href="/logout"
onClick={(e) => {
e.syntheticEvent.preventDefault();
navigate("/logout");
}}
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12'
>
👋 Logout
</MenuItem>
</Menu>
:
<Button size="sm" color="white" href="/login">
Connect
</Button>
)
}
<div className="relative h-36">
<motion.div
initial={{
opacity: 0,
y: '0'
}}
animate={searchOpen ? {
opacity: 1,
y: '0',
transition: { type: "spring", stiffness: 70 }
} : {
opacity: 0,
y: '-120px',
transition: {
ease: "easeIn"
}
}}
className='absolute top-0 right-0 flex items-center h-full'
>
<Search
width={326}
isOpen={searchOpen}
onClose={() => setSearchOpen(false)}
onResultClick={() => setSearchOpen(false)}
/>
{currentSection === 'apps' && <IconButton className='mr-16 self-center' onClick={openSearch}>
<BsSearch className='scale-125 text-gray-400' />
</IconButton>}
</motion.div>
{curUser !== undefined &&
(curUser ?
<Menu
align="end"
offsetY={4}
menuClassName='!p-8 !rounded-12'
menuButton={<MenuButton ><Avatar src={curUser.avatar} width={40} /> </MenuButton>}>
<MenuItem
href={createRoute({ type: 'profile', id: curUser.id, username: curUser.name })}
onClick={(e) => {
e.syntheticEvent.preventDefault();
navigate(createRoute({ type: 'profile', id: curUser.id, username: curUser.name }));
}}
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12'
>
👾 Profile
</MenuItem>
<MenuItem
href="/edit-profile"
onClick={(e) => {
e.syntheticEvent.preventDefault();
navigate("/edit-profile");
}}
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12'
>
Settings
</MenuItem>
<MenuItem
href="/logout"
onClick={(e) => {
e.syntheticEvent.preventDefault();
navigate("/logout");
}}
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12'
>
👋 Logout
</MenuItem>
</Menu>
:
<Button size="sm" color="white" href="/login">
Connect
</Button>
)
}
<div className="relative h-36">
<motion.div
initial={{
opacity: 0,
y: '0'
}}
animate={searchOpen ? {
opacity: 1,
y: '0',
transition: { type: "spring", stiffness: 70 }
} : {
opacity: 0,
y: '-120px',
transition: {
ease: "easeIn"
}
}}
className='absolute top-0 right-0 flex items-center h-full'
>
<Search
width={326}
isOpen={searchOpen}
onClose={() => setSearchOpen(false)}
onResultClick={() => setSearchOpen(false)}
/>
</motion.div>
</div>
</div>
</div>
</nav>

View File

@@ -14,7 +14,7 @@ import styles from './styles.module.css'
import '@szhsin/react-menu/dist/index.css';
import { Menu, MenuButton, MenuItem } from "@szhsin/react-menu";
import Avatar from "src/features/Profiles/Components/Avatar/Avatar";
import { createRoute } from "src/utils/routing";
import { createRoute, PAGES_ROUTES } from "src/utils/routing";
const navBtnVariant = {
menuHide: { rotate: 90, opacity: 0 },
@@ -73,68 +73,70 @@ export default function NavMobile() {
return (
<div className={`${styles.navMobile}`}>
<nav className={`bg-white h-[67px] w-full py-16`}>
<div className="content-container flex justify-between items-center">
<div className="content-container">
<div className="flex justify-between items-center">
<div className="flex-1 flex content-start">
<IconButton className='auto text-2xl w-[50px] h-[50px] hover:bg-gray-200 self-center' onClick={() => toggleDrawerOpen()}>
{!drawerOpen ? (<motion.div key={drawerOpen ? 1 : 0} variants={navBtnVariant} initial='menuHide' animate='menuShow'><FiMenu /></motion.div>)
: (<motion.div key={drawerOpen ? 1 : 0} variants={navBtnVariant} initial='closeHide' animate='closeShow'><GrClose /></motion.div>)}
</IconButton>
<div className="flex-1 flex content-start">
<IconButton className='auto text-2xl w-[50px] h-[50px] hover:bg-gray-200 self-center' onClick={() => toggleDrawerOpen()}>
{!drawerOpen ? (<motion.div key={drawerOpen ? 1 : 0} variants={navBtnVariant} initial='menuHide' animate='menuShow'><FiMenu /></motion.div>)
: (<motion.div key={drawerOpen ? 1 : 0} variants={navBtnVariant} initial='closeHide' animate='closeShow'><GrClose /></motion.div>)}
</IconButton>
</div>
<div className="flex-[2] flex justify-center">
<Link to="/">
<img className='max-h-32' src={ASSETS.Logo} alt="Bolt fun logo" />
</Link>
</div>
<div className="flex-1 flex justify-end">
{curUser ?
<Menu
align="end"
offsetY={4}
menuClassName='!p-8 !rounded-12'
menuButton={<MenuButton ><Avatar src={curUser.avatar} width={32} /> </MenuButton>}>
<MenuItem
href={createRoute({ type: 'profile', id: curUser.id, username: curUser.name })}
onClick={(e) => {
e.syntheticEvent.preventDefault();
navigate(createRoute({ type: 'profile', id: curUser.id, username: curUser.name }));
}}
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12'
>
👾 Profile
</MenuItem>
<MenuItem
href="/edit-profile"
onClick={(e) => {
e.syntheticEvent.preventDefault();
navigate("/edit-profile");
}}
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12'
>
Settings
</MenuItem>
<MenuItem
href="/logout"
onClick={(e) => {
e.syntheticEvent.preventDefault();
navigate("/logout");
}}
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12'
>
👋 Logout
</MenuItem>
</Menu>
:
<Button size="sm" color="none" className="!text-body5 whitespace-nowrap" href="/login">
Connect
</Button>
}</div>
</div>
<div className="flex-[2] flex justify-center">
<Link to="/">
<img className='max-h-32' src={ASSETS.Logo} alt="Bolt fun logo" />
</Link>
</div>
<div className="flex-1 flex justify-end">
{curUser ?
<Menu
align="end"
offsetY={4}
menuClassName='!p-8 !rounded-12'
menuButton={<MenuButton ><Avatar src={curUser.avatar} width={32} /> </MenuButton>}>
<MenuItem
href={createRoute({ type: 'profile', id: curUser.id, username: curUser.name })}
onClick={(e) => {
e.syntheticEvent.preventDefault();
navigate(createRoute({ type: 'profile', id: curUser.id, username: curUser.name }));
}}
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12'
>
👾 Profile
</MenuItem>
<MenuItem
href="/edit-profile"
onClick={(e) => {
e.syntheticEvent.preventDefault();
navigate("/edit-profile");
}}
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12'
>
Settings
</MenuItem>
<MenuItem
href="/logout"
onClick={(e) => {
e.syntheticEvent.preventDefault();
navigate("/logout");
}}
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12'
>
👋 Logout
</MenuItem>
</Menu>
:
<Button size="sm" color="none" className="!text-body5 whitespace-nowrap" href="/login">
Connect
</Button>
}</div>
</div>
</nav>
@@ -164,7 +166,15 @@ export default function NavMobile() {
Projects
</Link>
</li>
<li>
<li className="relative">
<Link
to={'/hackathons'}
onClick={() => toggleDrawerOpen(false)}
className='text-body4 font-bold hover:text-primary-600'>
Events
</Link>
</li>
{/* <li>
<button
className='text-body4 font-bold hover:text-primary-600 w-full flex justify-between'
onClick={() => toggleCommunityOpen()}
@@ -184,7 +194,7 @@ export default function NavMobile() {
>
<div className='flex flex-col gap-24 pt-16' >
<Link
to="/blog"
to={PAGES_ROUTES.blog.feed}
onClick={() => toggleDrawerOpen(false)}
className='font-medium flex gap-16 !rounded-12 '
>
@@ -235,7 +245,7 @@ export default function NavMobile() {
</Link>
</div>
</motion.div>}
</li>
</li> */}
<li className="relative">
<a
href={'https://bolt.fun/guide/'}

View File

@@ -18,7 +18,7 @@ interface Particle {
offsetY: number,
color: string
animation: 'fly-spark-1' | 'fly-spark-2',
animationSpeed: 1 | 2 | 3,
animationSpeed: number,
scale: number
}
@@ -108,11 +108,11 @@ export default function VoteButton({
id: (Math.random() + 1).toString(),
offsetX: random(-10, 99),
offsetY: random(10, 90),
animation: randomItem(styles.fly_spark_1, styles.fly_spark_1),
animation: randomItem(styles.fly_spark_1, styles.fly_spark_1) as any,
animationSpeed: randomItem(1, 1.5, 2),
color: `hsl(0deg 86% ${random(50, 63)}%)`,
scale: random(1, 1.5)
}))
} as const))
// if on mobile screen, reduce number of sparks particles to 60%
setSparks(oldSparks => [...oldSparks, ...newSparks])

21
src/api/auth.ts Normal file
View File

@@ -0,0 +1,21 @@
import { CONSTS } from "src/utils";
export async function fetchLnurlAuth() {
const res = await fetch(CONSTS.apiEndpoint + '/get-login-url', {
credentials: 'include'
})
const data = await res.json()
return data;
}
export async function fetchIsLoggedIn(session_token: string) {
const res = await fetch(CONSTS.apiEndpoint + '/is-logged-in', {
credentials: 'include',
headers: {
session_token
}
});
const data = await res.json();
return data.logged_in;
}

12
src/api/uploading.ts Normal file
View File

@@ -0,0 +1,12 @@
import axios from "axios";
import { CONSTS } from "src/utils";
export async function fetchUploadImageUrl({ filename }: { filename: string }) {
const res = await axios.post(CONSTS.apiEndpoint + '/upload-image-url', {
filename
}, {
withCredentials: true
})
return res.data;
}

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from "react"
import { useCallback, useEffect, useRef, useState } from "react"
import { Helmet } from "react-helmet";
import { Grid } from "react-loader-spinner";
import { useNavigate, useLocation } from "react-router-dom";
@@ -9,19 +9,15 @@ import { IoRocketOutline } from "react-icons/io5";
import Button from "src/Components/Button/Button";
import { FiCopy } from "react-icons/fi";
import useCopyToClipboard from "src/utils/hooks/useCopyToClipboard";
import { getPropertyFromUnknown, } from "src/utils/helperFunctions";
import { getPropertyFromUnknown, trimText, } from "src/utils/helperFunctions";
import { fetchIsLoggedIn, fetchLnurlAuth } from "src/api/auth";
import { useErrorHandler } from 'react-error-boundary';
const fetchLnurlAuth = async () => {
const res = await fetch(CONSTS.apiEndpoint + '/get-login-url', {
credentials: 'include'
})
const data = await res.json()
return data;
}
const useLnurlQuery = () => {
export const useLnurlQuery = () => {
const [loading, setLoading] = useState(true)
const [error, setError] = useState<any>(null);
const [data, setData] = useState<{ lnurl: string, session_token: string }>({ lnurl: '', session_token: '' })
@@ -33,7 +29,7 @@ const useLnurlQuery = () => {
const doFetch = async () => {
const res = await fetchLnurlAuth();
if (!res?.encoded)
setError(true)
setError(new Error("Response doesn't contain data"))
else {
setLoading(false);
setData({
@@ -43,7 +39,7 @@ const useLnurlQuery = () => {
timeOut = setTimeout(doFetch, 1000 * 60 * 2)
}
}
doFetch()
doFetch().catch(err => setError(err));
return () => clearTimeout(timeOut)
}, [])
@@ -62,9 +58,11 @@ export default function LoginPage() {
const location = useLocation();
const [copied, setCopied] = useState(false);
const canFetchIsLogged = useRef(true)
const { loadingLnurl, data: { lnurl, session_token }, error } = useLnurlQuery();
const clipboard = useCopyToClipboard()
useErrorHandler(error)
const clipboard = useCopyToClipboard()
useEffect(() => {
@@ -96,18 +94,20 @@ export default function LoginPage() {
const startPolling = useCallback(
() => {
const interval = setInterval(() => {
fetch(CONSTS.apiEndpoint + '/is-logged-in', {
credentials: 'include',
headers: {
session_token
}
}).then(data => data.json())
.then(data => {
if (data.logged_in) {
if (canFetchIsLogged.current === false) return;
canFetchIsLogged.current = false;
fetchIsLoggedIn(session_token)
.then(is_logged_in => {
if (is_logged_in) {
clearInterval(interval)
refetch();
}
})
.catch()
.finally(() => {
canFetchIsLogged.current = true;
})
}, 2000);
return interval;
@@ -123,6 +123,7 @@ export default function LoginPage() {
interval = startPolling();
return () => {
canFetchIsLogged.current = true;
clearInterval(interval)
}
}, [lnurl, startPolling])
@@ -147,14 +148,13 @@ export default function LoginPage() {
else if (isLoggedIn)
content = <div className="flex flex-col justify-center items-center">
<h3 className="text-body4">
Hello: <span className="font-bold">@{meQuery.data?.me?.name.slice(0, 10)}...</span>
Hello: <span className="font-bold">@{trimText(meQuery.data?.me?.name, 10)}</span>
</h3>
<img src={meQuery.data?.me?.avatar} className='w-80 h-80 object-cover' alt="" />
<img src={meQuery.data?.me?.avatar} className='w-80 h-80 object-cover rounded-full outline outline-2 outline-gray-200' alt="" />
</div>
else
content = <div className="max-w-[364px] border-2 border-gray-200 rounded-16 p-16 flex flex-col gap-24 items-center" >
content = <div className="max-w-[442px] bg-white border-2 border-gray-200 rounded-16 p-16 flex flex-col gap-24 items-center" >
<h2 className='text-h5 font-bold text-center'>Login with lightning </h2>
<a href={`lightning:${lnurl}`} >
<QRCodeSVG
@@ -174,26 +174,34 @@ export default function LoginPage() {
<p className="text-gray-600 text-body4 text-center">
Scan this code or copy + paste it to your lightning wallet. Or click to login with your browser's wallet.
</p>
<div className="w-full flex flex-col items-stretch gap-16">
<div className="w-full grid md:grid-cols-2 gap-16">
<a href={`lightning:${lnurl}`}
className='grow block text-body4 text-center text-white font-bolder bg-primary-500 hover:bg-primary-600 rounded-10 px-16 py-12 active:scale-90 transition-transform'
className='block text-body4 text-center text-white bg-primary-500 hover:bg-primary-600 rounded-10 px-16 py-12 active:scale-90 transition-transform'
>Click to connect <IoRocketOutline /></a>
<Button
color='gray'
className='grow'
onClick={copyToClipboard}
>{copied ? "Copied" : "Copy"} <FiCopy /></Button>
<a href={`https://makers.bolt.fun/blog/post/story/99/sign-in-with-lightning`} target='_blank' rel="noreferrer"
className='md:col-span-2 block text-body4 text-center text-gray-900 border border-gray-200 rounded-10 px-16 py-12 active:scale-90 transition-transform'
>What is a lightning wallet?</a>
</div>
</div>;
return (
<div className="min-h-[80vh] page-container flex flex-col justify-center items-center">
<>
<Helmet>
<title>{`makers.bolt.fun`}</title>
<meta property="og:title" content={`makers.bolt.fun`} />
</Helmet>
{content}
</div>
<div className="page-container">
<div className="min-h-[80vh] flex flex-col justify-center items-center">
{content}
</div>
</div>
</>
)
}

View File

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

Some files were not shown because too many files have changed in this diff Show More