Merge branch 'dev' into feature/list-your-product-ui

This commit is contained in:
MTG2000
2022-09-05 10:54:49 +03:00
83 changed files with 8870 additions and 685 deletions

2
.gitignore vendored
View File

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

View File

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

View File

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

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

View File

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

View File

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

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

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

View File

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

View File

@@ -1 +0,0 @@
REACT_APP_API_END_POINT = http://localhost:8888/dev

View File

@@ -1,3 +0,0 @@
REACT_APP_ENABLE_MOCKS= true
STORYBOOK_ENABLE_MOCKS= true

View File

@@ -0,0 +1,2 @@
REACT_APP_API_END_POINT = http://localhost:8888/dev

View File

@@ -0,0 +1,2 @@
REACT_APP_API_END_POINT = https://makers-bolt-fun-preview.netlify.app/.netlify/functions

5422
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "UserKey" ADD COLUMN "name" TEXT NOT NULL DEFAULT E'New Key Name';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@@ -4,5 +4,7 @@ query Me {
name
avatar
join_date
jobTitle
bio
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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`}>&#8226; {isLive ? "Live" : "Completed"}</p>
</div>
</li>
})}
</ul>
</div>
</Card>
)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -39,3 +39,7 @@
transition-timing-function: ease-in;
transition-duration: 400ms;
}
.tooltip-arrow::after {
border-color: var(--arrowColor, #101828) transparent !important;
}

View File

@@ -49,6 +49,9 @@ export const apolloClient = new ApolloClient({
httpLink
]),
cache: new InMemoryCache({
possibleTypes: {
BaseUser: ['User', 'MyProfile']
},
typePolicies: {
Query: {
fields: {