From 846924eaeb994eab5deb30c2e09175c6615a2919 Mon Sep 17 00:00:00 2001 From: MTG2000 Date: Sat, 4 Jun 2022 14:15:04 +0300 Subject: [PATCH] feat: base profile about card, update profile api, add extra fields to users table --- functions/graphql/nexus-typegen.ts | 95 ++++++++-- functions/graphql/schema.graphql | 43 ++++- functions/graphql/types/helpers.js | 13 +- functions/graphql/types/post.js | 21 ++- functions/graphql/types/users.js | 83 ++++++++- .../migration.sql | 6 + .../migration.sql | 3 + prisma/schema.prisma | 18 +- src/App.tsx | 2 + .../InsertImageModal/InsertImageModal.tsx | 7 + src/Components/Navbar/NavDesktop.tsx | 9 +- src/features/Auth/pages/me.graphql | 1 + .../Components/StoryForm/StoryForm.tsx | 4 +- .../Posts/pages/FeedPage/feed.graphql | 4 + src/features/Posts/types/posts.interface.ts | 4 +- .../Profiles/Components/Avatar/Avatar.tsx | 2 +- .../pages/ProfilePage/AboutCard/AboutCard.tsx | 74 ++++++++ .../pages/ProfilePage/ProfilePage.tsx | 37 ++++ .../pages/ProfilePage/profile.graphql | 18 ++ .../pages/ProfilePage/updateProfile.graphql | 16 ++ src/graphql/index.tsx | 175 +++++++++++++++++- src/redux/features/user.slice.ts | 1 + 22 files changed, 590 insertions(+), 46 deletions(-) create mode 100644 prisma/migrations/20220604085918_add_new_cols_to_user/migration.sql create mode 100644 prisma/migrations/20220604110334_add_more_cols_to_user_table/migration.sql create mode 100644 src/features/Profiles/pages/ProfilePage/AboutCard/AboutCard.tsx create mode 100644 src/features/Profiles/pages/ProfilePage/ProfilePage.tsx create mode 100644 src/features/Profiles/pages/ProfilePage/profile.graphql create mode 100644 src/features/Profiles/pages/ProfilePage/updateProfile.graphql diff --git a/functions/graphql/nexus-typegen.ts b/functions/graphql/nexus-typegen.ts index a2b816f..b659fbe 100644 --- a/functions/graphql/nexus-typegen.ts +++ b/functions/graphql/nexus-typegen.ts @@ -36,6 +36,19 @@ export interface NexusGenInputs { title: string; // String! topicId: number; // Int! } + UpdateProfileInput: { // input type + avatar?: string | null; // String + bio?: string | null; // String + email?: string | null; // String + github?: string | null; // String + jobTitle?: string | null; // String + lightning_address?: string | null; // String + linkedin?: string | null; // String + location?: string | null; // String + name?: string | null; // String + twitter?: string | null; // String + website?: string | null; // String + } } export interface NexusGenEnums { @@ -53,6 +66,12 @@ export interface NexusGenScalars { } export interface NexusGenObjects { + Author: { // root type + avatar: string; // String! + id: number; // Int! + join_date: NexusGenScalars['Date']; // Date! + name: string; // String! + } Award: { // root type id: number; // Int! image: string; // String! @@ -73,7 +92,7 @@ export interface NexusGenObjects { votes_count: number; // Int! } BountyApplication: { // root type - author: NexusGenRootTypes['User']; // User! + author: NexusGenRootTypes['Author']; // Author! date: string; // String! id: number; // Int! workplan: string; // String! @@ -116,7 +135,7 @@ export interface NexusGenObjects { } Mutation: {}; PostComment: { // root type - author: NexusGenRootTypes['User']; // User! + author: NexusGenRootTypes['Author']; // Author! body: string; // String! createdAt: NexusGenScalars['Date']; // Date! id: number; // Int! @@ -165,9 +184,19 @@ export interface NexusGenObjects { } User: { // 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! + role?: string | null; // String + twitter?: string | null; // String + website?: string | null; // String } Vote: { // root type amount_in_sat: number; // Int! @@ -193,6 +222,12 @@ export type NexusGenRootTypes = NexusGenInterfaces & NexusGenObjects & NexusGenU export type NexusGenAllTypes = NexusGenRootTypes & NexusGenScalars & NexusGenEnums export interface NexusGenFieldTypes { + Author: { // field return type + avatar: string; // String! + id: number; // Int! + join_date: NexusGenScalars['Date']; // Date! + name: string; // String! + } Award: { // field return type id: number; // Int! image: string; // String! @@ -203,7 +238,7 @@ export interface NexusGenFieldTypes { Bounty: { // field return type applicants_count: number; // Int! applications: NexusGenRootTypes['BountyApplication'][]; // [BountyApplication!]! - author: NexusGenRootTypes['User']; // User! + author: NexusGenRootTypes['Author']; // Author! body: string; // String! cover_image: string; // String! createdAt: NexusGenScalars['Date']; // Date! @@ -217,7 +252,7 @@ export interface NexusGenFieldTypes { votes_count: number; // Int! } BountyApplication: { // field return type - author: NexusGenRootTypes['User']; // User! + author: NexusGenRootTypes['Author']; // Author! date: string; // String! id: number; // Int! workplan: string; // String! @@ -268,10 +303,11 @@ export interface NexusGenFieldTypes { confirmVote: NexusGenRootTypes['Vote']; // Vote! createStory: NexusGenRootTypes['Story'] | null; // Story donate: NexusGenRootTypes['Donation']; // Donation! + updateProfile: NexusGenRootTypes['User'] | null; // User vote: NexusGenRootTypes['Vote']; // Vote! } PostComment: { // field return type - author: NexusGenRootTypes['User']; // User! + author: NexusGenRootTypes['Author']; // Author! body: string; // String! createdAt: NexusGenScalars['Date']; // Date! id: number; // Int! @@ -309,12 +345,13 @@ export interface NexusGenFieldTypes { me: NexusGenRootTypes['User'] | null; // User newProjects: NexusGenRootTypes['Project'][]; // [Project!]! popularTopics: NexusGenRootTypes['Topic'][]; // [Topic!]! + profile: NexusGenRootTypes['User'] | null; // User projectsByCategory: NexusGenRootTypes['Project'][]; // [Project!]! searchProjects: NexusGenRootTypes['Project'][]; // [Project!]! } Question: { // field return type answers_count: number; // Int! - author: NexusGenRootTypes['User']; // User! + author: NexusGenRootTypes['Author']; // Author! body: string; // String! comments: NexusGenRootTypes['PostComment'][]; // [PostComment!]! createdAt: NexusGenScalars['Date']; // Date! @@ -326,7 +363,7 @@ export interface NexusGenFieldTypes { votes_count: number; // Int! } Story: { // field return type - author: NexusGenRootTypes['User']; // User! + author: NexusGenRootTypes['Author']; // Author! body: string; // String! comments: NexusGenRootTypes['PostComment'][]; // [PostComment!]! comments_count: number; // Int! @@ -351,9 +388,19 @@ export interface NexusGenFieldTypes { } User: { // 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 + twitter: string | null; // String + website: string | null; // String } Vote: { // field return type amount_in_sat: number; // Int! @@ -375,6 +422,12 @@ export interface NexusGenFieldTypes { } export interface NexusGenFieldTypeNames { + Author: { // field return type name + avatar: 'String' + id: 'Int' + join_date: 'Date' + name: 'String' + } Award: { // field return type name id: 'Int' image: 'String' @@ -385,7 +438,7 @@ export interface NexusGenFieldTypeNames { Bounty: { // field return type name applicants_count: 'Int' applications: 'BountyApplication' - author: 'User' + author: 'Author' body: 'String' cover_image: 'String' createdAt: 'Date' @@ -399,7 +452,7 @@ export interface NexusGenFieldTypeNames { votes_count: 'Int' } BountyApplication: { // field return type name - author: 'User' + author: 'Author' date: 'String' id: 'Int' workplan: 'String' @@ -450,10 +503,11 @@ export interface NexusGenFieldTypeNames { confirmVote: 'Vote' createStory: 'Story' donate: 'Donation' + updateProfile: 'User' vote: 'Vote' } PostComment: { // field return type name - author: 'User' + author: 'Author' body: 'String' createdAt: 'Date' id: 'Int' @@ -491,12 +545,13 @@ export interface NexusGenFieldTypeNames { me: 'User' newProjects: 'Project' popularTopics: 'Topic' + profile: 'User' projectsByCategory: 'Project' searchProjects: 'Project' } Question: { // field return type name answers_count: 'Int' - author: 'User' + author: 'Author' body: 'String' comments: 'PostComment' createdAt: 'Date' @@ -508,7 +563,7 @@ export interface NexusGenFieldTypeNames { votes_count: 'Int' } Story: { // field return type name - author: 'User' + author: 'Author' body: 'String' comments: 'PostComment' comments_count: 'Int' @@ -533,9 +588,19 @@ export interface NexusGenFieldTypeNames { } User: { // 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' + twitter: 'String' + website: 'String' } Vote: { // field return type name amount_in_sat: 'Int' @@ -572,6 +637,9 @@ export interface NexusGenArgTypes { donate: { // args amount_in_sat: number; // Int! } + updateProfile: { // args + data?: NexusGenInputs['UpdateProfileInput'] | null; // UpdateProfileInput + } vote: { // args amount_in_sat: number; // Int! item_id: number; // Int! @@ -614,6 +682,9 @@ export interface NexusGenArgTypes { skip?: number | null; // Int take: number | null; // Int } + profile: { // args + id: number; // Int! + } projectsByCategory: { // args category_id: number; // Int! skip?: number | null; // Int diff --git a/functions/graphql/schema.graphql b/functions/graphql/schema.graphql index fe070c0..da0b861 100644 --- a/functions/graphql/schema.graphql +++ b/functions/graphql/schema.graphql @@ -2,6 +2,13 @@ ### Do not make changes to this file directly +type Author { + avatar: String! + id: Int! + join_date: Date! + name: String! +} + type Award { id: Int! image: String! @@ -13,7 +20,7 @@ type Award { type Bounty implements PostBase { applicants_count: Int! applications: [BountyApplication!]! - author: User! + author: Author! body: String! cover_image: String! createdAt: Date! @@ -28,7 +35,7 @@ type Bounty implements PostBase { } type BountyApplication { - author: User! + author: Author! date: String! id: Int! workplan: String! @@ -88,6 +95,7 @@ type Mutation { confirmVote(payment_request: String!, preimage: String!): Vote! createStory(data: StoryInputType): Story donate(amount_in_sat: Int!): Donation! + updateProfile(data: UpdateProfileInput): User vote(amount_in_sat: Int!, item_id: Int!, item_type: VOTE_ITEM_TYPE!): Vote! } @@ -109,7 +117,7 @@ interface PostBase { } type PostComment { - author: User! + author: Author! body: String! createdAt: Date! id: Int! @@ -149,13 +157,14 @@ type Query { me: User newProjects(skip: Int = 0, take: Int = 50): [Project!]! popularTopics: [Topic!]! + 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!]! } type Question implements PostBase { answers_count: Int! - author: User! + author: Author! body: String! comments: [PostComment!]! createdAt: Date! @@ -168,7 +177,7 @@ type Question implements PostBase { } type Story implements PostBase { - author: User! + author: Author! body: String! comments: [PostComment!]! comments_count: Int! @@ -203,11 +212,35 @@ type Topic { 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 User { 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 + twitter: String + website: String } enum VOTE_ITEM_TYPE { diff --git a/functions/graphql/types/helpers.js b/functions/graphql/types/helpers.js index ece6a9f..c1f108d 100644 --- a/functions/graphql/types/helpers.js +++ b/functions/graphql/types/helpers.js @@ -67,10 +67,21 @@ const paginationArgs = (args) => { } } +const removeNulls = (obj) => { + let res = {}; + for (const key in obj) { + if (obj[key] != null) { + res[key] = obj[key]; + } + } + return res +} + module.exports = { getPaymetRequestForItem, hexToUint8Array, lightningAddressToLnurl, getLnurlDetails, - paginationArgs + paginationArgs, + removeNulls } diff --git a/functions/graphql/types/post.js b/functions/graphql/types/post.js index 613e480..7a85f67 100644 --- a/functions/graphql/types/post.js +++ b/functions/graphql/types/post.js @@ -39,6 +39,16 @@ const Topic = objectType({ } }) +const Author = objectType({ + name: 'Author', + definition(t) { + t.nonNull.int('id'); + t.nonNull.string('name'); + t.nonNull.string('avatar'); + t.nonNull.date('join_date'); + } +}) + const allTopics = extendType({ type: "Query", @@ -129,7 +139,7 @@ const Story = objectType({ } }) t.nonNull.field('author', { - type: "User", + type: "Author", resolve: (parent) => prisma.story.findUnique({ where: { id: parent.id } }).user() @@ -214,7 +224,7 @@ const BountyApplication = objectType({ t.nonNull.string('date'); t.nonNull.string('workplan'); t.nonNull.field('author', { - type: "User" + type: "Author" }); } }) @@ -234,7 +244,7 @@ const Bounty = objectType({ type: "BountyApplication" }); t.nonNull.field('author', { - type: "User", + type: "Author", resolve: (parent) => { return prisma.bounty.findUnique({ where: { id: parent.id } }).user(); } @@ -270,7 +280,7 @@ const Question = objectType({ }); t.nonNull.field('author', { - type: "User", + type: "Author", resolve: (parent) => { return prisma.question.findUnique({ where: { id: parent.id } }).user(); } @@ -285,7 +295,7 @@ const PostComment = objectType({ t.nonNull.date('createdAt'); t.nonNull.string('body'); t.nonNull.field('author', { - type: "User" + type: "Author" }); t.int('parentId'); t.nonNull.int('votes_count'); @@ -391,6 +401,7 @@ const getPostById = extendType({ module.exports = { // Types POST_TYPE, + Author, Topic, PostBase, BountyApplication, diff --git a/functions/graphql/types/users.js b/functions/graphql/types/users.js index 224b74b..30e25e4 100644 --- a/functions/graphql/types/users.js +++ b/functions/graphql/types/users.js @@ -1,5 +1,7 @@ -const { objectType, extendType } = require("nexus"); +const { prisma } = require("../../prisma"); +const { objectType, extendType, intArg, nonNull, inputObjectType } = require("nexus"); const { getUserByPubKey } = require("../../auth/utils/helperFuncs"); +const { removeNulls } = require("./helpers"); @@ -10,6 +12,16 @@ const User = objectType({ t.nonNull.string('name'); t.nonNull.string('avatar'); t.nonNull.date('join_date'); + t.string('role'); + t.string('email') + t.string('jobTitle') + t.string('lightning_address') + t.string('website') + t.string('twitter') + t.string('github') + t.string('linkedin') + t.string('bio') + t.string('location') } }) @@ -27,10 +39,77 @@ const me = extendType({ } }) +const profile = extendType({ + type: "Query", + definition(t) { + t.field('profile', { + type: "User", + args: { + id: nonNull(intArg()) + }, + async resolve(parent, { id }) { + return prisma.user.findFirst({ + where: { id } + }) + } + }) + } +}) + +const UpdateProfileInput = inputObjectType({ + name: 'UpdateProfileInput', + definition(t) { + t.string('name'); + t.string('avatar'); + t.string('email') + t.string('jobTitle') + t.string('lightning_address') + t.string('website') + t.string('twitter') + t.string('github') + t.string('linkedin') + t.string('bio') + t.string('location') + } +}) + +const updateProfile = extendType({ + type: 'Mutation', + definition(t) { + t.field('updateProfile', { + type: 'User', + args: { data: UpdateProfileInput }, + async resolve(_root, args, ctx) { + const user = await getUserByPubKey(ctx.userPubKey); + + // Do some validation + if (!user) + throw new Error("You have to login"); + // TODO: validate new data + + + // Preprocess & insert + + return prisma.user.update({ + where: { + id: user.id, + }, + data: removeNulls(args.data) + }) + } + }) + }, +}) + + + module.exports = { // Types User, - + UpdateProfileInput, // Queries me, + profile, + // Mutations + updateProfile, } \ No newline at end of file diff --git a/prisma/migrations/20220604085918_add_new_cols_to_user/migration.sql b/prisma/migrations/20220604085918_add_new_cols_to_user/migration.sql new file mode 100644 index 0000000..26cd661 --- /dev/null +++ b/prisma/migrations/20220604085918_add_new_cols_to_user/migration.sql @@ -0,0 +1,6 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "bio" TEXT, +ADD COLUMN "github" TEXT, +ADD COLUMN "location" TEXT, +ADD COLUMN "role" TEXT NOT NULL DEFAULT E'user', +ADD COLUMN "twitter" TEXT; diff --git a/prisma/migrations/20220604110334_add_more_cols_to_user_table/migration.sql b/prisma/migrations/20220604110334_add_more_cols_to_user_table/migration.sql new file mode 100644 index 0000000..330d7b9 --- /dev/null +++ b/prisma/migrations/20220604110334_add_more_cols_to_user_table/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "jobTitle" TEXT, +ADD COLUMN "linkedin" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 6022e5a..df119fb 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -38,13 +38,23 @@ model Vote { model User { id Int @id @default(autoincrement()) pubKey String? @unique + name String? @unique + avatar String? + role String @default("user") + - name String? @unique email String? - website String? + jobTitle String? lightning_address String? - avatar String? - join_date DateTime @default(now()) + website String? + twitter String? + github String? + linkedin String? + bio String? + location String? + + + join_date DateTime @default(now()) stories Story[] questions Question[] diff --git a/src/App.tsx b/src/App.tsx index c225da3..24f8428 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -25,6 +25,7 @@ const HackathonsPage = React.lazy(() => import("./features/Hackathons/pages/Hack const DonatePage = React.lazy(() => import("./features/Donations/pages/DonatePage/DonatePage")) const LoginPage = React.lazy(() => import("./features/Auth/pages/LoginPage/LoginPage")) const LogoutPage = React.lazy(() => import("./features/Auth/pages/LogoutPage/LogoutPage")) +const ProfilePage = React.lazy(() => import("./features/Profiles/pages/ProfilePage/ProfilePage")) function App() { const { isWalletConnected } = useAppSelector(state => ({ @@ -77,6 +78,7 @@ function App() { } /> + } /> } /> } /> diff --git a/src/Components/Inputs/TextEditor/InsertImageModal/InsertImageModal.tsx b/src/Components/Inputs/TextEditor/InsertImageModal/InsertImageModal.tsx index 0178d44..0852dea 100644 --- a/src/Components/Inputs/TextEditor/InsertImageModal/InsertImageModal.tsx +++ b/src/Components/Inputs/TextEditor/InsertImageModal/InsertImageModal.tsx @@ -69,6 +69,13 @@ export default function InsertImageModal({ onClose, direction, callbackAction, . +
+ {urlInput && {altInput}} +