Merge pull request #129 from peakshift/feature/profile-roles-skills-ui

Feature - Profile Roles & Skills UI + Api
This commit is contained in:
Mohammed Taher Ghazal
2022-09-01 09:25:38 +03:00
committed by GitHub
45 changed files with 2111 additions and 186 deletions

View File

@@ -28,6 +28,13 @@ declare global {
}
export interface NexusGenInputs {
MakerRoleInput: { // input type
id: number; // Int!
level: NexusGenEnums['RoleLevelEnum']; // RoleLevelEnum!
}
MakerSkillInput: { // input type
id: number; // Int!
}
ProfileDetailsInput: { // input type
avatar?: string | null; // String
bio?: string | null; // String
@@ -41,6 +48,10 @@ 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
@@ -57,6 +68,7 @@ export interface NexusGenInputs {
export interface NexusGenEnums {
POST_TYPE: "Bounty" | "Question" | "Story"
RoleLevelEnum: 3 | 0 | 1 | 2 | 4
VOTE_ITEM_TYPE: "Bounty" | "PostComment" | "Project" | "Question" | "Story" | "User"
}
@@ -124,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!
@@ -140,6 +157,16 @@ 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!
@@ -208,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
@@ -315,6 +352,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!
@@ -332,6 +374,16 @@ 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!
@@ -339,6 +391,7 @@ export interface NexusGenFieldTypes {
deleteStory: NexusGenRootTypes['Story'] | null; // Story
donate: NexusGenRootTypes['Donation']; // Donation!
updateProfileDetails: NexusGenRootTypes['MyProfile'] | null; // MyProfile
updateProfileRoles: NexusGenRootTypes['MyProfile'] | null; // MyProfile
updateUserPreferences: NexusGenRootTypes['MyProfile']; // MyProfile!
vote: NexusGenRootTypes['Vote']; // Vote!
}
@@ -357,7 +410,11 @@ export interface NexusGenFieldTypes {
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
@@ -389,6 +446,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!]!
@@ -405,6 +464,7 @@ export interface NexusGenFieldTypes {
profile: NexusGenRootTypes['User'] | null; // User
projectsByCategory: NexusGenRootTypes['Project'][]; // [Project!]!
searchProjects: NexusGenRootTypes['Project'][]; // [Project!]!
similarMakers: NexusGenRootTypes['User'][]; // [User!]!
}
Question: { // field return type
author: NexusGenRootTypes['Author']; // Author!
@@ -442,6 +502,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
@@ -455,7 +526,11 @@ export interface NexusGenFieldTypes {
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
}
@@ -485,7 +560,11 @@ export interface NexusGenFieldTypes {
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
}
@@ -564,6 +643,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'
@@ -581,6 +665,16 @@ 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'
@@ -588,6 +682,7 @@ export interface NexusGenFieldTypeNames {
deleteStory: 'Story'
donate: 'Donation'
updateProfileDetails: 'MyProfile'
updateProfileRoles: 'MyProfile'
updateUserPreferences: 'MyProfile'
vote: 'Vote'
}
@@ -606,7 +701,11 @@ export interface NexusGenFieldTypeNames {
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'
@@ -638,6 +737,8 @@ export interface NexusGenFieldTypeNames {
allCategories: 'Category'
allProjects: 'Project'
getAllHackathons: 'Hackathon'
getAllMakersRoles: 'GenericMakerRole'
getAllMakersSkills: 'MakerSkill'
getCategory: 'Category'
getDonationsStats: 'DonationsStats'
getFeed: 'Post'
@@ -654,6 +755,7 @@ export interface NexusGenFieldTypeNames {
profile: 'User'
projectsByCategory: 'Project'
searchProjects: 'Project'
similarMakers: 'User'
}
Question: { // field return type name
author: 'Author'
@@ -691,6 +793,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'
@@ -704,7 +817,11 @@ export interface NexusGenFieldTypeNames {
location: 'String'
name: 'String'
role: 'String'
roles: 'MakerRole'
similar_makers: 'User'
skills: 'MakerSkill'
stories: 'Story'
tournaments: 'Tournament'
twitter: 'String'
website: 'String'
}
@@ -734,7 +851,11 @@ export interface NexusGenFieldTypeNames {
location: 'String'
name: 'String'
role: 'String'
roles: 'MakerRole'
similar_makers: 'User'
skills: 'MakerSkill'
stories: 'Story'
tournaments: 'Tournament'
twitter: 'String'
website: 'String'
}
@@ -772,6 +893,9 @@ export interface NexusGenArgTypes {
updateProfileDetails: { // args
data?: NexusGenInputs['ProfileDetailsInput'] | null; // ProfileDetailsInput
}
updateProfileRoles: { // args
data?: NexusGenInputs['ProfileRolesInput'] | null; // ProfileRolesInput
}
updateUserPreferences: { // args
userKeys?: NexusGenInputs['UserKeyInputType'][] | null; // [UserKeyInputType!]
}
@@ -833,6 +957,9 @@ export interface NexusGenArgTypes {
skip?: number | null; // Int
take: number | null; // Int
}
similarMakers: { // args
id: number; // Int!
}
}
}

View File

@@ -31,7 +31,11 @@ interface BaseUser {
location: String
name: String!
role: String
roles: [MakerRole!]!
similar_makers: [User!]!
skills: [MakerSkill!]!
stories: [Story!]!
tournaments: [Tournament!]!
twitter: String
website: String
}
@@ -92,6 +96,12 @@ type DonationsStats {
touranments: String!
}
type GenericMakerRole {
icon: String!
id: Int!
title: String!
}
type Hackathon {
cover_image: String!
description: String!
@@ -111,6 +121,27 @@ 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!
@@ -118,6 +149,7 @@ type Mutation {
deleteStory(id: Int!): Story
donate(amount_in_sat: Int!): Donation!
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!
}
@@ -137,7 +169,11 @@ type MyProfile implements BaseUser {
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
@@ -185,6 +221,11 @@ input ProfileDetailsInput {
website: String
}
input ProfileRolesInput {
roles: [MakerRoleInput!]!
skills: [MakerSkillInput!]!
}
type Project {
awards: [Award!]!
category: Category!
@@ -205,6 +246,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!]!
@@ -221,6 +264,7 @@ type Query {
profile(id: Int!): User
projectsByCategory(category_id: Int!, skip: Int = 0, take: Int = 10): [Project!]!
searchProjects(search: String!, skip: Int = 0, take: Int = 50): [Project!]!
similarMakers(id: Int!): [User!]!
}
type Question implements PostBase {
@@ -237,6 +281,14 @@ type Question implements PostBase {
votes_count: Int!
}
enum RoleLevelEnum {
Advanced
Beginner
Hobbyist
Intermediate
Pro
}
type Story implements PostBase {
author: Author!
body: String!
@@ -271,6 +323,18 @@ type Tag {
title: 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 implements BaseUser {
avatar: String!
bio: String
@@ -284,7 +348,11 @@ type User implements BaseUser {
location: String
name: String!
role: String
roles: [MakerRole!]!
similar_makers: [User!]!
skills: [MakerSkill!]!
stories: [Story!]!
tournaments: [Tournament!]!
twitter: String
website: 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,8 +1,9 @@
const { prisma } = require('../../../prisma');
const { objectType, extendType, intArg, nonNull, inputObjectType, interfaceType, list } = require("nexus");
const { objectType, extendType, intArg, nonNull, inputObjectType, interfaceType, list, enumType } = require("nexus");
const { getUserByPubKey } = require("../../../auth/utils/helperFuncs");
const { removeNulls } = require("./helpers");
const { Tournament } = require('./tournaments');
@@ -24,6 +25,54 @@ const BaseUser = interfaceType({
t.string('linkedin')
t.string('bio')
t.string('location')
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', {
@@ -40,6 +89,72 @@ const BaseUser = interfaceType({
},
})
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) {
@@ -93,6 +208,30 @@ const profile = extendType({
}
})
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) {
@@ -225,9 +364,88 @@ const updateUserPreferences = extendType({
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,
MyProfile,
@@ -235,7 +453,11 @@ module.exports = {
// Queries
me,
profile,
similarMakers,
getAllMakersRoles,
getAllMakersSkills,
// Mutations
updateProfileDetails,
updateUserPreferences,
updateProfileRoles,
}

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

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

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

@@ -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,7 +1,7 @@
import Card from 'src/Components/Card/Card';
import Skeleton from 'react-loading-skeleton';
export default function UpdateProfileAboutTabSkeleton() {
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">

View File

@@ -1,6 +1,5 @@
import { SubmitHandler, useForm } from "react-hook-form"
import Button from "src/Components/Button/Button";
import { User, useUpdateProfileAboutMutation, useMyProfileAboutQuery, UpdateProfileAboutMutationVariables } 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";
@@ -9,10 +8,10 @@ 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 LoadingPage from "src/Components/LoadingPage/LoadingPage";
import NotFoundPage from "src/features/Shared/pages/NotFoundPage/NotFoundPage";
import { setUser } from "src/redux/features/user.slice";
import UpdateProfileAboutTabSkeleton from "./UpdateMyProfileTab.Skeleton";
import UpdateProfileAboutTabSkeleton from "./BasicProfileInfoTab.Skeleton";
import { useApolloClient } from "@apollo/client";
interface Props {
}
@@ -53,7 +52,7 @@ const schema: yup.SchemaOf<IFormInputs> = yup.object({
}).required();
export default function UpdateMyProfileTab() {
export default function BasicProfileInfoTab() {
const { register, formState: { errors, isDirty, }, handleSubmit, reset } = useForm<IFormInputs>({
defaultValues: {},
@@ -61,7 +60,7 @@ export default function UpdateMyProfileTab() {
mode: 'onBlur',
});
const apolloClient = useApolloClient()
const profileQuery = useMyProfileAboutQuery({
onCompleted: data => {
if (data.me)
@@ -70,6 +69,7 @@ export default function UpdateMyProfileTab() {
})
const [mutate, mutationStatus] = useUpdateProfileAboutMutation();
const dispatch = useAppDispatch()
usePrompt('You may have some unsaved changes. You still want to leave?', isDirty)
@@ -106,6 +106,11 @@ export default function UpdateMyProfileTab() {
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 });
}
}

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,19 +2,23 @@ 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 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: "🎛️ Roles & Skills",
path: 'roles-skills',
},
{
text: "⚙️ Settings & Preferences",
@@ -83,8 +87,9 @@ 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 />} />
<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>

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 { 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,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="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,49 @@
import React, { useMemo } from 'react'
import { GrClose } from 'react-icons/gr'
import Card from 'src/Components/Card/Card'
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>}
</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

@@ -1,37 +0,0 @@
query MyProfileAbout {
me {
id
name
avatar
join_date
role
email
jobTitle
lightning_address
website
twitter
github
linkedin
bio
location
}
}
mutation updateProfileAbout($data: ProfileDetailsInput) {
updateProfileDetails(data: $data) {
id
name
avatar
join_date
role
email
jobTitle
lightning_address
website
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,16 @@ 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"
import { useEffect } from "react"
import { gql, useApolloClient } from "@apollo/client"
export default function ProfilePage() {
@@ -17,7 +24,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 +38,7 @@ export default function ProfilePage() {
if (!profileQuery.data?.profile)
return <NotFoundPage />
return (
<>
<Helmet>
@@ -39,12 +52,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>
<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>
<p className="text-body4 text-gray-800 font-medium">{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,5 +10,20 @@ query profile($profileId: Int!) {
icon
}
}
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;
}

View File

@@ -48,7 +48,11 @@ export type BaseUser = {
location: Maybe<Scalars['String']>;
name: Scalars['String'];
role: Maybe<Scalars['String']>;
roles: Array<MakerRole>;
similar_makers: Array<User>;
skills: Array<MakerSkill>;
stories: Array<Story>;
tournaments: Array<Tournament>;
twitter: Maybe<Scalars['String']>;
website: Maybe<Scalars['String']>;
};
@@ -111,6 +115,13 @@ export type DonationsStats = {
touranments: Scalars['String'];
};
export type GenericMakerRole = {
__typename?: 'GenericMakerRole';
icon: Scalars['String'];
id: Scalars['Int'];
title: Scalars['String'];
};
export type Hackathon = {
__typename?: 'Hackathon';
cover_image: Scalars['String'];
@@ -132,6 +143,29 @@ export type LnurlDetails = {
minSendable: Maybe<Scalars['Int']>;
};
export type MakerRole = {
__typename?: 'MakerRole';
icon: Scalars['String'];
id: Scalars['Int'];
level: RoleLevelEnum;
title: Scalars['String'];
};
export type MakerRoleInput = {
id: Scalars['Int'];
level: RoleLevelEnum;
};
export type MakerSkill = {
__typename?: 'MakerSkill';
id: Scalars['Int'];
title: Scalars['String'];
};
export type MakerSkillInput = {
id: Scalars['Int'];
};
export type Mutation = {
__typename?: 'Mutation';
confirmDonation: Donation;
@@ -140,6 +174,7 @@ export type Mutation = {
deleteStory: Maybe<Story>;
donate: Donation;
updateProfileDetails: Maybe<MyProfile>;
updateProfileRoles: Maybe<MyProfile>;
updateUserPreferences: MyProfile;
vote: Vote;
};
@@ -177,6 +212,11 @@ export type MutationUpdateProfileDetailsArgs = {
};
export type MutationUpdateProfileRolesArgs = {
data: InputMaybe<ProfileRolesInput>;
};
export type MutationUpdateUserPreferencesArgs = {
userKeys: InputMaybe<Array<UserKeyInputType>>;
};
@@ -204,7 +244,11 @@ export type MyProfile = BaseUser & {
nostr_prv_key: Maybe<Scalars['String']>;
nostr_pub_key: Maybe<Scalars['String']>;
role: Maybe<Scalars['String']>;
roles: Array<MakerRole>;
similar_makers: Array<User>;
skills: Array<MakerSkill>;
stories: Array<Story>;
tournaments: Array<Tournament>;
twitter: Maybe<Scalars['String']>;
walletsKeys: Array<WalletKey>;
website: Maybe<Scalars['String']>;
@@ -253,6 +297,11 @@ export type ProfileDetailsInput = {
website: InputMaybe<Scalars['String']>;
};
export type ProfileRolesInput = {
roles: Array<MakerRoleInput>;
skills: Array<MakerSkillInput>;
};
export type Project = {
__typename?: 'Project';
awards: Array<Award>;
@@ -275,6 +324,8 @@ export type Query = {
allCategories: Array<Category>;
allProjects: Array<Project>;
getAllHackathons: Array<Hackathon>;
getAllMakersRoles: Array<GenericMakerRole>;
getAllMakersSkills: Array<MakerSkill>;
getCategory: Category;
getDonationsStats: DonationsStats;
getFeed: Array<Post>;
@@ -291,6 +342,7 @@ export type Query = {
profile: Maybe<User>;
projectsByCategory: Array<Project>;
searchProjects: Array<Project>;
similarMakers: Array<User>;
};
@@ -370,6 +422,11 @@ export type QuerySearchProjectsArgs = {
take?: InputMaybe<Scalars['Int']>;
};
export type QuerySimilarMakersArgs = {
id: Scalars['Int'];
};
export type Question = PostBase & {
__typename?: 'Question';
author: Author;
@@ -385,6 +442,14 @@ export type Question = PostBase & {
votes_count: Scalars['Int'];
};
export enum RoleLevelEnum {
Advanced = 'Advanced',
Beginner = 'Beginner',
Hobbyist = 'Hobbyist',
Intermediate = 'Intermediate',
Pro = 'Pro'
}
export type Story = PostBase & {
__typename?: 'Story';
author: Author;
@@ -421,6 +486,19 @@ export type Tag = {
title: Scalars['String'];
};
export type Tournament = {
__typename?: 'Tournament';
cover_image: Scalars['String'];
description: Scalars['String'];
end_date: Scalars['Date'];
id: Scalars['Int'];
start_date: Scalars['Date'];
tags: Array<Tag>;
thumbnail_image: Scalars['String'];
title: Scalars['String'];
website: Scalars['String'];
};
export type User = BaseUser & {
__typename?: 'User';
avatar: Scalars['String'];
@@ -435,7 +513,11 @@ export type User = BaseUser & {
location: Maybe<Scalars['String']>;
name: Scalars['String'];
role: Maybe<Scalars['String']>;
roles: Array<MakerRole>;
similar_makers: Array<User>;
skills: Array<MakerSkill>;
stories: Array<Story>;
tournaments: Array<Tournament>;
twitter: Maybe<Scalars['String']>;
website: Maybe<Scalars['String']>;
};
@@ -570,17 +652,11 @@ export type PostDetailsQueryVariables = Exact<{
export type PostDetailsQuery = { __typename?: 'Query', getPostById: { __typename?: 'Bounty', id: number, title: string, createdAt: any, body: string, votes_count: number, type: string, cover_image: string | null, deadline: string, reward_amount: number, applicants_count: number, author: { __typename?: 'Author', id: number, name: string, avatar: string, join_date: any }, tags: Array<{ __typename?: 'Tag', id: number, title: string }>, applications: Array<{ __typename?: 'BountyApplication', id: number, date: string, workplan: string, author: { __typename?: 'Author', id: number, name: string, avatar: string } }> } | { __typename?: 'Question', id: number, title: string, createdAt: any, body: string, votes_count: number, type: string, author: { __typename?: 'Author', id: number, name: string, avatar: string, join_date: any }, tags: Array<{ __typename?: 'Tag', id: number, title: string }> } | { __typename?: 'Story', id: number, title: string, createdAt: any, body: string, votes_count: number, type: string, cover_image: string | null, is_published: boolean | null, author: { __typename?: 'Author', id: number, name: string, avatar: string, join_date: any }, tags: Array<{ __typename?: 'Tag', id: number, title: string }> } };
export type MyProfilePreferencesQueryVariables = Exact<{ [key: string]: never; }>;
type UserBasicInfo_MyProfile_Fragment = { __typename?: 'MyProfile', id: number, name: string, avatar: string, join_date: any, role: string | null, email: string | null, jobTitle: string | null, lightning_address: string | null, website: string | null, twitter: string | null, github: string | null, linkedin: string | null, bio: string | null, location: string | null };
type UserBasicInfo_User_Fragment = { __typename?: 'User', id: number, name: string, avatar: string, join_date: any, role: string | null, email: string | null, jobTitle: string | null, lightning_address: string | null, website: string | null, twitter: string | null, github: string | null, linkedin: string | null, bio: string | null, location: string | null };
export type MyProfilePreferencesQuery = { __typename?: 'Query', me: { __typename?: 'MyProfile', id: number, nostr_prv_key: string | null, nostr_pub_key: string | null, walletsKeys: Array<{ __typename?: 'WalletKey', key: string, name: string }> } | null };
export type UpdateUserPreferencesMutationVariables = Exact<{
walletsKeys: InputMaybe<Array<UserKeyInputType> | UserKeyInputType>;
}>;
export type UpdateUserPreferencesMutation = { __typename?: 'Mutation', updateUserPreferences: { __typename?: 'MyProfile', id: number, nostr_pub_key: string | null, nostr_prv_key: string | null, walletsKeys: Array<{ __typename?: 'WalletKey', key: string, name: string }> } };
export type UserBasicInfoFragment = UserBasicInfo_MyProfile_Fragment | UserBasicInfo_User_Fragment;
export type MyProfileAboutQueryVariables = Exact<{ [key: string]: never; }>;
@@ -594,12 +670,42 @@ export type UpdateProfileAboutMutationVariables = Exact<{
export type UpdateProfileAboutMutation = { __typename?: 'Mutation', updateProfileDetails: { __typename?: 'MyProfile', id: number, name: string, avatar: string, join_date: any, role: string | null, email: string | null, jobTitle: string | null, lightning_address: string | null, website: string | null, twitter: string | null, github: string | null, linkedin: string | null, bio: string | null, location: string | null } | null };
export type MyProfilePreferencesQueryVariables = Exact<{ [key: string]: never; }>;
export type MyProfilePreferencesQuery = { __typename?: 'Query', me: { __typename?: 'MyProfile', id: number, nostr_prv_key: string | null, nostr_pub_key: string | null, walletsKeys: Array<{ __typename?: 'WalletKey', key: string, name: string }> } | null };
export type UpdateUserPreferencesMutationVariables = Exact<{
walletsKeys: InputMaybe<Array<UserKeyInputType> | UserKeyInputType>;
}>;
export type UpdateUserPreferencesMutation = { __typename?: 'Mutation', updateUserPreferences: { __typename?: 'MyProfile', id: number, nostr_pub_key: string | null, nostr_prv_key: string | null, walletsKeys: Array<{ __typename?: 'WalletKey', key: string, name: string }> } };
type UserRolesSkills_MyProfile_Fragment = { __typename?: 'MyProfile', skills: Array<{ __typename?: 'MakerSkill', id: number, title: string }>, roles: Array<{ __typename?: 'MakerRole', id: number, title: string, icon: string, level: RoleLevelEnum }> };
type UserRolesSkills_User_Fragment = { __typename?: 'User', skills: Array<{ __typename?: 'MakerSkill', id: number, title: string }>, roles: Array<{ __typename?: 'MakerRole', id: number, title: string, icon: string, level: RoleLevelEnum }> };
export type UserRolesSkillsFragment = UserRolesSkills_MyProfile_Fragment | UserRolesSkills_User_Fragment;
export type MyProfileRolesSkillsQueryVariables = Exact<{ [key: string]: never; }>;
export type MyProfileRolesSkillsQuery = { __typename?: 'Query', me: { __typename?: 'MyProfile', id: number, skills: Array<{ __typename?: 'MakerSkill', id: number, title: string }>, roles: Array<{ __typename?: 'MakerRole', id: number, title: string, icon: string, level: RoleLevelEnum }> } | null, getAllMakersRoles: Array<{ __typename?: 'GenericMakerRole', id: number, title: string, icon: string }>, getAllMakersSkills: Array<{ __typename?: 'MakerSkill', id: number, title: string }> };
export type UpdateUserRolesSkillsMutationVariables = Exact<{
data: InputMaybe<ProfileRolesInput>;
}>;
export type UpdateUserRolesSkillsMutation = { __typename?: 'Mutation', updateProfileRoles: { __typename?: 'MyProfile', id: number, skills: Array<{ __typename?: 'MakerSkill', id: number, title: string }>, roles: Array<{ __typename?: 'MakerRole', id: number, title: string, icon: string, level: RoleLevelEnum }> } | null };
export type ProfileQueryVariables = Exact<{
profileId: Scalars['Int'];
}>;
export type ProfileQuery = { __typename?: 'Query', profile: { __typename?: 'User', id: number, name: string, avatar: string, join_date: any, role: string | null, email: string | null, jobTitle: string | null, lightning_address: string | null, website: string | null, twitter: string | null, github: string | null, linkedin: string | null, bio: string | null, location: string | null, stories: Array<{ __typename?: 'Story', id: number, title: string, createdAt: any, tags: Array<{ __typename?: 'Tag', id: number, title: string, icon: string | null }> }> } | null };
export type ProfileQuery = { __typename?: 'Query', profile: { __typename?: 'User', id: number, name: string, avatar: string, join_date: any, role: string | null, email: string | null, jobTitle: string | null, lightning_address: string | null, website: string | null, twitter: string | null, github: string | null, linkedin: string | null, bio: string | null, location: string | null, stories: Array<{ __typename?: 'Story', id: number, title: string, createdAt: any, tags: Array<{ __typename?: 'Tag', id: number, title: string, icon: string | null }> }>, tournaments: Array<{ __typename?: 'Tournament', id: number, title: string, thumbnail_image: string, start_date: any, end_date: any }>, similar_makers: Array<{ __typename?: 'User', id: number, name: string, avatar: string, jobTitle: string | null }>, skills: Array<{ __typename?: 'MakerSkill', id: number, title: string }>, roles: Array<{ __typename?: 'MakerRole', id: number, title: string, icon: string, level: RoleLevelEnum }> } | null };
export type CategoryPageQueryVariables = Exact<{
categoryId: Scalars['Int'];
@@ -647,7 +753,38 @@ export type ConfirmVoteMutationVariables = Exact<{
export type ConfirmVoteMutation = { __typename?: 'Mutation', confirmVote: { __typename?: 'Vote', id: number, amount_in_sat: number, payment_request: string, payment_hash: string, paid: boolean, item_type: Vote_Item_Type, item_id: number } };
export const UserBasicInfoFragmentDoc = gql`
fragment UserBasicInfo on BaseUser {
id
name
avatar
join_date
role
email
jobTitle
lightning_address
website
twitter
github
linkedin
bio
location
}
`;
export const UserRolesSkillsFragmentDoc = gql`
fragment UserRolesSkills on BaseUser {
skills {
id
title
}
roles {
id
title
icon
level
}
}
`;
export const OfficialTagsDocument = gql`
query OfficialTags {
officialTags {
@@ -1380,6 +1517,73 @@ export function usePostDetailsLazyQuery(baseOptions?: Apollo.LazyQueryHookOption
export type PostDetailsQueryHookResult = ReturnType<typeof usePostDetailsQuery>;
export type PostDetailsLazyQueryHookResult = ReturnType<typeof usePostDetailsLazyQuery>;
export type PostDetailsQueryResult = Apollo.QueryResult<PostDetailsQuery, PostDetailsQueryVariables>;
export const MyProfileAboutDocument = gql`
query MyProfileAbout {
me {
...UserBasicInfo
}
}
${UserBasicInfoFragmentDoc}`;
/**
* __useMyProfileAboutQuery__
*
* To run a query within a React component, call `useMyProfileAboutQuery` and pass it any options that fit your needs.
* When your component renders, `useMyProfileAboutQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useMyProfileAboutQuery({
* variables: {
* },
* });
*/
export function useMyProfileAboutQuery(baseOptions?: Apollo.QueryHookOptions<MyProfileAboutQuery, MyProfileAboutQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<MyProfileAboutQuery, MyProfileAboutQueryVariables>(MyProfileAboutDocument, options);
}
export function useMyProfileAboutLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<MyProfileAboutQuery, MyProfileAboutQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<MyProfileAboutQuery, MyProfileAboutQueryVariables>(MyProfileAboutDocument, options);
}
export type MyProfileAboutQueryHookResult = ReturnType<typeof useMyProfileAboutQuery>;
export type MyProfileAboutLazyQueryHookResult = ReturnType<typeof useMyProfileAboutLazyQuery>;
export type MyProfileAboutQueryResult = Apollo.QueryResult<MyProfileAboutQuery, MyProfileAboutQueryVariables>;
export const UpdateProfileAboutDocument = gql`
mutation updateProfileAbout($data: ProfileDetailsInput) {
updateProfileDetails(data: $data) {
...UserBasicInfo
}
}
${UserBasicInfoFragmentDoc}`;
export type UpdateProfileAboutMutationFn = Apollo.MutationFunction<UpdateProfileAboutMutation, UpdateProfileAboutMutationVariables>;
/**
* __useUpdateProfileAboutMutation__
*
* To run a mutation, you first call `useUpdateProfileAboutMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUpdateProfileAboutMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [updateProfileAboutMutation, { data, loading, error }] = useUpdateProfileAboutMutation({
* variables: {
* data: // value for 'data'
* },
* });
*/
export function useUpdateProfileAboutMutation(baseOptions?: Apollo.MutationHookOptions<UpdateProfileAboutMutation, UpdateProfileAboutMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<UpdateProfileAboutMutation, UpdateProfileAboutMutationVariables>(UpdateProfileAboutDocument, options);
}
export type UpdateProfileAboutMutationHookResult = ReturnType<typeof useUpdateProfileAboutMutation>;
export type UpdateProfileAboutMutationResult = Apollo.MutationResult<UpdateProfileAboutMutation>;
export type UpdateProfileAboutMutationOptions = Apollo.BaseMutationOptions<UpdateProfileAboutMutation, UpdateProfileAboutMutationVariables>;
export const MyProfilePreferencesDocument = gql`
query MyProfilePreferences {
me {
@@ -1459,116 +1663,96 @@ export function useUpdateUserPreferencesMutation(baseOptions?: Apollo.MutationHo
export type UpdateUserPreferencesMutationHookResult = ReturnType<typeof useUpdateUserPreferencesMutation>;
export type UpdateUserPreferencesMutationResult = Apollo.MutationResult<UpdateUserPreferencesMutation>;
export type UpdateUserPreferencesMutationOptions = Apollo.BaseMutationOptions<UpdateUserPreferencesMutation, UpdateUserPreferencesMutationVariables>;
export const MyProfileAboutDocument = gql`
query MyProfileAbout {
export const MyProfileRolesSkillsDocument = gql`
query MyProfileRolesSkills {
me {
id
name
avatar
join_date
role
email
jobTitle
lightning_address
website
twitter
github
linkedin
bio
location
...UserRolesSkills
}
getAllMakersRoles {
id
title
icon
}
getAllMakersSkills {
id
title
}
}
`;
${UserRolesSkillsFragmentDoc}`;
/**
* __useMyProfileAboutQuery__
* __useMyProfileRolesSkillsQuery__
*
* To run a query within a React component, call `useMyProfileAboutQuery` and pass it any options that fit your needs.
* When your component renders, `useMyProfileAboutQuery` returns an object from Apollo Client that contains loading, error, and data properties
* To run a query within a React component, call `useMyProfileRolesSkillsQuery` and pass it any options that fit your needs.
* When your component renders, `useMyProfileRolesSkillsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useMyProfileAboutQuery({
* const { data, loading, error } = useMyProfileRolesSkillsQuery({
* variables: {
* },
* });
*/
export function useMyProfileAboutQuery(baseOptions?: Apollo.QueryHookOptions<MyProfileAboutQuery, MyProfileAboutQueryVariables>) {
export function useMyProfileRolesSkillsQuery(baseOptions?: Apollo.QueryHookOptions<MyProfileRolesSkillsQuery, MyProfileRolesSkillsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<MyProfileAboutQuery, MyProfileAboutQueryVariables>(MyProfileAboutDocument, options);
return Apollo.useQuery<MyProfileRolesSkillsQuery, MyProfileRolesSkillsQueryVariables>(MyProfileRolesSkillsDocument, options);
}
export function useMyProfileAboutLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<MyProfileAboutQuery, MyProfileAboutQueryVariables>) {
export function useMyProfileRolesSkillsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<MyProfileRolesSkillsQuery, MyProfileRolesSkillsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<MyProfileAboutQuery, MyProfileAboutQueryVariables>(MyProfileAboutDocument, options);
return Apollo.useLazyQuery<MyProfileRolesSkillsQuery, MyProfileRolesSkillsQueryVariables>(MyProfileRolesSkillsDocument, options);
}
export type MyProfileAboutQueryHookResult = ReturnType<typeof useMyProfileAboutQuery>;
export type MyProfileAboutLazyQueryHookResult = ReturnType<typeof useMyProfileAboutLazyQuery>;
export type MyProfileAboutQueryResult = Apollo.QueryResult<MyProfileAboutQuery, MyProfileAboutQueryVariables>;
export const UpdateProfileAboutDocument = gql`
mutation updateProfileAbout($data: ProfileDetailsInput) {
updateProfileDetails(data: $data) {
export type MyProfileRolesSkillsQueryHookResult = ReturnType<typeof useMyProfileRolesSkillsQuery>;
export type MyProfileRolesSkillsLazyQueryHookResult = ReturnType<typeof useMyProfileRolesSkillsLazyQuery>;
export type MyProfileRolesSkillsQueryResult = Apollo.QueryResult<MyProfileRolesSkillsQuery, MyProfileRolesSkillsQueryVariables>;
export const UpdateUserRolesSkillsDocument = gql`
mutation UpdateUserRolesSkills($data: ProfileRolesInput) {
updateProfileRoles(data: $data) {
id
name
avatar
join_date
role
email
jobTitle
lightning_address
website
twitter
github
linkedin
bio
location
skills {
id
title
}
roles {
id
title
icon
level
}
}
}
`;
export type UpdateProfileAboutMutationFn = Apollo.MutationFunction<UpdateProfileAboutMutation, UpdateProfileAboutMutationVariables>;
export type UpdateUserRolesSkillsMutationFn = Apollo.MutationFunction<UpdateUserRolesSkillsMutation, UpdateUserRolesSkillsMutationVariables>;
/**
* __useUpdateProfileAboutMutation__
* __useUpdateUserRolesSkillsMutation__
*
* To run a mutation, you first call `useUpdateProfileAboutMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUpdateProfileAboutMutation` returns a tuple that includes:
* To run a mutation, you first call `useUpdateUserRolesSkillsMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUpdateUserRolesSkillsMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [updateProfileAboutMutation, { data, loading, error }] = useUpdateProfileAboutMutation({
* const [updateUserRolesSkillsMutation, { data, loading, error }] = useUpdateUserRolesSkillsMutation({
* variables: {
* data: // value for 'data'
* },
* });
*/
export function useUpdateProfileAboutMutation(baseOptions?: Apollo.MutationHookOptions<UpdateProfileAboutMutation, UpdateProfileAboutMutationVariables>) {
export function useUpdateUserRolesSkillsMutation(baseOptions?: Apollo.MutationHookOptions<UpdateUserRolesSkillsMutation, UpdateUserRolesSkillsMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<UpdateProfileAboutMutation, UpdateProfileAboutMutationVariables>(UpdateProfileAboutDocument, options);
return Apollo.useMutation<UpdateUserRolesSkillsMutation, UpdateUserRolesSkillsMutationVariables>(UpdateUserRolesSkillsDocument, options);
}
export type UpdateProfileAboutMutationHookResult = ReturnType<typeof useUpdateProfileAboutMutation>;
export type UpdateProfileAboutMutationResult = Apollo.MutationResult<UpdateProfileAboutMutation>;
export type UpdateProfileAboutMutationOptions = Apollo.BaseMutationOptions<UpdateProfileAboutMutation, UpdateProfileAboutMutationVariables>;
export type UpdateUserRolesSkillsMutationHookResult = ReturnType<typeof useUpdateUserRolesSkillsMutation>;
export type UpdateUserRolesSkillsMutationResult = Apollo.MutationResult<UpdateUserRolesSkillsMutation>;
export type UpdateUserRolesSkillsMutationOptions = Apollo.BaseMutationOptions<UpdateUserRolesSkillsMutation, UpdateUserRolesSkillsMutationVariables>;
export const ProfileDocument = gql`
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
@@ -1579,9 +1763,25 @@ export const ProfileDocument = gql`
icon
}
}
tournaments {
id
title
thumbnail_image
start_date
end_date
}
similar_makers {
id
name
avatar
jobTitle
}
...UserBasicInfo
...UserRolesSkills
}
}
`;
${UserBasicInfoFragmentDoc}
${UserRolesSkillsFragmentDoc}`;
/**
* __useProfileQuery__

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 { user } from "./data/users";
import { allMakersRoles, allMakersSkills, user } from "./data/users";
export const MOCK_DATA = {
projects,
@@ -10,5 +10,7 @@ export const MOCK_DATA = {
feed,
hackathons,
generatePostComments: generatePostComments,
user: user
user: user,
allMakersRoles: allMakersRoles,
allMakersSkills: allMakersSkills,
}

View File

@@ -1,5 +1,101 @@
import { MyProfile, 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";
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 user: User & MyProfile = {
id: 123,
@@ -25,8 +121,55 @@ export const user: User & MyProfile = {
name: "My Alby wallet key"
},
{
key: "6643534534534534543",
key: "66345134234235",
name: "My Phoenix wallet key"
},],
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[]
}

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 } from './resolvers'
import { allCategories, getAllHackathons, getAllMakersRoles, getAllMakersSkills, getCategory, getFeed, getMyDrafts, getPostById, getProject, getTrendingPosts, hottestProjects, me, newProjects, popularTags, profile, projectsByCategory, searchProjects } from './resolvers'
import {
NavCategoriesQuery,
ExploreProjectsQuery,
@@ -31,6 +31,7 @@ import {
GetMyDraftsQuery,
MyProfileAboutQuery,
MyProfilePreferencesQuery,
MyProfileRolesSkillsQuery,
} from 'src/graphql'
const delay = (ms = 1000) => new Promise((res) => setTimeout(res, ms + Math.random() * 1000))
@@ -197,8 +198,6 @@ export const handlers = [
graphql.query<MeQuery>('Me', async (req, res, ctx) => {
await delay()
console.log("ME");
return res(
ctx.data({
me: me()
@@ -226,6 +225,17 @@ export const handlers = [
}),
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

@@ -84,6 +84,13 @@ export function profile() {
return { ...MOCK_DATA['user'], __typename: 'User' } as User
}
export function getAllMakersRoles() {
return MOCK_DATA['allMakersRoles']
}
export function getAllMakersSkills() {
return MOCK_DATA['allMakersSkills']
}
export function getMyDrafts(): Query['getMyDrafts'] {
return MOCK_DATA['posts'].stories;
}

View File

@@ -8,8 +8,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/PreferencesTab/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";

View File

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