mirror of
https://github.com/aljazceru/landscape-template.git
synced 2026-02-22 06:54:24 +01:00
Merge branch 'dev' into feature/list-your-product-ui
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -12,6 +12,7 @@ envs/server
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
/envs/server
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
@@ -31,4 +32,5 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
TODO
|
||||
TODO.md
|
||||
NOTES
|
||||
|
||||
@@ -24,19 +24,18 @@ declare global {
|
||||
|
||||
|
||||
declare global {
|
||||
interface NexusGen extends NexusGenTypes {}
|
||||
interface NexusGen extends NexusGenTypes { }
|
||||
}
|
||||
|
||||
export interface NexusGenInputs {
|
||||
StoryInputType: { // input type
|
||||
body: string; // String!
|
||||
cover_image?: string | null; // String
|
||||
id?: number | null; // Int
|
||||
is_published?: boolean | null; // Boolean
|
||||
tags: string[]; // [String!]!
|
||||
title: string; // String!
|
||||
MakerRoleInput: { // input type
|
||||
id: number; // Int!
|
||||
level: NexusGenEnums['RoleLevelEnum']; // RoleLevelEnum!
|
||||
}
|
||||
UpdateProfileInput: { // input type
|
||||
MakerSkillInput: { // input type
|
||||
id: number; // Int!
|
||||
}
|
||||
ProfileDetailsInput: { // input type
|
||||
avatar?: string | null; // String
|
||||
bio?: string | null; // String
|
||||
email?: string | null; // String
|
||||
@@ -49,11 +48,27 @@ export interface NexusGenInputs {
|
||||
twitter?: string | null; // String
|
||||
website?: string | null; // String
|
||||
}
|
||||
ProfileRolesInput: { // input type
|
||||
roles: NexusGenInputs['MakerRoleInput'][]; // [MakerRoleInput!]!
|
||||
skills: NexusGenInputs['MakerSkillInput'][]; // [MakerSkillInput!]!
|
||||
}
|
||||
StoryInputType: { // input type
|
||||
body: string; // String!
|
||||
cover_image?: string | null; // String
|
||||
id?: number | null; // Int
|
||||
is_published?: boolean | null; // Boolean
|
||||
tags: string[]; // [String!]!
|
||||
title: string; // String!
|
||||
}
|
||||
UserKeyInputType: { // input type
|
||||
key: string; // String!
|
||||
name: string; // String!
|
||||
}
|
||||
}
|
||||
|
||||
export interface NexusGenEnums {
|
||||
POST_TYPE: "Bounty" | "Question" | "Story"
|
||||
TEAM_MEMBER_ROLE: "Admin" | "Maker"
|
||||
RoleLevelEnum: 3 | 0 | 1 | 2 | 4
|
||||
VOTE_ITEM_TYPE: "Bounty" | "PostComment" | "Project" | "Question" | "Story" | "User"
|
||||
}
|
||||
|
||||
@@ -121,6 +136,11 @@ export interface NexusGenObjects {
|
||||
prizes: string; // String!
|
||||
touranments: string; // String!
|
||||
}
|
||||
GenericMakerRole: { // root type
|
||||
icon: string; // String!
|
||||
id: number; // Int!
|
||||
title: string; // String!
|
||||
}
|
||||
Hackathon: { // root type
|
||||
cover_image: string; // String!
|
||||
description: string; // String!
|
||||
@@ -137,7 +157,35 @@ export interface NexusGenObjects {
|
||||
metadata?: string | null; // String
|
||||
minSendable?: number | null; // Int
|
||||
}
|
||||
MakerRole: { // root type
|
||||
icon: string; // String!
|
||||
id: number; // Int!
|
||||
level: NexusGenEnums['RoleLevelEnum']; // RoleLevelEnum!
|
||||
title: string; // String!
|
||||
}
|
||||
MakerSkill: { // root type
|
||||
id: number; // Int!
|
||||
title: string; // String!
|
||||
}
|
||||
Mutation: {};
|
||||
MyProfile: { // root type
|
||||
avatar: string; // String!
|
||||
bio?: string | null; // String
|
||||
email?: string | null; // String
|
||||
github?: string | null; // String
|
||||
id: number; // Int!
|
||||
jobTitle?: string | null; // String
|
||||
join_date: NexusGenScalars['Date']; // Date!
|
||||
lightning_address?: string | null; // String
|
||||
linkedin?: string | null; // String
|
||||
location?: string | null; // String
|
||||
name: string; // String!
|
||||
nostr_prv_key?: string | null; // String
|
||||
nostr_pub_key?: string | null; // String
|
||||
role?: string | null; // String
|
||||
twitter?: string | null; // String
|
||||
website?: string | null; // String
|
||||
}
|
||||
PostComment: { // root type
|
||||
author: NexusGenRootTypes['Author']; // Author!
|
||||
body: string; // String!
|
||||
@@ -187,6 +235,16 @@ export interface NexusGenObjects {
|
||||
isOfficial?: boolean | null; // Boolean
|
||||
title: string; // String!
|
||||
}
|
||||
Tournament: { // root type
|
||||
cover_image: string; // String!
|
||||
description: string; // String!
|
||||
end_date: NexusGenScalars['Date']; // Date!
|
||||
id: number; // Int!
|
||||
start_date: NexusGenScalars['Date']; // Date!
|
||||
thumbnail_image: string; // String!
|
||||
title: string; // String!
|
||||
website: string; // String!
|
||||
}
|
||||
User: { // root type
|
||||
avatar: string; // String!
|
||||
bio?: string | null; // String
|
||||
@@ -199,8 +257,6 @@ export interface NexusGenObjects {
|
||||
linkedin?: string | null; // String
|
||||
location?: string | null; // String
|
||||
name: string; // String!
|
||||
nostr_prv_key?: string | null; // String
|
||||
nostr_pub_key?: string | null; // String
|
||||
role?: string | null; // String
|
||||
twitter?: string | null; // String
|
||||
website?: string | null; // String
|
||||
@@ -214,9 +270,15 @@ export interface NexusGenObjects {
|
||||
payment_hash: string; // String!
|
||||
payment_request: string; // String!
|
||||
}
|
||||
WalletKey: { // root type
|
||||
is_current: boolean; // Boolean!
|
||||
key: string; // String!
|
||||
name: string; // String!
|
||||
}
|
||||
}
|
||||
|
||||
export interface NexusGenInterfaces {
|
||||
BaseUser: NexusGenRootTypes['MyProfile'] | NexusGenRootTypes['User'];
|
||||
PostBase: NexusGenRootTypes['Bounty'] | NexusGenRootTypes['Question'] | NexusGenRootTypes['Story'];
|
||||
}
|
||||
|
||||
@@ -291,6 +353,11 @@ export interface NexusGenFieldTypes {
|
||||
prizes: string; // String!
|
||||
touranments: string; // String!
|
||||
}
|
||||
GenericMakerRole: { // field return type
|
||||
icon: string; // String!
|
||||
id: number; // Int!
|
||||
title: string; // String!
|
||||
}
|
||||
Hackathon: { // field return type
|
||||
cover_image: string; // String!
|
||||
description: string; // String!
|
||||
@@ -308,15 +375,51 @@ export interface NexusGenFieldTypes {
|
||||
metadata: string | null; // String
|
||||
minSendable: number | null; // Int
|
||||
}
|
||||
MakerRole: { // field return type
|
||||
icon: string; // String!
|
||||
id: number; // Int!
|
||||
level: NexusGenEnums['RoleLevelEnum']; // RoleLevelEnum!
|
||||
title: string; // String!
|
||||
}
|
||||
MakerSkill: { // field return type
|
||||
id: number; // Int!
|
||||
title: string; // String!
|
||||
}
|
||||
Mutation: { // field return type
|
||||
confirmDonation: NexusGenRootTypes['Donation']; // Donation!
|
||||
confirmVote: NexusGenRootTypes['Vote']; // Vote!
|
||||
createStory: NexusGenRootTypes['Story'] | null; // Story
|
||||
deleteStory: NexusGenRootTypes['Story'] | null; // Story
|
||||
donate: NexusGenRootTypes['Donation']; // Donation!
|
||||
updateProfile: NexusGenRootTypes['User'] | null; // User
|
||||
updateProfileDetails: NexusGenRootTypes['MyProfile'] | null; // MyProfile
|
||||
updateProfileRoles: NexusGenRootTypes['MyProfile'] | null; // MyProfile
|
||||
updateUserPreferences: NexusGenRootTypes['MyProfile']; // MyProfile!
|
||||
vote: NexusGenRootTypes['Vote']; // Vote!
|
||||
}
|
||||
MyProfile: { // field return type
|
||||
avatar: string; // String!
|
||||
bio: string | null; // String
|
||||
email: string | null; // String
|
||||
github: string | null; // String
|
||||
id: number; // Int!
|
||||
jobTitle: string | null; // String
|
||||
join_date: NexusGenScalars['Date']; // Date!
|
||||
lightning_address: string | null; // String
|
||||
linkedin: string | null; // String
|
||||
location: string | null; // String
|
||||
name: string; // String!
|
||||
nostr_prv_key: string | null; // String
|
||||
nostr_pub_key: string | null; // String
|
||||
role: string | null; // String
|
||||
roles: NexusGenRootTypes['MakerRole'][]; // [MakerRole!]!
|
||||
similar_makers: NexusGenRootTypes['User'][]; // [User!]!
|
||||
skills: NexusGenRootTypes['MakerSkill'][]; // [MakerSkill!]!
|
||||
stories: NexusGenRootTypes['Story'][]; // [Story!]!
|
||||
tournaments: NexusGenRootTypes['Tournament'][]; // [Tournament!]!
|
||||
twitter: string | null; // String
|
||||
walletsKeys: NexusGenRootTypes['WalletKey'][]; // [WalletKey!]!
|
||||
website: string | null; // String
|
||||
}
|
||||
PostComment: { // field return type
|
||||
author: NexusGenRootTypes['Author']; // Author!
|
||||
body: string; // String!
|
||||
@@ -344,6 +447,8 @@ export interface NexusGenFieldTypes {
|
||||
allCategories: NexusGenRootTypes['Category'][]; // [Category!]!
|
||||
allProjects: NexusGenRootTypes['Project'][]; // [Project!]!
|
||||
getAllHackathons: NexusGenRootTypes['Hackathon'][]; // [Hackathon!]!
|
||||
getAllMakersRoles: NexusGenRootTypes['GenericMakerRole'][]; // [GenericMakerRole!]!
|
||||
getAllMakersSkills: NexusGenRootTypes['MakerSkill'][]; // [MakerSkill!]!
|
||||
getCategory: NexusGenRootTypes['Category']; // Category!
|
||||
getDonationsStats: NexusGenRootTypes['DonationsStats']; // DonationsStats!
|
||||
getFeed: NexusGenRootTypes['Post'][]; // [Post!]!
|
||||
@@ -353,14 +458,14 @@ export interface NexusGenFieldTypes {
|
||||
getProject: NexusGenRootTypes['Project']; // Project!
|
||||
getTrendingPosts: NexusGenRootTypes['Post'][]; // [Post!]!
|
||||
hottestProjects: NexusGenRootTypes['Project'][]; // [Project!]!
|
||||
me: NexusGenRootTypes['User'] | null; // User
|
||||
me: NexusGenRootTypes['MyProfile'] | null; // MyProfile
|
||||
newProjects: NexusGenRootTypes['Project'][]; // [Project!]!
|
||||
officialTags: NexusGenRootTypes['Tag'][]; // [Tag!]!
|
||||
popularTags: NexusGenRootTypes['Tag'][]; // [Tag!]!
|
||||
profile: NexusGenRootTypes['User'] | null; // User
|
||||
projectsByCategory: NexusGenRootTypes['Project'][]; // [Project!]!
|
||||
searchProjects: NexusGenRootTypes['Project'][]; // [Project!]!
|
||||
searchUsers: NexusGenRootTypes['User'][]; // [User!]!
|
||||
similarMakers: NexusGenRootTypes['User'][]; // [User!]!
|
||||
}
|
||||
Question: { // field return type
|
||||
author: NexusGenRootTypes['Author']; // Author!
|
||||
@@ -398,6 +503,17 @@ export interface NexusGenFieldTypes {
|
||||
isOfficial: boolean | null; // Boolean
|
||||
title: string; // String!
|
||||
}
|
||||
Tournament: { // field return type
|
||||
cover_image: string; // String!
|
||||
description: string; // String!
|
||||
end_date: NexusGenScalars['Date']; // Date!
|
||||
id: number; // Int!
|
||||
start_date: NexusGenScalars['Date']; // Date!
|
||||
tags: NexusGenRootTypes['Tag'][]; // [Tag!]!
|
||||
thumbnail_image: string; // String!
|
||||
title: string; // String!
|
||||
website: string; // String!
|
||||
}
|
||||
User: { // field return type
|
||||
avatar: string; // String!
|
||||
bio: string | null; // String
|
||||
@@ -410,10 +526,12 @@ export interface NexusGenFieldTypes {
|
||||
linkedin: string | null; // String
|
||||
location: string | null; // String
|
||||
name: string; // String!
|
||||
nostr_prv_key: string | null; // String
|
||||
nostr_pub_key: string | null; // String
|
||||
role: string | null; // String
|
||||
roles: NexusGenRootTypes['MakerRole'][]; // [MakerRole!]!
|
||||
similar_makers: NexusGenRootTypes['User'][]; // [User!]!
|
||||
skills: NexusGenRootTypes['MakerSkill'][]; // [MakerSkill!]!
|
||||
stories: NexusGenRootTypes['Story'][]; // [Story!]!
|
||||
tournaments: NexusGenRootTypes['Tournament'][]; // [Tournament!]!
|
||||
twitter: string | null; // String
|
||||
website: string | null; // String
|
||||
}
|
||||
@@ -426,6 +544,32 @@ export interface NexusGenFieldTypes {
|
||||
payment_hash: string; // String!
|
||||
payment_request: string; // String!
|
||||
}
|
||||
WalletKey: { // field return type
|
||||
is_current: boolean; // Boolean!
|
||||
key: string; // String!
|
||||
name: string; // String!
|
||||
}
|
||||
BaseUser: { // field return type
|
||||
avatar: string; // String!
|
||||
bio: string | null; // String
|
||||
email: string | null; // String
|
||||
github: string | null; // String
|
||||
id: number; // Int!
|
||||
jobTitle: string | null; // String
|
||||
join_date: NexusGenScalars['Date']; // Date!
|
||||
lightning_address: string | null; // String
|
||||
linkedin: string | null; // String
|
||||
location: string | null; // String
|
||||
name: string; // String!
|
||||
role: string | null; // String
|
||||
roles: NexusGenRootTypes['MakerRole'][]; // [MakerRole!]!
|
||||
similar_makers: NexusGenRootTypes['User'][]; // [User!]!
|
||||
skills: NexusGenRootTypes['MakerSkill'][]; // [MakerSkill!]!
|
||||
stories: NexusGenRootTypes['Story'][]; // [Story!]!
|
||||
tournaments: NexusGenRootTypes['Tournament'][]; // [Tournament!]!
|
||||
twitter: string | null; // String
|
||||
website: string | null; // String
|
||||
}
|
||||
PostBase: { // field return type
|
||||
body: string; // String!
|
||||
createdAt: NexusGenScalars['Date']; // Date!
|
||||
@@ -501,6 +645,11 @@ export interface NexusGenFieldTypeNames {
|
||||
prizes: 'String'
|
||||
touranments: 'String'
|
||||
}
|
||||
GenericMakerRole: { // field return type name
|
||||
icon: 'String'
|
||||
id: 'Int'
|
||||
title: 'String'
|
||||
}
|
||||
Hackathon: { // field return type name
|
||||
cover_image: 'String'
|
||||
description: 'String'
|
||||
@@ -518,15 +667,51 @@ export interface NexusGenFieldTypeNames {
|
||||
metadata: 'String'
|
||||
minSendable: 'Int'
|
||||
}
|
||||
MakerRole: { // field return type name
|
||||
icon: 'String'
|
||||
id: 'Int'
|
||||
level: 'RoleLevelEnum'
|
||||
title: 'String'
|
||||
}
|
||||
MakerSkill: { // field return type name
|
||||
id: 'Int'
|
||||
title: 'String'
|
||||
}
|
||||
Mutation: { // field return type name
|
||||
confirmDonation: 'Donation'
|
||||
confirmVote: 'Vote'
|
||||
createStory: 'Story'
|
||||
deleteStory: 'Story'
|
||||
donate: 'Donation'
|
||||
updateProfile: 'User'
|
||||
updateProfileDetails: 'MyProfile'
|
||||
updateProfileRoles: 'MyProfile'
|
||||
updateUserPreferences: 'MyProfile'
|
||||
vote: 'Vote'
|
||||
}
|
||||
MyProfile: { // field return type name
|
||||
avatar: 'String'
|
||||
bio: 'String'
|
||||
email: 'String'
|
||||
github: 'String'
|
||||
id: 'Int'
|
||||
jobTitle: 'String'
|
||||
join_date: 'Date'
|
||||
lightning_address: 'String'
|
||||
linkedin: 'String'
|
||||
location: 'String'
|
||||
name: 'String'
|
||||
nostr_prv_key: 'String'
|
||||
nostr_pub_key: 'String'
|
||||
role: 'String'
|
||||
roles: 'MakerRole'
|
||||
similar_makers: 'User'
|
||||
skills: 'MakerSkill'
|
||||
stories: 'Story'
|
||||
tournaments: 'Tournament'
|
||||
twitter: 'String'
|
||||
walletsKeys: 'WalletKey'
|
||||
website: 'String'
|
||||
}
|
||||
PostComment: { // field return type name
|
||||
author: 'Author'
|
||||
body: 'String'
|
||||
@@ -554,6 +739,8 @@ export interface NexusGenFieldTypeNames {
|
||||
allCategories: 'Category'
|
||||
allProjects: 'Project'
|
||||
getAllHackathons: 'Hackathon'
|
||||
getAllMakersRoles: 'GenericMakerRole'
|
||||
getAllMakersSkills: 'MakerSkill'
|
||||
getCategory: 'Category'
|
||||
getDonationsStats: 'DonationsStats'
|
||||
getFeed: 'Post'
|
||||
@@ -563,14 +750,14 @@ export interface NexusGenFieldTypeNames {
|
||||
getProject: 'Project'
|
||||
getTrendingPosts: 'Post'
|
||||
hottestProjects: 'Project'
|
||||
me: 'User'
|
||||
me: 'MyProfile'
|
||||
newProjects: 'Project'
|
||||
officialTags: 'Tag'
|
||||
popularTags: 'Tag'
|
||||
profile: 'User'
|
||||
projectsByCategory: 'Project'
|
||||
searchProjects: 'Project'
|
||||
searchUsers: 'User'
|
||||
similarMakers: 'User'
|
||||
}
|
||||
Question: { // field return type name
|
||||
author: 'Author'
|
||||
@@ -608,6 +795,17 @@ export interface NexusGenFieldTypeNames {
|
||||
isOfficial: 'Boolean'
|
||||
title: 'String'
|
||||
}
|
||||
Tournament: { // field return type name
|
||||
cover_image: 'String'
|
||||
description: 'String'
|
||||
end_date: 'Date'
|
||||
id: 'Int'
|
||||
start_date: 'Date'
|
||||
tags: 'Tag'
|
||||
thumbnail_image: 'String'
|
||||
title: 'String'
|
||||
website: 'String'
|
||||
}
|
||||
User: { // field return type name
|
||||
avatar: 'String'
|
||||
bio: 'String'
|
||||
@@ -620,10 +818,12 @@ export interface NexusGenFieldTypeNames {
|
||||
linkedin: 'String'
|
||||
location: 'String'
|
||||
name: 'String'
|
||||
nostr_prv_key: 'String'
|
||||
nostr_pub_key: 'String'
|
||||
role: 'String'
|
||||
roles: 'MakerRole'
|
||||
similar_makers: 'User'
|
||||
skills: 'MakerSkill'
|
||||
stories: 'Story'
|
||||
tournaments: 'Tournament'
|
||||
twitter: 'String'
|
||||
website: 'String'
|
||||
}
|
||||
@@ -636,6 +836,32 @@ export interface NexusGenFieldTypeNames {
|
||||
payment_hash: 'String'
|
||||
payment_request: 'String'
|
||||
}
|
||||
WalletKey: { // field return type name
|
||||
is_current: 'Boolean'
|
||||
key: 'String'
|
||||
name: 'String'
|
||||
}
|
||||
BaseUser: { // field return type name
|
||||
avatar: 'String'
|
||||
bio: 'String'
|
||||
email: 'String'
|
||||
github: 'String'
|
||||
id: 'Int'
|
||||
jobTitle: 'String'
|
||||
join_date: 'Date'
|
||||
lightning_address: 'String'
|
||||
linkedin: 'String'
|
||||
location: 'String'
|
||||
name: 'String'
|
||||
role: 'String'
|
||||
roles: 'MakerRole'
|
||||
similar_makers: 'User'
|
||||
skills: 'MakerSkill'
|
||||
stories: 'Story'
|
||||
tournaments: 'Tournament'
|
||||
twitter: 'String'
|
||||
website: 'String'
|
||||
}
|
||||
PostBase: { // field return type name
|
||||
body: 'String'
|
||||
createdAt: 'Date'
|
||||
@@ -667,8 +893,14 @@ export interface NexusGenArgTypes {
|
||||
donate: { // args
|
||||
amount_in_sat: number; // Int!
|
||||
}
|
||||
updateProfile: { // args
|
||||
data?: NexusGenInputs['UpdateProfileInput'] | null; // UpdateProfileInput
|
||||
updateProfileDetails: { // args
|
||||
data?: NexusGenInputs['ProfileDetailsInput'] | null; // ProfileDetailsInput
|
||||
}
|
||||
updateProfileRoles: { // args
|
||||
data?: NexusGenInputs['ProfileRolesInput'] | null; // ProfileRolesInput
|
||||
}
|
||||
updateUserPreferences: { // args
|
||||
userKeys?: NexusGenInputs['UserKeyInputType'][] | null; // [UserKeyInputType!]
|
||||
}
|
||||
vote: { // args
|
||||
amount_in_sat: number; // Int!
|
||||
@@ -728,21 +960,24 @@ export interface NexusGenArgTypes {
|
||||
skip?: number | null; // Int
|
||||
take: number | null; // Int
|
||||
}
|
||||
searchUsers: { // args
|
||||
value: string; // String!
|
||||
similarMakers: { // args
|
||||
id: number; // Int!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface NexusGenAbstractTypeMembers {
|
||||
Post: "Bounty" | "Question" | "Story"
|
||||
BaseUser: "MyProfile" | "User"
|
||||
PostBase: "Bounty" | "Question" | "Story"
|
||||
}
|
||||
|
||||
export interface NexusGenTypeInterfaces {
|
||||
Bounty: "PostBase"
|
||||
MyProfile: "BaseUser"
|
||||
Question: "PostBase"
|
||||
Story: "PostBase"
|
||||
User: "BaseUser"
|
||||
}
|
||||
|
||||
export type NexusGenObjectNames = keyof NexusGenObjects;
|
||||
@@ -759,7 +994,7 @@ export type NexusGenUnionNames = keyof NexusGenUnions;
|
||||
|
||||
export type NexusGenObjectsUsingAbstractStrategyIsTypeOf = never;
|
||||
|
||||
export type NexusGenAbstractsUsingStrategyResolveType = "Post" | "PostBase";
|
||||
export type NexusGenAbstractsUsingStrategyResolveType = "BaseUser" | "Post" | "PostBase";
|
||||
|
||||
export type NexusGenFeaturesConfig = {
|
||||
abstractTypeStrategies: {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
### This file was generated by Nexus Schema
|
||||
### Do not make changes to this file directly
|
||||
|
||||
|
||||
type Author {
|
||||
avatar: String!
|
||||
id: Int!
|
||||
@@ -18,6 +17,28 @@ type Award {
|
||||
url: String!
|
||||
}
|
||||
|
||||
interface BaseUser {
|
||||
avatar: String!
|
||||
bio: String
|
||||
email: String
|
||||
github: String
|
||||
id: Int!
|
||||
jobTitle: String
|
||||
join_date: Date!
|
||||
lightning_address: String
|
||||
linkedin: String
|
||||
location: String
|
||||
name: String!
|
||||
role: String
|
||||
roles: [MakerRole!]!
|
||||
similar_makers: [User!]!
|
||||
skills: [MakerSkill!]!
|
||||
stories: [Story!]!
|
||||
tournaments: [Tournament!]!
|
||||
twitter: String
|
||||
website: String
|
||||
}
|
||||
|
||||
type Bounty implements PostBase {
|
||||
applicants_count: Int!
|
||||
applications: [BountyApplication!]!
|
||||
@@ -54,7 +75,9 @@ type Category {
|
||||
votes_sum: Int!
|
||||
}
|
||||
|
||||
"""Date custom scalar type"""
|
||||
"""
|
||||
Date custom scalar type
|
||||
"""
|
||||
scalar Date
|
||||
|
||||
type Donation {
|
||||
@@ -74,6 +97,12 @@ type DonationsStats {
|
||||
touranments: String!
|
||||
}
|
||||
|
||||
type GenericMakerRole {
|
||||
icon: String!
|
||||
id: Int!
|
||||
title: String!
|
||||
}
|
||||
|
||||
type Hackathon {
|
||||
cover_image: String!
|
||||
description: String!
|
||||
@@ -93,16 +122,64 @@ type LnurlDetails {
|
||||
minSendable: Int
|
||||
}
|
||||
|
||||
type MakerRole {
|
||||
icon: String!
|
||||
id: Int!
|
||||
level: RoleLevelEnum!
|
||||
title: String!
|
||||
}
|
||||
|
||||
input MakerRoleInput {
|
||||
id: Int!
|
||||
level: RoleLevelEnum!
|
||||
}
|
||||
|
||||
type MakerSkill {
|
||||
id: Int!
|
||||
title: String!
|
||||
}
|
||||
|
||||
input MakerSkillInput {
|
||||
id: Int!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
confirmDonation(payment_request: String!, preimage: String!): Donation!
|
||||
confirmVote(payment_request: String!, preimage: String!): Vote!
|
||||
createStory(data: StoryInputType): Story
|
||||
deleteStory(id: Int!): Story
|
||||
donate(amount_in_sat: Int!): Donation!
|
||||
updateProfile(data: UpdateProfileInput): User
|
||||
updateProfileDetails(data: ProfileDetailsInput): MyProfile
|
||||
updateProfileRoles(data: ProfileRolesInput): MyProfile
|
||||
updateUserPreferences(userKeys: [UserKeyInputType!]): MyProfile!
|
||||
vote(amount_in_sat: Int!, item_id: Int!, item_type: VOTE_ITEM_TYPE!): Vote!
|
||||
}
|
||||
|
||||
type MyProfile implements BaseUser {
|
||||
avatar: String!
|
||||
bio: String
|
||||
email: String
|
||||
github: String
|
||||
id: Int!
|
||||
jobTitle: String
|
||||
join_date: Date!
|
||||
lightning_address: String
|
||||
linkedin: String
|
||||
location: String
|
||||
name: String!
|
||||
nostr_prv_key: String
|
||||
nostr_pub_key: String
|
||||
role: String
|
||||
roles: [MakerRole!]!
|
||||
similar_makers: [User!]!
|
||||
skills: [MakerSkill!]!
|
||||
stories: [Story!]!
|
||||
tournaments: [Tournament!]!
|
||||
twitter: String
|
||||
walletsKeys: [WalletKey!]!
|
||||
website: String
|
||||
}
|
||||
|
||||
enum POST_TYPE {
|
||||
Bounty
|
||||
Question
|
||||
@@ -131,6 +208,25 @@ type PostComment {
|
||||
votes_count: Int!
|
||||
}
|
||||
|
||||
input ProfileDetailsInput {
|
||||
avatar: String
|
||||
bio: String
|
||||
email: String
|
||||
github: String
|
||||
jobTitle: String
|
||||
lightning_address: String
|
||||
linkedin: String
|
||||
location: String
|
||||
name: String
|
||||
twitter: String
|
||||
website: String
|
||||
}
|
||||
|
||||
input ProfileRolesInput {
|
||||
roles: [MakerRoleInput!]!
|
||||
skills: [MakerSkillInput!]!
|
||||
}
|
||||
|
||||
type Project {
|
||||
awards: [Award!]!
|
||||
category: Category!
|
||||
@@ -151,6 +247,8 @@ type Query {
|
||||
allCategories: [Category!]!
|
||||
allProjects(skip: Int = 0, take: Int = 50): [Project!]!
|
||||
getAllHackathons(sortBy: String, tag: Int): [Hackathon!]!
|
||||
getAllMakersRoles: [GenericMakerRole!]!
|
||||
getAllMakersSkills: [MakerSkill!]!
|
||||
getCategory(id: Int!): Category!
|
||||
getDonationsStats: DonationsStats!
|
||||
getFeed(skip: Int = 0, sortBy: String, tag: Int = 0, take: Int = 10): [Post!]!
|
||||
@@ -160,14 +258,19 @@ type Query {
|
||||
getProject(id: Int!): Project!
|
||||
getTrendingPosts: [Post!]!
|
||||
hottestProjects(skip: Int = 0, take: Int = 50): [Project!]!
|
||||
me: User
|
||||
me: MyProfile
|
||||
newProjects(skip: Int = 0, take: Int = 50): [Project!]!
|
||||
officialTags: [Tag!]!
|
||||
popularTags: [Tag!]!
|
||||
profile(id: Int!): User
|
||||
projectsByCategory(category_id: Int!, skip: Int = 0, take: Int = 10): [Project!]!
|
||||
projectsByCategory(
|
||||
category_id: Int!
|
||||
skip: Int = 0
|
||||
take: Int = 10
|
||||
): [Project!]!
|
||||
searchProjects(search: String!, skip: Int = 0, take: Int = 50): [Project!]!
|
||||
searchUsers(value: String!): [User!]!
|
||||
similarMakers(id: Int!): [User!]!
|
||||
}
|
||||
|
||||
type Question implements PostBase {
|
||||
@@ -184,6 +287,14 @@ type Question implements PostBase {
|
||||
votes_count: Int!
|
||||
}
|
||||
|
||||
enum RoleLevelEnum {
|
||||
Advanced
|
||||
Beginner
|
||||
Hobbyist
|
||||
Intermediate
|
||||
Pro
|
||||
}
|
||||
|
||||
type Story implements PostBase {
|
||||
author: Author!
|
||||
body: String!
|
||||
@@ -223,21 +334,19 @@ type Tag {
|
||||
title: String!
|
||||
}
|
||||
|
||||
input UpdateProfileInput {
|
||||
avatar: String
|
||||
bio: String
|
||||
email: String
|
||||
github: String
|
||||
jobTitle: String
|
||||
lightning_address: String
|
||||
linkedin: String
|
||||
location: String
|
||||
name: String
|
||||
twitter: String
|
||||
website: String
|
||||
type Tournament {
|
||||
cover_image: String!
|
||||
description: String!
|
||||
end_date: Date!
|
||||
id: Int!
|
||||
start_date: Date!
|
||||
tags: [Tag!]!
|
||||
thumbnail_image: String!
|
||||
title: String!
|
||||
website: String!
|
||||
}
|
||||
|
||||
type User {
|
||||
type User implements BaseUser {
|
||||
avatar: String!
|
||||
bio: String
|
||||
email: String
|
||||
@@ -249,14 +358,21 @@ type User {
|
||||
linkedin: String
|
||||
location: String
|
||||
name: String!
|
||||
nostr_prv_key: String
|
||||
nostr_pub_key: String
|
||||
role: String
|
||||
roles: [MakerRole!]!
|
||||
similar_makers: [User!]!
|
||||
skills: [MakerSkill!]!
|
||||
stories: [Story!]!
|
||||
tournaments: [Tournament!]!
|
||||
twitter: String
|
||||
website: String
|
||||
}
|
||||
|
||||
input UserKeyInputType {
|
||||
key: String!
|
||||
name: String!
|
||||
}
|
||||
|
||||
enum VOTE_ITEM_TYPE {
|
||||
Bounty
|
||||
PostComment
|
||||
@@ -274,4 +390,10 @@ type Vote {
|
||||
paid: Boolean!
|
||||
payment_hash: String!
|
||||
payment_request: String!
|
||||
}
|
||||
}
|
||||
|
||||
type WalletKey {
|
||||
is_current: Boolean!
|
||||
key: String!
|
||||
name: String!
|
||||
}
|
||||
|
||||
55
api/functions/graphql/types/tournaments.js
Normal file
55
api/functions/graphql/types/tournaments.js
Normal file
@@ -0,0 +1,55 @@
|
||||
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,
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
|
||||
const { prisma } = require('../../../prisma');
|
||||
const { objectType, extendType, intArg, nonNull, inputObjectType, stringArg } = require("nexus");
|
||||
const { objectType, extendType, intArg, nonNull, inputObjectType, stringArg, interfaceType, list, enumType } = require("nexus");
|
||||
const { getUserByPubKey } = require("../../../auth/utils/helperFuncs");
|
||||
const { removeNulls } = require("./helpers");
|
||||
const { Tournament } = require('./tournaments');
|
||||
|
||||
|
||||
|
||||
const User = objectType({
|
||||
name: 'User',
|
||||
|
||||
const BaseUser = interfaceType({
|
||||
name: 'BaseUser',
|
||||
definition(t) {
|
||||
t.nonNull.int('id');
|
||||
t.nonNull.string('name');
|
||||
@@ -23,8 +25,55 @@ const User = objectType({
|
||||
t.string('linkedin')
|
||||
t.string('bio')
|
||||
t.string('location')
|
||||
t.string('nostr_prv_key')
|
||||
t.string('nostr_pub_key')
|
||||
t.nonNull.list.nonNull.field('roles', {
|
||||
type: MakerRole,
|
||||
resolve: async (parent) => {
|
||||
const data = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: parent.id
|
||||
},
|
||||
select: {
|
||||
roles: {
|
||||
select: {
|
||||
role: true,
|
||||
level: true
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
return data.roles.map(data => {
|
||||
return ({ ...data.role, level: data.level })
|
||||
})
|
||||
}
|
||||
})
|
||||
t.nonNull.list.nonNull.field('skills', {
|
||||
type: MakerSkill,
|
||||
resolve: (parent) => {
|
||||
return prisma.user.findUnique({ where: { id: parent.id } }).skills();
|
||||
}
|
||||
})
|
||||
t.nonNull.list.nonNull.field('tournaments', {
|
||||
type: Tournament,
|
||||
resolve: (parent) => {
|
||||
return []
|
||||
}
|
||||
})
|
||||
t.nonNull.list.nonNull.field('similar_makers', {
|
||||
type: "User",
|
||||
resolve(parent,) {
|
||||
return prisma.user.findMany({
|
||||
where: {
|
||||
AND: {
|
||||
id: {
|
||||
not: parent.id
|
||||
}
|
||||
}
|
||||
},
|
||||
take: 3,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
t.nonNull.list.nonNull.field('stories', {
|
||||
type: "Story",
|
||||
@@ -32,6 +81,101 @@ const User = objectType({
|
||||
return prisma.story.findMany({ where: { user_id: parent.id, is_published: true }, orderBy: { createdAt: "desc" } });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
},
|
||||
resolveType() {
|
||||
return null
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
|
||||
const RoleLevelEnum = enumType({
|
||||
name: 'RoleLevelEnum',
|
||||
members: {
|
||||
Beginner: 0,
|
||||
Hobbyist: 1,
|
||||
Intermediate: 2,
|
||||
Advanced: 3,
|
||||
Pro: 4,
|
||||
},
|
||||
});
|
||||
|
||||
const GenericMakerRole = objectType({
|
||||
name: 'GenericMakerRole',
|
||||
definition(t) {
|
||||
t.nonNull.int('id');
|
||||
t.nonNull.string('title');
|
||||
t.nonNull.string('icon');
|
||||
}
|
||||
})
|
||||
|
||||
const MakerRole = objectType({
|
||||
name: 'MakerRole',
|
||||
definition(t) {
|
||||
t.nonNull.int('id');
|
||||
t.nonNull.string('title');
|
||||
t.nonNull.string('icon');
|
||||
t.nonNull.field('level', { type: RoleLevelEnum })
|
||||
}
|
||||
})
|
||||
|
||||
const getAllMakersRoles = extendType({
|
||||
type: "Query",
|
||||
definition(t) {
|
||||
t.nonNull.list.nonNull.field('getAllMakersRoles', {
|
||||
type: GenericMakerRole,
|
||||
async resolve(parent, args, context) {
|
||||
return prisma.workRole.findMany();
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const MakerSkill = objectType({
|
||||
name: 'MakerSkill',
|
||||
definition(t) {
|
||||
t.nonNull.int('id');
|
||||
t.nonNull.string('title');
|
||||
}
|
||||
})
|
||||
|
||||
const getAllMakersSkills = extendType({
|
||||
type: "Query",
|
||||
definition(t) {
|
||||
t.nonNull.list.nonNull.field('getAllMakersSkills', {
|
||||
type: MakerSkill,
|
||||
async resolve(parent, args, context) {
|
||||
return prisma.skill.findMany();
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const User = objectType({
|
||||
name: 'User',
|
||||
definition(t) {
|
||||
t.implements('BaseUser')
|
||||
}
|
||||
})
|
||||
|
||||
const MyProfile = objectType({
|
||||
name: 'MyProfile',
|
||||
definition(t) {
|
||||
t.implements('BaseUser')
|
||||
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 }))
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
@@ -40,7 +184,7 @@ const me = extendType({
|
||||
type: "Query",
|
||||
definition(t) {
|
||||
t.field('me', {
|
||||
type: "User",
|
||||
type: "MyProfile",
|
||||
async resolve(parent, args, context) {
|
||||
const user = await getUserByPubKey(context.userPubKey)
|
||||
return user
|
||||
@@ -58,14 +202,7 @@ const profile = extendType({
|
||||
id: nonNull(intArg())
|
||||
},
|
||||
async resolve(parent, { id }, ctx) {
|
||||
const user = await getUserByPubKey(ctx.userPubKey);
|
||||
const isSelf = user?.id === id;
|
||||
const profile = await prisma.user.findFirst({
|
||||
where: { id },
|
||||
});
|
||||
if (!isSelf)
|
||||
profile.nostr_prv_key = null;
|
||||
return profile;
|
||||
return prisma.user.findUnique({ where: { id } })
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -94,8 +231,32 @@ const searchUsers = extendType({
|
||||
})
|
||||
|
||||
|
||||
const UpdateProfileInput = inputObjectType({
|
||||
name: 'UpdateProfileInput',
|
||||
const similarMakers = extendType({
|
||||
type: "Query",
|
||||
definition(t) {
|
||||
t.nonNull.list.nonNull.field('similarMakers', {
|
||||
type: "User",
|
||||
args: {
|
||||
id: nonNull(intArg())
|
||||
},
|
||||
async resolve(parent, { id }, ctx) {
|
||||
return prisma.user.findMany({
|
||||
where: {
|
||||
AND: {
|
||||
id: {
|
||||
not: id
|
||||
}
|
||||
}
|
||||
},
|
||||
take: 3,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const ProfileDetailsInput = inputObjectType({
|
||||
name: 'ProfileDetailsInput',
|
||||
definition(t) {
|
||||
t.string('name');
|
||||
t.string('avatar');
|
||||
@@ -111,12 +272,12 @@ const UpdateProfileInput = inputObjectType({
|
||||
}
|
||||
})
|
||||
|
||||
const updateProfile = extendType({
|
||||
const updateProfileDetails = extendType({
|
||||
type: 'Mutation',
|
||||
definition(t) {
|
||||
t.field('updateProfile', {
|
||||
type: 'User',
|
||||
args: { data: UpdateProfileInput },
|
||||
t.field('updateProfileDetails', {
|
||||
type: 'MyProfile',
|
||||
args: { data: ProfileDetailsInput },
|
||||
async resolve(_root, args, ctx) {
|
||||
const user = await getUserByPubKey(ctx.userPubKey);
|
||||
|
||||
@@ -140,15 +301,188 @@ const updateProfile = extendType({
|
||||
})
|
||||
|
||||
|
||||
const WalletKey = objectType({
|
||||
name: 'WalletKey',
|
||||
definition(t) {
|
||||
t.nonNull.string('key');
|
||||
t.nonNull.string('name');
|
||||
t.nonNull.boolean('is_current')
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
const UserKeyInputType = inputObjectType({
|
||||
name: 'UserKeyInputType',
|
||||
definition(t) {
|
||||
t.nonNull.string('key');
|
||||
t.nonNull.string('name');
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
const updateUserPreferences = extendType({
|
||||
type: 'Mutation',
|
||||
definition(t) {
|
||||
t.nonNull.field('updateUserPreferences', {
|
||||
type: 'MyProfile',
|
||||
args: { userKeys: list(nonNull(UserKeyInputType)) },
|
||||
async resolve(_root, args, ctx) {
|
||||
|
||||
const user = await getUserByPubKey(ctx.userPubKey);
|
||||
if (!user)
|
||||
throw new Error("You have to login");
|
||||
|
||||
|
||||
//Update the userkeys
|
||||
//--------------------
|
||||
|
||||
// Check if all the sent keys belong to the user
|
||||
const userKeys = (await prisma.userKey.findMany({
|
||||
where: {
|
||||
AND: {
|
||||
user_id: {
|
||||
equals: user.id,
|
||||
},
|
||||
key: {
|
||||
in: args.userKeys.map(i => i.key)
|
||||
}
|
||||
},
|
||||
},
|
||||
select: {
|
||||
key: true
|
||||
}
|
||||
})).map(i => i.key);
|
||||
|
||||
const newKeys = [];
|
||||
for (let i = 0; i < args.userKeys.length; i++) {
|
||||
const item = args.userKeys[i];
|
||||
if (userKeys.includes(item.key))
|
||||
newKeys.push(item);
|
||||
}
|
||||
|
||||
|
||||
if (newKeys.length === 0)
|
||||
throw new Error("You can't delete all your wallets keys")
|
||||
|
||||
await prisma.userKey.deleteMany({
|
||||
where: {
|
||||
user_id: user.id
|
||||
}
|
||||
})
|
||||
|
||||
await prisma.userKey.createMany({
|
||||
data: newKeys.map(i => ({
|
||||
user_id: user.id,
|
||||
key: i.key,
|
||||
name: i.name,
|
||||
}))
|
||||
})
|
||||
|
||||
return prisma.user.findUnique({ where: { id: user.id } });
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
const MakerRoleInput = inputObjectType({
|
||||
name: "MakerRoleInput",
|
||||
definition(t) {
|
||||
t.nonNull.int('id');
|
||||
t.nonNull.field('level', { type: RoleLevelEnum })
|
||||
}
|
||||
})
|
||||
|
||||
const MakerSkillInput = inputObjectType({
|
||||
name: "MakerSkillInput",
|
||||
definition(t) {
|
||||
t.nonNull.int('id');
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const ProfileRolesInput = inputObjectType({
|
||||
name: 'ProfileRolesInput',
|
||||
definition(t) {
|
||||
t.nonNull.list.nonNull.field('roles', {
|
||||
type: MakerRoleInput,
|
||||
})
|
||||
t.nonNull.list.nonNull.field('skills', {
|
||||
type: MakerSkillInput,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const updateProfileRoles = extendType({
|
||||
type: 'Mutation',
|
||||
definition(t) {
|
||||
t.field('updateProfileRoles', {
|
||||
type: 'MyProfile',
|
||||
args: { data: ProfileRolesInput },
|
||||
async resolve(_root, args, ctx) {
|
||||
const user = await getUserByPubKey(ctx.userPubKey);
|
||||
|
||||
// Do some validation
|
||||
if (!user)
|
||||
throw new Error("You have to login");
|
||||
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
skills: {
|
||||
set: [],
|
||||
},
|
||||
roles: {
|
||||
deleteMany: {}
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
return prisma.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
skills: {
|
||||
connect: args.data.skills,
|
||||
},
|
||||
roles: {
|
||||
create: args.data.roles.map(r => ({ roleId: r.id, level: r.level }))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
module.exports = {
|
||||
// Types
|
||||
|
||||
BaseUser,
|
||||
User,
|
||||
UpdateProfileInput,
|
||||
MyProfile,
|
||||
WalletKey,
|
||||
// Queries
|
||||
me,
|
||||
profile,
|
||||
searchUsers,
|
||||
similarMakers,
|
||||
getAllMakersRoles,
|
||||
getAllMakersSkills,
|
||||
// Mutations
|
||||
updateProfile,
|
||||
}
|
||||
updateProfileDetails,
|
||||
updateUserPreferences,
|
||||
updateProfileRoles,
|
||||
}
|
||||
|
||||
@@ -37,15 +37,11 @@ const loginHandler = async (req, res) => {
|
||||
if (existingKeys.length >= 3)
|
||||
return res.status(400).json({ status: 'ERROR', reason: "Can only link up to 3 wallets" })
|
||||
|
||||
if (existingKeys.includes(key))
|
||||
return res.status(400).json({ status: 'ERROR', reason: "Wallet already linked" });
|
||||
|
||||
// Remove old linking for this key if existing
|
||||
await prisma.userKey.deleteMany({
|
||||
where: { key }
|
||||
})
|
||||
|
||||
|
||||
await prisma.userKey.create({
|
||||
data: {
|
||||
key,
|
||||
@@ -53,6 +49,7 @@ const loginHandler = async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return res
|
||||
.status(200)
|
||||
.json({ status: "OK" })
|
||||
|
||||
49
api/functions/upload-image-url/upload-image-url.js
Normal file
49
api/functions/upload-image-url/upload-image-url.js
Normal file
@@ -0,0 +1,49 @@
|
||||
const serverless = require('serverless-http')
|
||||
const { createExpressApp } = require('../../modules')
|
||||
const express = require('express')
|
||||
const extractKeyFromCookie = require('../../utils/extractKeyFromCookie')
|
||||
const { getUserByPubKey } = require('../../auth/utils/helperFuncs')
|
||||
const { getDirectUploadUrl } = require('../../services/imageUpload.service')
|
||||
const { prisma } = require('../../prisma')
|
||||
|
||||
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)
|
||||
|
||||
if (!user) return res.status(401).json({ status: 'ERROR', reason: 'Not Authenticated' })
|
||||
|
||||
const { filename } = req.body
|
||||
|
||||
if (!filename) return res.status(422).json({ status: 'ERROR', reason: "The field 'filename' is required`" })
|
||||
|
||||
try {
|
||||
const uploadUrl = await getDirectUploadUrl()
|
||||
|
||||
await prisma.hostedImage.create({
|
||||
data: { id: uploadUrl.id, filename },
|
||||
})
|
||||
|
||||
return res.status(200).json(uploadUrl)
|
||||
} catch (error) {
|
||||
res.status(500).send('Unexpected error happened, please try again')
|
||||
}
|
||||
}
|
||||
|
||||
let app
|
||||
|
||||
if (process.env.LOCAL) {
|
||||
app = createExpressApp()
|
||||
app.post('/upload-image-url', postUploadImageUrl)
|
||||
} else {
|
||||
const router = express.Router()
|
||||
router.post('/upload-image-url', postUploadImageUrl)
|
||||
app = createExpressApp(router)
|
||||
}
|
||||
|
||||
const handler = serverless(app)
|
||||
exports.handler = async (event, context) => {
|
||||
return await handler(event, context)
|
||||
}
|
||||
35
api/services/imageUpload.service.js
Normal file
35
api/services/imageUpload.service.js
Normal file
@@ -0,0 +1,35 @@
|
||||
const { CONSTS } = require('../utils')
|
||||
const axios = require('axios')
|
||||
const FormData = require('form-data')
|
||||
|
||||
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`,
|
||||
}
|
||||
|
||||
async function getDirectUploadUrl() {
|
||||
const url = operationUrls['image.uploadUrl']
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('requireSignedURLs', 'false')
|
||||
|
||||
const config = {
|
||||
headers: {
|
||||
Authorization: `Bearer ${CONSTS.CLOUDFLARE_IMAGE_API_KEY}`,
|
||||
...formData.getHeaders(),
|
||||
},
|
||||
}
|
||||
|
||||
const result = await axios.post(url, formData, config)
|
||||
|
||||
if (!result.data.success) {
|
||||
throw new Error(result.data, { cause: result.data.errors })
|
||||
}
|
||||
|
||||
return result.data.result
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getDirectUploadUrl,
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
const BOLT_FUN_LIGHTNING_ADDRESS = 'johns@getalby.com'; // #TODO, replace it by bolt-fun lightning address if there exist one
|
||||
const JWT_SECRET = process.env.JWT_SECRET;
|
||||
const BOLT_FUN_LIGHTNING_ADDRESS = 'johns@getalby.com' // #TODO, replace it by bolt-fun lightning address if there exist one
|
||||
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 CONSTS = {
|
||||
JWT_SECRET,
|
||||
BOLT_FUN_LIGHTNING_ADDRESS,
|
||||
LNURL_AUTH_HOST,
|
||||
CLOUDFLARE_IMAGE_ACCOUNT_ID,
|
||||
CLOUDFLARE_IMAGE_API_KEY,
|
||||
}
|
||||
|
||||
module.exports = CONSTS;
|
||||
module.exports = CONSTS
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
REACT_APP_API_END_POINT = http://localhost:8888/dev
|
||||
@@ -1,3 +0,0 @@
|
||||
|
||||
REACT_APP_ENABLE_MOCKS= true
|
||||
STORYBOOK_ENABLE_MOCKS= true
|
||||
2
envs/client/dev-server.env
Normal file
2
envs/client/dev-server.env
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
REACT_APP_API_END_POINT = http://localhost:8888/dev
|
||||
2
envs/client/preview-server.env
Normal file
2
envs/client/preview-server.env
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
REACT_APP_API_END_POINT = https://makers-bolt-fun-preview.netlify.app/.netlify/functions
|
||||
5422
package-lock.json
generated
5422
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
21
package.json
21
package.json
@@ -41,6 +41,7 @@
|
||||
"env-cmd": "^10.1.0",
|
||||
"express": "^4.18.1",
|
||||
"express-session": "^1.17.3",
|
||||
"form-data": "^4.0.0",
|
||||
"framer-motion": "^6.3.0",
|
||||
"fslightbox-react": "^1.6.2-2",
|
||||
"graphql": "^16.3.0",
|
||||
@@ -72,6 +73,7 @@
|
||||
"react-loader-spinner": "^6.0.0-0",
|
||||
"react-loading-skeleton": "^3.1.0",
|
||||
"react-modal": "^3.15.1",
|
||||
"react-popper-tooltip": "^4.4.2",
|
||||
"react-query": "^3.35.0",
|
||||
"react-redux": "^8.0.0",
|
||||
"react-router-dom": "^6.3.0",
|
||||
@@ -90,20 +92,20 @@
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
"scripts": {
|
||||
"client:prod-server": "env-cmd -f ./envs/client/.dev.prod-server.env react-scripts start",
|
||||
"client:preview-server": "env-cmd -f ./envs/client/.dev.preview-server.env react-scripts start",
|
||||
"client:mocks": "env-cmd -f ./envs/client/.dev.mock-server.env react-scripts start",
|
||||
"client:dev-server": "env-cmd -f ./envs/client/.dev.server.env react-scripts start",
|
||||
"client:dev-server": "env-cmd -f ./envs/client/dev-server.env react-scripts start",
|
||||
"server:dev": "env-cmd -f ./envs/server/local.env serverless offline",
|
||||
"client:preview-server": "env-cmd -f ./envs/client/preview-server.env react-scripts start",
|
||||
"server:preview": "env-cmd -f ./envs/server/preview.env serverless offline",
|
||||
"client:prod-server": "env-cmd -f ./envs/client/prod-server.env react-scripts start",
|
||||
"server:prod": "env-cmd -f ./envs/server/prod.env serverless offline",
|
||||
"client:mocks": "env-cmd -f ./envs/client/mock-server.env react-scripts start",
|
||||
"generate-graphql": "graphql-codegen",
|
||||
"storybook": "env-cmd -f ./envs/client/.dev.preview-server.env start-storybook -p 6006 -s public",
|
||||
"storybook:mocks": "env-cmd -f ./envs/client/.dev.mock-server.env start-storybook -p 6006 -s public",
|
||||
"storybook": "env-cmd -f ./envs/client/preview-server.env start-storybook -p 6006 -s public",
|
||||
"storybook:mocks": "env-cmd -f ./envs/client/mock-server.env start-storybook -p 6006 -s public",
|
||||
"build": "react-scripts build",
|
||||
"build:mocks": "env-cmd -f ./envs/client/.prod.mock-server.env react-scripts build",
|
||||
"build-storybook": "env-cmd -f ./envs/client/.dev.preview-server.env build-storybook -s public",
|
||||
"build-storybook:mocks": "env-cmd -f ./envs/client/.prod.mock-server.env build-storybook -s public",
|
||||
"build:mocks": "env-cmd -f ./envs/client/mock-server.env react-scripts build",
|
||||
"build-storybook": "env-cmd -f ./envs/client/preview-server.env build-storybook -s public",
|
||||
"build-storybook:mocks": "env-cmd -f ./envs/client/mock-server.env build-storybook -s public",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"db:migrate-dev": "prisma migrate dev",
|
||||
@@ -179,6 +181,7 @@
|
||||
"netlify-cli": "^10.0.0",
|
||||
"postcss": "^8.4.12",
|
||||
"readable-stream": "^4.1.0",
|
||||
"serverless": "^3.22.0",
|
||||
"serverless-offline": "^8.7.0",
|
||||
"tailwindcss": "^3.0.24",
|
||||
"webpack": "^5.72.0"
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "UserKey" ADD COLUMN "name" TEXT NOT NULL DEFAULT E'New Key Name';
|
||||
@@ -0,0 +1,57 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "UserKey" ALTER COLUMN "name" SET DEFAULT E'My new wallet key';
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "UsersOnWorkRoles" (
|
||||
"userId" INTEGER NOT NULL,
|
||||
"roleId" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "UsersOnWorkRoles_pkey" PRIMARY KEY ("userId","roleId")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "WorkRole" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"icon" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "WorkRole_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Skill" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "Skill_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_SkillToUser" (
|
||||
"A" INTEGER NOT NULL,
|
||||
"B" INTEGER NOT NULL
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "WorkRole_title_key" ON "WorkRole"("title");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Skill_title_key" ON "Skill"("title");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "_SkillToUser_AB_unique" ON "_SkillToUser"("A", "B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_SkillToUser_B_index" ON "_SkillToUser"("B");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "UsersOnWorkRoles" ADD CONSTRAINT "UsersOnWorkRoles_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "UsersOnWorkRoles" ADD CONSTRAINT "UsersOnWorkRoles_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "WorkRole"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_SkillToUser" ADD FOREIGN KEY ("A") REFERENCES "Skill"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_SkillToUser" ADD FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Added the required column `level` to the `UsersOnWorkRoles` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "UsersOnWorkRoles" ADD COLUMN "level" TEXT NOT NULL;
|
||||
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Changed the type of `level` on the `UsersOnWorkRoles` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "UsersOnWorkRoles" DROP COLUMN "level",
|
||||
ADD COLUMN "level" INTEGER NOT NULL;
|
||||
@@ -0,0 +1,9 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "HostedImage" (
|
||||
"id" TEXT NOT NULL,
|
||||
"filename" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"is_used" BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
CONSTRAINT "HostedImage_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
@@ -65,6 +65,8 @@ model User {
|
||||
posts_comments PostComment[]
|
||||
donations Donation[]
|
||||
userKeys UserKey[]
|
||||
skills Skill[]
|
||||
roles UsersOnWorkRoles[]
|
||||
}
|
||||
|
||||
model UserKey {
|
||||
@@ -75,6 +77,31 @@ model UserKey {
|
||||
user_id Int?
|
||||
}
|
||||
|
||||
model UsersOnWorkRoles {
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId Int
|
||||
role WorkRole @relation(fields: [roleId], references: [id])
|
||||
roleId Int
|
||||
|
||||
level Int
|
||||
|
||||
@@id([userId, roleId])
|
||||
}
|
||||
|
||||
model WorkRole {
|
||||
id Int @id @default(autoincrement())
|
||||
title String @unique
|
||||
icon String
|
||||
users UsersOnWorkRoles[]
|
||||
}
|
||||
|
||||
model Skill {
|
||||
id Int @id @default(autoincrement())
|
||||
title String @unique
|
||||
|
||||
users User[]
|
||||
}
|
||||
|
||||
// -----------------
|
||||
// Projects
|
||||
// -----------------
|
||||
@@ -221,3 +248,13 @@ model GeneratedK1 {
|
||||
sid String?
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
// -----------------
|
||||
// Hosted Image
|
||||
// -----------------
|
||||
model HostedImage {
|
||||
id String @id
|
||||
filename String
|
||||
createdAt DateTime @default(now())
|
||||
is_used Boolean @default(false)
|
||||
}
|
||||
|
||||
@@ -418,10 +418,103 @@ const hackathons = [
|
||||
},
|
||||
]
|
||||
|
||||
const roles = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Frontend Dev",
|
||||
icon: "💄"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Backend Dev",
|
||||
icon: "💻️"
|
||||
}, {
|
||||
id: 3,
|
||||
title: "UI/UX Designer",
|
||||
icon: "🌈️️"
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "Community Manager",
|
||||
icon: "🎉️️"
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: "Founder",
|
||||
icon: "🦄️"
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: "Marketer",
|
||||
icon: "🚨️"
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
title: "Content Creator",
|
||||
icon: "🎥️"
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
title: "Researcher",
|
||||
icon: "🔬"
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
title: "Data engineer",
|
||||
icon: "💿️"
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
title: "Growth hacker",
|
||||
icon: "📉️"
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
title: "Technical Writer",
|
||||
icon: "✍️️"
|
||||
},
|
||||
]
|
||||
|
||||
const skills = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Figma"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Prototyping"
|
||||
}, {
|
||||
id: 3,
|
||||
title: "Writing"
|
||||
}, {
|
||||
id: 4,
|
||||
title: "CSS"
|
||||
}, {
|
||||
id: 5,
|
||||
title: "React.js"
|
||||
}, {
|
||||
id: 6,
|
||||
title: "Wordpress"
|
||||
}, {
|
||||
id: 7,
|
||||
title: "Principle app"
|
||||
}, {
|
||||
id: 8,
|
||||
title: "UX design"
|
||||
}, {
|
||||
id: 9,
|
||||
title: "User research"
|
||||
}, {
|
||||
id: 10,
|
||||
title: "User testing"
|
||||
},
|
||||
]
|
||||
|
||||
module.exports = {
|
||||
categories,
|
||||
projects,
|
||||
tags,
|
||||
hackathons,
|
||||
roles,
|
||||
skills,
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
const { PrismaClient } = require("@prisma/client");
|
||||
const { generatePrivateKey, getPublicKey } = require("../../api/utils/nostr-tools");
|
||||
const { categories, projects, tags, hackathons } = require("./data");
|
||||
const { categories, projects, tags, hackathons, roles, skills } = require("./data");
|
||||
const Chance = require('chance');
|
||||
const { getCoverImage, randomItems, random } = require("./helpers");
|
||||
|
||||
@@ -58,7 +58,11 @@ async function main() {
|
||||
|
||||
// await createHackathons();
|
||||
|
||||
await fillUserKeysTable()
|
||||
// await fillUserKeysTable()
|
||||
|
||||
// await createRoles();
|
||||
|
||||
// await createSkills();
|
||||
|
||||
}
|
||||
|
||||
@@ -169,6 +173,26 @@ async function fillUserKeysTable() {
|
||||
})
|
||||
}
|
||||
|
||||
async function createRoles() {
|
||||
console.log("Creating Users Roles");
|
||||
await prisma.workRole.createMany({
|
||||
data: roles.map(item => ({
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
icon: item.icon,
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
async function createSkills() {
|
||||
console.log("Creating Users Skills");
|
||||
await prisma.skill.createMany({
|
||||
data: skills.map(item => ({
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
main()
|
||||
|
||||
21
public/assets/icons/nut.svg
Normal file
21
public/assets/icons/nut.svg
Normal file
@@ -0,0 +1,21 @@
|
||||
<svg width="19" height="18" viewBox="0 0 19 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.1645 2.44122L13.8999 1.89167L13.2918 1.93933L5.88553 2.51977L5.44117 2.5546L5.20243 2.931L2.07304 7.86481L1.81474 8.27205L2.01427 8.71108L4.91607 15.096L5.13423 15.5761L5.66001 15.616L13.2938 16.1965L13.9287 16.2448L14.1803 15.6599L17.0157 9.06762L17.1762 8.69453L16.9999 8.32862L14.1645 2.44122Z" fill="#C4C4C4" stroke="#333333" stroke-width="1.78281"/>
|
||||
<g filter="url(#filter0_i_1824_6762)">
|
||||
<path d="M13.3614 2.82806L5.95518 3.40851L2.82579 8.34232L5.72759 14.7273L13.3614 15.3077L16.1968 8.71547L13.3614 2.82806Z" fill="url(#paint0_linear_1824_6762)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_i_1824_6762" x="2.82579" y="2.82806" width="13.371" height="12.4796" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="-0.445701" dy="1.3371"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.9375 0 0 0 0 0.9375 0 0 0 0 0.9375 0 0 0 0.25 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_1824_6762"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_1824_6762" x1="9.51131" y1="2.82806" x2="9.51131" y2="15.3077" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.286458" stop-color="#B7B7B7"/>
|
||||
<stop offset="1" stop-color="#9B9B9B"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
BIN
public/assets/images/nut_3d.png
Normal file
BIN
public/assets/images/nut_3d.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.9 KiB |
@@ -76,3 +76,10 @@ functions:
|
||||
- http:
|
||||
path: pubkeys-to-users
|
||||
method: post
|
||||
|
||||
upload-image-url:
|
||||
handler: api/functions/upload-image-url/upload-image-url.handler
|
||||
events:
|
||||
- http:
|
||||
path: upload-image-url
|
||||
method: post
|
||||
|
||||
@@ -25,7 +25,7 @@ const btnStylesFill: UnionToObjectKeys<Props, 'color'> = {
|
||||
gray: 'bg-gray-100 hover:bg-gray-200 text-gray-900 active:bg-gray-300',
|
||||
white: 'border border-gray-300 text-gray-900 bg-gray-25 hover:bg-gray-50',
|
||||
black: 'text-white bg-black hover:bg-gray-900',
|
||||
red: "bg-red-600 hover:bg-red-500 active:bg-red-700 text-white",
|
||||
red: "bg-red-500 hover:bg-red-600 active:bg-red-700 text-white",
|
||||
}
|
||||
|
||||
const loadingColor: UnionToObjectKeys<Props, 'color'> = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
import { Link } from 'react-router-dom'
|
||||
import { UnionToObjectKeys } from 'src/utils/types/utils'
|
||||
|
||||
@@ -6,7 +6,6 @@ interface Props {
|
||||
onClick?: () => void;
|
||||
onKeyDown?: (v: any) => void
|
||||
href?: string;
|
||||
children: JSX.Element
|
||||
className?: string
|
||||
size?: "sm" | 'md' | 'lg'
|
||||
variant?: 'blank' | 'fill'
|
||||
@@ -26,7 +25,7 @@ const baseBtnStyles: UnionToObjectKeys<Props, 'variant'> = {
|
||||
blank: "bg-gray-900 bg-opacity-0 hover:bg-opacity-5 active:bg-opacity-10 active:scale-95 !border-0"
|
||||
}
|
||||
|
||||
const IconButton = React.forwardRef<any, Props>(({
|
||||
const IconButton = React.forwardRef<any, PropsWithChildren<Props>>(({
|
||||
href,
|
||||
size = "md",
|
||||
className = "",
|
||||
|
||||
15
src/Components/InfoCard/InfoCard.tsx
Normal file
15
src/Components/InfoCard/InfoCard.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React, { PropsWithChildren } from 'react'
|
||||
|
||||
interface Props {
|
||||
|
||||
}
|
||||
|
||||
export default function InfoCard(props: PropsWithChildren<Props>) {
|
||||
return (
|
||||
<div className="bg-gray-50 p-16 rounded-12 border border-gray-200 mt-24">
|
||||
<p className="text-body5 text-gray-600">
|
||||
{props.children}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { MdLocalFireDepartment } from 'react-icons/md'
|
||||
import Button from 'src/Components/Button/Button'
|
||||
import { useAppSelector, usePressHolder, useResizeListener, useVote } from 'src/utils/hooks'
|
||||
import { ComponentProps, SyntheticEvent, useRef, useState } from 'react'
|
||||
import { ComponentProps, SyntheticEvent, useRef, useState, useEffect } from 'react'
|
||||
import styles from './styles.module.scss'
|
||||
import { random, randomItem, numberFormatter } from 'src/utils/helperFunctions'
|
||||
import { useDebouncedCallback, useMountEffect, useThrottledCallback } from '@react-hookz/web'
|
||||
@@ -179,28 +179,44 @@ export default function VoteButton({
|
||||
onHold();
|
||||
}
|
||||
|
||||
useMountEffect(() => {
|
||||
const bodyRect = document.body.getBoundingClientRect();
|
||||
const btnRect = btnContainerRef.current.getBoundingClientRect()
|
||||
setBtnPosition({
|
||||
top: btnRect.top - bodyRect.top,
|
||||
left: btnRect.left - bodyRect.left,
|
||||
width: btnRect.width,
|
||||
height: btnRect.height
|
||||
});
|
||||
const updateParticlesContainerPos = useDebouncedCallback(
|
||||
() => {
|
||||
const bodyRect = document.body.getBoundingClientRect();
|
||||
const btnRect = btnContainerRef.current.getBoundingClientRect()
|
||||
setBtnPosition({
|
||||
top: btnRect.top - bodyRect.top,
|
||||
left: btnRect.left - bodyRect.left,
|
||||
width: btnRect.width,
|
||||
height: btnRect.height
|
||||
});
|
||||
},
|
||||
[],
|
||||
300
|
||||
)
|
||||
|
||||
})
|
||||
|
||||
useResizeListener(() => {
|
||||
const bodyRect = document.body.getBoundingClientRect();
|
||||
const btnRect = btnContainerRef.current.getBoundingClientRect()
|
||||
setBtnPosition({
|
||||
top: btnRect.top - bodyRect.top,
|
||||
left: btnRect.left - bodyRect.left,
|
||||
width: btnRect.width,
|
||||
height: btnRect.height
|
||||
});
|
||||
}, { debounce: 300 })
|
||||
useEffect(() => {
|
||||
updateParticlesContainerPos();
|
||||
document.addEventListener('scroll', updateParticlesContainerPos)
|
||||
document.addEventListener('resize', updateParticlesContainerPos)
|
||||
|
||||
return () => {
|
||||
|
||||
document.removeEventListener('scroll', updateParticlesContainerPos)
|
||||
document.removeEventListener('resize', updateParticlesContainerPos)
|
||||
}
|
||||
}, [updateParticlesContainerPos])
|
||||
|
||||
// useResizeListener(() => {
|
||||
// const bodyRect = document.body.getBoundingClientRect();
|
||||
// const btnRect = btnContainerRef.current.getBoundingClientRect()
|
||||
// setBtnPosition({
|
||||
// top: btnRect.top - bodyRect.top,
|
||||
// left: btnRect.left - bodyRect.left,
|
||||
// width: btnRect.width,
|
||||
// height: btnRect.height
|
||||
// });
|
||||
// }, { debounce: 300 })
|
||||
|
||||
return (
|
||||
<button
|
||||
|
||||
@@ -153,20 +153,29 @@ export default function LoginPage() {
|
||||
</div>
|
||||
|
||||
else
|
||||
content = <div className="max-w-[326px] border-2 border-gray-200 rounded-16 p-16 flex flex-col gap-16 items-center" >
|
||||
<p className="text-body1 font-bolder text-center">
|
||||
Login with lightning ⚡
|
||||
</p>
|
||||
<QRCodeSVG
|
||||
width={160}
|
||||
height={160}
|
||||
value={lnurl}
|
||||
/>
|
||||
content = <div className="max-w-[364px] border-2 border-gray-200 rounded-16 p-16 flex flex-col gap-24 items-center" >
|
||||
|
||||
<h2 className='text-h5 font-bold text-center'>Login with lightning ⚡</h2>
|
||||
<a href={`lightning:${lnurl}`} >
|
||||
<QRCodeSVG
|
||||
width={280}
|
||||
height={280}
|
||||
value={lnurl}
|
||||
bgColor='transparent'
|
||||
imageSettings={{
|
||||
src: '/assets/images/nut_3d.png',
|
||||
width: 16,
|
||||
height: 16,
|
||||
excavate: true,
|
||||
|
||||
}}
|
||||
/>
|
||||
</a>
|
||||
<p className="text-gray-600 text-body4 text-center">
|
||||
Scan this code or copy + paste it to your lightning wallet. Or click to login with your browser's wallet.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-16">
|
||||
<a href={lnurl}
|
||||
<div className="w-full flex flex-col items-stretch gap-16">
|
||||
<a href={`lightning:${lnurl}`}
|
||||
className='grow block text-body4 text-center text-white font-bolder bg-primary-500 hover:bg-primary-600 rounded-10 px-16 py-12 active:scale-90 transition-transform'
|
||||
>Click to connect <IoRocketOutline /></a>
|
||||
<Button
|
||||
|
||||
@@ -4,5 +4,7 @@ query Me {
|
||||
name
|
||||
avatar
|
||||
join_date
|
||||
jobTitle
|
||||
bio
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Avatar from 'src/features/Profiles/Components/Avatar/Avatar';
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -14,7 +14,7 @@ export default function TrendingCard() {
|
||||
|
||||
return (
|
||||
<Card onlyMd>
|
||||
<h3 className="text-body2 font-bolder mb-16">Trending on BOLT.FUN</h3>
|
||||
<h3 className="text-body2 font-bolder mb-16">Trending on BOLT🔩FUN</h3>
|
||||
<ul className='flex flex-col'>
|
||||
{
|
||||
trendingPosts.loading ?
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import Button from 'src/Components/Button/Button';
|
||||
import { useAppDispatch } from 'src/utils/hooks';
|
||||
import { openModal } from 'src/redux/features/modals.slice';
|
||||
import Card from 'src/Components/Card/Card';
|
||||
|
||||
|
||||
interface Props {
|
||||
|
||||
}
|
||||
|
||||
export default function AccountCard({ }: Props) {
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const connectNewWallet = () => {
|
||||
dispatch(openModal({ Modal: "LinkingAccountModal" }))
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<p className="text-body2 font-bold">🔒 Linking Accounts</p>
|
||||
<div className='mt-24 flex flex-col gap-16'>
|
||||
<p className="text-body3 font-bold">Linked Wallets</p>
|
||||
<p className="text-body4 text-gray-600">
|
||||
These are the wallets that you can login to this account from.
|
||||
<br />
|
||||
You can add a new wallet from the button below.
|
||||
</p>
|
||||
<Button color='primary' className='' onClick={connectNewWallet}>
|
||||
Connect new wallet ⚡
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import Card from 'src/Components/Card/Card';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
export default function BasicProfileInfoTabSkeleton() {
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-24">
|
||||
<div className="col-span-2 flex flex-col gap-24">
|
||||
<Card className="md:col-span-2" defaultPadding={false}>
|
||||
<div className="bg-gray-100 relative h-[160px] rounded-t-16">
|
||||
<div className="absolute left-24 bottom-0 translate-y-1/2">
|
||||
<div className="w-[120px] aspect-square rounded-full bg-gray-50 border border-gray-200"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-16 md:p-24 mt-64">
|
||||
|
||||
<p className="text-body2 font-bold"><Skeleton width="12ch" /></p>
|
||||
<p className="text-body4 text-gray-600 mt-8">
|
||||
<Skeleton width="50%" />
|
||||
</p>
|
||||
<div className="py-[250px]"></div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,33 +1,22 @@
|
||||
import { SubmitHandler, useForm } from "react-hook-form"
|
||||
import Button from "src/Components/Button/Button";
|
||||
import { User, useUpdateProfileAboutMutation } from "src/graphql";
|
||||
import { useUpdateProfileAboutMutation, useMyProfileAboutQuery, UpdateProfileAboutMutationVariables, UserBasicInfoFragmentDoc } from "src/graphql";
|
||||
import { NotificationsService } from "src/services/notifications.service";
|
||||
import * as yup from "yup";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import Avatar from "src/features/Profiles/Components/Avatar/Avatar";
|
||||
import { usePrompt } from "src/utils/hooks";
|
||||
import { useAppDispatch, usePrompt } from "src/utils/hooks";
|
||||
import SaveChangesCard from "../SaveChangesCard/SaveChangesCard";
|
||||
import { toast } from "react-toastify";
|
||||
import Card from "src/Components/Card/Card";
|
||||
import NotFoundPage from "src/features/Shared/pages/NotFoundPage/NotFoundPage";
|
||||
import { setUser } from "src/redux/features/user.slice";
|
||||
import UpdateProfileAboutTabSkeleton from "./BasicProfileInfoTab.Skeleton";
|
||||
import { useApolloClient } from "@apollo/client";
|
||||
|
||||
interface Props {
|
||||
data: Pick<User,
|
||||
| 'name'
|
||||
| 'email'
|
||||
| 'lightning_address'
|
||||
| 'jobTitle'
|
||||
| 'avatar'
|
||||
| 'website'
|
||||
| 'github'
|
||||
| 'twitter'
|
||||
| 'linkedin'
|
||||
| 'location'
|
||||
| 'bio'
|
||||
>,
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
type IFormInputs = Props['data'];
|
||||
type IFormInputs = NonNullable<UpdateProfileAboutMutationVariables['data']>;
|
||||
|
||||
const schema: yup.SchemaOf<IFormInputs> = yup.object({
|
||||
name: yup.string().trim().required().min(2),
|
||||
@@ -63,21 +52,36 @@ const schema: yup.SchemaOf<IFormInputs> = yup.object({
|
||||
|
||||
}).required();
|
||||
|
||||
export default function UpdateMyProfileTab({ data, onClose }: Props) {
|
||||
export default function BasicProfileInfoTab() {
|
||||
|
||||
const { register, formState: { errors, isDirty, }, handleSubmit, reset } = useForm<IFormInputs>({
|
||||
defaultValues: data,
|
||||
defaultValues: {},
|
||||
resolver: yupResolver(schema),
|
||||
mode: 'onBlur',
|
||||
});
|
||||
|
||||
const apolloClient = useApolloClient()
|
||||
const profileQuery = useMyProfileAboutQuery({
|
||||
onCompleted: data => {
|
||||
if (data.me)
|
||||
reset(data.me)
|
||||
}
|
||||
})
|
||||
const [mutate, mutationStatus] = useUpdateProfileAboutMutation();
|
||||
|
||||
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
usePrompt('You may have some unsaved changes. You still want to leave?', isDirty)
|
||||
|
||||
|
||||
|
||||
if (profileQuery.loading)
|
||||
return <UpdateProfileAboutTabSkeleton />
|
||||
|
||||
if (!profileQuery.data?.me)
|
||||
return <NotFoundPage />
|
||||
|
||||
|
||||
const onSubmit: SubmitHandler<IFormInputs> = data => {
|
||||
|
||||
const toastId = toast.loading("Saving changes...", NotificationsService.defaultOptions)
|
||||
@@ -98,9 +102,17 @@ export default function UpdateMyProfileTab({ data, onClose }: Props) {
|
||||
website: data.website,
|
||||
}
|
||||
},
|
||||
onCompleted: () => {
|
||||
reset(data);
|
||||
toast.update(toastId, { render: "Saved changes successfully", type: "success", ...NotificationsService.defaultOptions, isLoading: false });
|
||||
onCompleted: ({ updateProfileDetails: data }) => {
|
||||
if (data) {
|
||||
dispatch(setUser(data))
|
||||
reset(data);
|
||||
apolloClient.writeFragment({
|
||||
id: `User:${data?.id}`,
|
||||
data,
|
||||
fragment: UserBasicInfoFragmentDoc,
|
||||
})
|
||||
toast.update(toastId, { render: "Saved changes successfully", type: "success", ...NotificationsService.defaultOptions, isLoading: false });
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -114,7 +126,7 @@ export default function UpdateMyProfileTab({ data, onClose }: Props) {
|
||||
<Card className="md:col-span-2" defaultPadding={false}>
|
||||
<div className="bg-gray-600 relative h-[160px] rounded-t-16">
|
||||
<div className="absolute left-24 bottom-0 translate-y-1/2">
|
||||
<Avatar src={data.avatar} width={120} />
|
||||
<Avatar src={profileQuery.data.me.avatar} width={120} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-16 md:p-24 mt-64">
|
||||
@@ -0,0 +1,28 @@
|
||||
fragment UserBasicInfo on BaseUser {
|
||||
id
|
||||
name
|
||||
avatar
|
||||
join_date
|
||||
role
|
||||
email
|
||||
jobTitle
|
||||
lightning_address
|
||||
website
|
||||
twitter
|
||||
github
|
||||
linkedin
|
||||
bio
|
||||
location
|
||||
}
|
||||
|
||||
query MyProfileAbout {
|
||||
me {
|
||||
...UserBasicInfo
|
||||
}
|
||||
}
|
||||
|
||||
mutation updateProfileAbout($data: ProfileDetailsInput) {
|
||||
updateProfileDetails(data: $data) {
|
||||
...UserBasicInfo
|
||||
}
|
||||
}
|
||||
@@ -2,27 +2,26 @@ import { Navigate, NavLink, Route, Routes } from "react-router-dom";
|
||||
import LoadingPage from "src/Components/LoadingPage/LoadingPage";
|
||||
import NotFoundPage from "src/features/Shared/pages/NotFoundPage/NotFoundPage";
|
||||
import Slider from "src/Components/Slider/Slider";
|
||||
import { useProfileQuery } from "src/graphql";
|
||||
import { useAppSelector, useMediaQuery } from "src/utils/hooks";
|
||||
import UpdateMyProfileTab from "./UpdateMyProfileTab/UpdateMyProfileTab";
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { MEDIA_QUERIES } from "src/utils/theme";
|
||||
import AccountCard from "./AccountCard/AccountCard";
|
||||
import PreferencesTab from "./PreferencesTab/PreferencesTab";
|
||||
import RolesSkillsTab from "./RolesSkillsTab/RolesSkillsTab";
|
||||
import Card from "src/Components/Card/Card";
|
||||
import BasicProfileInfoTab from "./BasicProfileInfoTab/BasicProfileInfoTab";
|
||||
|
||||
|
||||
const links = [
|
||||
{
|
||||
text: "👾 My Profile",
|
||||
path: 'my-profile',
|
||||
text: "🤠 Basic information",
|
||||
path: 'basic-info',
|
||||
},
|
||||
// {
|
||||
// text: "🙍♂️ Account",
|
||||
// path: 'account',
|
||||
// },
|
||||
{
|
||||
text: "⚙️ Preferences",
|
||||
text: "🎛️ Roles & Skills",
|
||||
path: 'roles-skills',
|
||||
},
|
||||
{
|
||||
text: "⚙️ Settings & Preferences",
|
||||
path: 'preferences',
|
||||
}
|
||||
]
|
||||
@@ -30,24 +29,16 @@ const links = [
|
||||
|
||||
|
||||
export default function EditProfilePage() {
|
||||
const userId = useAppSelector(state => state.user.me?.id)
|
||||
const profileQuery = useProfileQuery({
|
||||
variables: {
|
||||
profileId: userId!,
|
||||
},
|
||||
skip: !userId,
|
||||
})
|
||||
const isMediumScreen = useMediaQuery(MEDIA_QUERIES.isMedium)
|
||||
|
||||
const isMediumScreen = useMediaQuery(MEDIA_QUERIES.isMedium);
|
||||
|
||||
|
||||
const user = useAppSelector(state => state.user.me)
|
||||
|
||||
|
||||
if (!userId || profileQuery.loading)
|
||||
if (!user)
|
||||
return <LoadingPage />
|
||||
|
||||
if (!profileQuery.data?.profile)
|
||||
return <NotFoundPage />
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
@@ -96,10 +87,10 @@ export default function EditProfilePage() {
|
||||
</aside>
|
||||
<main className="md:col-span-3">
|
||||
<Routes>
|
||||
<Route index element={<Navigate to='my-profile' />} />
|
||||
<Route path='my-profile' element={<UpdateMyProfileTab data={profileQuery.data.profile} />} />
|
||||
<Route path='account' element={<AccountCard />} />
|
||||
<Route path='preferences' element={<PreferencesTab nostr_prv_key={profileQuery.data.profile.nostr_prv_key} nostr_pub_key={profileQuery.data.profile.nostr_pub_key} isOwner={true} />
|
||||
<Route index element={<Navigate to='basic-info' />} />
|
||||
<Route path='basic-info' element={<BasicProfileInfoTab />} />
|
||||
<Route path='roles-skills' element={<RolesSkillsTab />} />
|
||||
<Route path='preferences' element={<PreferencesTab />
|
||||
} />
|
||||
</Routes>
|
||||
</main>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { MOCK_DATA } from 'src/mocks/data';
|
||||
import CommentsSettingsCard from './CommentsSettingsCard';
|
||||
|
||||
export default {
|
||||
title: 'Profiles/Profile Page/Comments Settings Card',
|
||||
title: 'Profiles/Edit Profile Page/Comments Settings Card',
|
||||
component: CommentsSettingsCard,
|
||||
argTypes: {
|
||||
backgroundColor: { control: 'color' },
|
||||
@@ -16,5 +16,6 @@ const Template: ComponentStory<typeof CommentsSettingsCard> = (args) => <Comment
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
|
||||
nostr_prv_key: "1234389753205473258327580937245",
|
||||
nostr_pub_key: "55234231277835473258327580937245",
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import AccountCard from './AccountCard';
|
||||
import { MOCK_DATA } from 'src/mocks/data';
|
||||
import AccountCard from './LinkedAccountsCard';
|
||||
|
||||
export default {
|
||||
title: 'Profiles/Profile Page/Account Card',
|
||||
title: 'Profiles/Edit Profile Page/Linked Wallets Card',
|
||||
component: AccountCard,
|
||||
argTypes: {
|
||||
backgroundColor: { control: 'color' },
|
||||
@@ -15,5 +16,6 @@ const Template: ComponentStory<typeof AccountCard> = (args) => <AccountCard {...
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
|
||||
value: MOCK_DATA['user'].walletsKeys,
|
||||
onChange: () => { }
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import Button from 'src/Components/Button/Button';
|
||||
import { useAppDispatch } from 'src/utils/hooks';
|
||||
import { openModal } from 'src/redux/features/modals.slice';
|
||||
import Card from 'src/Components/Card/Card';
|
||||
import { MyProfile } from 'src/graphql';
|
||||
import WalletKey from './WalletKey';
|
||||
import InfoCard from 'src/Components/InfoCard/InfoCard';
|
||||
|
||||
|
||||
export type WalletKeyType = MyProfile['walletsKeys'][number]
|
||||
|
||||
interface Props {
|
||||
value: WalletKeyType[],
|
||||
onChange: (newValue: WalletKeyType[]) => void
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default function LinkedAccountsCard({ value, onChange }: Props) {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const connectNewWallet = () => {
|
||||
dispatch(openModal({ Modal: "LinkingAccountModal" }))
|
||||
}
|
||||
|
||||
const updateKeyName = (idx: number, newName: string) => {
|
||||
onChange(value.map((item, i) => {
|
||||
if (i === idx)
|
||||
return {
|
||||
...item,
|
||||
name: newName
|
||||
}
|
||||
return item;
|
||||
}))
|
||||
}
|
||||
|
||||
const deleteKey = (idx: number,) => {
|
||||
onChange([...value.slice(0, idx), ...value.slice(idx + 1)])
|
||||
}
|
||||
|
||||
const hasMultiWallets = value.length > 1;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<p className="text-body2 font-bold">🔐 Linked Wallets</p>
|
||||
<p className="text-body4 text-gray-600 mt-8">
|
||||
These are the wallets that you can login to this account from. You can add up to 3 wallets.
|
||||
</p>
|
||||
<div className='mt-24 flex flex-col gap-16'>
|
||||
<ul className="mt-8 relative flex flex-col gap-8">
|
||||
{value.map((item, idx) =>
|
||||
<WalletKey
|
||||
key={idx}
|
||||
hasMultiWallets={hasMultiWallets}
|
||||
walletKey={item}
|
||||
onRename={v => updateKeyName(idx, v)}
|
||||
onDelete={() => deleteKey(idx)}
|
||||
/>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
{value.length < 3 &&
|
||||
<Button color='none' size='sm' className='mt-16 text-gray-600 hover:bg-gray-50' onClick={connectNewWallet}>
|
||||
+ Add another wallet
|
||||
</Button>}
|
||||
<InfoCard>
|
||||
<span className="font-bold">💡 Note:</span> if you link a wallet that was used to create another account previously, you won't be able to login to that account until you remove it from here.
|
||||
</InfoCard>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import { useToggle } from '@react-hookz/web';
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { FiTrash2, FiLock } from 'react-icons/fi';
|
||||
import Button from 'src/Components/Button/Button';
|
||||
import IconButton from 'src/Components/IconButton/IconButton';
|
||||
import { useReduxEffect } from 'src/utils/hooks/useReduxEffect';
|
||||
import { WalletKeyType } from './LinkedAccountsCard'
|
||||
import { useAppDispatch } from "src/utils/hooks";
|
||||
import { openModal } from "src/redux/features/modals.slice";
|
||||
import 'react-popper-tooltip/dist/styles.css';
|
||||
import { usePopperTooltip } from 'react-popper-tooltip'
|
||||
|
||||
interface Props {
|
||||
walletKey: WalletKeyType,
|
||||
hasMultiWallets?: boolean;
|
||||
onRename: (newName: string) => void
|
||||
onDelete: () => void
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default function WalletKey({ walletKey, hasMultiWallets, onRename, onDelete }: Props) {
|
||||
|
||||
const ref = useRef<HTMLInputElement>(null!);
|
||||
const [name, setName] = useState(walletKey.name);
|
||||
const [editMode, toggleEditMode] = useToggle(false);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
|
||||
|
||||
const {
|
||||
getArrowProps,
|
||||
getTooltipProps,
|
||||
setTooltipRef,
|
||||
setTriggerRef,
|
||||
visible,
|
||||
} = usePopperTooltip();
|
||||
|
||||
const CONFIRM_DELETE_WALLET = useMemo(() => createAction<{ confirmed?: boolean }>(`CONFIRM_DELETE_WALLET_${walletKey.key.slice(0, 10)}`)({}), [walletKey.key])
|
||||
|
||||
const saveNameChanges = () => {
|
||||
toggleEditMode();
|
||||
onRename(name);
|
||||
}
|
||||
|
||||
const onConfirmDelete = useCallback(({ payload: { confirmed } }: typeof CONFIRM_DELETE_WALLET) => {
|
||||
if (confirmed)
|
||||
onDelete()
|
||||
}, [onDelete])
|
||||
|
||||
useReduxEffect(onConfirmDelete, CONFIRM_DELETE_WALLET.type);
|
||||
|
||||
useEffect(() => {
|
||||
if (editMode)
|
||||
ref.current.focus()
|
||||
}, [editMode])
|
||||
|
||||
const handleDelete = () => {
|
||||
dispatch(openModal({
|
||||
Modal: "RemoveWalletKeyModal",
|
||||
props: {
|
||||
callbackAction: {
|
||||
type: CONFIRM_DELETE_WALLET.type,
|
||||
payload: { confirmed: false }
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<li key={walletKey.key} className="flex gap-16 items-center">
|
||||
<div className="input-wrapper relative min-w-0">
|
||||
<span className="input-icon !pr-0">🔑</span>
|
||||
<input
|
||||
ref={ref}
|
||||
disabled={!editMode}
|
||||
type='text'
|
||||
value={name}
|
||||
className="input-text overflow-hidden text-ellipsis"
|
||||
placeholder='e.g My Alby Key'
|
||||
onChange={e => setName(e.target.value)}
|
||||
/>
|
||||
{!editMode && <Button size='sm' color='none' className='text-blue-400 shrink-0' onClick={() => toggleEditMode()}>Rename</Button>}
|
||||
{editMode &&
|
||||
<Button
|
||||
size='sm'
|
||||
color='none'
|
||||
className='text-blue-400 shrink-0'
|
||||
disabled={name.length === 0}
|
||||
onClick={saveNameChanges}
|
||||
>Save</Button>}
|
||||
</div>
|
||||
{hasMultiWallets && <div className="min-w-[60px] flex justify-center">
|
||||
{!walletKey.is_current ?
|
||||
<IconButton
|
||||
size='sm'
|
||||
className='text-red-500 shrink-0 mx-auto'
|
||||
onClick={() => handleDelete()}
|
||||
><FiTrash2 /> </IconButton>
|
||||
: <>
|
||||
<span ref={setTriggerRef} >
|
||||
<FiLock className="text-body4 text-gray-400" />
|
||||
</span>
|
||||
{visible && (
|
||||
<div
|
||||
ref={setTooltipRef}
|
||||
{...getTooltipProps({ className: 'tooltip-container !bg-gray-900 !text-white text-body5 !rounded-12 !p-12' })}
|
||||
>
|
||||
<div {...getArrowProps({ className: 'tooltip-arrow' })} />
|
||||
You're now logged-in with this wallet. <br /> To remove it, login to your account with a different wallet.
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
}
|
||||
|
||||
</div>}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import { QRCodeSVG } from 'qrcode.react';
|
||||
import Button from "src/Components/Button/Button";
|
||||
import { FiCopy } from "react-icons/fi";
|
||||
import useCopyToClipboard from "src/utils/hooks/useCopyToClipboard";
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { IoClose } from 'react-icons/io5';
|
||||
|
||||
|
||||
|
||||
@@ -57,7 +59,8 @@ export default function LinkingAccountModal({ onClose, direction, ...props }: Mo
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const { loadingLnurl, data: { lnurl }, error } = useLnurlQuery();
|
||||
const clipboard = useCopyToClipboard()
|
||||
const clipboard = useCopyToClipboard();
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
|
||||
|
||||
@@ -71,41 +74,50 @@ export default function LinkingAccountModal({ onClose, direction, ...props }: Mo
|
||||
clipboard(lnurl);
|
||||
}
|
||||
|
||||
const done = () => {
|
||||
apolloClient.refetchQueries({
|
||||
include: ['MyProfilePreferences']
|
||||
})
|
||||
onClose?.()
|
||||
}
|
||||
|
||||
|
||||
let content = <></>
|
||||
|
||||
if (error)
|
||||
content = <div className="flex flex-col gap-24 items-center">
|
||||
<p className="text-body3 text-red-500 font-bold">Something wrong happened...</p>
|
||||
<a href='/login' className="text body4 text-gray-500 hover:underline">Refresh the page</a>
|
||||
content = <div className="flex flex-col gap-24 items-center my-32">
|
||||
<p className="text-body3 text-red-500 font-bold">Ooops...😵</p>
|
||||
<p className="text-body4 text-gray-600 text-center">An error happened while fetching the link, please check your internet connection and try again.</p>
|
||||
</div>
|
||||
|
||||
else if (loadingLnurl)
|
||||
content = <div className="flex flex-col gap-24 items-center">
|
||||
<Grid color="var(--primary)" width="150" />
|
||||
<p className="text-body3 font-bold">Fetching Lnurl-Auth...</p>
|
||||
content = <div className="flex flex-col gap-24 items-center my-32">
|
||||
<Grid color="var(--primary)" width="80" />
|
||||
<p className="text-body4 text-gray-600 font-bold">Fetching Lnurl-Auth Link...</p>
|
||||
</div>
|
||||
|
||||
else
|
||||
else {
|
||||
content =
|
||||
<>
|
||||
<p className="text-body1 font-bolder text-center">
|
||||
Link your account ⚡
|
||||
</p>
|
||||
<QRCodeSVG
|
||||
width={160}
|
||||
height={160}
|
||||
value={lnurl}
|
||||
/>
|
||||
<div className='flex flex-col gap-24 items-center mt-32 '>
|
||||
<a href={`lightning:${lnurl}`} >
|
||||
<QRCodeSVG
|
||||
width={280}
|
||||
height={280}
|
||||
level='H'
|
||||
value={`lightning:${lnurl}`}
|
||||
bgColor='transparent'
|
||||
imageSettings={{
|
||||
src: '/assets/images/nut_3d.png',
|
||||
width: 28,
|
||||
height: 28,
|
||||
excavate: true
|
||||
}}
|
||||
/>
|
||||
</a>
|
||||
<p className="text-gray-600 text-body4 text-center">
|
||||
Scan this code or copy + paste it to your other lightning wallet to be able to login later with it to this account.
|
||||
<br />
|
||||
When done, click the button below to close this modal.
|
||||
Scan this code or copy + paste it to your lightning wallet to connect another account to your maker profile. You can also click the QR code to open your WebLN wallet. When done, click the button below to close this modal.
|
||||
</p>
|
||||
<div className="flex flex-col w-full gap-16">
|
||||
{/* <a href={lnurl}
|
||||
className='grow block text-body4 text-center text-white font-bolder bg-primary-500 hover:bg-primary-600 rounded-10 px-16 py-12 active:scale-90 transition-transform'
|
||||
>Click to connect <IoRocketOutline /></a> */}
|
||||
<Button
|
||||
color='gray'
|
||||
className='grow'
|
||||
@@ -114,15 +126,14 @@ export default function LinkingAccountModal({ onClose, direction, ...props }: Mo
|
||||
>{copied ? "Copied" : "Copy"} <FiCopy /></Button>
|
||||
<Button
|
||||
color='primary'
|
||||
onClick={onClose}
|
||||
onClick={done}
|
||||
fullWidth
|
||||
className='mt-16'
|
||||
>
|
||||
Done?
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
@@ -132,8 +143,10 @@ export default function LinkingAccountModal({ onClose, direction, ...props }: Mo
|
||||
initial='initial'
|
||||
animate="animate"
|
||||
exit='exit'
|
||||
className="modal-card w-full max-w-[326px] bg-white border-2 border-gray-200 rounded-16 p-16 flex flex-col gap-16 items-center"
|
||||
className="modal-card max-w-[442px] p-24 rounded-xl relative"
|
||||
>
|
||||
<IoClose className='absolute text-body2 top-24 right-24 hover:cursor-pointer' onClick={onClose} />
|
||||
<h2 className='text-h5 font-bold text-center'>Connect another ⚡️ wallet</h2>
|
||||
{content}
|
||||
</motion.div>
|
||||
)
|
||||
@@ -0,0 +1,48 @@
|
||||
import React from 'react'
|
||||
import Card from 'src/Components/Card/Card';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
export default function PreferencesTabSkeleton() {
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-24">
|
||||
<div className="col-span-2 flex flex-col gap-24">
|
||||
<Card>
|
||||
<p className="text-body2 font-bold"><Skeleton width="15ch" /></p>
|
||||
<p className="text-body4 text-gray-600 mt-8">
|
||||
<Skeleton width="70%" />
|
||||
<Skeleton width="35%" />
|
||||
</p>
|
||||
|
||||
<div className='mt-24 flex flex-col gap-16'>
|
||||
<ul className="mt-8 relative flex flex-col gap-8">
|
||||
{Array(3).fill(0).map((_, idx) =>
|
||||
<div key={idx} className='flex gap-16'>
|
||||
<li className="grow flex flex-wrap gap-16 justify-between items-center text-body4 border-b py-12 px-16 border border-gray-200 rounded-16 focus-within:ring-1 ring-primary-200">
|
||||
<div className='p-0 border-0 focus:border-0 focus:outline-none grow
|
||||
focus:ring-0 placeholder:!text-gray-400' >
|
||||
<Skeleton width='20ch'></Skeleton>
|
||||
</div>
|
||||
</li>
|
||||
<div className="min-w-[60px]"></div>
|
||||
</div>
|
||||
)}
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<p className="text-body2 font-bold"><Skeleton width="12ch" /></p>
|
||||
<p className="text-body4 text-gray-600 mt-8">
|
||||
<Skeleton width="80%" />
|
||||
<Skeleton width="50%" />
|
||||
<Skeleton width="65%" />
|
||||
</p>
|
||||
<div className="py-42"></div>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,20 +1,105 @@
|
||||
import { Nullable } from 'remirror';
|
||||
import LinkedAccountsCard from './LinkedAccountsCard/LinkedAccountsCard';
|
||||
import CommentsSettingsCard from './CommentsSettingsCard/CommentsSettingsCard';
|
||||
import { UpdateUserPreferencesMutationVariables, useMyProfilePreferencesQuery, useUpdateUserPreferencesMutation } from 'src/graphql';
|
||||
import NotFoundPage from "src/features/Shared/pages/NotFoundPage/NotFoundPage";
|
||||
import PreferencesTabSkeleton from './PreferencesTab.Skeleton'
|
||||
import * as yup from "yup";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import { Controller, SubmitHandler, useForm } from 'react-hook-form';
|
||||
import SaveChangesCard from '../SaveChangesCard/SaveChangesCard';
|
||||
import { toast } from 'react-toastify';
|
||||
import { NotificationsService } from 'src/services';
|
||||
import { NetworkStatus } from '@apollo/client';
|
||||
import { usePrompt } from 'src/utils/hooks';
|
||||
|
||||
|
||||
interface Props {
|
||||
isOwner?: boolean;
|
||||
nostr_pub_key: Nullable<string>;
|
||||
nostr_prv_key: Nullable<string>;
|
||||
|
||||
}
|
||||
|
||||
export default function PreferencesTab({ nostr_prv_key, nostr_pub_key, isOwner }: Props) {
|
||||
export type IProfilePreferencesForm = NonNullable<UpdateUserPreferencesMutationVariables>;
|
||||
|
||||
const schema: yup.SchemaOf<IProfilePreferencesForm> = yup.object({
|
||||
walletsKeys: yup.array().of(yup.object().shape({
|
||||
name: yup.string().required(),
|
||||
key: yup.string().trim().required(),
|
||||
}).required())
|
||||
.required(),
|
||||
}).required();
|
||||
|
||||
export default function PreferencesTab() {
|
||||
|
||||
const { formState: { isDirty, }, handleSubmit, reset, control } = useForm<IProfilePreferencesForm>({
|
||||
defaultValues: {
|
||||
walletsKeys: []
|
||||
},
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const query = useMyProfilePreferencesQuery({
|
||||
onCompleted: data => {
|
||||
if (data.me) reset(data.me)
|
||||
},
|
||||
notifyOnNetworkStatusChange: true,
|
||||
});
|
||||
const [mutate, mutationStatus] = useUpdateUserPreferencesMutation();
|
||||
|
||||
|
||||
usePrompt('You may have some unsaved changes. You still want to leave?', isDirty)
|
||||
|
||||
|
||||
if (query.networkStatus === NetworkStatus.loading)
|
||||
return <PreferencesTabSkeleton />
|
||||
|
||||
if (!query.data?.me)
|
||||
return <NotFoundPage />
|
||||
|
||||
|
||||
const onSubmit: SubmitHandler<IProfilePreferencesForm> = data => {
|
||||
if (!Array.isArray(data.walletsKeys))
|
||||
return;
|
||||
|
||||
const toastId = toast.loading("Saving changes...", NotificationsService.defaultOptions)
|
||||
|
||||
|
||||
mutate({
|
||||
variables: {
|
||||
walletsKeys: data.walletsKeys.map(({ key, name }) => ({ key, name })),
|
||||
},
|
||||
onCompleted: ({ updateUserPreferences }) => {
|
||||
if (updateUserPreferences) {
|
||||
reset(updateUserPreferences);
|
||||
toast.update(toastId, { render: "Saved changes successfully", type: "success", ...NotificationsService.defaultOptions, isLoading: false });
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
toast.update(toastId, { render: "A network error happened", type: "error", ...NotificationsService.defaultOptions, isLoading: false });
|
||||
mutationStatus.reset()
|
||||
})
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-24">
|
||||
<div className="col-span-2">
|
||||
<CommentsSettingsCard nostr_prv_key={nostr_prv_key} nostr_pub_key={nostr_pub_key} />
|
||||
<div className="md:col-span-2 flex flex-col gap-24">
|
||||
<Controller
|
||||
control={control}
|
||||
name="walletsKeys"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<LinkedAccountsCard value={value as any} onChange={v => {
|
||||
onChange(v);
|
||||
handleSubmit(onSubmit)();
|
||||
}} />
|
||||
)}
|
||||
/>
|
||||
<CommentsSettingsCard nostr_prv_key={query.data.me.nostr_prv_key} nostr_pub_key={query.data.me.nostr_pub_key} />
|
||||
</div>
|
||||
<div className="self-start sticky-side-element">
|
||||
{/* <SaveChangesCard
|
||||
isLoading={mutationStatus.loading}
|
||||
isDirty={isDirty}
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
onCancel={() => reset()}
|
||||
/> */}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { ModalCard, modalCardVariants } from 'src/Components/Modals/ModalsContainer/ModalsContainer'
|
||||
import { motion } from 'framer-motion'
|
||||
import { IoClose } from 'react-icons/io5'
|
||||
import Button from 'src/Components/Button/Button'
|
||||
import { useAppDispatch } from 'src/utils/hooks'
|
||||
import { PayloadAction } from '@reduxjs/toolkit'
|
||||
|
||||
interface Props extends ModalCard {
|
||||
callbackAction: PayloadAction<{ confirmed: boolean }>
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default function RemoveWalletKeyModal({
|
||||
onClose, direction, callbackAction,
|
||||
}: Props) {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleConfirm = () => {
|
||||
const action = Object.assign({}, callbackAction);
|
||||
action.payload = { confirmed: true }
|
||||
dispatch(action)
|
||||
onClose?.();
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
custom={direction}
|
||||
variants={modalCardVariants}
|
||||
initial='initial'
|
||||
animate="animate"
|
||||
exit='exit'
|
||||
className="modal-card max-w-[326px] p-24 rounded-xl relative"
|
||||
>
|
||||
<IoClose className='absolute text-body2 top-24 right-24 hover:cursor-pointer' onClick={onClose} />
|
||||
<h2 className='text-h5 font-bold text-center'>Remove key?</h2>
|
||||
<div className="pt-16 flex flex-col gap-24 mt-16">
|
||||
<p className="text-h1 text-center">🔑</p>
|
||||
<p className="text-body4 text-gray-600 text-center font-light">Are you sure you want to remove this key from your account? Once deleted, you won’t be able to recover it.</p>
|
||||
<div className="grid grid-cols-2 gap-16">
|
||||
<Button color='gray' onClick={onClose} >Cancel</Button>
|
||||
<Button color='red' onClick={handleConfirm} >Remove</Button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { lazyModal } from 'src/utils/helperFunctions';
|
||||
|
||||
export const { LazyComponent: RemoveWalletKeyModal } = lazyModal(() => import('./RemoveWalletKeyModal'))
|
||||
@@ -0,0 +1,24 @@
|
||||
query MyProfilePreferences {
|
||||
me {
|
||||
id
|
||||
walletsKeys {
|
||||
key
|
||||
name
|
||||
is_current
|
||||
}
|
||||
nostr_prv_key
|
||||
nostr_pub_key
|
||||
}
|
||||
}
|
||||
|
||||
mutation UpdateUserPreferences($walletsKeys: [UserKeyInputType!]) {
|
||||
updateUserPreferences(userKeys: $walletsKeys) {
|
||||
id
|
||||
walletsKeys {
|
||||
key
|
||||
name
|
||||
}
|
||||
nostr_pub_key
|
||||
nostr_prv_key
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from 'react'
|
||||
import Card from 'src/Components/Card/Card';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
import { random } from 'src/utils/helperFunctions';
|
||||
|
||||
export default function RolesSkillsTabSkeleton() {
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-24">
|
||||
<div className="col-span-2 flex flex-col gap-24">
|
||||
<Card>
|
||||
<p className="text-body2 font-bold"><Skeleton width="15ch" /></p>
|
||||
<p className="text-body4 text-gray-600 mt-8">
|
||||
<Skeleton width="90%" />
|
||||
</p>
|
||||
<ul className=' flex flex-wrap gap-8 mt-24'>
|
||||
{Array(10).fill(0).map((_, idx) => {
|
||||
|
||||
return <div
|
||||
key={idx}
|
||||
className={`px-12 py-8 border rounded-10 text-body5 font-medium`}
|
||||
><Skeleton width={`${Math.round(random(8, 15))}ch`} />
|
||||
</div>
|
||||
})}
|
||||
</ul>
|
||||
<div className="py-80"></div>
|
||||
</Card>
|
||||
<Card>
|
||||
<p className="text-body2 font-bold"><Skeleton width="12ch" /></p>
|
||||
<p className="text-body4 text-gray-600 mt-8">
|
||||
<Skeleton width="80%" />
|
||||
</p>
|
||||
<ul className=' flex flex-wrap gap-x-8 gap-y-20 mt-16'>
|
||||
{Array(8).fill(0).map((_, idx) => <li key={idx} className="px-16 py-8 bg-gray-100 rounded-48 text-body5 font-medium">
|
||||
<Skeleton width={`${Math.round(random(3, 12))}ch`} />
|
||||
</li>)}
|
||||
</ul>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
|
||||
import NotFoundPage from "src/features/Shared/pages/NotFoundPage/NotFoundPage";
|
||||
import * as yup from "yup";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import { Controller, SubmitHandler, useForm } from 'react-hook-form';
|
||||
import SaveChangesCard from '../SaveChangesCard/SaveChangesCard';
|
||||
import { toast } from 'react-toastify';
|
||||
import { NotificationsService } from 'src/services';
|
||||
import { gql, NetworkStatus, useApolloClient } from '@apollo/client';
|
||||
import { usePrompt } from 'src/utils/hooks';
|
||||
import { UpdateUserRolesSkillsMutationVariables, useMyProfileRolesSkillsQuery, useUpdateUserRolesSkillsMutation, UserRolesSkillsFragmentDoc } from 'src/graphql'
|
||||
import UpdateRolesCard from "./UpdateRolesCard/UpdateRolesCard";
|
||||
import UpdateSkillsCard from "./UpdateSkillsCard/UpdateSkillsCard";
|
||||
import RolesSkillsTabSkeleton from "./RolesSkillsTab.Skeleton";
|
||||
|
||||
|
||||
interface Props {
|
||||
}
|
||||
|
||||
|
||||
|
||||
export type IRolesSkillsForm = NonNullable<UpdateUserRolesSkillsMutationVariables['data']>;
|
||||
|
||||
const schema: yup.SchemaOf<IRolesSkillsForm> = yup.object({
|
||||
roles: yup.array().of(
|
||||
yup.object().shape({
|
||||
id: yup.number().required(),
|
||||
level: yup.string().required(),
|
||||
}).required()
|
||||
).required(),
|
||||
skills: yup.array().of(
|
||||
yup.object().shape({
|
||||
id: yup.number().required(),
|
||||
}).required()
|
||||
).required(),
|
||||
}).required();
|
||||
|
||||
export default function PreferencesTab() {
|
||||
|
||||
const { formState: { isDirty, }, handleSubmit, reset, control } = useForm<IRolesSkillsForm>({
|
||||
defaultValues: {
|
||||
roles: [],
|
||||
skills: [],
|
||||
},
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const query = useMyProfileRolesSkillsQuery({
|
||||
onCompleted: data => {
|
||||
if (data.me) reset(data.me)
|
||||
},
|
||||
notifyOnNetworkStatusChange: true,
|
||||
});
|
||||
|
||||
const apolloClient = useApolloClient()
|
||||
const [mutate, mutationStatus] = useUpdateUserRolesSkillsMutation({
|
||||
onCompleted: ({ updateProfileRoles: data }) => {
|
||||
apolloClient.writeFragment({
|
||||
id: `User:${data?.id}`,
|
||||
data: {
|
||||
roles: data?.roles,
|
||||
skills: data?.skills
|
||||
},
|
||||
fragment: UserRolesSkillsFragmentDoc,
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
usePrompt('You may have some unsaved changes. You still want to leave?', isDirty)
|
||||
|
||||
|
||||
if (query.networkStatus === NetworkStatus.loading)
|
||||
return <RolesSkillsTabSkeleton />
|
||||
|
||||
if (!query.data || !query.data?.me)
|
||||
return <NotFoundPage />
|
||||
|
||||
if (!query.data?.getAllMakersRoles || !query.data?.getAllMakersSkills)
|
||||
return null;
|
||||
|
||||
const onSubmit: SubmitHandler<IRolesSkillsForm> = data => {
|
||||
const toastId = toast.loading("Saving changes...", NotificationsService.defaultOptions)
|
||||
mutate({
|
||||
variables: {
|
||||
data: {
|
||||
roles: data.roles.map(v => ({ id: v.id, level: v.level })),
|
||||
skills: data.skills.map(v => ({ id: v.id })),
|
||||
}
|
||||
},
|
||||
onCompleted: ({ updateProfileRoles }) => {
|
||||
if (updateProfileRoles) {
|
||||
reset(updateProfileRoles);
|
||||
toast.update(toastId, { render: "Saved changes successfully", type: "success", ...NotificationsService.defaultOptions, isLoading: false });
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
toast.update(toastId, { render: "A network error happened", type: "error", ...NotificationsService.defaultOptions, isLoading: false });
|
||||
mutationStatus.reset()
|
||||
})
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-24">
|
||||
<div className="md:col-span-2 flex flex-col gap-24">
|
||||
<Controller
|
||||
control={control}
|
||||
name="roles"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<UpdateRolesCard
|
||||
allRoles={query.data?.getAllMakersRoles!}
|
||||
value={value}
|
||||
onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="skills"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<UpdateSkillsCard
|
||||
allSkills={query.data?.getAllMakersSkills!}
|
||||
value={value}
|
||||
onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="self-start sticky-side-element">
|
||||
<SaveChangesCard
|
||||
isLoading={mutationStatus.loading}
|
||||
isDirty={isDirty}
|
||||
onSubmit={handleSubmit(onSubmit, e => console.log(e))}
|
||||
onCancel={() => reset()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { MOCK_DATA } from 'src/mocks/data';
|
||||
import UpdateRolesCard from './UpdateRolesCard';
|
||||
|
||||
export default {
|
||||
title: 'Profiles/Edit Profile Page/Update Roles Card',
|
||||
component: UpdateRolesCard,
|
||||
argTypes: {
|
||||
backgroundColor: { control: 'color' },
|
||||
},
|
||||
|
||||
} as ComponentMeta<typeof UpdateRolesCard>;
|
||||
|
||||
|
||||
const Template: ComponentStory<typeof UpdateRolesCard> = (args) => <UpdateRolesCard {...args} ></UpdateRolesCard>
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
value: MOCK_DATA['user'].roles,
|
||||
onChange: () => { }
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import React from 'react'
|
||||
import { Control, useFieldArray } from 'react-hook-form'
|
||||
import Card from 'src/Components/Card/Card'
|
||||
import { GenericMakerRole, MakerRole, RoleLevelEnum } from 'src/graphql'
|
||||
import { IRolesSkillsForm } from '../RolesSkillsTab'
|
||||
|
||||
type Value = Pick<MakerRole, 'id' | 'level'>
|
||||
|
||||
interface Props {
|
||||
allRoles: Pick<GenericMakerRole, 'id' | 'title' | 'icon'>[];
|
||||
value: Value[],
|
||||
onChange: (newValue: Value[]) => void
|
||||
}
|
||||
|
||||
export default function UpdateRolesCard(props: Props) {
|
||||
|
||||
const add = (idx: number) => {
|
||||
props.onChange([...props.value.slice(-2), { ...props.allRoles[idx], level: RoleLevelEnum.Beginner }])
|
||||
}
|
||||
|
||||
const remove = (idx: number) => {
|
||||
props.onChange(props.value.filter(v => v.id !== props.allRoles[idx].id))
|
||||
}
|
||||
|
||||
const setLevel = (roleId: number, level: RoleLevelEnum) => {
|
||||
props.onChange(props.value.map(v => {
|
||||
if (v.id !== roleId) return v;
|
||||
return {
|
||||
...v,
|
||||
level
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<p className="text-body2 font-bold">🎛️ Roles</p>
|
||||
<p className="text-body4 text-gray-600 mt-8"> Select your top 3 roles, and let other makers know what your level is.</p>
|
||||
<div className=' flex flex-wrap gap-8 mt-24'>
|
||||
{props.allRoles.map((role, idx) => {
|
||||
const isActive = props.value.some(v => v.id === role.id);
|
||||
|
||||
return <button
|
||||
key={role.id}
|
||||
className={`
|
||||
px-12 py-8 border rounded-10 text-body5 font-medium
|
||||
active:scale-95 transition-transform
|
||||
${!isActive ? "bg-gray-100 hover:bg-gray-200 border-gray-200" : "bg-primary-100 text-primary-600 border-primary-200"}
|
||||
`}
|
||||
onClick={() => isActive ? remove(idx) : add(idx)}
|
||||
>{role.icon} {role.title}
|
||||
</button>
|
||||
})}
|
||||
</div>
|
||||
|
||||
{props.value.length > 0 && <div className="pt-24 mt-24 border-t border-gray-200">
|
||||
<ul className="grid grid-cols-1 2xl:grid-cols-[auto_1fr] items-center gap-16">
|
||||
{props.value.map(role => {
|
||||
const { title, icon } = props.allRoles.find(r => r.id === role.id)!;
|
||||
|
||||
return <React.Fragment key={role.id}>
|
||||
<p className="shrink-0 font-medium text-body4 whitespace-nowrap">{icon} {title}</p>
|
||||
<div className="flex flex-wrap gap-8 grow text-body5 mb-8 last-of-type:mb-0">
|
||||
{[RoleLevelEnum.Beginner, RoleLevelEnum.Hobbyist, RoleLevelEnum.Intermediate, RoleLevelEnum.Advanced, RoleLevelEnum.Pro].map(r =>
|
||||
<button
|
||||
key={r}
|
||||
className={`
|
||||
px-12 py-4 bg-gray-100 border rounded-8 flex-1
|
||||
active:scale-95 transition-transform font-medium
|
||||
${r !== role.level ? "bg-gray-100 hover:bg-gray-200 border-gray-200" : "bg-primary-100 text-primary-600 border-primary-200"}
|
||||
`}
|
||||
onClick={() => setLevel(role.id, r)}
|
||||
>{r}</button>
|
||||
)}</div>
|
||||
</React.Fragment >
|
||||
})}
|
||||
</ul>
|
||||
</div>}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
|
||||
|
||||
import Select from 'react-select';
|
||||
import { OnChangeValue, StylesConfig, components, OptionProps } from "react-select";
|
||||
import Avatar from "src/features/Profiles/Components/Avatar/Avatar";
|
||||
import { FiSearch } from 'react-icons/fi';
|
||||
import { useState } from 'react';
|
||||
import { MyProfileRolesSkillsQuery } from 'src/graphql';
|
||||
|
||||
|
||||
|
||||
type Skill = MyProfileRolesSkillsQuery['getAllMakersSkills'][number]
|
||||
|
||||
interface Props {
|
||||
classes?: {
|
||||
container?: string
|
||||
input?: string
|
||||
}
|
||||
placeholder?: string,
|
||||
onSelect?: (selectedUser: Skill) => void
|
||||
options: Skill[]
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// const OptionComponent = (props: OptionProps<Skill>) => {
|
||||
// return (
|
||||
// <div>
|
||||
// <components.Option {...props} className='!flex items-center gap-16 !py-16'>
|
||||
// <Avatar src={props.data.avatar} width={48} />
|
||||
// <div>
|
||||
// <p className="font-medium self-center">
|
||||
// {props.data.name}
|
||||
// </p>
|
||||
// <p className="text-body5 text-gray-500">
|
||||
// {props.data.jobTitle}
|
||||
// </p>
|
||||
// </div>
|
||||
// </components.Option>
|
||||
|
||||
// </div>
|
||||
// );
|
||||
// };
|
||||
|
||||
|
||||
const colourStyles: StylesConfig = {
|
||||
|
||||
control: (styles, state) => ({
|
||||
...styles,
|
||||
padding: '5px 16px',
|
||||
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'
|
||||
}
|
||||
|
||||
}),
|
||||
multiValueRemove: (styles) => ({
|
||||
...styles,
|
||||
":hover": {
|
||||
background: 'none'
|
||||
}
|
||||
}),
|
||||
indicatorsContainer: () => ({ display: 'none' }),
|
||||
clearIndicator: () => ({ display: 'none' }),
|
||||
indicatorSeparator: () => ({ display: "none" }),
|
||||
input: (styles, state) => ({
|
||||
...styles,
|
||||
" input": {
|
||||
boxShadow: 'none !important'
|
||||
},
|
||||
}),
|
||||
multiValue: styles => ({
|
||||
...styles,
|
||||
padding: '4px 12px',
|
||||
borderRadius: 48,
|
||||
fontWeight: 500
|
||||
}),
|
||||
valueContainer: (styles) => ({
|
||||
...styles,
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
export default function SkillsInput({
|
||||
classes,
|
||||
...props }: Props) {
|
||||
|
||||
const handleChange = (newValue: OnChangeValue<Skill, false>,) => {
|
||||
if (newValue)
|
||||
props.onSelect?.(newValue);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${classes?.container}`}>
|
||||
<Select
|
||||
value={null}
|
||||
placeholder={'Search and add skill'}
|
||||
options={props.options}
|
||||
onChange={handleChange as any}
|
||||
styles={colourStyles as any}
|
||||
getOptionLabel={o => o?.title!}
|
||||
maxMenuHeight={Math.max(200, Math.min(window.innerHeight / 5, 400))}
|
||||
theme={(theme) => ({
|
||||
...theme,
|
||||
borderRadius: 8,
|
||||
colors: {
|
||||
...theme.colors,
|
||||
primary: 'var(--primary)',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { MOCK_DATA } from 'src/mocks/data';
|
||||
import UpdateSkillsCard from './UpdateSkillsCard';
|
||||
|
||||
export default {
|
||||
title: 'Profiles/Edit Profile Page/Update Skills Card',
|
||||
component: UpdateSkillsCard,
|
||||
argTypes: {
|
||||
backgroundColor: { control: 'color' },
|
||||
},
|
||||
|
||||
} as ComponentMeta<typeof UpdateSkillsCard>;
|
||||
|
||||
|
||||
const Template: ComponentStory<typeof UpdateSkillsCard> = (args) => <UpdateSkillsCard {...args} ></UpdateSkillsCard>
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
value: MOCK_DATA['user'].skills,
|
||||
onChange: () => { }
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import { GrClose } from 'react-icons/gr'
|
||||
import Card from 'src/Components/Card/Card'
|
||||
import InfoCard from 'src/Components/InfoCard/InfoCard'
|
||||
import { MakerSkill } from 'src/graphql'
|
||||
import SkillsInput from './SkillsInput'
|
||||
|
||||
type Value = Pick<MakerSkill, 'id'>
|
||||
|
||||
interface Props {
|
||||
allSkills: Pick<MakerSkill, 'id' | 'title'>[];
|
||||
value: Value[],
|
||||
onChange: (newValue: Value[]) => void
|
||||
}
|
||||
|
||||
export default function UpdateSkillsCard(props: Props) {
|
||||
|
||||
const add = (newValue: Value) => {
|
||||
props.onChange([...props.value, newValue])
|
||||
}
|
||||
|
||||
const idToValue = useMemo(() => {
|
||||
const map = new Map<number, Props['allSkills'][number]>();
|
||||
for (let i = 0; i < props.allSkills.length; i++) {
|
||||
const element = props.allSkills[i];
|
||||
map.set(element.id, element);
|
||||
}
|
||||
return map;
|
||||
}, [props.allSkills])
|
||||
|
||||
const remove = (id: number) => {
|
||||
props.onChange(props.value.filter(v => v.id !== id))
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<p className="text-body2 font-bold">🌈 Skills</p>
|
||||
<p className="text-body4 text-gray-600 mt-8">Add some of your skills and let other makers know what you’re good at.</p>
|
||||
<div className="mt-16">
|
||||
<SkillsInput options={props.allSkills.filter(skill => !props.value.some(v => v.id === skill.id))} onSelect={add} />
|
||||
</div>
|
||||
{props.value.length > 0 && <ul className=' flex flex-wrap gap-x-8 gap-y-20 mt-16'>
|
||||
{props.value.map((skill) => <li key={skill.id} className="px-16 py-8 bg-gray-100 rounded-48 text-body5 font-medium">
|
||||
{idToValue.get(skill.id)?.title} <button className='ml-8' onClick={() => remove(skill.id)}><GrClose /></button>
|
||||
</li>)}
|
||||
</ul>}
|
||||
|
||||
<InfoCard>
|
||||
<span className="font-bold">ℹ️ Can't find a specific skill?</span> You can suggest it to be added <a href="https://github.com/peakshift/makers.bolt.fun/discussions/143" target='_blank' rel="noreferrer" className='font-bold underline'>here</a>
|
||||
</InfoCard>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
fragment UserRolesSkills on BaseUser {
|
||||
skills {
|
||||
id
|
||||
title
|
||||
}
|
||||
roles {
|
||||
id
|
||||
title
|
||||
icon
|
||||
level
|
||||
}
|
||||
}
|
||||
|
||||
query MyProfileRolesSkills {
|
||||
me {
|
||||
id
|
||||
...UserRolesSkills
|
||||
}
|
||||
getAllMakersRoles {
|
||||
id
|
||||
title
|
||||
icon
|
||||
}
|
||||
getAllMakersSkills {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
|
||||
mutation UpdateUserRolesSkills($data: ProfileRolesInput) {
|
||||
updateProfileRoles(data: $data) {
|
||||
id
|
||||
skills {
|
||||
id
|
||||
title
|
||||
}
|
||||
roles {
|
||||
id
|
||||
title
|
||||
icon
|
||||
level
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import { Link } from 'react-router-dom'
|
||||
import Button from 'src/Components/Button/Button'
|
||||
import Card from 'src/Components/Card/Card'
|
||||
import Avatar from 'src/features/Profiles/Components/Avatar/Avatar'
|
||||
import { useProfileQuery } from 'src/graphql'
|
||||
import { trimText } from 'src/utils/helperFunctions'
|
||||
import { useAppSelector } from 'src/utils/hooks'
|
||||
import { createRoute } from 'src/utils/routing'
|
||||
@@ -17,14 +16,10 @@ interface Props {
|
||||
|
||||
export default function SaveChangesCard(props: Props) {
|
||||
|
||||
const userId = useAppSelector(state => state.user.me?.id!)
|
||||
const profileQuery = useProfileQuery({
|
||||
variables: {
|
||||
profileId: userId,
|
||||
},
|
||||
})
|
||||
const user = useAppSelector(state => state.user.me)
|
||||
|
||||
if (!profileQuery.data?.profile)
|
||||
|
||||
if (!user)
|
||||
return <></>
|
||||
|
||||
|
||||
@@ -38,18 +33,18 @@ export default function SaveChangesCard(props: Props) {
|
||||
<div className='hidden md:flex gap-8'>
|
||||
<Link
|
||||
className='shrink-0'
|
||||
to={createRoute({ type: 'profile', id: profileQuery.data.profile.id, username: profileQuery.data.profile.name })}>
|
||||
<Avatar width={48} src={profileQuery.data.profile.avatar!} />
|
||||
to={createRoute({ type: 'profile', id: user.id, username: user.name })}>
|
||||
<Avatar width={48} src={user.avatar!} />
|
||||
</Link>
|
||||
<div className='overflow-hidden'>
|
||||
<p className={`text-body4 text-black font-medium overflow-hidden text-ellipsis`}>{profileQuery.data.profile ? trimText(profileQuery.data.profile.name, 30) : "Anonymouse"}</p>
|
||||
{profileQuery.data.profile.jobTitle && <p className={`text-body6 text-gray-600`}>{profileQuery.data.profile.jobTitle}</p>}
|
||||
<p className={`text-body4 text-black font-medium overflow-hidden text-ellipsis`}>{user ? trimText(user.name, 30) : "Anonymouse"}</p>
|
||||
{user.jobTitle && <p className={`text-body6 text-gray-600`}>{user.jobTitle}</p>}
|
||||
</div>
|
||||
{/* {showTimeAgo && <p className={`${nameSize[size]} text-gray-500 ml-auto `}>
|
||||
{dayjs().diff(props.date, 'hour') < 24 ? `${dayjs().diff(props.date, 'hour')}h ago` : undefined}
|
||||
</p>} */}
|
||||
</div>
|
||||
<p className="hidden md:block text-body5">{trimText(profileQuery.data.profile.bio, 120)}</p>
|
||||
<p className="hidden md:block text-body5">{trimText(user.bio, 120)}</p>
|
||||
<div className="flex flex-col gap-16">
|
||||
<Button
|
||||
color="primary"
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
mutation updateProfileAbout($data: UpdateProfileInput) {
|
||||
updateProfile(data: $data) {
|
||||
id
|
||||
name
|
||||
avatar
|
||||
join_date
|
||||
website
|
||||
role
|
||||
email
|
||||
lightning_address
|
||||
jobTitle
|
||||
twitter
|
||||
github
|
||||
linkedin
|
||||
bio
|
||||
location
|
||||
}
|
||||
}
|
||||
@@ -29,24 +29,28 @@ export default function AboutCard({ user, isOwner }: Props) {
|
||||
hasValue: user.website,
|
||||
text: user.website?.replace(/(^\w+:|^)\/\//, '').replace(/\/$/, ""),
|
||||
icon: FiGlobe,
|
||||
colors: "bg-gray-100 text-gray-900",
|
||||
url: user.website && withHttp(user.website)
|
||||
},
|
||||
{
|
||||
hasValue: user.twitter,
|
||||
text: user.twitter,
|
||||
icon: FiTwitter,
|
||||
colors: "bg-blue-100 text-blue-500",
|
||||
url: `https://twitter.com/@${user.twitter}`
|
||||
},
|
||||
{
|
||||
hasValue: user.github,
|
||||
text: user.github,
|
||||
icon: FiGithub,
|
||||
colors: "bg-pink-100 text-pink-600",
|
||||
url: `https://github.com/${user.github}`
|
||||
},
|
||||
{
|
||||
hasValue: user.linkedin,
|
||||
text: "LinkedIn",
|
||||
icon: FiLinkedin,
|
||||
colors: "bg-sky-100 text-cyan-600",
|
||||
url: user.linkedin && withHttp(user.linkedin),
|
||||
}
|
||||
];
|
||||
@@ -65,37 +69,41 @@ export default function AboutCard({ user, isOwner }: Props) {
|
||||
</div>
|
||||
<div className="p-24 pt-0">
|
||||
<div className="flex flex-col gap-16">
|
||||
<h1 className="text-h2 font-bolder break-words">
|
||||
{user.name}
|
||||
</h1>
|
||||
<div>
|
||||
<h1 className="text-h2 font-bolder break-words">
|
||||
{user.name}
|
||||
</h1>
|
||||
|
||||
{links.some(link => link.hasValue) && <div className="flex flex-wrap gap-16">
|
||||
{<p className="text-body3 font-medium text-gray-600">
|
||||
{user.jobTitle ? user.jobTitle : "No job title added"}
|
||||
</p>}
|
||||
</div>
|
||||
{<div className="flex flex-wrap gap-16">
|
||||
{links.filter(link => link.hasValue || isOwner).map((link, idx) => link.hasValue ?
|
||||
<a
|
||||
key={idx}
|
||||
href={link.url!}
|
||||
className="text-body4 text-primary-700 font-medium"
|
||||
className={`w-40 aspect-square rounded-full flex justify-center items-center ${link.colors}`}
|
||||
target='_blank'
|
||||
rel="noreferrer">
|
||||
<link.icon className="scale-125 mr-8" /> <span className="align-middle">{link.text}</span>
|
||||
</a> :
|
||||
<p
|
||||
key={idx}
|
||||
className="text-body4 text-primary-700 font-medium"
|
||||
>
|
||||
<link.icon className="scale-125 mr-8" /> <span className="align-middle">---</span>
|
||||
</p>)}
|
||||
<link.icon className="scale-125" />
|
||||
</a>
|
||||
:
|
||||
(isOwner &&
|
||||
<p
|
||||
key={idx}
|
||||
className={`text-body4 py-8 px-16 rounded-24 font-medium ${link.colors}`}
|
||||
>
|
||||
<link.icon className="scale-125 mr-8" /> <span className="align-middle">---</span>
|
||||
</p>))}
|
||||
</div>}
|
||||
|
||||
{(user.jobTitle || isOwner) && <p className="text-body4 font-medium">
|
||||
{user.jobTitle ? user.jobTitle : "No job title added"}
|
||||
</p>}
|
||||
|
||||
{(user.lightning_address || isOwner) && <p className="text-body5 font-medium">
|
||||
{<p className="text-body5 font-medium">
|
||||
{user.lightning_address ? `⚡${user.lightning_address}` : "⚡ No lightning address"}
|
||||
</p>}
|
||||
|
||||
{(user.bio || isOwner) && <p className="text-body5 font-medium">
|
||||
{<p className="text-body5 font-medium">
|
||||
{user.bio ? user.bio : "No bio added"}
|
||||
</p>}
|
||||
</div>
|
||||
|
||||
@@ -4,9 +4,14 @@ import NotFoundPage from "src/features/Shared/pages/NotFoundPage/NotFoundPage"
|
||||
import { useProfileQuery } from "src/graphql"
|
||||
import AboutCard from "./AboutCard/AboutCard"
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { useAppSelector } from 'src/utils/hooks';
|
||||
import { useAppSelector, useMediaQuery } from 'src/utils/hooks';
|
||||
import styles from './styles.module.scss'
|
||||
import StoriesCard from "./StoriesCard/StoriesCard"
|
||||
import RolesCard from "./RolesCard/RolesCard"
|
||||
import SkillsCard from "./SkillsCard/SkillsCard"
|
||||
import TournamentsCard from "./TournamentsCard/TournamentsCard"
|
||||
import { MEDIA_QUERIES } from "src/utils/theme"
|
||||
import SimilarMakersCard from "./SimilarMakersCard/SimilarMakersCard"
|
||||
|
||||
export default function ProfilePage() {
|
||||
|
||||
@@ -17,7 +22,12 @@ export default function ProfilePage() {
|
||||
},
|
||||
skip: isNaN(Number(id)),
|
||||
})
|
||||
const isOwner = useAppSelector(state => Boolean(state.user.me?.id && state.user.me?.id === profileQuery.data?.profile?.id))
|
||||
const { isOwner } = useAppSelector(state => ({
|
||||
isOwner: Boolean(state.user.me?.id && state.user.me?.id === profileQuery.data?.profile?.id),
|
||||
}))
|
||||
|
||||
const isMediumScreen = useMediaQuery(MEDIA_QUERIES.isMedium)
|
||||
|
||||
|
||||
|
||||
if (profileQuery.loading)
|
||||
@@ -26,6 +36,7 @@ export default function ProfilePage() {
|
||||
if (!profileQuery.data?.profile)
|
||||
return <NotFoundPage />
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
@@ -39,12 +50,33 @@ export default function ProfilePage() {
|
||||
</Helmet>
|
||||
<div className={`page-container ${styles.grid}`}
|
||||
>
|
||||
<aside></aside>
|
||||
<main className="flex flex-col gap-24">
|
||||
<AboutCard user={profileQuery.data.profile} isOwner={isOwner} />
|
||||
<StoriesCard stories={profileQuery.data.profile.stories} isOwner={isOwner} />
|
||||
</main>
|
||||
<aside></aside>
|
||||
{isMediumScreen ?
|
||||
<>
|
||||
<aside>
|
||||
|
||||
<RolesCard roles={profileQuery.data.profile.roles} isOwner={isOwner} />
|
||||
<SkillsCard skills={profileQuery.data.profile.skills} isOwner={isOwner} />
|
||||
<TournamentsCard tournaments={profileQuery.data.profile.tournaments} isOwner={isOwner} />
|
||||
</aside>
|
||||
<main>
|
||||
|
||||
<AboutCard user={profileQuery.data.profile} isOwner={isOwner} />
|
||||
<StoriesCard stories={profileQuery.data.profile.stories} isOwner={isOwner} />
|
||||
</main>
|
||||
<aside className="min-w-0">
|
||||
<SimilarMakersCard makers={profileQuery.data.profile.similar_makers} />
|
||||
</aside>
|
||||
</>
|
||||
:
|
||||
<>
|
||||
<main>
|
||||
<AboutCard user={profileQuery.data.profile} isOwner={isOwner} />
|
||||
<RolesCard roles={profileQuery.data.profile.roles} isOwner={isOwner} />
|
||||
<SkillsCard skills={profileQuery.data.profile.skills} isOwner={isOwner} />
|
||||
<StoriesCard stories={profileQuery.data.profile.stories} isOwner={isOwner} />
|
||||
</main>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { MOCK_DATA } from 'src/mocks/data';
|
||||
import RolesCard from './RolesCard';
|
||||
|
||||
export default {
|
||||
title: 'Profiles/Profile Page/Roles Card',
|
||||
component: RolesCard,
|
||||
argTypes: {
|
||||
backgroundColor: { control: 'color' },
|
||||
},
|
||||
|
||||
} as ComponentMeta<typeof RolesCard>;
|
||||
|
||||
|
||||
const Template: ComponentStory<typeof RolesCard> = (args) => <div className="max-w-[326px]"><RolesCard {...args} ></RolesCard></div>
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
roles: MOCK_DATA['user'].roles
|
||||
}
|
||||
|
||||
export const Empty = Template.bind({});
|
||||
Empty.args = {
|
||||
roles: [],
|
||||
}
|
||||
|
||||
export const EmptyOwner = Template.bind({});
|
||||
EmptyOwner.args = {
|
||||
roles: [],
|
||||
isOwner: true
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import Card from 'src/Components/Card/Card'
|
||||
import Button from 'src/Components/Button/Button'
|
||||
import { RoleLevelEnum, User } from 'src/graphql';
|
||||
|
||||
|
||||
|
||||
interface Props {
|
||||
roles: User['roles'][number][]
|
||||
isOwner?: boolean;
|
||||
}
|
||||
|
||||
export default function RolesCard({ roles, isOwner }: Props) {
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<p className="text-body2 font-bold">🎛️ Roles</p>
|
||||
<div className="mt-16">
|
||||
{roles.length === 0 && <>
|
||||
<p className="text-gray-700 text-body4">No roles added</p>
|
||||
{isOwner && <Button color='primary' className='mt-16' size='sm' href='/edit-profile/roles-skills'>Add roles</Button>}
|
||||
</>}
|
||||
<ul className=' flex flex-col gap-16'>
|
||||
{
|
||||
roles
|
||||
.map(role => {
|
||||
|
||||
let levelInt = 0;
|
||||
if (role.level === RoleLevelEnum.Hobbyist) levelInt = 1;
|
||||
if (role.level === RoleLevelEnum.Intermediate) levelInt = 2;
|
||||
if (role.level === RoleLevelEnum.Advanced) levelInt = 3;
|
||||
if (role.level === RoleLevelEnum.Pro) levelInt = 4;
|
||||
return {
|
||||
...role,
|
||||
level: levelInt
|
||||
}
|
||||
})
|
||||
.sort((a, b) => b.level - a.level)
|
||||
.map((role) => <li
|
||||
key={role.id}
|
||||
className={`flex gap-16 items-center rounded-8 cursor-pointer font-bold p-4active:scale-95 transition-transform`}
|
||||
>
|
||||
<span className={`bg-gray-50 rounded-8 w-40 h-40 text-center py-8`}>{role.icon}</span>
|
||||
<div className='grow'>
|
||||
<p className="font-medium text-body5 text-gray-800">
|
||||
{role.title}
|
||||
</p>
|
||||
<div className="flex gap-4 mt-16">
|
||||
{Array(5).fill(0).map((_, idx) => {
|
||||
return <div key={idx} className={`flex-1 h-[2px] rounded-4 ${(idx) <= role.level ? "bg-primary-500" : "bg-gray-100"}`} />
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { MOCK_DATA } from 'src/mocks/data';
|
||||
|
||||
import SimilarMakersCard from './SimilarMakersCard';
|
||||
|
||||
export default {
|
||||
title: 'Profiles/Profile Page/Similar Makers Card',
|
||||
component: SimilarMakersCard,
|
||||
argTypes: {
|
||||
backgroundColor: { control: 'color' },
|
||||
},
|
||||
} as ComponentMeta<typeof SimilarMakersCard>;
|
||||
|
||||
|
||||
const Template: ComponentStory<typeof SimilarMakersCard> = (args) => <div className="max-w-[326px]"><SimilarMakersCard {...args as any} ></SimilarMakersCard></div>
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
makers: MOCK_DATA['user'].similar_makers
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
|
||||
import { Link } from 'react-router-dom'
|
||||
import Card from 'src/Components/Card/Card'
|
||||
import Avatar from 'src/features/Profiles/Components/Avatar/Avatar'
|
||||
import { User } from 'src/graphql'
|
||||
import { createRoute } from 'src/utils/routing'
|
||||
|
||||
interface Props {
|
||||
makers: Pick<User,
|
||||
| "id"
|
||||
| "name"
|
||||
| "jobTitle"
|
||||
| 'avatar'
|
||||
>[]
|
||||
}
|
||||
|
||||
export default function SimilarMakersCard({ makers }: Props) {
|
||||
|
||||
|
||||
return (
|
||||
<Card onlyMd>
|
||||
<h3 className="text-body2 font-bolder">Similar makers</h3>
|
||||
<ul className='flex flex-col'>
|
||||
{makers.map(maker => {
|
||||
return <Link key={maker.id} to={createRoute({ type: "profile", id: maker.id, username: maker.name })} className="border-b py-16 last-of-type:border-b-0 last-of-type:pb-0">
|
||||
<li className="flex items-start gap-8">
|
||||
<Avatar width={40} src={maker.avatar} />
|
||||
<div className='overflow-hidden'>
|
||||
<p className="text-body4 text-gray-800 font-medium overflow-hidden text-ellipsis">{maker.name}</p>
|
||||
<p className="text-body5 text-gray-500 font-medium">{maker.jobTitle}</p>
|
||||
</div>
|
||||
</li>
|
||||
</Link>
|
||||
})}
|
||||
</ul>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { MOCK_DATA } from 'src/mocks/data';
|
||||
import SkillsCard from './SkillsCard';
|
||||
|
||||
export default {
|
||||
title: 'Profiles/Profile Page/Skills Card',
|
||||
component: SkillsCard,
|
||||
argTypes: {
|
||||
backgroundColor: { control: 'color' },
|
||||
},
|
||||
|
||||
} as ComponentMeta<typeof SkillsCard>;
|
||||
|
||||
|
||||
const Template: ComponentStory<typeof SkillsCard> = (args) => <div className="max-w-[326px]"><SkillsCard {...args} ></SkillsCard></div>
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
skills: MOCK_DATA['user'].skills
|
||||
}
|
||||
|
||||
export const Empty = Template.bind({});
|
||||
Empty.args = {
|
||||
skills: [],
|
||||
}
|
||||
|
||||
export const EmptyOwner = Template.bind({});
|
||||
EmptyOwner.args = {
|
||||
skills: [],
|
||||
isOwner: true
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import Card from 'src/Components/Card/Card'
|
||||
import Button from 'src/Components/Button/Button'
|
||||
import { User } from 'src/graphql';
|
||||
|
||||
|
||||
|
||||
interface Props {
|
||||
skills: User['skills'][number][]
|
||||
isOwner: boolean;
|
||||
}
|
||||
|
||||
export default function SkillsCard({ skills, isOwner }: Props) {
|
||||
return (
|
||||
<Card>
|
||||
<p className="text-body2 font-bold">🌈 Skills</p>
|
||||
<div className="mt-16">
|
||||
{skills.length === 0 && <>
|
||||
<p className="text-gray-700 text-body4">No skills added</p>
|
||||
{isOwner && <Button color='primary' className='mt-16' size='sm' href='/edit-profile/roles-skills'>Add skills</Button>}
|
||||
</>}
|
||||
<ul className=' flex flex-wrap gap-x-8 gap-y-20'>
|
||||
{skills.map((skill) => <li key={skill.id} className="text-body5 border border-gray-200 px-12 py-4 bg-gray-100 rounded-48 font-medium">{skill.title}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { MOCK_DATA } from 'src/mocks/data';
|
||||
import TournamentsCard from './TournamentsCard';
|
||||
|
||||
export default {
|
||||
title: 'Profiles/Profile Page/Tournaments Card',
|
||||
component: TournamentsCard,
|
||||
argTypes: {
|
||||
backgroundColor: { control: 'color' },
|
||||
},
|
||||
|
||||
} as ComponentMeta<typeof TournamentsCard>;
|
||||
|
||||
|
||||
const Template: ComponentStory<typeof TournamentsCard> = (args) => <div className="max-w-[326px]"><TournamentsCard {...args} ></TournamentsCard></div>
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
tournaments: MOCK_DATA['user'].tournaments
|
||||
}
|
||||
|
||||
export const Empty = Template.bind({});
|
||||
Empty.args = {
|
||||
tournaments: [],
|
||||
}
|
||||
|
||||
export const EmptyOwner = Template.bind({});
|
||||
EmptyOwner.args = {
|
||||
tournaments: [],
|
||||
isOwner: true
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import Card from 'src/Components/Card/Card'
|
||||
import Button from 'src/Components/Button/Button'
|
||||
import { RoleLevelEnum, User } from 'src/graphql';
|
||||
|
||||
|
||||
|
||||
interface Props {
|
||||
tournaments: Pick<User['tournaments'][number],
|
||||
| 'id'
|
||||
| 'title'
|
||||
| 'thumbnail_image'
|
||||
| 'start_date'
|
||||
| 'end_date'
|
||||
>[]
|
||||
isOwner?: boolean;
|
||||
}
|
||||
|
||||
export default function TournamentsCard({ tournaments, isOwner }: Props) {
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<p className="text-body2 font-bold">🏆 Tournaments </p>
|
||||
<div className="mt-16">
|
||||
{tournaments.length === 0 && <>
|
||||
<p className="text-gray-700 text-body4">No tournaments entered.</p>
|
||||
</>}
|
||||
<ul className=' flex flex-wrap gap-x-8 gap-y-20'>
|
||||
{
|
||||
tournaments.map((tournament) => {
|
||||
|
||||
const isLive = ((new Date() < new Date(tournament.end_date)) && (new Date() > new Date(tournament.start_date)));
|
||||
|
||||
return <li key={tournament.id} className="flex gap-16 items-center">
|
||||
<img src={tournament.thumbnail_image} className='w-48 border-2 border-gray-100 aspect-square rounded-16 object-cover' alt="" />
|
||||
<div>
|
||||
<p className="text-gray-900 font-medium">{tournament.title}</p>
|
||||
<p className={`${isLive ? "text-green-500" : "text-warning-500"} text-body5 font-medium`}>• {isLive ? "Live" : "Completed"}</p>
|
||||
</div>
|
||||
</li>
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,19 +1,5 @@
|
||||
query profile($profileId: Int!) {
|
||||
profile(id: $profileId) {
|
||||
id
|
||||
name
|
||||
avatar
|
||||
join_date
|
||||
role
|
||||
email
|
||||
jobTitle
|
||||
lightning_address
|
||||
website
|
||||
twitter
|
||||
github
|
||||
linkedin
|
||||
bio
|
||||
location
|
||||
stories {
|
||||
id
|
||||
title
|
||||
@@ -24,7 +10,20 @@ query profile($profileId: Int!) {
|
||||
icon
|
||||
}
|
||||
}
|
||||
nostr_prv_key
|
||||
nostr_pub_key
|
||||
tournaments {
|
||||
id
|
||||
title
|
||||
thumbnail_image
|
||||
start_date
|
||||
end_date
|
||||
}
|
||||
similar_makers {
|
||||
id
|
||||
name
|
||||
avatar
|
||||
jobTitle
|
||||
}
|
||||
...UserBasicInfo
|
||||
...UserRolesSkills
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,21 @@
|
||||
grid-template-areas: "main";
|
||||
|
||||
> aside:first-of-type {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
grid-area: aside1;
|
||||
}
|
||||
> main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
grid-area: main;
|
||||
}
|
||||
> aside:last-of-type {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
grid-area: aside2;
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
import { hackathons } from "./data/hackathon";
|
||||
import { posts, feed, generatePostComments } from "./data/posts";
|
||||
import { categories, projects } from "./data/projects";
|
||||
import { users } from "./data/users";
|
||||
import { allMakersRoles, allMakersSkills, users } from "./data/users";
|
||||
|
||||
export const MOCK_DATA = {
|
||||
projects,
|
||||
@@ -12,4 +12,6 @@ export const MOCK_DATA = {
|
||||
generatePostComments: generatePostComments,
|
||||
user: users[0],
|
||||
users: users,
|
||||
allMakersRoles: allMakersRoles,
|
||||
allMakersSkills: allMakersSkills,
|
||||
}
|
||||
@@ -1,7 +1,103 @@
|
||||
import { User } from "src/graphql";
|
||||
import { Chance } from "chance";
|
||||
import { GenericMakerRole, MakerSkill, MyProfile, RoleLevelEnum, User } from "src/graphql";
|
||||
import { randomItem, randomItems } from "src/utils/helperFunctions";
|
||||
import { posts } from "./posts";
|
||||
import { getCoverImage, getAvatarImage } from "./utils";
|
||||
|
||||
export const users: User[] = [{
|
||||
const chance = new Chance();
|
||||
export const allMakersRoles: GenericMakerRole[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Frontend Dev",
|
||||
icon: "💄"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Backend Dev",
|
||||
icon: "💻️"
|
||||
}, {
|
||||
id: 3,
|
||||
title: "UI/UX Designer",
|
||||
icon: "🌈️️"
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "Community Manager",
|
||||
icon: "🎉️️"
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: "Founder",
|
||||
icon: "🦄️"
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: "Marketer",
|
||||
icon: "🚨️"
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
title: "Content Creator",
|
||||
icon: "🎥️"
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
title: "Researcher",
|
||||
icon: "🔬"
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
title: "Data engineer",
|
||||
icon: "💿️"
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
title: "Growth hacker",
|
||||
icon: "📉️"
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
title: "Technical Writer",
|
||||
icon: "✍️️"
|
||||
},
|
||||
]
|
||||
|
||||
export const allMakersSkills: MakerSkill[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Figma"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Prototyping"
|
||||
}, {
|
||||
id: 3,
|
||||
title: "Writing"
|
||||
}, {
|
||||
id: 4,
|
||||
title: "CSS"
|
||||
}, {
|
||||
id: 5,
|
||||
title: "React.js"
|
||||
}, {
|
||||
id: 6,
|
||||
title: "Wordpress"
|
||||
}, {
|
||||
id: 7,
|
||||
title: "Principle app"
|
||||
}, {
|
||||
id: 8,
|
||||
title: "UX design"
|
||||
}, {
|
||||
id: 9,
|
||||
title: "User research"
|
||||
}, {
|
||||
id: 10,
|
||||
title: "User testing"
|
||||
},
|
||||
]
|
||||
|
||||
export const users: (User | MyProfile)[] = [{
|
||||
id: 123,
|
||||
email: "mtg0987654321@gmail.com",
|
||||
avatar: "https://avatars.dicebear.com/api/bottts/Mtgmtg.svg",
|
||||
@@ -14,13 +110,69 @@ export const users: User[] = [{
|
||||
linkedin: "https://www.linkedin.com/in/mtg-softwares-dev/",
|
||||
location: "Germany, Berlin",
|
||||
role: "user",
|
||||
twitter: "john-doe",
|
||||
twitter: "mtg",
|
||||
website: "https://mtg-dev.tech",
|
||||
stories: posts.stories,
|
||||
nostr_prv_key: "123123124asdfsadfsa8d7fsadfasdf",
|
||||
nostr_pub_key: "123124123123dfsadfsa8d7f11sadfasdf",
|
||||
},
|
||||
{
|
||||
walletsKeys: [
|
||||
{
|
||||
key: "1645h234j2421zxvertw",
|
||||
name: "My Alby wallet key",
|
||||
is_current: true
|
||||
},
|
||||
{
|
||||
key: "66345134234235",
|
||||
name: "My Phoenix wallet key",
|
||||
is_current: false
|
||||
},],
|
||||
roles: randomItems(3, ...allMakersRoles).map(role => ({ ...role, level: randomItem(...Object.values(RoleLevelEnum)) })),
|
||||
skills: randomItems(7, ...allMakersSkills),
|
||||
tournaments: [
|
||||
{
|
||||
id: 1,
|
||||
title: "BreezConf",
|
||||
description: chance.paragraph(),
|
||||
cover_image: getCoverImage(),
|
||||
thumbnail_image: getCoverImage(),
|
||||
start_date: new Date(2021, 3).toISOString(),
|
||||
end_date: new Date(2021, 4).toISOString(),
|
||||
tags: [],
|
||||
website: "https://breez-conf.com"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Shock the Web 3",
|
||||
description: chance.paragraph(),
|
||||
cover_image: getCoverImage(),
|
||||
thumbnail_image: getCoverImage(),
|
||||
start_date: new Date(2022, 7).toISOString(),
|
||||
end_date: new Date(2022, 11).toISOString(),
|
||||
tags: [],
|
||||
website: "https://shock-the-web.com"
|
||||
},
|
||||
],
|
||||
similar_makers: [
|
||||
{
|
||||
id: 144,
|
||||
name: "Johns Beharry",
|
||||
jobTitle: "Manager",
|
||||
avatar: getAvatarImage(),
|
||||
},
|
||||
{
|
||||
id: 155,
|
||||
name: "Edward P",
|
||||
jobTitle: "Front-end Developer",
|
||||
avatar: getAvatarImage(),
|
||||
},
|
||||
{
|
||||
id: 166,
|
||||
name: "Mohammed T",
|
||||
jobTitle: "Front-end Developer",
|
||||
avatar: getAvatarImage(),
|
||||
},
|
||||
] as User[]
|
||||
}, {
|
||||
id: 441,
|
||||
email: "eddy@gmail.com",
|
||||
avatar: "https://avatars.dicebear.com/api/bottts/Eduardu.svg",
|
||||
@@ -77,3 +229,5 @@ export const users: User[] = [{
|
||||
nostr_prv_key: "123123124asdfsadfsa8d7fsadfasdf",
|
||||
nostr_pub_key: "123124123123dfsadfsa8d7f11sadfasdf",
|
||||
}]
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
import { graphql } from 'msw'
|
||||
import { allCategories, getAllHackathons, getCategory, getFeed, getMyDrafts, getPostById, getProject, getTrendingPosts, hottestProjects, me, newProjects, popularTags, profile, projectsByCategory, searchProjects, searchUsers } from './resolvers'
|
||||
import { allCategories, getAllHackathons, getAllMakersRoles, getAllMakersSkills, getCategory, getFeed, getMyDrafts, getPostById, getProject, getTrendingPosts, hottestProjects, me, newProjects, popularTags, profile, projectsByCategory, searchProjects, searchUsers } from './resolvers'
|
||||
import {
|
||||
NavCategoriesQuery,
|
||||
ExploreProjectsQuery,
|
||||
@@ -31,6 +31,9 @@ import {
|
||||
GetMyDraftsQuery,
|
||||
SearchUsersQuery,
|
||||
SearchUsersQueryVariables,
|
||||
MyProfileAboutQuery,
|
||||
MyProfilePreferencesQuery,
|
||||
MyProfileRolesSkillsQuery,
|
||||
} from 'src/graphql'
|
||||
|
||||
const delay = (ms = 1000) => new Promise((res) => setTimeout(res, ms + Math.random() * 1000))
|
||||
@@ -197,7 +200,6 @@ export const handlers = [
|
||||
|
||||
graphql.query<MeQuery>('Me', async (req, res, ctx) => {
|
||||
await delay()
|
||||
|
||||
return res(
|
||||
ctx.data({
|
||||
me: me()
|
||||
@@ -205,6 +207,38 @@ export const handlers = [
|
||||
)
|
||||
}),
|
||||
|
||||
|
||||
graphql.query<MyProfileAboutQuery>('MyProfileAbout', async (req, res, ctx) => {
|
||||
await delay()
|
||||
return res(
|
||||
ctx.data({
|
||||
me: me(),
|
||||
})
|
||||
)
|
||||
}),
|
||||
|
||||
graphql.query<MyProfilePreferencesQuery>('MyProfilePreferences', async (req, res, ctx) => {
|
||||
await delay()
|
||||
return res(
|
||||
ctx.data({
|
||||
me: me(),
|
||||
})
|
||||
)
|
||||
}),
|
||||
|
||||
|
||||
graphql.query<MyProfileRolesSkillsQuery>('MyProfileRolesSkills', async (req, res, ctx) => {
|
||||
await delay()
|
||||
return res(
|
||||
ctx.data({
|
||||
me: { ...me() },
|
||||
getAllMakersRoles: getAllMakersRoles(),
|
||||
getAllMakersSkills: getAllMakersSkills(),
|
||||
})
|
||||
)
|
||||
}),
|
||||
|
||||
|
||||
graphql.query<ProfileQuery>('profile', async (req, res, ctx) => {
|
||||
await delay()
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { MOCK_DATA } from "./data";
|
||||
import { Query, QueryGetFeedArgs, QueryGetPostByIdArgs } from 'src/graphql'
|
||||
import { MyProfile, Query, QueryGetFeedArgs, QueryGetPostByIdArgs, User } from 'src/graphql'
|
||||
import { Chance } from "chance";
|
||||
import { tags } from "./data/tags";
|
||||
import { hackathons } from "./data/hackathon";
|
||||
@@ -72,13 +72,25 @@ export function getAllHackathons() {
|
||||
}
|
||||
|
||||
export function me() {
|
||||
return MOCK_DATA['user']
|
||||
return {
|
||||
...MOCK_DATA['user'],
|
||||
__typename: "MyProfile",
|
||||
} as MyProfile
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function profile() {
|
||||
return MOCK_DATA['user']
|
||||
return { ...MOCK_DATA['user'], __typename: 'User' } as User
|
||||
}
|
||||
|
||||
export function getAllMakersRoles() {
|
||||
return MOCK_DATA['allMakersRoles']
|
||||
}
|
||||
|
||||
export function getAllMakersSkills() {
|
||||
return MOCK_DATA['allMakersSkills']
|
||||
}
|
||||
export function searchUsers(value: string) {
|
||||
return MOCK_DATA['users'].filter(u => u.name.toLowerCase().indexOf(value.toLowerCase()) !== -1);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ import { InsertLinkModal } from 'src/Components/Inputs/TextEditor/InsertLinkModa
|
||||
import { Claim_FundWithdrawCard, Claim_CopySignatureCard, Claim_GenerateSignatureCard, Claim_SubmittedCard } from "src/features/Projects/pages/ProjectPage/ClaimProject";
|
||||
import { ModalCard } from "src/Components/Modals/ModalsContainer/ModalsContainer";
|
||||
import { ConfirmModal } from "src/Components/Modals/ConfirmModal";
|
||||
import { LinkingAccountModal } from "src/features/Profiles/pages/EditProfilePage/AccountCard/LinkingAccountModal";
|
||||
import { RemoveWalletKeyModal } from "src/features/Profiles/pages/EditProfilePage/PreferencesTab/RemoveWalletKeyModal";
|
||||
import { LinkingAccountModal } from "src/features/Profiles/pages/EditProfilePage/PreferencesTab/LinkingAccountModal";
|
||||
|
||||
import { ComponentProps } from "react";
|
||||
import { generateId } from "src/utils/helperFunctions";
|
||||
@@ -25,20 +26,28 @@ export enum Direction {
|
||||
|
||||
|
||||
export const ALL_MODALS = {
|
||||
//Projects
|
||||
ProjectDetailsCard,
|
||||
|
||||
// Auth
|
||||
Login_ScanningWalletCard,
|
||||
Login_NativeWalletCard,
|
||||
Login_SuccessCard,
|
||||
Login_ExternalWalletCard,
|
||||
VoteCard,
|
||||
Claim_GenerateSignatureCard,
|
||||
Claim_CopySignatureCard,
|
||||
Claim_SubmittedCard,
|
||||
Claim_FundWithdrawCard,
|
||||
|
||||
// Misc
|
||||
ConfirmModal,
|
||||
VoteCard,
|
||||
NoWeblnModal,
|
||||
LinkingAccountModal,
|
||||
ProjectListedModal,
|
||||
|
||||
// User Wallets Keys
|
||||
LinkingAccountModal,
|
||||
RemoveWalletKeyModal,
|
||||
// Text Editor Modals
|
||||
InsertImageModal,
|
||||
InsertVideoModal,
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { MyProfile } from "src/graphql";
|
||||
|
||||
interface StoreState {
|
||||
me: {
|
||||
id: number;
|
||||
name: string;
|
||||
avatar: string;
|
||||
join_date: string;
|
||||
}
|
||||
me: Pick<MyProfile,
|
||||
| 'id'
|
||||
| "name"
|
||||
| "avatar"
|
||||
| "bio"
|
||||
| "jobTitle"
|
||||
| 'join_date'>
|
||||
| undefined // fetching user data if exist
|
||||
| null // user not logged in
|
||||
|
||||
|
||||
@@ -39,3 +39,7 @@
|
||||
transition-timing-function: ease-in;
|
||||
transition-duration: 400ms;
|
||||
}
|
||||
|
||||
.tooltip-arrow::after {
|
||||
border-color: var(--arrowColor, #101828) transparent !important;
|
||||
}
|
||||
|
||||
@@ -49,6 +49,9 @@ export const apolloClient = new ApolloClient({
|
||||
httpLink
|
||||
]),
|
||||
cache: new InMemoryCache({
|
||||
possibleTypes: {
|
||||
BaseUser: ['User', 'MyProfile']
|
||||
},
|
||||
typePolicies: {
|
||||
Query: {
|
||||
fields: {
|
||||
|
||||
Reference in New Issue
Block a user