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

Turn your hackathon project into a startup

Schedule an office hour with Fulgur Ventures

diff --git a/src/Components/Button/Button.tsx b/src/Components/Button/Button.tsx index 8d4c378..81fa3c1 100644 --- a/src/Components/Button/Button.tsx +++ b/src/Components/Button/Button.tsx @@ -5,7 +5,7 @@ import { TailSpin } from 'react-loader-spinner'; type Props = { color?: 'primary' | 'red' | 'white' | 'gray' | "black" | 'none', - variant?: 'fill' | 'outline' + variant?: 'fill' | 'outline' | 'text' size?: 'sm' | 'md' | 'lg' children: ReactNode; href?: string; @@ -40,7 +40,7 @@ const loadingColor: UnionToObjectKeys = { const btnStylesOutline: UnionToObjectKeys = { none: "", primary: "text-primary-600", - gray: 'text-gray-700', + gray: 'text-gray-600', white: 'text-gray-900', black: 'text-black', red: "text-red-500", @@ -48,7 +48,8 @@ const btnStylesOutline: UnionToObjectKeys = { const baseBtnStyles: UnionToObjectKeys = { fill: "transition-transform active:scale-95", - outline: "transition-transform bg-gray-900 bg-opacity-0 hover:bg-opacity-5 active:bg-opacity-10 border border-gray-200 active:scale-95 " + outline: "transition-transform bg-gray-900 bg-opacity-0 hover:bg-opacity-5 active:bg-opacity-10 border border-gray-200 active:scale-95 ", + text: "transition-transform active:scale-95 hover:bg-gray-100 active:bg-gray-50" } @@ -88,7 +89,7 @@ const Button = React.forwardRef(({ color = 'white', ${variant === 'fill' ? btnStylesFill[color] : btnStylesOutline[color]} ${isLoading && disableOnLoading && 'bg-opacity-70 pointer-events-none'} ${fullWidth && 'w-full'} - ${disabled && 'opacity-40 pointer-events-none'} + ${disabled && 'opacity-90 pointer-events-none'} `; diff --git a/src/Components/ErrorBoundary/ErrorBoundary.tsx b/src/Components/ErrorBoundary/ErrorBoundary.tsx index 7fcfef3..6ddc18b 100644 --- a/src/Components/ErrorBoundary/ErrorBoundary.tsx +++ b/src/Components/ErrorBoundary/ErrorBoundary.tsx @@ -1,5 +1,5 @@ import React, { Component, ErrorInfo, ReactNode } from "react"; -import ErrorMessage from "../ErrorMessage/ErrorMessage"; +import ErrorMessage from "../Errors/ErrorMessage/ErrorMessage"; interface Props { place?: string diff --git a/src/Components/Errors/ErrorCard/ErrorCard.stories.tsx b/src/Components/Errors/ErrorCard/ErrorCard.stories.tsx new file mode 100644 index 0000000..a73c3d2 --- /dev/null +++ b/src/Components/Errors/ErrorCard/ErrorCard.stories.tsx @@ -0,0 +1,21 @@ +import { ComponentStory, ComponentMeta } from '@storybook/react'; + +import ErrorCard from './ErrorCard'; + +export default { + title: 'Shared/ErrorCard', + component: ErrorCard, + +} as ComponentMeta; + +const Template: ComponentStory = (args) => ; + +export const Default = Template.bind({}); +Default.args = { + error: { + name: "Error Name", + message: "Error Message", + } +} + + diff --git a/src/Components/Errors/ErrorCard/ErrorCard.tsx b/src/Components/Errors/ErrorCard/ErrorCard.tsx new file mode 100644 index 0000000..d9ee8e6 --- /dev/null +++ b/src/Components/Errors/ErrorCard/ErrorCard.tsx @@ -0,0 +1,40 @@ +import { useToggle } from "@react-hookz/web"; +import { FallbackProps } from "react-error-boundary"; +import { RiErrorWarningFill } from "react-icons/ri"; +import Button from "src/Components/Button/Button"; +import Card from "src/Components/Card/Card"; + + + +export default function ErrorCard({ error, resetErrorBoundary }: FallbackProps) { + + const [showDetails, toggleDetails] = useToggle(false) + + + + return ( + +
+
+

Ooops...
Looks like something unexpected went wrong, please check your internet connection & try again.

+
+ + +
+ +
+ + + {showDetails && +
+

{error.name}

+

{error.message}

+

{error.stack}

+
+ } +
+
+
+ + ) +} diff --git a/src/Components/ErrorMessage/ErrorMessage.stories.tsx b/src/Components/Errors/ErrorMessage/ErrorMessage.stories.tsx similarity index 100% rename from src/Components/ErrorMessage/ErrorMessage.stories.tsx rename to src/Components/Errors/ErrorMessage/ErrorMessage.stories.tsx diff --git a/src/Components/ErrorMessage/ErrorMessage.tsx b/src/Components/Errors/ErrorMessage/ErrorMessage.tsx similarity index 100% rename from src/Components/ErrorMessage/ErrorMessage.tsx rename to src/Components/Errors/ErrorMessage/ErrorMessage.tsx diff --git a/src/Components/Errors/ErrorPage/ErrorPage.tsx b/src/Components/Errors/ErrorPage/ErrorPage.tsx new file mode 100644 index 0000000..5357f4c --- /dev/null +++ b/src/Components/Errors/ErrorPage/ErrorPage.tsx @@ -0,0 +1,18 @@ +import React from 'react' +import { Helmet } from 'react-helmet' +import { FallbackProps } from 'react-error-boundary' +import ErrorCard from '../ErrorCard/ErrorCard' + + +export default function ErrorPage({ error, resetErrorBoundary }: FallbackProps) { + return ( +
+ + {`Something went wrong...`} + +
+ +
+
+ ) +} diff --git a/src/Components/IconButton/IconButton.tsx b/src/Components/IconButton/IconButton.tsx index 407052e..7a3ea33 100644 --- a/src/Components/IconButton/IconButton.tsx +++ b/src/Components/IconButton/IconButton.tsx @@ -9,6 +9,7 @@ interface Props { className?: string size?: "sm" | 'md' | 'lg' variant?: 'blank' | 'fill' + isDisabled?: boolean } @@ -32,7 +33,8 @@ const IconButton = React.forwardRef>(({ children, onClick = () => { }, onKeyDown, - variant = 'blank' + variant = 'blank', + isDisabled, }, ref) => { if (href) @@ -57,12 +59,16 @@ const IconButton = React.forwardRef>(({ ref={ref} type='button' className={` + ${className} ${sizeToPadding[size]} ${baseBtnStyles[variant]} - active:scale-95 rounded-full ${className}`} + active:scale-95 rounded-full + ${isDisabled && "opacity-60"} + `} style={{ lineHeight: 0 }} onClick={onClick} onKeyDown={onKeyDown} + disabled={isDisabled} > {children} diff --git a/src/Components/InfoCard/InfoCard.tsx b/src/Components/InfoCard/InfoCard.tsx index 6e41f47..c14e977 100644 --- a/src/Components/InfoCard/InfoCard.tsx +++ b/src/Components/InfoCard/InfoCard.tsx @@ -1,12 +1,12 @@ import React, { PropsWithChildren } from 'react' interface Props { - + className?: string } export default function InfoCard(props: PropsWithChildren) { return ( -
+

{props.children}

diff --git a/src/Components/Inputs/Autocomplete/Autocomplete.stories.tsx b/src/Components/Inputs/Autocomplete/Autocomplete.stories.tsx deleted file mode 100644 index 02df6f4..0000000 --- a/src/Components/Inputs/Autocomplete/Autocomplete.stories.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import { ComponentStory, ComponentMeta } from '@storybook/react'; -import { useWatch } from 'react-hook-form'; -import { WrapForm } from 'src/utils/storybook/decorators'; - -import Autocomplete from './Autocomplete'; - -export default { - title: 'Shared/Inputs/AutoComplete', - component: Autocomplete, - decorators: [WrapForm({ - defaultValues: { - autocomplete: null - } - })], - -} as ComponentMeta; - -const options = [ - { - "id": "20f0eb8d-c0cd-4e12-8a08-0d9846fc8704", - "name": "Nichole Bailey", - "username": "Cassie14", - "email": "Daisy_Auer50@hotmail.com", - "address": { - "street": "Anastasia Tunnel", - "suite": 95587, - "city": "Port Casperview", - "zipcode": "04167-6996", - "geo": { - "lat": "-73.4727", - "lng": "-142.9435" - } - }, - "phone": "324-615-9195 x5902", - "website": "ron.net", - "company": { - "name": "Roberts, Tremblay and Christiansen", - "catchPhrase": "Vision-oriented actuating access", - "bs": "bricks-and-clicks strategize portals" - } - }, - { - "id": "62b70f76-85ba-4241-9ffd-07582008c497", - "name": "Robert Blick", - "username": "Madilyn93", - "email": "Ronaldo82@gmail.com", - "address": { - "street": "Charlie Plain", - "suite": 83070, - "city": "Lake Bonitaland", - "zipcode": "01109", - "geo": { - "lat": "50.0971", - "lng": "-2.3057" - } - }, - "phone": "1-541-367-2047 x9006", - "website": "jovani.com", - "company": { - "name": "Parisian - Kling", - "catchPhrase": "Multi-tiered tertiary toolset", - "bs": "plug-and-play benchmark content" - } - }, - { - "id": "d02f74d9-bf99-4e41-b678-15e903abc1b3", - "name": "Eli O'Kon", - "username": "Rosario.Davis", - "email": "Mckayla59@hotmail.com", - "address": { - "street": "Wilford Drive", - "suite": 69742, - "city": "North Dianna", - "zipcode": "80620", - "geo": { - "lat": "-61.4191", - "lng": "126.7878" - } - }, - "phone": "(339) 709-4080", - "website": "clay.name", - "company": { - "name": "Gerlach - Metz", - "catchPhrase": "Pre-emptive user-facing service-desk", - "bs": "frictionless monetize markets" - } - }, - { - "id": "21077fa6-6a53-4b84-8407-6cd949718945", - "name": "Marilie Feil", - "username": "Antwon.Carter92", - "email": "Demario.Hyatt20@yahoo.com", - "address": { - "street": "Kenton Spurs", - "suite": 20079, - "city": "Beahanberg", - "zipcode": "79385", - "geo": { - "lat": "-70.7199", - "lng": "4.6977" - } - }, - "phone": "608.750.4947", - "website": "jacynthe.org", - "company": { - "name": "Kuhn and Sons", - "catchPhrase": "Total eco-centric matrices", - "bs": "out-of-the-box target communities" - } - }, - { - "id": "e07cf1b4-ff43-4c4a-a670-fd7417d6bbaf", - "name": "Ella Pagac", - "username": "Damien.Jaskolski", - "email": "Delmer1@gmail.com", - "address": { - "street": "VonRueden Shoals", - "suite": 14035, - "city": "Starkmouth", - "zipcode": "72448-1915", - "geo": { - "lat": "55.2157", - "lng": "98.0822" - } - }, - "phone": "(165) 247-5332 x71067", - "website": "chad.info", - "company": { - "name": "Nicolas, Doyle and Rempel", - "catchPhrase": "Adaptive real-time strategy", - "bs": "innovative whiteboard supply-chains" - } - } -] -const Template: ComponentStory = (args) => { - - const value = useWatch({ name: 'autocomplete' }) - - console.log(value); - - - return -} - - -export const Default = Template.bind({}); -Default.args = { - onChange: console.log -} - - -export const Lodaing = Template.bind({}); -Lodaing.args = { - isLoading: true -} - -export const Clearable = Template.bind({}); -Clearable.args = { - isClearable: true -} - -export const MultipleAllowed = Template.bind({}); -MultipleAllowed.args = { - isMulti: true -} - diff --git a/src/Components/Inputs/Autocomplete/Autocomplete.tsx b/src/Components/Inputs/Autocomplete/Autocomplete.tsx deleted file mode 100644 index bd7b873..0000000 --- a/src/Components/Inputs/Autocomplete/Autocomplete.tsx +++ /dev/null @@ -1,95 +0,0 @@ - -import { useMemo } from "react"; -import Select, { StylesConfig } from "react-select"; -import { ControlledStateHandler } from "src/utils/interfaces"; - - -type Props = { - options: T[]; - labelField?: keyof T - valueField?: keyof T - placeholder?: string - disabled?: boolean - isLoading?: boolean; - isClearable?: boolean; - control?: any, - name?: string, - className?: string, - onBlur?: () => void; - size?: 'sm' | 'md' | 'lg' -} & ControlledStateHandler - - - - -export default function AutoComplete({ - options, - labelField, - valueField, - placeholder = "Select Option...", - isMulti, - isClearable, - disabled, - className, - value, - onChange, - onBlur, - size = 'md', - ...props -}: Props) { - - - const colourStyles: StylesConfig = useMemo(() => ({ - control: (styles, state) => ({ - ...styles, - padding: size === 'md' ? '1px 4px' : '8px 12px', - borderRadius: size === 'md' ? 8 : 12, - }), - indicatorSeparator: (styles, state) => ({ - ...styles, - display: "none" - }), - input: (styles, state) => ({ - ...styles, - " input": { - boxShadow: 'none !important' - }, - }), - }), [size]) - - return ( -
- onAddFiles(e.target.files)} - ref={fileInputRef} - type="file" - className="hidden" - multiple={multiple} - accept={allowedType} - /> -
- ); -} diff --git a/src/Components/Inputs/FilesInput/FileInput.stories.tsx b/src/Components/Inputs/FilesInput/FileInput.stories.tsx deleted file mode 100644 index 5b02efa..0000000 --- a/src/Components/Inputs/FilesInput/FileInput.stories.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { ComponentStory, ComponentMeta } from '@storybook/react'; -import { BsImages } from 'react-icons/bs'; -import Button from 'src/Components/Button/Button'; - -import FilesInput from './FilesInput'; -import FileDropInput from './FilesDropInput'; - -export default { - title: 'Shared/Inputs/Files Input', - component: FilesInput, - -} as ComponentMeta; - -const Template: ComponentStory = (args) => - - -export const DefaultButton = Template.bind({}); -DefaultButton.args = { -} - -export const CustomizedButton = Template.bind({}); -CustomizedButton.args = { - multiple: true, - uploadBtn: -} - -const DropTemplate: ComponentStory = (args) =>
-export const DropZoneInput = DropTemplate.bind({}); -DropZoneInput.args = { - onChange: console.log, -} \ No newline at end of file diff --git a/src/Components/Inputs/FilesInput/FileThumbnail.tsx b/src/Components/Inputs/FilesInput/FileThumbnail.tsx deleted file mode 100644 index b03fb5b..0000000 --- a/src/Components/Inputs/FilesInput/FileThumbnail.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { useMemo } from "react"; -import { MdClose } from "react-icons/md"; -import IconButton from "src/Components/IconButton/IconButton"; - -interface Props { - file: File | string, - onRemove?: () => void -} - -function getFileType(file: File | string) { - if (typeof file === 'string') { - if (/^http[^?]*.(jpg|jpeg|gif|png|tiff|bmp)(\?(.*))?$/gmi.test(file)) - return 'image' - if (/\.(pdf|doc|docx)$/.test(file)) - return 'document'; - - return 'unknown' - } - else { - if (file['type'].split('/')[0] === 'image') - return 'image' - - return 'unknown' - } -} - -type ThumbnailFile = { - name: string; - src: string; - type: ReturnType -} - -function processFile(file: Props['file']): ThumbnailFile { - - const fileType = getFileType(file); - - if (typeof file === 'string') return { name: file, src: file, type: fileType }; - - return { - name: file.name, - src: URL.createObjectURL(file), - type: fileType - }; - -} - - -export default function FileThumbnail({ file: f, onRemove }: Props) { - - const file = useMemo(() => processFile(f), [f]) - - return ( -
- -
- - - -
-
- ) -} diff --git a/src/Components/Inputs/FilesInput/FilesDropInput.tsx b/src/Components/Inputs/FilesInput/FilesDropInput.tsx deleted file mode 100644 index 6d96622..0000000 --- a/src/Components/Inputs/FilesInput/FilesDropInput.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { FaImage } from "react-icons/fa"; -import { UnionToObjectKeys } from "src/utils/types/utils"; -import DropInput from "./DropInput"; - - -type Props = { - height?: number - multiple?: boolean; - value?: File[] | string[] | string; - max?: number; - onBlur?: () => void; - onChange?: (files: (File | string)[] | null) => void - uploadBtn?: JSX.Element - uploadText?: string; - allowedType?: 'images'; - classes?: Partial<{ - base: string, - idle: string, - dragging: string, - hasFiles: string - }> -} - -const fileAccept: UnionToObjectKeys = { - images: ".png, .jpg, .jpeg" -} as const; - -const fileUrlToObject = async (url: string, fileName: string = 'filename') => { - const res = await fetch(url); - const contentType = res.headers.get('content-type') as string; - const blob = await res.blob() - const file = new File([blob], fileName, { contentType } as any) - return file -} - -export default function FilesInput({ - height = 200, - multiple, - value, - max = 3, - onBlur, - onChange, - allowedType = 'images', - classes, - ...props -}: Props) { - - - const baseClasses = classes?.base ?? 'p-32 rounded-8 text-center flex flex-col justify-center items-center' - const idleClasses = classes?.idle ?? 'bg-primary-50 hover:bg-primary-25 border border-dashed border-primary-500 text-gray-800' - const draggingClasses = classes?.dragging ?? 'bg-primary-500 text-white' - - return ( - - ) -} - -const defaultEmptyContent = ( - <> -
- {" "} - Drop your files here -
-

- or {" "} -

- -); - -const defaultDraggingContent =

Drop your files here ⬇⬇⬇

; - -const defaultHasFilesContent = ( -

Files Uploaded Successfully!!

-); \ No newline at end of file diff --git a/src/Components/Inputs/FilesInput/FilesInput.tsx b/src/Components/Inputs/FilesInput/FilesInput.tsx deleted file mode 100644 index 85e6cdb..0000000 --- a/src/Components/Inputs/FilesInput/FilesInput.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { createAction } from "@reduxjs/toolkit"; -import React, { ChangeEvent, useCallback, useRef } from "react" -import { BsUpload } from "react-icons/bs"; -import { FaImage } from "react-icons/fa"; -import Button from "src/Components/Button/Button" -import { openModal } from "src/redux/features/modals.slice"; -import { useAppDispatch } from "src/utils/hooks"; -import { useReduxEffect } from "src/utils/hooks/useReduxEffect"; -import { UnionToObjectKeys } from "src/utils/types/utils"; -import FilesThumbnails from "./FilesThumbnails"; - - -type Props = { - multiple?: boolean; - value?: File[] | string[] | string; - max?: number; - onBlur?: () => void; - onChange?: (files: (File | string)[] | null) => void - uploadBtn?: JSX.Element - uploadText?: string; - allowedType?: 'images'; -} - -const fileAccept: UnionToObjectKeys = { - images: ".png, .jpg, .jpeg" -} as const; - -const fileUrlToObject = async (url: string, fileName: string = 'filename') => { - const res = await fetch(url); - const contentType = res.headers.get('content-type') as string; - const blob = await res.blob() - const file = new File([blob], fileName, { contentType } as any) - return file -} - -const INSERT_IMAGE_ACTION = createAction<{ src: string, alt?: string }>('COVER_IMAGE_INSERTED')({ src: '', alt: "" }) - -const FilesInput = React.forwardRef(({ - multiple, - value, - max = 3, - onBlur, - onChange, - allowedType = 'images', - uploadText = 'Upload files', - ...props -}, ref) => { - - - const dispatch = useAppDispatch(); - - const handleClick = () => { - // ref.current.click(); - dispatch(openModal({ - Modal: "InsertImageModal", - props: { - callbackAction: { - type: INSERT_IMAGE_ACTION.type, - payload: { - src: "", - alt: "" - } - } - } - })) - } - - const onInsertImgUrl = useCallback(({ payload: { src, alt } }: typeof INSERT_IMAGE_ACTION) => { - if (typeof value === 'string') - onChange?.([value, src]); - else - onChange?.([...(value ?? []), src]); - }, [onChange, value]) - - useReduxEffect(onInsertImgUrl, INSERT_IMAGE_ACTION.type) - - const handleChange = (e: ChangeEvent) => { - const files = e.target.files && Array.from(e.target.files).slice(0, max); - if (typeof value === 'string') - onChange?.([value, ...(files ?? [])]); - else - onChange?.([...(value ?? []), ...(files ?? [])]); - } - - const handleRemove = async (idx: number) => { - if (!value) return onChange?.([]); - if (typeof value === 'string') - onChange?.([]); - else { - let files = [...value] - files.splice(idx, 1); - - //change all files urls to file objects - const filesConverted = await Promise.all(files.map(async file => { - if (typeof file === 'string') return await fileUrlToObject(file, "") - else return file; - })) - - onChange?.(filesConverted); - } - } - - const canUploadMore = multiple ? - !value || (value && value.length < max) - : - !value || value.length === 0 - - - const uploadBtn = props.uploadBtn ? - React.cloneElement(props.uploadBtn, { onClick: handleClick }) - : - - - return ( - <> - - { - canUploadMore && - <> - {uploadBtn} - - - } - - ) -}) - - -export default FilesInput; \ No newline at end of file diff --git a/src/Components/Inputs/FilesInput/FilesThumbnails.tsx b/src/Components/Inputs/FilesInput/FilesThumbnails.tsx deleted file mode 100644 index 362528e..0000000 --- a/src/Components/Inputs/FilesInput/FilesThumbnails.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { useMemo } from 'react' -import FileThumbnail from './FileThumbnail'; - -interface Props { - files?: (File | string)[] | string; - onRemove?: (idx: number) => void -} - -function processFiles(files: Props['files']) { - - if (!files) return []; - if (typeof files === 'string') return [files]; - return files; -} - -export default function FilesThumbnails({ files, onRemove }: Props) { - const filesConverted = useMemo(() => processFiles(files), [files]) - - return ( -
- { - filesConverted.map((file, idx) => onRemove?.(idx)} />) - } -
- ) -} diff --git a/src/Components/Inputs/FilesInputs/AvatarInput/AvatarInput.stories.tsx b/src/Components/Inputs/FilesInputs/AvatarInput/AvatarInput.stories.tsx new file mode 100644 index 0000000..73f451e --- /dev/null +++ b/src/Components/Inputs/FilesInputs/AvatarInput/AvatarInput.stories.tsx @@ -0,0 +1,29 @@ +import React from 'react' +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import AvatarInput from './AvatarInput'; +import { WrapFormController } from 'src/utils/storybook/decorators'; +import { ImageType } from '../SingleImageUploadInput/SingleImageUploadInput'; + +export default { + title: 'Shared/Inputs/Files Inputs/Avatar ', + component: AvatarInput, + decorators: [ + WrapFormController<{ avatar: ImageType | null }>({ + logValues: true, + name: "avatar", + defaultValues: { + avatar: null + } + })] +} as ComponentMeta; + +const Template: ComponentStory = (args, context) => { + + return + +} + + +export const Default = Template.bind({}); +Default.args = { +} \ No newline at end of file diff --git a/src/Components/Inputs/FilesInputs/AvatarInput/AvatarInput.tsx b/src/Components/Inputs/FilesInputs/AvatarInput/AvatarInput.tsx new file mode 100644 index 0000000..e912eca --- /dev/null +++ b/src/Components/Inputs/FilesInputs/AvatarInput/AvatarInput.tsx @@ -0,0 +1,105 @@ +import { motion } from 'framer-motion'; +import React, { ComponentProps, useRef } from 'react' +import { AiOutlineCloudUpload } from 'react-icons/ai'; +import { CgArrowsExchangeV } from 'react-icons/cg'; +import { FiCamera } from 'react-icons/fi'; +import { IoMdClose } from 'react-icons/io'; +import { RotatingLines } from 'react-loader-spinner'; +import { Nullable } from 'remirror'; +import { useIsDraggingOnElement } from 'src/utils/hooks'; +import SingleImageUploadInput from '../SingleImageUploadInput/SingleImageUploadInput' + +type Value = ComponentProps['value'] + +interface Props { + width?: number; + isRemovable?: boolean + value: Value; + onChange: (new_value: Nullable) => void +} + +export default function AvatarInput(props: Props) { + + const dropAreaRef = useRef(null!) + const isDragging = useIsDraggingOnElement({ ref: dropAreaRef }); + + return ( +
+ +
+ {!img && +
+

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

+
+ Drop a COVER IMAGE here or
Click to browse +
+
} + {img && <> + + {!isUploading && +
+ + + +
+ } + } + {isUploading && +
+ +
+ } + {isDraggingOnWindow && +
+ + + +
+ Drop here to upload +
+
+ } +
} + /> +
+ + ) +} diff --git a/src/Components/Inputs/FilesInputs/FileUploadInput/FileUploadInput.stories.tsx b/src/Components/Inputs/FilesInputs/FileUploadInput/FileUploadInput.stories.tsx new file mode 100644 index 0000000..2e5b4dd --- /dev/null +++ b/src/Components/Inputs/FilesInputs/FileUploadInput/FileUploadInput.stories.tsx @@ -0,0 +1,16 @@ +import React from 'react' +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import FileUploadInput from './FileUploadInput'; + +export default { + title: 'Shared/Inputs/Files Inputs/Basic', + component: FileUploadInput, + +} as ComponentMeta; + +const Template: ComponentStory = (args) => + + +export const DefaultButton = Template.bind({}); +DefaultButton.args = { +} \ No newline at end of file diff --git a/src/Components/Inputs/FilesInputs/FileUploadInput/FileUploadInput.tsx b/src/Components/Inputs/FilesInputs/FileUploadInput/FileUploadInput.tsx new file mode 100644 index 0000000..ebfef3c --- /dev/null +++ b/src/Components/Inputs/FilesInputs/FileUploadInput/FileUploadInput.tsx @@ -0,0 +1,141 @@ +import Uploady, { useUploady, useRequestPreSend, UPLOADER_EVENTS, } from "@rpldy/uploady"; +import { asUploadButton } from "@rpldy/upload-button"; +import Button from "src/Components/Button/Button"; +import { fetchUploadUrl } from "../fetch-upload-img-url"; +import ImagePreviews from "./ImagePreviews"; +import { FaImage } from "react-icons/fa"; +import UploadDropZone from "@rpldy/upload-drop-zone"; +import { forwardRef, useCallback } from "react"; +import styles from './styles.module.scss' +import { MdFileUpload } from "react-icons/md"; +import { AiOutlineCloudUpload } from "react-icons/ai"; +import { motion } from "framer-motion"; + +interface Props { + url: string; +} + +const UploadBtn = asUploadButton((props: any) => { + + useRequestPreSend(async (data) => { + + const filename = data.items?.[0].file.name ?? '' + + const url = await fetchUploadUrl({ filename }); + return { + options: { + destination: { + url + } + } + } + }) + + // const handleClick = async () => { + // // Make a request to get the url + // try { + // var bodyFormData = new FormData(); + // bodyFormData.append('requireSignedURLs', "false"); + // const res = await axios({ + // url: 'https://cors-anywhere.herokuapp.com/https://api.cloudflare.com/client/v4/accounts/783da4f06e5fdb9012c0632959a6f5b3/images/v2/direct_upload', + // method: 'POST', + // data: bodyFormData, + // headers: { + // "Authorization": "Bearer Xx2-CdsTliYkq6Ayz-1GX4CZubdQVxMwOSDbajP0", + // } + // }) + // uploady.upload(res.data.result.uploadUrl, { + // destination: res.data.result.uploadUrl + // }) + // } catch (error) { + // console.log(error); + + // } + + + // // make the request with the files + // // uploady.upload() + // } + + return +}); + + +const DropZone = forwardRef((props, ref) => { + const { onClick, ...buttonProps } = props; + + useRequestPreSend(async (data) => { + + const filename = data.items?.[0].file.name ?? '' + + const url = await fetchUploadUrl({ filename }); + return { + options: { + destination: { + url + } + } + } + }) + + const onZoneClick = useCallback( + (e: any) => { + if (onClick) { + onClick(e); + } + }, + [onClick] + ); + + return +
+ Drop your IMAGES here or +
+ + + Drop it to upload + +
+}) + +const DropZoneButton = asUploadButton(DropZone); + + +export default function FileUploadInput(props: Props) { + return ( + { + const { id, filename, variants } = item?.uploadResponse?.data?.result ?? {} + if (id) { + console.log(id, filename, variants); + } + } + }} + > + + {/* */} + + + ) +} diff --git a/src/Components/Inputs/FilesInputs/FileUploadInput/ImagePreviews.tsx b/src/Components/Inputs/FilesInputs/FileUploadInput/ImagePreviews.tsx new file mode 100644 index 0000000..e28b375 --- /dev/null +++ b/src/Components/Inputs/FilesInputs/FileUploadInput/ImagePreviews.tsx @@ -0,0 +1,92 @@ +import UploadPreview, { PreviewComponentProps, PreviewMethods } from '@rpldy/upload-preview' +import { useAbortItem, useItemAbortListener, useItemCancelListener, useItemErrorListener, useItemProgressListener } from '@rpldy/uploady'; +import React, { useState } from 'react' +import { RotatingLines } from 'react-loader-spinner'; + +export default function ImagePreviews() { + return ( +
+ +
+ ) +} + +function CustomImagePreview({ id, url }: PreviewComponentProps) { + + const [progress, setProgress] = useState(0); + const [itemState, setItemState] = useState(STATES.PROGRESS); + + + useItemProgressListener(item => { + if (item.completed > progress) { + setProgress(() => item.completed); + + if (item.completed === 100) { + setItemState(STATES.DONE) + } else { + setItemState(STATES.PROGRESS) + } + } + }, id); + + + + useItemAbortListener(item => { + setItemState(STATES.CANCELLED); + }, id); + + + useItemCancelListener(item => { + setItemState(STATES.CANCELLED); + }, id); + + useItemErrorListener(item => { + setItemState(STATES.ERROR); + }, id); + + return
+ +
+
+ {itemState === STATES.PROGRESS && +
+ +
} + {itemState === STATES.ERROR && +
+ Failed... +
} + {itemState === STATES.CANCELLED && +
+ Cancelled +
} +
; +}; + +const STATES = { + PROGRESS: "PROGRESS", + DONE: "DONE", + CANCELLED: "CANCELLED", + ERROR: "ERROR" +}; + +const STATE_COLORS = { + [STATES.PROGRESS]: "#f4e4a4", + [STATES.DONE]: "#a5f7b3", + [STATES.CANCELLED]: "#f7cdcd", + [STATES.ERROR]: "#ee4c4c" +}; \ No newline at end of file diff --git a/src/Components/Inputs/FilesInputs/FileUploadInput/styles.module.scss b/src/Components/Inputs/FilesInputs/FileUploadInput/styles.module.scss new file mode 100644 index 0000000..217ca39 --- /dev/null +++ b/src/Components/Inputs/FilesInputs/FileUploadInput/styles.module.scss @@ -0,0 +1,25 @@ +.zone { + background-color: #f2f4f7; + border-color: #e4e7ec; + + .active_content { + display: none; + } + + .idle_content { + display: block; + } + + &.active { + background-color: #b3a0ff; + border-color: #9e88ff; + + .active_content { + display: block; + } + + .idle_content { + display: none; + } + } +} diff --git a/src/Components/Inputs/FilesInputs/ScreenshotsInput/ImagePreviews.tsx b/src/Components/Inputs/FilesInputs/ScreenshotsInput/ImagePreviews.tsx new file mode 100644 index 0000000..74d4d64 --- /dev/null +++ b/src/Components/Inputs/FilesInputs/ScreenshotsInput/ImagePreviews.tsx @@ -0,0 +1,97 @@ +import UploadPreview, { PreviewComponentProps, PreviewMethods } from '@rpldy/upload-preview' +import { useAbortItem, useItemAbortListener, useItemCancelListener, useItemErrorListener, useItemProgressListener } from '@rpldy/uploady'; +import { useState } from 'react' +import ScreenShotsThumbnail from './ScreenshotThumbnail' + +export default function ImagePreviews() { + return ( + + ) +} + +function CustomImagePreview({ id, url }: PreviewComponentProps) { + + const [progress, setProgress] = useState(0); + const [itemState, setItemState] = useState(STATES.PROGRESS); + + const abortItem = useAbortItem(); + + + useItemProgressListener(item => { + if (item.completed > progress) { + setProgress(() => item.completed); + + if (item.completed === 100) { + setItemState(STATES.DONE) + } else { + setItemState(STATES.PROGRESS) + } + } + }, id); + + + + useItemAbortListener(item => { + setItemState(STATES.CANCELLED); + }, id); + + + useItemCancelListener(item => { + setItemState(STATES.CANCELLED); + }, id); + + useItemErrorListener(item => { + setItemState(STATES.ERROR); + }, id); + + if (itemState === STATES.DONE || itemState === STATES.CANCELLED) + return null + + return { + abortItem(id) + }} + /> + + // return
+ // + //
+ //
+ // {itemState === STATES.PROGRESS && + //
+ // + //
} + // {itemState === STATES.ERROR && + //
+ // Failed... + //
} + // {itemState === STATES.CANCELLED && + //
+ // Cancelled + //
} + //
; +}; + +const STATES = { + PROGRESS: "PROGRESS", + DONE: "DONE", + CANCELLED: "CANCELLED", + ERROR: "ERROR" +}; diff --git a/src/Components/Inputs/FilesInputs/ScreenshotsInput/ScreenshotThumbnail.tsx b/src/Components/Inputs/FilesInputs/ScreenshotsInput/ScreenshotThumbnail.tsx new file mode 100644 index 0000000..0003c43 --- /dev/null +++ b/src/Components/Inputs/FilesInputs/ScreenshotsInput/ScreenshotThumbnail.tsx @@ -0,0 +1,52 @@ +import React from 'react' +import { FaTimes } from 'react-icons/fa'; +import { RotatingLines } from 'react-loader-spinner'; + +interface Props { + url?: string, + isLoading?: boolean; + isError?: boolean; + onCancel?: () => void; + +} + +export default function ScreenshotThumbnail({ url, isLoading, isError, onCancel }: Props) { + + const isEmpty = !url; + + return ( +
+ {!isEmpty && } +
+
+ {isLoading && +
+ +
} + {isError && +
+ Failed... +
} + {!isEmpty && + + } +
+ ) +} diff --git a/src/Components/Inputs/FilesInputs/ScreenshotsInput/ScreenshotsInput.stories.tsx b/src/Components/Inputs/FilesInputs/ScreenshotsInput/ScreenshotsInput.stories.tsx new file mode 100644 index 0000000..1575707 --- /dev/null +++ b/src/Components/Inputs/FilesInputs/ScreenshotsInput/ScreenshotsInput.stories.tsx @@ -0,0 +1,84 @@ +import React from 'react' +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import ScreenshotsInput, { ScreenshotType } from './ScreenshotsInput'; +import { WrapForm, WrapFormController } from 'src/utils/storybook/decorators'; + +export default { + title: 'Shared/Inputs/Files Inputs/Screenshots', + component: ScreenshotsInput, + decorators: [ + WrapFormController<{ screenshots: Array }>({ + logValues: true, + name: "screenshots", + defaultValues: { + screenshots: [] + } + })] +} as ComponentMeta; + +const Template: ComponentStory = (args, context) => { + + return + +} + + +export const Empty = Template.bind({}); +Empty.args = { +} + +export const WithValues = Template.bind({}); +WithValues.decorators = [ + WrapFormController<{ screenshots: Array }>({ + logValues: true, + name: "screenshots", + defaultValues: { + screenshots: [{ + id: '123', + name: 'tree', + url: "https://picsum.photos/id/1021/800/800.jpg" + }, + { + id: '555', + name: 'whatever', + url: "https://picsum.photos/id/600/800/800.jpg" + },] + } + }) as any +]; +WithValues.args = { +} + +export const Full = Template.bind({}); +Full.decorators = [ + WrapFormController<{ screenshots: Array }>({ + logValues: true, + name: "screenshots", + defaultValues: { + screenshots: [ + { + id: '123', + name: 'tree', + url: "https://picsum.photos/id/1021/800/800.jpg" + }, + { + id: '555', + name: 'whatever', + url: "https://picsum.photos/id/600/800/800.jpg" + }, + { + id: '562', + name: 'Moon', + url: "https://picsum.photos/id/32/800/800.jpg" + }, + { + id: '342', + name: 'Sun', + url: "https://picsum.photos/id/523/800/800.jpg" + }, + ] + } + }) as any +]; +Full.args = { +} \ No newline at end of file diff --git a/src/Components/Inputs/FilesInputs/ScreenshotsInput/ScreenshotsInput.tsx b/src/Components/Inputs/FilesInputs/ScreenshotsInput/ScreenshotsInput.tsx new file mode 100644 index 0000000..b5bf6c0 --- /dev/null +++ b/src/Components/Inputs/FilesInputs/ScreenshotsInput/ScreenshotsInput.tsx @@ -0,0 +1,151 @@ +import Uploady, { useRequestPreSend, UPLOADER_EVENTS } from "@rpldy/uploady"; +import { asUploadButton } from "@rpldy/upload-button"; +// import { fetchUploadUrl } from "./fetch-upload-img-url"; +import ImagePreviews from "./ImagePreviews"; +import UploadDropZone from "@rpldy/upload-drop-zone"; +import { forwardRef, useCallback, useState } from "react"; +import styles from './styles.module.scss' +import { AiOutlineCloudUpload } from "react-icons/ai"; +import { motion } from "framer-motion"; +import { getMockSenderEnhancer } from "@rpldy/mock-sender"; +import ScreenshotThumbnail from "./ScreenshotThumbnail"; +import { FiCamera } from "react-icons/fi"; +import { Control, Path, useController } from "react-hook-form"; + + + +const mockSenderEnhancer = getMockSenderEnhancer({ + delay: 1500, +}); + +const MAX_UPLOAD_COUNT = 4 as const; + +export interface ScreenshotType { + id: string, + name: string, + url: string; +} + +interface Props { + value: ScreenshotType[], + onChange: (new_value: ScreenshotType[]) => void +} + + +export default function ScreenshotsInput(props: Props) { + + const { value: uploadedFiles, onChange } = props; + + + const [uploadingCount, setUploadingCount] = useState(0) + + + const canUploadMore = uploadingCount + uploadedFiles.length < MAX_UPLOAD_COUNT; + const placeholdersCount = (MAX_UPLOAD_COUNT - (uploadingCount + uploadedFiles.length + 1)); + + + return ( + { + setUploadingCount(v => v + batch.items.length) + }, + [UPLOADER_EVENTS.ITEM_FINALIZE]: () => setUploadingCount(v => v - 1), + [UPLOADER_EVENTS.ITEM_FINISH]: (item) => { + + // Just for mocking purposes + const dataUrl = URL.createObjectURL(item.file); + + const { id, filename, variants } = item?.uploadResponse?.data?.result ?? { + id: Math.random().toString(), + filename: item.file.name, + variants: [ + "", + dataUrl + ] + } + if (id) { + onChange([...uploadedFiles, { id, name: filename, url: variants[1] }].slice(-MAX_UPLOAD_COUNT)) + } + } + }} + > + +
+ {canUploadMore && } + {uploadedFiles.map(f => { + onChange(uploadedFiles.filter(file => file.id !== f.id)) + }} />)} + + {(placeholdersCount > 0) && + Array(placeholdersCount).fill(0).map((_, idx) => )} +
+
+ ) +} + +const DropZone = forwardRef((props, ref) => { + const { onClick, ...buttonProps } = props; + + + useRequestPreSend(async (data) => { + + const filename = data.items?.[0].file.name ?? '' + + // const url = await fetchUploadUrl({ filename }); + return { + options: { + destination: { + url: "URL" + } + } + } + }) + + const onZoneClick = useCallback( + (e: any) => { + if (onClick) { + onClick(e); + } + }, + [onClick] + ); + + return +
+

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

+
+ Add Image +
+ } + {isUploading && +
+ +
+ } +
} + /> +
+ + ) +} diff --git a/src/Components/Inputs/FilesInputs/fetch-upload-img-url.tsx b/src/Components/Inputs/FilesInputs/fetch-upload-img-url.tsx new file mode 100644 index 0000000..e2c9b19 --- /dev/null +++ b/src/Components/Inputs/FilesInputs/fetch-upload-img-url.tsx @@ -0,0 +1,25 @@ +import axios from "axios"; +import { NotificationsService } from "src/services"; + +export async function fetchUploadUrl(options?: Partial<{ filename: string }>) { + + const { filename } = options ?? {} + + try { + const bodyFormData = new FormData(); + bodyFormData.append('requireSignedURLs', "false"); + const res = await axios({ + url: 'https://cors-anywhere.herokuapp.com/https://api.cloudflare.com/client/v4/accounts/783da4f06e5fdb9012c0632959a6f5b3/images/v2/direct_upload', + method: 'POST', + data: bodyFormData, + headers: { + "Authorization": "Bearer XXX", + } + }) + return res.data.result.uploadURL as string; + } catch (error) { + console.log(error); + NotificationsService.error("A network error happened.") + return "couldnt fetch upload url"; + } +} \ No newline at end of file diff --git a/src/Components/Inputs/Selects/BasicSelectInput/BasicSelectInput.stories.tsx b/src/Components/Inputs/Selects/BasicSelectInput/BasicSelectInput.stories.tsx new file mode 100644 index 0000000..72af3cc --- /dev/null +++ b/src/Components/Inputs/Selects/BasicSelectInput/BasicSelectInput.stories.tsx @@ -0,0 +1,31 @@ +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import { MdOutlineKingBed } from 'react-icons/md'; + +import BasicSelectInput from './BasicSelectInput'; + +export default { + title: 'Shared/Inputs/Select/Basic', + component: BasicSelectInput, + +} as ComponentMeta; + +const Template: ComponentStory = (args) => + + +export const Default = Template.bind({}); +Default.args = { + // defaultValue: 4, + options: [ + { value: 1, label: "Option 1" }, + { value: 2, label: 'Option 2' }, + { value: 3, label: 'Option 3' }, + { value: 4, label: 'Option 4' }, + ], + onChange: (nv) => alert("New value is: " + nv), + labelField: 'label', + valueField: "value" +} + + + + diff --git a/src/Components/Inputs/Selects/BasicSelectInput/BasicSelectInput.tsx b/src/Components/Inputs/Selects/BasicSelectInput/BasicSelectInput.tsx new file mode 100644 index 0000000..00d3d0a --- /dev/null +++ b/src/Components/Inputs/Selects/BasicSelectInput/BasicSelectInput.tsx @@ -0,0 +1,144 @@ + +import { useCallback } from "react"; +import Select, { OnChangeValue, StylesConfig, components, OptionProps } from "react-select"; +import { ControlledStateHandler } from "src/utils/interfaces"; + + + +type Props, IsMulti extends boolean = boolean> = { + options: T[]; + labelField: keyof T + valueField: keyof T + placeholder?: string + disabled?: boolean + isLoading?: boolean; + isClearable?: boolean; + control?: any, + name?: string, + className?: string, + renderOption?: (option: OptionProps) => JSX.Element +} & ControlledStateHandler + + + + + + +export const selectCustomStyle: StylesConfig = ({ + control: (styles, state) => ({ + ...styles, + padding: '5px 12px', + borderRadius: 12, + // border: 'none', + // boxShadow: 'none', + + ":hover": { + cursor: "pointer" + }, + ":focus-within": { + '--tw-border-opacity': '1', + borderColor: 'rgb(179 160 255 / var(--tw-border-opacity))', + outlineColor: '#9E88FF', + '--tw-ring-offset-shadow': 'var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)', + '--tw-ring-shadow': 'var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color)', + boxShadow: 'var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000)', + '--tw-ring-color': 'rgb(179 160 255 / var(--tw-ring-opacity))', + '--tw-ring-opacity': '0.5' + } + }), + indicatorSeparator: (styles, state) => ({ + ...styles, + display: "none" + }), + input: (styles, state) => ({ + ...styles, + " input": { + boxShadow: 'none !important' + }, + }), + menu: (styles, state) => ({ + ...styles, + padding: 8, + borderRadius: "16px !important" + }), +}) + + + +export default function BasicSelectInput, IsMulti extends boolean>({ + options, + labelField, + valueField, + placeholder = "Select Option...", + isMulti, + isClearable, + disabled, + className, + value, + onChange, + onBlur, + renderOption, + ...props +}: Props) { + + + + + return ( +
+ 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)', + }, + })} + /> + {/*
+ {(value as Tag[]).map((tag, idx) => handleRemove(idx)} >{tag.title})} +
*/} +
+ ) +} + + 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,) => { - 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 ( -
- setUrlInput(e.target.value)} - placeholder='https://images.com/my-image' - /> -
-
-
-

- Alt Text -

-
- setAltInput(e.target.value)} - placeholder='' - /> -
-
-
-
- {urlInput && {altInput}} -
-
- - -
- - - - ) -} diff --git a/src/Components/Inputs/TextEditor/InsertImageModal/InsertImageModal.stories.tsx b/src/Components/Modals/InsertImageModal/InsertImageModal.stories.tsx similarity index 68% rename from src/Components/Inputs/TextEditor/InsertImageModal/InsertImageModal.stories.tsx rename to src/Components/Modals/InsertImageModal/InsertImageModal.stories.tsx index ed4f283..b5d7197 100644 --- a/src/Components/Inputs/TextEditor/InsertImageModal/InsertImageModal.stories.tsx +++ b/src/Components/Modals/InsertImageModal/InsertImageModal.stories.tsx @@ -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 = (args) => ; export const Default = Template.bind({}); +Default.args = { + callbackAction: { + type: "INSERT_IMAGE_IN_STORY", + payload: { + src: "", + alt: "", + } + } +} diff --git a/src/Components/Modals/InsertImageModal/InsertImageModal.tsx b/src/Components/Modals/InsertImageModal/InsertImageModal.tsx new file mode 100644 index 0000000..4a1205d --- /dev/null +++ b/src/Components/Modals/InsertImageModal/InsertImageModal.tsx @@ -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(null) + const [altInput, setAltInput] = useState("") + const dispatch = useAppDispatch(); + + const dropAreaRef = useRef(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 ( + + +

Add Image

+
+ {/*
+
+

+ Image URL +

+
+ setUrlInput(e.target.value)} + placeholder='https://images.com/my-image' + /> +
+
+
+

+ Alt Text +

+
+ setAltInput(e.target.value)} + placeholder='' + /> +
+
+
+
+ {urlInput && {altInput}} +
*/} +
+ {img && <> + + {!isUploading && + } + } + {!img && + <> +

+
+ Drop an IMAGE here or
Click to browse +
+ } + {isUploading && +
+ +
+ } + {isDraggingOnWindow && +
+ + + +
+ Drop here to upload +
+
+ } +
} + /> +
+

+ Alternative Text +

+
+ setAltInput(e.target.value)} + placeholder='A description for the content of this image' + /> +
+
+
+ + +
+ + +
+ ) +} diff --git a/src/Components/Inputs/TextEditor/InsertImageModal/index.tsx b/src/Components/Modals/InsertImageModal/index.tsx similarity index 100% rename from src/Components/Inputs/TextEditor/InsertImageModal/index.tsx rename to src/Components/Modals/InsertImageModal/index.tsx diff --git a/src/Components/Modals/Modal/Modal.tsx b/src/Components/Modals/Modal/Modal.tsx index 66fe3d3..dc5a5e8 100644 --- a/src/Components/Modals/Modal/Modal.tsx +++ b/src/Components/Modals/Modal/Modal.tsx @@ -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 dispatch(removeClosedModal(props.id))} + onAfterClose={onAfterClose} contentElement={(_props, children) =>
{openModals.map((modal, idx) => { const Child = ALL_MODALS[modal.Modal]; - return ( -
- -

- Bolt fun logo -

- - + Guide + + +
  • + + Donate + +
  • + -
    +
    - + - {/* */} - {/* {isWalletConnected ? + {/* {isWalletConnected ? : } */} - {currentSection === 'apps' && - - } - - {curUser !== undefined && - (curUser ? - }> - { - 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 - - { - e.syntheticEvent.preventDefault(); - navigate("/edit-profile"); - }} - className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12' - > - ⚙️ Settings - - { - e.syntheticEvent.preventDefault(); - navigate("/logout"); - }} - className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12' - > - 👋 Logout - - - - : - - ) - } -
    - - setSearchOpen(false)} - onResultClick={() => setSearchOpen(false)} - /> + {currentSection === 'apps' && + + } + {curUser !== undefined && + (curUser ? + }> + { + 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 + + { + e.syntheticEvent.preventDefault(); + navigate("/edit-profile"); + }} + className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12' + > + ⚙️ Settings + + { + e.syntheticEvent.preventDefault(); + navigate("/logout"); + }} + className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12' + > + 👋 Logout + + + + : + + ) + } +
    + + setSearchOpen(false)} + onResultClick={() => setSearchOpen(false)} + /> + +
    +
    diff --git a/src/Components/Navbar/NavMobile.tsx b/src/Components/Navbar/NavMobile.tsx index 08fadad..b775ea0 100644 --- a/src/Components/Navbar/NavMobile.tsx +++ b/src/Components/Navbar/NavMobile.tsx @@ -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 (
    @@ -164,7 +166,15 @@ export default function NavMobile() { Projects -
  • +
  • + toggleDrawerOpen(false)} + className='text-body4 font-bold hover:text-primary-600'> + Events + +
  • + {/*
  • + */}
  • [...oldSparks, ...newSparks]) diff --git a/src/api/auth.ts b/src/api/auth.ts new file mode 100644 index 0000000..e154815 --- /dev/null +++ b/src/api/auth.ts @@ -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; +} \ No newline at end of file diff --git a/src/api/uploading.ts b/src/api/uploading.ts new file mode 100644 index 0000000..59e77d7 --- /dev/null +++ b/src/api/uploading.ts @@ -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; +} \ No newline at end of file diff --git a/src/features/Auth/pages/LoginPage/LoginPage.tsx b/src/features/Auth/pages/LoginPage/LoginPage.tsx index 6fb0d75..5b4b37e 100644 --- a/src/features/Auth/pages/LoginPage/LoginPage.tsx +++ b/src/features/Auth/pages/LoginPage/LoginPage.tsx @@ -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(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 =

    - Hello: @{meQuery.data?.me?.name.slice(0, 10)}... + Hello: @{trimText(meQuery.data?.me?.name, 10)}

    - +
    else - content =
    - + content =

    Login with lightning ⚡

    Scan this code or copy + paste it to your lightning wallet. Or click to login with your browser's wallet.

    -
    ; + + return ( -
    + <> {`makers.bolt.fun`} - {content} -
    +
    +
    + {content} +
    +
    + ) } diff --git a/src/features/Hackathons/Components/HackathonsList/HackathonsList.tsx b/src/features/Hackathons/Components/HackathonsList/HackathonsList.tsx index c51889d..27c6832 100644 --- a/src/features/Hackathons/Components/HackathonsList/HackathonsList.tsx +++ b/src/features/Hackathons/Components/HackathonsList/HackathonsList.tsx @@ -19,6 +19,7 @@ export default function HackathonsList(props: Props) { + }
    diff --git a/src/features/Hackathons/Components/SortByFilter/SortByFilter.tsx b/src/features/Hackathons/Components/SortByFilter/SortByFilter.tsx index 037b88c..7f3eb4d 100644 --- a/src/features/Hackathons/Components/SortByFilter/SortByFilter.tsx +++ b/src/features/Hackathons/Components/SortByFilter/SortByFilter.tsx @@ -1,5 +1,4 @@ import React, { useState } from 'react' -import AutoComplete from 'src/Components/Inputs/Autocomplete/Autocomplete'; import { useMediaQuery } from 'src/utils/hooks'; import { MEDIA_QUERIES } from 'src/utils/theme'; diff --git a/src/features/Hackathons/pages/HackathonsPage/HackathonsPage.tsx b/src/features/Hackathons/pages/HackathonsPage/HackathonsPage.tsx index 83b99f0..e572057 100644 --- a/src/features/Hackathons/pages/HackathonsPage/HackathonsPage.tsx +++ b/src/features/Hackathons/pages/HackathonsPage/HackathonsPage.tsx @@ -7,6 +7,10 @@ import SortByFilter from '../../Components/SortByFilter/SortByFilter' import styles from './styles.module.scss' import { Helmet } from 'react-helmet' import { Fulgur } from 'src/Components/Ads/Fulgur' +import { IoLocationOutline } from 'react-icons/io5' +import { Link } from 'react-router-dom' +import { createRoute } from 'src/utils/routing' +import { bannerData } from 'src/features/Projects/pages/ExplorePage/Header/Header' export default function HackathonsPage() { @@ -28,36 +32,55 @@ export default function HackathonsPage() {
    - -
    - -
    + {/* */} +
    + +
    +
    ) } diff --git a/src/features/Posts/Components/PostCard/StoryCard/StoryCard.tsx b/src/features/Posts/Components/PostCard/StoryCard/StoryCard.tsx index 286a316..d31164c 100644 --- a/src/features/Posts/Components/PostCard/StoryCard/StoryCard.tsx +++ b/src/features/Posts/Components/PostCard/StoryCard/StoryCard.tsx @@ -39,7 +39,7 @@ export default function StoryCard({ story }: Props) { {story.cover_image && }
    - +

    {story.title}

    {story.excerpt}...

    diff --git a/src/features/Posts/pages/CreatePostPage/Components/BountyForm/BountyForm.tsx b/src/features/Posts/pages/CreatePostPage/Components/BountyForm/BountyForm.tsx index 017bd37..368a9b6 100644 --- a/src/features/Posts/pages/CreatePostPage/Components/BountyForm/BountyForm.tsx +++ b/src/features/Posts/pages/CreatePostPage/Components/BountyForm/BountyForm.tsx @@ -2,8 +2,9 @@ import { yupResolver } from "@hookform/resolvers/yup"; import { Controller, FormProvider, NestedValue, Resolver, SubmitHandler, useForm } from "react-hook-form"; import Button from "src/Components/Button/Button"; import DatePicker from "src/Components/Inputs/DatePicker/DatePicker"; -import FilesInput from "src/Components/Inputs/FilesInput/FilesInput"; import TagsInput from "src/Components/Inputs/TagsInput/TagsInput"; +import { Tag } from "src/graphql"; +import { imageSchema } from "src/utils/validation"; import * as yup from "yup"; import ContentEditor from "../ContentEditor/ContentEditor"; @@ -31,29 +32,14 @@ const schema = yup.object({ .string() .required() .min(50, 'you have to write at least 10 words'), - cover_image: yup - .lazy((value: string | File[]) => { - switch (typeof value) { - case 'object': - return yup - .array() - .test("fileSize", "File Size is too large", (files) => (files as File[]).every(file => file.size <= 5242880)) - .test("fileType", "Unsupported File Format, only png/jpg/jpeg images are allowed", - (files) => (files as File[]).every((file: File) => - ["image/jpeg", "image/png", "image/jpg"].includes(file.type))) - case 'string': - return yup.string().url(); - default: - return yup.mixed() - } - }) + cover_image: imageSchema, }).required(); interface IFormInputs { title: string deadline: Date bounty_amount: number - tags: NestedValue + tags: NestedValue cover_image: NestedValue | string body: string } @@ -86,7 +72,7 @@ export default function BountyForm() {
    - ( @@ -97,7 +83,7 @@ export default function BountyForm() { uploadText='Add a cover image' /> )} - /> + /> */}

    {errors.cover_image?.message}

    @@ -155,10 +141,20 @@ export default function BountyForm() {

    Tags

    - ( + + )} /> + {errors.tags &&

    {errors.tags.message}

    } diff --git a/src/features/Posts/pages/CreatePostPage/Components/DraftsContainer/DraftContainer.stories.tsx b/src/features/Posts/pages/CreatePostPage/Components/DraftsContainer/DraftContainer.stories.tsx index 05d2fc1..f47f3a3 100644 --- a/src/features/Posts/pages/CreatePostPage/Components/DraftsContainer/DraftContainer.stories.tsx +++ b/src/features/Posts/pages/CreatePostPage/Components/DraftsContainer/DraftContainer.stories.tsx @@ -14,7 +14,6 @@ export default { decorators: [WithModals, WrapForm({ defaultValues: { tags: [], - cover_image: [], } })] } as ComponentMeta; diff --git a/src/features/Posts/pages/CreatePostPage/Components/DraftsContainer/DraftsContainer.tsx b/src/features/Posts/pages/CreatePostPage/Components/DraftsContainer/DraftsContainer.tsx index 0ef4c01..6ba29a8 100644 --- a/src/features/Posts/pages/CreatePostPage/Components/DraftsContainer/DraftsContainer.tsx +++ b/src/features/Posts/pages/CreatePostPage/Components/DraftsContainer/DraftsContainer.tsx @@ -10,7 +10,7 @@ import { NotificationsService } from 'src/services'; import { getDateDifference } from 'src/utils/helperFunctions'; import { useAppDispatch } from 'src/utils/hooks'; import { useReduxEffect } from 'src/utils/hooks/useReduxEffect'; -import { IStoryFormInputs } from '../../CreateStoryPage/CreateStoryPage'; +import { CreateStoryType, IStoryFormInputs } from '../../CreateStoryPage/CreateStoryPage'; interface Props { id?: string; @@ -28,7 +28,7 @@ export default function DraftsContainer({ id, type, onDraftLoad }: Props) { const [deleteStory] = useDeleteStoryMutation({ refetchQueries: ['GetMyDrafts'] }) - const { setValue } = useFormContext() + const { setValue } = useFormContext() const dispatch = useAppDispatch(); const [loading, setLoading] = useState(false) @@ -45,7 +45,7 @@ export default function DraftsContainer({ id, type, onDraftLoad }: Props) { setValue('title', data.getPostById.title); setValue('tags', data.getPostById.tags); setValue('body', data.getPostById.body); - setValue('cover_image', data.getPostById.cover_image ? [data.getPostById.cover_image] : []); + setValue('cover_image', data.getPostById.cover_image ? { url: data.getPostById.cover_image, id: null, name: null } : null); setValue('is_published', data.getPostById.is_published); } diff --git a/src/features/Posts/pages/CreatePostPage/Components/QuestionForm/QuestionForm.tsx b/src/features/Posts/pages/CreatePostPage/Components/QuestionForm/QuestionForm.tsx index 7a5d86f..9643781 100644 --- a/src/features/Posts/pages/CreatePostPage/Components/QuestionForm/QuestionForm.tsx +++ b/src/features/Posts/pages/CreatePostPage/Components/QuestionForm/QuestionForm.tsx @@ -1,8 +1,8 @@ import { yupResolver } from "@hookform/resolvers/yup"; import { Controller, FormProvider, NestedValue, Resolver, SubmitHandler, useForm } from "react-hook-form"; import Button from "src/Components/Button/Button"; -import FilesInput from "src/Components/Inputs/FilesInput/FilesInput"; import TagsInput from "src/Components/Inputs/TagsInput/TagsInput"; +import { Tag } from "src/graphql"; import * as yup from "yup"; import ContentEditor from "../ContentEditor/ContentEditor"; @@ -29,7 +29,7 @@ const schema = yup.object({ interface IFormInputs { title: string - tags: NestedValue + tags: NestedValue cover_image: NestedValue | string body: string } @@ -60,7 +60,7 @@ export default function QuestionForm() {
    - ( @@ -71,7 +71,7 @@ export default function QuestionForm() { uploadText='Add a cover image' /> )} - /> + /> */}

    {errors.cover_image?.message}

    @@ -95,9 +95,18 @@ export default function QuestionForm() {

    Tags

    - ( + + )} /> {errors.tags &&

    {errors.tags.message} diff --git a/src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm.stories.tsx b/src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm.stories.tsx index ea4d469..e89e15b 100644 --- a/src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm.stories.tsx +++ b/src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm.stories.tsx @@ -13,7 +13,6 @@ export default { decorators: [WithModals, WrapForm({ defaultValues: { tags: [], - cover_image: [], } })] } as ComponentMeta; diff --git a/src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm.tsx b/src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm.tsx index e18a6d2..fe60d0c 100644 --- a/src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm.tsx +++ b/src/features/Posts/pages/CreatePostPage/Components/StoryForm/StoryForm.tsx @@ -1,7 +1,6 @@ import { useEffect, useRef, useState } from 'react' import { Controller, useFormContext } from "react-hook-form"; import Button from "src/Components/Button/Button"; -import FilesInput from "src/Components/Inputs/FilesInput/FilesInput"; import TagsInput from "src/Components/Inputs/TagsInput/TagsInput"; import ContentEditor from "../ContentEditor/ContentEditor"; import { useCreateStoryMutation } from 'src/graphql' @@ -13,7 +12,8 @@ import { createRoute } from 'src/utils/routing'; import PreviewPostCard from '../PreviewPostCard/PreviewPostCard' import { StorageService } from 'src/services'; import { useThrottledCallback } from '@react-hookz/web'; -import { CreateStoryType, IStoryFormInputs } from '../../CreateStoryPage/CreateStoryPage'; +import { CreateStoryType } from '../../CreateStoryPage/CreateStoryPage'; +import CoverImageInput from 'src/Components/Inputs/FilesInputs/CoverImageInput/CoverImageInput'; interface Props { isUpdating?: boolean; @@ -29,7 +29,7 @@ export default function StoryForm(props: Props) { const dispatch = useAppDispatch(); const navigate = useNavigate(); - const { handleSubmit, control, register, trigger, getValues, watch, reset } = useFormContext(); + const { handleSubmit, control, register, trigger, getValues, watch, reset } = useFormContext(); const [editMode, setEditMode] = useState(true) @@ -80,7 +80,7 @@ export default function StoryForm(props: Props) { refetchQueries: ['GetMyDrafts'] }); - const clickSubmit = (publish_now: boolean) => handleSubmit(data => { + const clickSubmit = (publish_now: boolean) => handleSubmit(data => { setLoading(true); createStory({ variables: { @@ -90,7 +90,7 @@ export default function StoryForm(props: Props) { body: data.body, tags: data.tags.map(t => t.title), is_published: publish_now, - cover_image: (data.cover_image[0] ?? null) as string | null, + cover_image: data.cover_image, }, } }) @@ -103,6 +103,8 @@ export default function StoryForm(props: Props) { const { ref: registerTitleRef, ...titleRegisteration } = register('title'); + + return ( <>

    @@ -117,19 +119,21 @@ export default function StoryForm(props: Props) {
    - ( - + { + onChange(e) + }} + // uploadText='Add a cover image' /> - )} - /> + + } + /> +
    @@ -153,11 +157,21 @@ export default function StoryForm(props: Props) { />
    - ( + + )} /> +
    } - {!editMode && } + {!editMode && }
    -
    -
    - {postType === 'story' && <> - {/*

    + onClick={() => navigate(-1)} + > + + +

    +
    + {postType === 'story' && <> + {/*

    Write a Story

    */} - - } - {postType === 'bounty' && <> -

    - Write a Bounty -

    - - } - {postType === 'question' && <> -

    - Write a Question -

    - - } + + } + {/* {postType === 'bounty' && <> +

    + Write a Bounty +

    + + } + {postType === 'question' && <> +

    + Write a Question +

    + + } */} +
    diff --git a/src/features/Posts/pages/CreatePostPage/CreateStoryPage/CreateStoryPage.tsx b/src/features/Posts/pages/CreatePostPage/CreateStoryPage/CreateStoryPage.tsx index d5f8b51..1db1541 100644 --- a/src/features/Posts/pages/CreatePostPage/CreateStoryPage/CreateStoryPage.tsx +++ b/src/features/Posts/pages/CreatePostPage/CreateStoryPage/CreateStoryPage.tsx @@ -1,74 +1,64 @@ import { yupResolver } from "@hookform/resolvers/yup"; import { useRef, useState } from "react"; +import { ErrorBoundary, withErrorBoundary } from "react-error-boundary"; import { FormProvider, NestedValue, Resolver, useForm } from "react-hook-form"; -import { Post_Type } from "src/graphql"; +import ErrorPage from "src/Components/Errors/ErrorPage/ErrorPage"; +import { CreateStoryMutationVariables, Post_Type } from "src/graphql"; import { StorageService } from "src/services"; import { useAppSelector } from "src/utils/hooks"; import { Override } from "src/utils/interfaces"; +import { imageSchema, tagSchema } from "src/utils/validation"; import * as yup from "yup"; import DraftsContainer from "../Components/DraftsContainer/DraftsContainer"; import ErrorsContainer from "../Components/ErrorsContainer/ErrorsContainer"; import StoryForm from "../Components/StoryForm/StoryForm"; import styles from './styles.module.scss' -const FileSchema = yup.lazy((value: string | File[]) => { - switch (typeof value) { - case 'object': - return yup.mixed() - .test("fileSize", "File Size is too large", file => file.size <= 5242880) - .test("fileType", "Unsupported File Format, only png/jpg/jpeg images are allowed", - (file: File) => - ["image/jpeg", "image/png", "image/jpg"].includes(file.type)) - case 'string': - return yup.string().url(); - default: - return yup.mixed() - } -}) + const schema = yup.object({ title: yup.string().trim().required().min(10, 'Story title must be 2+ words').transform(v => v.replace(/(\r\n|\n|\r)/gm, "")), - tags: yup.array().required().min(1, 'Add at least one tag'), - body: yup.string().required().min(50, 'Post must contain at least 10+ words'), - cover_image: yup.array().of(FileSchema as any) + tags: yup.array().of(tagSchema).required().min(1, 'Add at least one tag'), + body: yup.string().required("Write some content in the post").min(50, 'Post must contain at least 10+ words'), + cover_image: imageSchema.nullable(true), }).required(); -export interface IStoryFormInputs { - id: number | null - title: string - tags: NestedValue<{ title: string }[]> - cover_image: NestedValue | NestedValue - body: string - is_published: boolean | null +type ApiStoryInput = NonNullable; + +export type IStoryFormInputs = { + id: ApiStoryInput['id'] + title: ApiStoryInput['title'] + body: ApiStoryInput['body'] + cover_image: NestedValue> | null + tags: NestedValue + is_published: ApiStoryInput['is_published'] } - - export type CreateStoryType = Override const storageService = new StorageService('story-edit'); -export default function CreateStoryPage() { +function CreateStoryPage() { const { story } = useAppSelector(state => ({ story: state.staging.story || storageService.get() })) - const formMethods = useForm({ - resolver: yupResolver(schema) as Resolver, + const formMethods = useForm({ + resolver: yupResolver(schema) as Resolver, shouldFocusError: false, defaultValues: { id: story?.id ?? null, title: story?.title ?? '', - cover_image: story?.cover_image ?? [], + cover_image: story?.cover_image, tags: story?.tags ?? [], body: story?.body ?? '', is_published: story?.is_published ?? false, @@ -103,3 +93,6 @@ export default function CreateStoryPage() { ) } + +// TODO: change the default cover_image on error +export default withErrorBoundary(CreateStoryPage, { FallbackComponent: ErrorPage, onError: () => { storageService.set({ ...storageService.get()!, cover_image: null as any }) } }) \ No newline at end of file diff --git a/src/features/Posts/pages/FeedPage/FeedPage.tsx b/src/features/Posts/pages/FeedPage/FeedPage.tsx index a4257cb..c887bcd 100644 --- a/src/features/Posts/pages/FeedPage/FeedPage.tsx +++ b/src/features/Posts/pages/FeedPage/FeedPage.tsx @@ -13,6 +13,10 @@ import Button from 'src/Components/Button/Button' import { FaDiscord } from 'react-icons/fa' import { FiArrowRight } from 'react-icons/fi' import { capitalize } from 'src/utils/helperFunctions' +import { bannerData } from 'src/features/Projects/pages/ExplorePage/Header/Header' +import { createRoute, PAGES_ROUTES } from 'src/utils/routing' +import { Link } from 'react-router-dom' +import { IoLocationOutline } from 'react-icons/io5' export default function FeedPage() { @@ -39,82 +43,93 @@ export default function FeedPage() { return ( <> - {`Bolt.Fun Stories`} - + {`Bolt.Fun`} +
    -
    - {tagFilter &&

    - setTagFilter(null)}>Stories - - {tagFilter.title} -

    } -

    { - tagFilter ? - <>{tagFilter.icon} {capitalize(tagFilter.title)} - : - "Stories ✍🏼" - }

    -
    -
    - -
    -
    - -
    - - + +
    +
    + {tagFilter &&

    + setTagFilter(null)}>Stories + + {tagFilter.title} +

    } +

    { + tagFilter && + <>{tagFilter.icon} {capitalize(tagFilter.title)} + }

    +
    +
    + +
    +
    + +
    + + +
    ) diff --git a/src/features/Posts/pages/FeedPage/PopularTagsFilter/PopularTagsFilter.tsx b/src/features/Posts/pages/FeedPage/PopularTagsFilter/PopularTagsFilter.tsx index b52c5e5..35d0cce 100644 --- a/src/features/Posts/pages/FeedPage/PopularTagsFilter/PopularTagsFilter.tsx +++ b/src/features/Posts/pages/FeedPage/PopularTagsFilter/PopularTagsFilter.tsx @@ -32,7 +32,6 @@ export default function PopularTagsFilter({ value, onChange }: Props) {
    {isMdScreen ? -

    Popular Tags

      {tagsQuery.loading ? diff --git a/src/features/Posts/pages/PostDetailsPage/Components/PostActions/PostActions.tsx b/src/features/Posts/pages/PostDetailsPage/Components/PostActions/PostActions.tsx index b2362db..fbcd1be 100644 --- a/src/features/Posts/pages/PostDetailsPage/Components/PostActions/PostActions.tsx +++ b/src/features/Posts/pages/PostDetailsPage/Components/PostActions/PostActions.tsx @@ -6,6 +6,7 @@ import VoteButton from "src/Components/VoteButton/VoteButton" import { Post } from "src/features/Posts/types" import { Vote_Item_Type } from "src/graphql" import { useVote } from "src/utils/hooks" +import { PAGES_ROUTES } from "src/utils/routing" interface Props { post: Pick