diff --git a/api/functions/graphql/nexus-typegen.ts b/api/functions/graphql/nexus-typegen.ts index 78a7cfd..bd8c67e 100644 --- a/api/functions/graphql/nexus-typegen.ts +++ b/api/functions/graphql/nexus-typegen.ts @@ -28,15 +28,7 @@ declare global { } export interface NexusGenInputs { - StoryInputType: { // input type - body: string; // String! - cover_image?: string | null; // String - id?: number | null; // Int - is_published?: boolean | null; // Boolean - tags: string[]; // [String!]! - title: string; // String! - } - UpdateProfileInput: { // input type + ProfileDetailsInput: { // input type avatar?: string | null; // String bio?: string | null; // String email?: string | null; // String @@ -49,6 +41,18 @@ export interface NexusGenInputs { twitter?: string | null; // String website?: string | null; // String } + StoryInputType: { // input type + body: string; // String! + cover_image?: string | null; // String + id?: number | null; // Int + is_published?: boolean | null; // Boolean + tags: string[]; // [String!]! + title: string; // String! + } + UserKeyInputType: { // input type + key: string; // String! + name: string; // String! + } } export interface NexusGenEnums { @@ -137,6 +141,24 @@ export interface NexusGenObjects { minSendable?: number | null; // Int } Mutation: {}; + MyProfile: { // root type + avatar: string; // String! + bio?: string | null; // String + email?: string | null; // String + github?: string | null; // String + id: number; // Int! + jobTitle?: string | null; // String + join_date: NexusGenScalars['Date']; // Date! + lightning_address?: string | null; // String + linkedin?: string | null; // String + location?: string | null; // String + name: string; // String! + nostr_prv_key?: string | null; // String + nostr_pub_key?: string | null; // String + role?: string | null; // String + twitter?: string | null; // String + website?: string | null; // String + } PostComment: { // root type author: NexusGenRootTypes['Author']; // Author! body: string; // String! @@ -198,8 +220,6 @@ export interface NexusGenObjects { linkedin?: string | null; // String location?: string | null; // String name: string; // String! - nostr_prv_key?: string | null; // String - nostr_pub_key?: string | null; // String role?: string | null; // String twitter?: string | null; // String website?: string | null; // String @@ -213,9 +233,14 @@ export interface NexusGenObjects { payment_hash: string; // String! payment_request: string; // String! } + WalletKey: { // root type + key: string; // String! + name: string; // String! + } } export interface NexusGenInterfaces { + BaseUser: NexusGenRootTypes['MyProfile'] | NexusGenRootTypes['User']; PostBase: NexusGenRootTypes['Bounty'] | NexusGenRootTypes['Question'] | NexusGenRootTypes['Story']; } @@ -313,9 +338,30 @@ export interface NexusGenFieldTypes { createStory: NexusGenRootTypes['Story'] | null; // Story deleteStory: NexusGenRootTypes['Story'] | null; // Story donate: NexusGenRootTypes['Donation']; // Donation! - updateProfile: NexusGenRootTypes['User'] | null; // User + updateProfileDetails: NexusGenRootTypes['MyProfile'] | null; // MyProfile + updateUserPreferences: NexusGenRootTypes['MyProfile']; // MyProfile! vote: NexusGenRootTypes['Vote']; // Vote! } + MyProfile: { // field return type + avatar: string; // String! + bio: string | null; // String + email: string | null; // String + github: string | null; // String + id: number; // Int! + jobTitle: string | null; // String + join_date: NexusGenScalars['Date']; // Date! + lightning_address: string | null; // String + linkedin: string | null; // String + location: string | null; // String + name: string; // String! + nostr_prv_key: string | null; // String + nostr_pub_key: string | null; // String + role: string | null; // String + stories: NexusGenRootTypes['Story'][]; // [Story!]! + twitter: string | null; // String + walletsKeys: NexusGenRootTypes['WalletKey'][]; // [WalletKey!]! + website: string | null; // String + } PostComment: { // field return type author: NexusGenRootTypes['Author']; // Author! body: string; // String! @@ -352,7 +398,7 @@ export interface NexusGenFieldTypes { getProject: NexusGenRootTypes['Project']; // Project! getTrendingPosts: NexusGenRootTypes['Post'][]; // [Post!]! hottestProjects: NexusGenRootTypes['Project'][]; // [Project!]! - me: NexusGenRootTypes['User'] | null; // User + me: NexusGenRootTypes['MyProfile'] | null; // MyProfile newProjects: NexusGenRootTypes['Project'][]; // [Project!]! officialTags: NexusGenRootTypes['Tag'][]; // [Tag!]! popularTags: NexusGenRootTypes['Tag'][]; // [Tag!]! @@ -408,8 +454,6 @@ export interface NexusGenFieldTypes { linkedin: string | null; // String location: string | null; // String name: string; // String! - nostr_prv_key: string | null; // String - nostr_pub_key: string | null; // String role: string | null; // String stories: NexusGenRootTypes['Story'][]; // [Story!]! twitter: string | null; // String @@ -424,6 +468,27 @@ export interface NexusGenFieldTypes { payment_hash: string; // String! payment_request: string; // String! } + WalletKey: { // field return type + key: string; // String! + name: string; // String! + } + BaseUser: { // field return type + avatar: string; // String! + bio: string | null; // String + email: string | null; // String + github: string | null; // String + id: number; // Int! + jobTitle: string | null; // String + join_date: NexusGenScalars['Date']; // Date! + lightning_address: string | null; // String + linkedin: string | null; // String + location: string | null; // String + name: string; // String! + role: string | null; // String + stories: NexusGenRootTypes['Story'][]; // [Story!]! + twitter: string | null; // String + website: string | null; // String + } PostBase: { // field return type body: string; // String! createdAt: NexusGenScalars['Date']; // Date! @@ -522,9 +587,30 @@ export interface NexusGenFieldTypeNames { createStory: 'Story' deleteStory: 'Story' donate: 'Donation' - updateProfile: 'User' + updateProfileDetails: 'MyProfile' + updateUserPreferences: 'MyProfile' vote: 'Vote' } + MyProfile: { // field return type name + avatar: 'String' + bio: 'String' + email: 'String' + github: 'String' + id: 'Int' + jobTitle: 'String' + join_date: 'Date' + lightning_address: 'String' + linkedin: 'String' + location: 'String' + name: 'String' + nostr_prv_key: 'String' + nostr_pub_key: 'String' + role: 'String' + stories: 'Story' + twitter: 'String' + walletsKeys: 'WalletKey' + website: 'String' + } PostComment: { // field return type name author: 'Author' body: 'String' @@ -561,7 +647,7 @@ export interface NexusGenFieldTypeNames { getProject: 'Project' getTrendingPosts: 'Post' hottestProjects: 'Project' - me: 'User' + me: 'MyProfile' newProjects: 'Project' officialTags: 'Tag' popularTags: 'Tag' @@ -617,8 +703,6 @@ export interface NexusGenFieldTypeNames { linkedin: 'String' location: 'String' name: 'String' - nostr_prv_key: 'String' - nostr_pub_key: 'String' role: 'String' stories: 'Story' twitter: 'String' @@ -633,6 +717,27 @@ export interface NexusGenFieldTypeNames { payment_hash: 'String' payment_request: 'String' } + WalletKey: { // field return type name + key: 'String' + name: 'String' + } + BaseUser: { // field return type name + avatar: 'String' + bio: 'String' + email: 'String' + github: 'String' + id: 'Int' + jobTitle: 'String' + join_date: 'Date' + lightning_address: 'String' + linkedin: 'String' + location: 'String' + name: 'String' + role: 'String' + stories: 'Story' + twitter: 'String' + website: 'String' + } PostBase: { // field return type name body: 'String' createdAt: 'Date' @@ -664,8 +769,11 @@ export interface NexusGenArgTypes { donate: { // args amount_in_sat: number; // Int! } - updateProfile: { // args - data?: NexusGenInputs['UpdateProfileInput'] | null; // UpdateProfileInput + updateProfileDetails: { // args + data?: NexusGenInputs['ProfileDetailsInput'] | null; // ProfileDetailsInput + } + updateUserPreferences: { // args + userKeys?: NexusGenInputs['UserKeyInputType'][] | null; // [UserKeyInputType!] } vote: { // args amount_in_sat: number; // Int! @@ -730,13 +838,16 @@ export interface NexusGenArgTypes { export interface NexusGenAbstractTypeMembers { Post: "Bounty" | "Question" | "Story" + BaseUser: "MyProfile" | "User" PostBase: "Bounty" | "Question" | "Story" } export interface NexusGenTypeInterfaces { Bounty: "PostBase" + MyProfile: "BaseUser" Question: "PostBase" Story: "PostBase" + User: "BaseUser" } export type NexusGenObjectNames = keyof NexusGenObjects; @@ -753,7 +864,7 @@ export type NexusGenUnionNames = keyof NexusGenUnions; export type NexusGenObjectsUsingAbstractStrategyIsTypeOf = never; -export type NexusGenAbstractsUsingStrategyResolveType = "Post" | "PostBase"; +export type NexusGenAbstractsUsingStrategyResolveType = "BaseUser" | "Post" | "PostBase"; export type NexusGenFeaturesConfig = { abstractTypeStrategies: { diff --git a/api/functions/graphql/schema.graphql b/api/functions/graphql/schema.graphql index b2994b5..8a3947c 100644 --- a/api/functions/graphql/schema.graphql +++ b/api/functions/graphql/schema.graphql @@ -18,6 +18,24 @@ type Award { url: String! } +interface BaseUser { + avatar: String! + bio: String + email: String + github: String + id: Int! + jobTitle: String + join_date: Date! + lightning_address: String + linkedin: String + location: String + name: String! + role: String + stories: [Story!]! + twitter: String + website: String +} + type Bounty implements PostBase { applicants_count: Int! applications: [BountyApplication!]! @@ -99,10 +117,32 @@ type Mutation { createStory(data: StoryInputType): Story deleteStory(id: Int!): Story donate(amount_in_sat: Int!): Donation! - updateProfile(data: UpdateProfileInput): User + updateProfileDetails(data: ProfileDetailsInput): MyProfile + updateUserPreferences(userKeys: [UserKeyInputType!]): MyProfile! vote(amount_in_sat: Int!, item_id: Int!, item_type: VOTE_ITEM_TYPE!): Vote! } +type MyProfile implements BaseUser { + avatar: String! + bio: String + email: String + github: String + id: Int! + jobTitle: String + join_date: Date! + lightning_address: String + linkedin: String + location: String + name: String! + nostr_prv_key: String + nostr_pub_key: String + role: String + stories: [Story!]! + twitter: String + walletsKeys: [WalletKey!]! + website: String +} + enum POST_TYPE { Bounty Question @@ -131,6 +171,20 @@ type PostComment { votes_count: Int! } +input ProfileDetailsInput { + avatar: String + bio: String + email: String + github: String + jobTitle: String + lightning_address: String + linkedin: String + location: String + name: String + twitter: String + website: String +} + type Project { awards: [Award!]! category: Category! @@ -160,7 +214,7 @@ type Query { getProject(id: Int!): Project! getTrendingPosts: [Post!]! hottestProjects(skip: Int = 0, take: Int = 50): [Project!]! - me: User + me: MyProfile newProjects(skip: Int = 0, take: Int = 50): [Project!]! officialTags: [Tag!]! popularTags: [Tag!]! @@ -217,21 +271,7 @@ type Tag { title: String! } -input UpdateProfileInput { - avatar: String - bio: String - email: String - github: String - jobTitle: String - lightning_address: String - linkedin: String - location: String - name: String - twitter: String - website: String -} - -type User { +type User implements BaseUser { avatar: String! bio: String email: String @@ -243,14 +283,17 @@ type User { linkedin: String location: String name: String! - nostr_prv_key: String - nostr_pub_key: String role: String stories: [Story!]! twitter: String website: String } +input UserKeyInputType { + key: String! + name: String! +} + enum VOTE_ITEM_TYPE { Bounty PostComment @@ -268,4 +311,9 @@ type Vote { paid: Boolean! payment_hash: String! payment_request: String! +} + +type WalletKey { + key: String! + name: String! } \ No newline at end of file diff --git a/api/functions/graphql/types/users.js b/api/functions/graphql/types/users.js index 824cc76..b633464 100644 --- a/api/functions/graphql/types/users.js +++ b/api/functions/graphql/types/users.js @@ -1,13 +1,14 @@ const { prisma } = require('../../../prisma'); -const { objectType, extendType, intArg, nonNull, inputObjectType } = require("nexus"); +const { objectType, extendType, intArg, nonNull, inputObjectType, interfaceType, list } = require("nexus"); const { getUserByPubKey } = require("../../../auth/utils/helperFuncs"); const { removeNulls } = require("./helpers"); -const User = objectType({ - name: 'User', + +const BaseUser = interfaceType({ + name: 'BaseUser', definition(t) { t.nonNull.int('id'); t.nonNull.string('name'); @@ -23,8 +24,7 @@ const User = objectType({ t.string('linkedin') t.string('bio') t.string('location') - t.string('nostr_prv_key') - t.string('nostr_pub_key') + t.nonNull.list.nonNull.field('stories', { type: "Story", @@ -32,6 +32,35 @@ const User = objectType({ return prisma.story.findMany({ where: { user_id: parent.id, is_published: true }, orderBy: { createdAt: "desc" } }); } }); + + + }, + resolveType() { + return null + }, +}) + +const User = objectType({ + name: 'User', + definition(t) { + t.implements('BaseUser') + } +}) + +const MyProfile = objectType({ + name: 'MyProfile', + definition(t) { + t.implements('BaseUser') + t.string('nostr_prv_key') + t.string('nostr_pub_key') + + t.nonNull.list.nonNull.field('walletsKeys', { + type: "WalletKey", + resolve: (parent) => { + return prisma.user.findUnique({ where: { id: parent.id } }).userKeys(); + + } + }); } }) @@ -40,7 +69,7 @@ const me = extendType({ type: "Query", definition(t) { t.field('me', { - type: "User", + type: "MyProfile", async resolve(parent, args, context) { const user = await getUserByPubKey(context.userPubKey) return user @@ -58,21 +87,14 @@ const profile = extendType({ id: nonNull(intArg()) }, async resolve(parent, { id }, ctx) { - const user = await getUserByPubKey(ctx.userPubKey); - const isSelf = user?.id === id; - const profile = await prisma.user.findFirst({ - where: { id }, - }); - if (!isSelf) - profile.nostr_prv_key = null; - return profile; + return prisma.user.findUnique({ where: { id } }) } }) } }) -const UpdateProfileInput = inputObjectType({ - name: 'UpdateProfileInput', +const ProfileDetailsInput = inputObjectType({ + name: 'ProfileDetailsInput', definition(t) { t.string('name'); t.string('avatar'); @@ -88,12 +110,12 @@ const UpdateProfileInput = inputObjectType({ } }) -const updateProfile = extendType({ +const updateProfileDetails = extendType({ type: 'Mutation', definition(t) { - t.field('updateProfile', { - type: 'User', - args: { data: UpdateProfileInput }, + t.field('updateProfileDetails', { + type: 'MyProfile', + args: { data: ProfileDetailsInput }, async resolve(_root, args, ctx) { const user = await getUserByPubKey(ctx.userPubKey); @@ -117,14 +139,103 @@ const updateProfile = extendType({ }) +const WalletKey = objectType({ + name: 'WalletKey', + definition(t) { + t.nonNull.string('key'); + t.nonNull.string('name'); + } +}) + + + +const UserKeyInputType = inputObjectType({ + name: 'UserKeyInputType', + definition(t) { + t.nonNull.string('key'); + t.nonNull.string('name'); + } +}) + + + +const updateUserPreferences = extendType({ + type: 'Mutation', + definition(t) { + t.nonNull.field('updateUserPreferences', { + type: 'MyProfile', + args: { userKeys: list(nonNull(UserKeyInputType)) }, + async resolve(_root, args, ctx) { + + const user = await getUserByPubKey(ctx.userPubKey); + if (!user) + throw new Error("You have to login"); + + + //Update the userkeys + //-------------------- + + // Check if all the sent keys belong to the user + const userKeys = (await prisma.userKey.findMany({ + where: { + AND: { + user_id: { + equals: user.id, + }, + key: { + in: args.userKeys.map(i => i.key) + } + }, + }, + select: { + key: true + } + })).map(i => i.key); + + const newKeys = []; + for (let i = 0; i < args.userKeys.length; i++) { + const item = args.userKeys[i]; + if (userKeys.includes(item.key)) + newKeys.push(item); + } + + + if (newKeys.length === 0) + throw new Error("You can't delete all your wallets keys") + + await prisma.userKey.deleteMany({ + where: { + user_id: user.id + } + }) + + await prisma.userKey.createMany({ + data: newKeys.map(i => ({ + user_id: user.id, + key: i.key, + name: i.name, + })) + }) + + return prisma.user.findUnique({ where: { id: user.id } }); + } + }) + } +}) + + + module.exports = { // Types + BaseUser, User, - UpdateProfileInput, + MyProfile, + WalletKey, // Queries me, profile, // Mutations - updateProfile, -} \ No newline at end of file + updateProfileDetails, + updateUserPreferences, +} diff --git a/api/functions/login/login.js b/api/functions/login/login.js index 83a7b5c..782f195 100644 --- a/api/functions/login/login.js +++ b/api/functions/login/login.js @@ -37,15 +37,11 @@ const loginHandler = async (req, res) => { if (existingKeys.length >= 3) return res.status(400).json({ status: 'ERROR', reason: "Can only link up to 3 wallets" }) - if (existingKeys.includes(key)) - return res.status(400).json({ status: 'ERROR', reason: "Wallet already linked" }); - // Remove old linking for this key if existing await prisma.userKey.deleteMany({ where: { key } }) - await prisma.userKey.create({ data: { key, @@ -53,6 +49,7 @@ const loginHandler = async (req, res) => { } }); + return res .status(200) .json({ status: "OK" }) diff --git a/prisma/migrations/20220809081452_add_name_to_user_key/migration.sql b/prisma/migrations/20220809081452_add_name_to_user_key/migration.sql new file mode 100644 index 0000000..e04904a --- /dev/null +++ b/prisma/migrations/20220809081452_add_name_to_user_key/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "UserKey" ADD COLUMN "name" TEXT NOT NULL DEFAULT E'New Key Name'; diff --git a/public/assets/icons/nut.svg b/public/assets/icons/nut.svg new file mode 100644 index 0000000..b7590c0 --- /dev/null +++ b/public/assets/icons/nut.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/images/nut_3d.png b/public/assets/images/nut_3d.png new file mode 100644 index 0000000..955694b Binary files /dev/null and b/public/assets/images/nut_3d.png differ diff --git a/src/Components/Button/Button.tsx b/src/Components/Button/Button.tsx index 2c58f20..8d4c378 100644 --- a/src/Components/Button/Button.tsx +++ b/src/Components/Button/Button.tsx @@ -25,7 +25,7 @@ const btnStylesFill: UnionToObjectKeys = { gray: 'bg-gray-100 hover:bg-gray-200 text-gray-900 active:bg-gray-300', white: 'border border-gray-300 text-gray-900 bg-gray-25 hover:bg-gray-50', black: 'text-white bg-black hover:bg-gray-900', - red: "bg-red-600 hover:bg-red-500 active:bg-red-700 text-white", + red: "bg-red-500 hover:bg-red-600 active:bg-red-700 text-white", } const loadingColor: UnionToObjectKeys = { diff --git a/src/Components/IconButton/IconButton.tsx b/src/Components/IconButton/IconButton.tsx index 7356efc..407052e 100644 --- a/src/Components/IconButton/IconButton.tsx +++ b/src/Components/IconButton/IconButton.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { PropsWithChildren } from 'react'; import { Link } from 'react-router-dom' import { UnionToObjectKeys } from 'src/utils/types/utils' @@ -6,7 +6,6 @@ interface Props { onClick?: () => void; onKeyDown?: (v: any) => void href?: string; - children: JSX.Element className?: string size?: "sm" | 'md' | 'lg' variant?: 'blank' | 'fill' @@ -26,7 +25,7 @@ const baseBtnStyles: UnionToObjectKeys = { blank: "bg-gray-900 bg-opacity-0 hover:bg-opacity-5 active:bg-opacity-10 active:scale-95 !border-0" } -const IconButton = React.forwardRef(({ +const IconButton = React.forwardRef>(({ href, size = "md", className = "", diff --git a/src/features/Auth/pages/LoginPage/LoginPage.tsx b/src/features/Auth/pages/LoginPage/LoginPage.tsx index f3b9ccb..475fb7e 100644 --- a/src/features/Auth/pages/LoginPage/LoginPage.tsx +++ b/src/features/Auth/pages/LoginPage/LoginPage.tsx @@ -153,20 +153,29 @@ export default function LoginPage() { else - content =
-

- Login with lightning ⚑ -

- + content =
+ +

Login with lightning ⚑

+ + +

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

-
- + Click to connect -
- - ) -} diff --git a/src/features/Profiles/pages/EditProfilePage/EditProfilePage.tsx b/src/features/Profiles/pages/EditProfilePage/EditProfilePage.tsx index 380a716..870bf14 100644 --- a/src/features/Profiles/pages/EditProfilePage/EditProfilePage.tsx +++ b/src/features/Profiles/pages/EditProfilePage/EditProfilePage.tsx @@ -7,7 +7,6 @@ import { useAppSelector, useMediaQuery } from "src/utils/hooks"; import UpdateMyProfileTab from "./UpdateMyProfileTab/UpdateMyProfileTab"; import { Helmet } from 'react-helmet' import { MEDIA_QUERIES } from "src/utils/theme"; -import AccountCard from "./AccountCard/AccountCard"; import PreferencesTab from "./PreferencesTab/PreferencesTab"; import Card from "src/Components/Card/Card"; @@ -17,12 +16,8 @@ const links = [ text: "πŸ‘Ύ My Profile", path: 'my-profile', }, - // { - // text: "πŸ™β€β™‚οΈ Account", - // path: 'account', - // }, { - text: "βš™οΈ Preferences", + text: "βš™οΈ Settings & Preferences", path: 'preferences', } ] @@ -30,24 +25,16 @@ const links = [ export default function EditProfilePage() { - const userId = useAppSelector(state => state.user.me?.id) - const profileQuery = useProfileQuery({ - variables: { - profileId: userId!, - }, - skip: !userId, - }) - const isMediumScreen = useMediaQuery(MEDIA_QUERIES.isMedium) + + const isMediumScreen = useMediaQuery(MEDIA_QUERIES.isMedium); + const user = useAppSelector(state => state.user.me) - if (!userId || profileQuery.loading) + if (!user) return - if (!profileQuery.data?.profile) - return - return ( <> @@ -97,9 +84,8 @@ export default function EditProfilePage() {
} /> - } /> - } /> - + } /> + } />
diff --git a/src/features/Profiles/pages/EditProfilePage/AccountCard/AccountCard.stories.tsx b/src/features/Profiles/pages/EditProfilePage/PreferencesTab/LinkedAccountsCard/LinkedAccountsCard.stories.tsx similarity index 90% rename from src/features/Profiles/pages/EditProfilePage/AccountCard/AccountCard.stories.tsx rename to src/features/Profiles/pages/EditProfilePage/PreferencesTab/LinkedAccountsCard/LinkedAccountsCard.stories.tsx index 7463ed6..319570a 100644 --- a/src/features/Profiles/pages/EditProfilePage/AccountCard/AccountCard.stories.tsx +++ b/src/features/Profiles/pages/EditProfilePage/PreferencesTab/LinkedAccountsCard/LinkedAccountsCard.stories.tsx @@ -1,5 +1,5 @@ import { ComponentStory, ComponentMeta } from '@storybook/react'; -import AccountCard from './AccountCard'; +import AccountCard from './LinkedAccountsCard'; export default { title: 'Profiles/Profile Page/Account Card', diff --git a/src/features/Profiles/pages/EditProfilePage/PreferencesTab/LinkedAccountsCard/LinkedAccountsCard.tsx b/src/features/Profiles/pages/EditProfilePage/PreferencesTab/LinkedAccountsCard/LinkedAccountsCard.tsx new file mode 100644 index 0000000..207a4f6 --- /dev/null +++ b/src/features/Profiles/pages/EditProfilePage/PreferencesTab/LinkedAccountsCard/LinkedAccountsCard.tsx @@ -0,0 +1,87 @@ +import Button from 'src/Components/Button/Button'; +import { useAppDispatch } from 'src/utils/hooks'; +import { openModal } from 'src/redux/features/modals.slice'; +import Card from 'src/Components/Card/Card'; +import { MyProfile } from 'src/graphql'; +import WalletKey from './WalletKey'; + + +export type WalletKeyType = MyProfile['walletsKeys'][number] + +interface Props { + value: WalletKeyType[], + onChange: (newValue: WalletKeyType[]) => void +} + + + +export default function LinkedAccountsCard({ value, onChange }: Props) { + + const dispatch = useAppDispatch(); + + const connectNewWallet = () => { + dispatch(openModal({ Modal: "LinkingAccountModal" })) + } + + const updateKeyName = (idx: number, newName: string) => { + onChange(value.map((item, i) => { + if (i === idx) + return { + ...item, + name: newName + } + return item; + })) + } + + const deleteKey = (idx: number,) => { + onChange([...value.slice(0, idx), ...value.slice(idx + 1)]) + } + + + return ( + +

πŸ” Linked Wallets

+

+ These are the wallets that you can login to this account from. You can add up to 3 wallets. +

+
+
    + {value.map((item, idx) => + 1} + onRename={v => updateKeyName(idx, v)} + onDelete={() => deleteKey(idx)} + /> + )} +
+ {/*
+ + +
*/} +
+ {value.length < 3 && + } +

Note: if you link a wallet that was used to create another account previously, you won't be able to login to that account until you remove it from here.

+
+ ) +} diff --git a/src/features/Profiles/pages/EditProfilePage/PreferencesTab/LinkedAccountsCard/WalletKey.tsx b/src/features/Profiles/pages/EditProfilePage/PreferencesTab/LinkedAccountsCard/WalletKey.tsx new file mode 100644 index 0000000..de2f77d --- /dev/null +++ b/src/features/Profiles/pages/EditProfilePage/PreferencesTab/LinkedAccountsCard/WalletKey.tsx @@ -0,0 +1,90 @@ +import { useToggle } from '@react-hookz/web'; +import { createAction } from '@reduxjs/toolkit'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { FiTrash2 } from 'react-icons/fi'; +import Button from 'src/Components/Button/Button'; +import IconButton from 'src/Components/IconButton/IconButton'; +import { useReduxEffect } from 'src/utils/hooks/useReduxEffect'; +import { WalletKeyType } from './LinkedAccountsCard' +import { useAppDispatch } from "src/utils/hooks"; +import { openModal } from "src/redux/features/modals.slice"; + +interface Props { + walletKey: WalletKeyType, + canDelete: boolean; + onRename: (newName: string) => void + onDelete: () => void +} + + + +export default function WalletKey({ walletKey, canDelete, onRename, onDelete }: Props) { + + const ref = useRef(null!); + const [name, setName] = useState(walletKey.name); + const [editMode, toggleEditMode] = useToggle(false); + const dispatch = useAppDispatch(); + + + const CONFIRM_DELETE_WALLET = useMemo(() => createAction<{ confirmed?: boolean }>(`CONFIRM_DELETE_WALLET_${walletKey.key.slice(0, 10)}`)({}), [walletKey.key]) + + const saveNameChanges = () => { + toggleEditMode(); + onRename(name); + } + + const onConfirmDelete = useCallback(({ payload: { confirmed } }: typeof CONFIRM_DELETE_WALLET) => { + if (confirmed) + onDelete() + }, [onDelete]) + + useReduxEffect(onConfirmDelete, CONFIRM_DELETE_WALLET.type); + + useEffect(() => { + if (editMode) + ref.current.focus() + }, [editMode]) + + const handleDelete = () => { + dispatch(openModal({ + Modal: "RemoveWalletKeyModal", + props: { + callbackAction: { + type: CONFIRM_DELETE_WALLET.type, + payload: { confirmed: false } + } + } + })) + } + + return ( +
  • +
    + πŸ”‘ + setName(e.target.value)} + /> + {!editMode && } + {editMode && + } +
    + {canDelete && handleDelete()} + > } +
  • + ) +} diff --git a/src/features/Profiles/pages/EditProfilePage/AccountCard/LinkingAccountModal/LinkingAccountModal.tsx b/src/features/Profiles/pages/EditProfilePage/PreferencesTab/LinkingAccountModal/LinkingAccountModal.tsx similarity index 76% rename from src/features/Profiles/pages/EditProfilePage/AccountCard/LinkingAccountModal/LinkingAccountModal.tsx rename to src/features/Profiles/pages/EditProfilePage/PreferencesTab/LinkingAccountModal/LinkingAccountModal.tsx index 2c3ad78..14f7d5e 100644 --- a/src/features/Profiles/pages/EditProfilePage/AccountCard/LinkingAccountModal/LinkingAccountModal.tsx +++ b/src/features/Profiles/pages/EditProfilePage/PreferencesTab/LinkingAccountModal/LinkingAccountModal.tsx @@ -7,6 +7,8 @@ import { QRCodeSVG } from 'qrcode.react'; import Button from "src/Components/Button/Button"; import { FiCopy } from "react-icons/fi"; import useCopyToClipboard from "src/utils/hooks/useCopyToClipboard"; +import { useApolloClient } from '@apollo/client'; +import { IoClose } from 'react-icons/io5'; @@ -57,7 +59,8 @@ export default function LinkingAccountModal({ onClose, direction, ...props }: Mo const [copied, setCopied] = useState(false); const { loadingLnurl, data: { lnurl }, error } = useLnurlQuery(); - const clipboard = useCopyToClipboard() + const clipboard = useCopyToClipboard(); + const apolloClient = useApolloClient(); @@ -71,19 +74,26 @@ export default function LinkingAccountModal({ onClose, direction, ...props }: Mo clipboard(lnurl); } + const done = () => { + apolloClient.refetchQueries({ + include: ['MyProfilePreferences'] + }) + onClose?.() + } + let content = <> if (error) - content =
    -

    Something wrong happened...

    - Refresh the page + content =
    +

    Ooops...😡

    +

    An error happened while fetching the link, please check your internet connection and try again.

    else if (loadingLnurl) - content =
    - -

    Fetching Lnurl-Auth...

    + content =
    + +

    Fetching Lnurl-Auth Link...

    else { @@ -105,14 +115,9 @@ export default function LinkingAccountModal({ onClose, direction, ...props }: Mo />

    - Scan this code or copy + paste it to your other lightning wallet to be able to login later with it to this account. -
    - When done, click the button below to close this modal. + Scan this code or copy + paste it to your lightning wallet to connect another account to your maker profile. You can also click the QR code to open your WebLN wallet. When done, click the button below to close this modal.

    - {/* Click to connect */}
    @@ -141,6 +145,8 @@ export default function LinkingAccountModal({ onClose, direction, ...props }: Mo exit='exit' className="modal-card max-w-[442px] p-24 rounded-xl relative" > + +

    Connect another ⚑️ wallet

    Connect another ⚑️ wallet

    {content} diff --git a/src/features/Profiles/pages/EditProfilePage/AccountCard/LinkingAccountModal/index.ts b/src/features/Profiles/pages/EditProfilePage/PreferencesTab/LinkingAccountModal/index.ts similarity index 100% rename from src/features/Profiles/pages/EditProfilePage/AccountCard/LinkingAccountModal/index.ts rename to src/features/Profiles/pages/EditProfilePage/PreferencesTab/LinkingAccountModal/index.ts diff --git a/src/features/Profiles/pages/EditProfilePage/PreferencesTab/PreferencesTab.Skeleton.tsx b/src/features/Profiles/pages/EditProfilePage/PreferencesTab/PreferencesTab.Skeleton.tsx new file mode 100644 index 0000000..2e236a1 --- /dev/null +++ b/src/features/Profiles/pages/EditProfilePage/PreferencesTab/PreferencesTab.Skeleton.tsx @@ -0,0 +1,45 @@ +import React from 'react' +import Card from 'src/Components/Card/Card'; +import Skeleton from 'react-loading-skeleton'; + +export default function PreferencesTabSkeleton() { + return ( +
    +
    + +

    +

    + + +

    + +
    +
      + {Array(3).fill(0).map((_, idx) => +
    • +
      + +
      +
    • + )} +
    + +
    +
    + +

    +

    + + + +

    +
    +
    +
    +
    + +
    +
    + ) +} diff --git a/src/features/Profiles/pages/EditProfilePage/PreferencesTab/PreferencesTab.tsx b/src/features/Profiles/pages/EditProfilePage/PreferencesTab/PreferencesTab.tsx index 2711363..ac7ba73 100644 --- a/src/features/Profiles/pages/EditProfilePage/PreferencesTab/PreferencesTab.tsx +++ b/src/features/Profiles/pages/EditProfilePage/PreferencesTab/PreferencesTab.tsx @@ -1,20 +1,105 @@ -import { Nullable } from 'remirror'; +import LinkedAccountsCard from './LinkedAccountsCard/LinkedAccountsCard'; import CommentsSettingsCard from './CommentsSettingsCard/CommentsSettingsCard'; +import { UpdateUserPreferencesMutationVariables, useMyProfilePreferencesQuery, useUpdateUserPreferencesMutation } from 'src/graphql'; +import NotFoundPage from "src/features/Shared/pages/NotFoundPage/NotFoundPage"; +import PreferencesTabSkeleton from './PreferencesTab.Skeleton' +import * as yup from "yup"; +import { yupResolver } from "@hookform/resolvers/yup"; +import { Controller, SubmitHandler, useForm } from 'react-hook-form'; +import SaveChangesCard from '../SaveChangesCard/SaveChangesCard'; +import { toast } from 'react-toastify'; +import { NotificationsService } from 'src/services'; +import { NetworkStatus } from '@apollo/client'; +import { usePrompt } from 'src/utils/hooks'; interface Props { - isOwner?: boolean; - nostr_pub_key: Nullable; - nostr_prv_key: Nullable; - } -export default function PreferencesTab({ nostr_prv_key, nostr_pub_key, isOwner }: Props) { +export type IProfilePreferencesForm = NonNullable; + +const schema: yup.SchemaOf = yup.object({ + walletsKeys: yup.array().of(yup.object().shape({ + name: yup.string().required(), + key: yup.string().trim().required(), + }).required()) + .required(), +}).required(); + +export default function PreferencesTab() { + + const { formState: { isDirty, }, handleSubmit, reset, control } = useForm({ + defaultValues: { + walletsKeys: [] + }, + resolver: yupResolver(schema), + }); + + const query = useMyProfilePreferencesQuery({ + onCompleted: data => { + if (data.me) reset(data.me) + }, + notifyOnNetworkStatusChange: true, + }); + const [mutate, mutationStatus] = useUpdateUserPreferencesMutation(); + + + usePrompt('You may have some unsaved changes. You still want to leave?', isDirty) + + + if (query.networkStatus === NetworkStatus.loading) + return + + if (!query.data?.me) + return + + + const onSubmit: SubmitHandler = data => { + if (!Array.isArray(data.walletsKeys)) + return; + + const toastId = toast.loading("Saving changes...", NotificationsService.defaultOptions) + + + mutate({ + variables: { + walletsKeys: data.walletsKeys.map(({ key, name }) => ({ key, name })), + }, + onCompleted: ({ updateUserPreferences }) => { + if (updateUserPreferences) { + reset(updateUserPreferences); + toast.update(toastId, { render: "Saved changes successfully", type: "success", ...NotificationsService.defaultOptions, isLoading: false }); + } + } + }) + .catch(() => { + toast.update(toastId, { render: "A network error happened", type: "error", ...NotificationsService.defaultOptions, isLoading: false }); + mutationStatus.reset() + }) + }; return (
    -
    - +
    + ( + { + onChange(v); + handleSubmit(onSubmit)(); + }} /> + )} + /> + +
    +
    + {/* reset()} + /> */}
    ) diff --git a/src/features/Profiles/pages/EditProfilePage/PreferencesTab/RemoveWalletKeyModal/RemoveWalletKeyModal.tsx b/src/features/Profiles/pages/EditProfilePage/PreferencesTab/RemoveWalletKeyModal/RemoveWalletKeyModal.tsx new file mode 100644 index 0000000..53f9d46 --- /dev/null +++ b/src/features/Profiles/pages/EditProfilePage/PreferencesTab/RemoveWalletKeyModal/RemoveWalletKeyModal.tsx @@ -0,0 +1,48 @@ +import { ModalCard, modalCardVariants } from 'src/Components/Modals/ModalsContainer/ModalsContainer' +import { motion } from 'framer-motion' +import { IoClose } from 'react-icons/io5' +import Button from 'src/Components/Button/Button' +import { useAppDispatch } from 'src/utils/hooks' +import { PayloadAction } from '@reduxjs/toolkit' + +interface Props extends ModalCard { + callbackAction: PayloadAction<{ confirmed: boolean }> +} + + + +export default function RemoveWalletKeyModal({ + onClose, direction, callbackAction, +}: Props) { + + const dispatch = useAppDispatch(); + + const handleConfirm = () => { + const action = Object.assign({}, callbackAction); + action.payload = { confirmed: true } + dispatch(action) + onClose?.(); + } + + return ( + + +

    Remove key?

    +
    +

    πŸ”‘

    +

    Are you sure you want to remove this key from your account? Once deleted, you won’t be able to recover it.

    +
    + + +
    +
    +
    + ) +} diff --git a/src/features/Profiles/pages/EditProfilePage/PreferencesTab/RemoveWalletKeyModal/index.ts b/src/features/Profiles/pages/EditProfilePage/PreferencesTab/RemoveWalletKeyModal/index.ts new file mode 100644 index 0000000..c056033 --- /dev/null +++ b/src/features/Profiles/pages/EditProfilePage/PreferencesTab/RemoveWalletKeyModal/index.ts @@ -0,0 +1,3 @@ +import { lazyModal } from 'src/utils/helperFunctions'; + +export const { LazyComponent: RemoveWalletKeyModal } = lazyModal(() => import('./RemoveWalletKeyModal')) \ No newline at end of file diff --git a/src/features/Profiles/pages/EditProfilePage/PreferencesTab/profilePreferences.graphql b/src/features/Profiles/pages/EditProfilePage/PreferencesTab/profilePreferences.graphql new file mode 100644 index 0000000..0f23571 --- /dev/null +++ b/src/features/Profiles/pages/EditProfilePage/PreferencesTab/profilePreferences.graphql @@ -0,0 +1,23 @@ +query MyProfilePreferences { + me { + id + walletsKeys { + key + name + } + nostr_prv_key + nostr_pub_key + } +} + +mutation UpdateUserPreferences($walletsKeys: [UserKeyInputType!]) { + updateUserPreferences(userKeys: $walletsKeys) { + id + walletsKeys { + key + name + } + nostr_pub_key + nostr_prv_key + } +} diff --git a/src/features/Profiles/pages/EditProfilePage/SaveChangesCard/SaveChangesCard.tsx b/src/features/Profiles/pages/EditProfilePage/SaveChangesCard/SaveChangesCard.tsx index 3f616c7..941814c 100644 --- a/src/features/Profiles/pages/EditProfilePage/SaveChangesCard/SaveChangesCard.tsx +++ b/src/features/Profiles/pages/EditProfilePage/SaveChangesCard/SaveChangesCard.tsx @@ -3,7 +3,6 @@ import { Link } from 'react-router-dom' import Button from 'src/Components/Button/Button' import Card from 'src/Components/Card/Card' import Avatar from 'src/features/Profiles/Components/Avatar/Avatar' -import { useProfileQuery } from 'src/graphql' import { trimText } from 'src/utils/helperFunctions' import { useAppSelector } from 'src/utils/hooks' import { createRoute } from 'src/utils/routing' @@ -17,14 +16,10 @@ interface Props { export default function SaveChangesCard(props: Props) { - const userId = useAppSelector(state => state.user.me?.id!) - const profileQuery = useProfileQuery({ - variables: { - profileId: userId, - }, - }) + const user = useAppSelector(state => state.user.me) - if (!profileQuery.data?.profile) + + if (!user) return <> @@ -38,18 +33,18 @@ export default function SaveChangesCard(props: Props) {
    - + to={createRoute({ type: 'profile', id: user.id, username: user.name })}> +
    -

    {profileQuery.data.profile ? trimText(profileQuery.data.profile.name, 30) : "Anonymouse"}

    - {profileQuery.data.profile.jobTitle &&

    {profileQuery.data.profile.jobTitle}

    } +

    {user ? trimText(user.name, 30) : "Anonymouse"}

    + {user.jobTitle &&

    {user.jobTitle}

    }
    {/* {showTimeAgo &&

    {dayjs().diff(props.date, 'hour') < 24 ? `${dayjs().diff(props.date, 'hour')}h ago` : undefined}

    } */}
    -

    {trimText(profileQuery.data.profile.bio, 120)}

    +

    {trimText(user.bio, 120)}

    + ) +} diff --git a/src/features/Profiles/pages/EditProfilePage/UpdateMyProfileTab/UpdateMyProfileTab.tsx b/src/features/Profiles/pages/EditProfilePage/UpdateMyProfileTab/UpdateMyProfileTab.tsx index 9f354a6..2af4dbe 100644 --- a/src/features/Profiles/pages/EditProfilePage/UpdateMyProfileTab/UpdateMyProfileTab.tsx +++ b/src/features/Profiles/pages/EditProfilePage/UpdateMyProfileTab/UpdateMyProfileTab.tsx @@ -1,33 +1,23 @@ import { SubmitHandler, useForm } from "react-hook-form" import Button from "src/Components/Button/Button"; -import { User, useUpdateProfileAboutMutation } from "src/graphql"; +import { User, useUpdateProfileAboutMutation, useMyProfileAboutQuery, UpdateProfileAboutMutationVariables } from "src/graphql"; import { NotificationsService } from "src/services/notifications.service"; import * as yup from "yup"; import { yupResolver } from "@hookform/resolvers/yup"; import Avatar from "src/features/Profiles/Components/Avatar/Avatar"; -import { usePrompt } from "src/utils/hooks"; +import { useAppDispatch, usePrompt } from "src/utils/hooks"; import SaveChangesCard from "../SaveChangesCard/SaveChangesCard"; import { toast } from "react-toastify"; import Card from "src/Components/Card/Card"; +import 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"; interface Props { - data: Pick, - onClose?: () => void; } -type IFormInputs = Props['data']; +type IFormInputs = NonNullable; const schema: yup.SchemaOf = yup.object({ name: yup.string().trim().required().min(2), @@ -63,21 +53,35 @@ const schema: yup.SchemaOf = yup.object({ }).required(); -export default function UpdateMyProfileTab({ data, onClose }: Props) { +export default function UpdateMyProfileTab() { const { register, formState: { errors, isDirty, }, handleSubmit, reset } = useForm({ - defaultValues: data, + defaultValues: {}, resolver: yupResolver(schema), mode: 'onBlur', }); + + const profileQuery = useMyProfileAboutQuery({ + onCompleted: data => { + if (data.me) + reset(data.me) + } + }) const [mutate, mutationStatus] = useUpdateProfileAboutMutation(); - - + const dispatch = useAppDispatch() usePrompt('You may have some unsaved changes. You still want to leave?', isDirty) + + if (profileQuery.loading) + return + + if (!profileQuery.data?.me) + return + + const onSubmit: SubmitHandler = data => { const toastId = toast.loading("Saving changes...", NotificationsService.defaultOptions) @@ -98,9 +102,12 @@ export default function UpdateMyProfileTab({ data, onClose }: Props) { website: data.website, } }, - onCompleted: () => { - reset(data); - toast.update(toastId, { render: "Saved changes successfully", type: "success", ...NotificationsService.defaultOptions, isLoading: false }); + onCompleted: ({ updateProfileDetails: data }) => { + if (data) { + dispatch(setUser(data)) + reset(data); + toast.update(toastId, { render: "Saved changes successfully", type: "success", ...NotificationsService.defaultOptions, isLoading: false }); + } } }) .catch(() => { @@ -114,7 +121,7 @@ export default function UpdateMyProfileTab({ data, onClose }: Props) {
    - +
    diff --git a/src/features/Profiles/pages/EditProfilePage/UpdateMyProfileTab/profileAbout.graphql b/src/features/Profiles/pages/EditProfilePage/UpdateMyProfileTab/profileAbout.graphql new file mode 100644 index 0000000..3517a9b --- /dev/null +++ b/src/features/Profiles/pages/EditProfilePage/UpdateMyProfileTab/profileAbout.graphql @@ -0,0 +1,37 @@ +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 + } +} diff --git a/src/features/Profiles/pages/EditProfilePage/UpdateMyProfileTab/updateProfile.graphql b/src/features/Profiles/pages/EditProfilePage/UpdateMyProfileTab/updateProfile.graphql deleted file mode 100644 index a1b690f..0000000 --- a/src/features/Profiles/pages/EditProfilePage/UpdateMyProfileTab/updateProfile.graphql +++ /dev/null @@ -1,18 +0,0 @@ -mutation updateProfileAbout($data: UpdateProfileInput) { - updateProfile(data: $data) { - id - name - avatar - join_date - website - role - email - lightning_address - jobTitle - twitter - github - linkedin - bio - location - } -} diff --git a/src/features/Profiles/pages/ProfilePage/profile.graphql b/src/features/Profiles/pages/ProfilePage/profile.graphql index a118e4e..60feb92 100644 --- a/src/features/Profiles/pages/ProfilePage/profile.graphql +++ b/src/features/Profiles/pages/ProfilePage/profile.graphql @@ -24,7 +24,5 @@ query profile($profileId: Int!) { icon } } - nostr_prv_key - nostr_pub_key } } diff --git a/src/graphql/index.tsx b/src/graphql/index.tsx index 612d9a3..f9f95ac 100644 --- a/src/graphql/index.tsx +++ b/src/graphql/index.tsx @@ -35,6 +35,24 @@ export type Award = { url: Scalars['String']; }; +export type BaseUser = { + avatar: Scalars['String']; + bio: Maybe; + email: Maybe; + github: Maybe; + id: Scalars['Int']; + jobTitle: Maybe; + join_date: Scalars['Date']; + lightning_address: Maybe; + linkedin: Maybe; + location: Maybe; + name: Scalars['String']; + role: Maybe; + stories: Array; + twitter: Maybe; + website: Maybe; +}; + export type Bounty = PostBase & { __typename?: 'Bounty'; applicants_count: Scalars['Int']; @@ -121,7 +139,8 @@ export type Mutation = { createStory: Maybe; deleteStory: Maybe; donate: Donation; - updateProfile: Maybe; + updateProfileDetails: Maybe; + updateUserPreferences: MyProfile; vote: Vote; }; @@ -153,8 +172,13 @@ export type MutationDonateArgs = { }; -export type MutationUpdateProfileArgs = { - data: InputMaybe; +export type MutationUpdateProfileDetailsArgs = { + data: InputMaybe; +}; + + +export type MutationUpdateUserPreferencesArgs = { + userKeys: InputMaybe>; }; @@ -164,6 +188,28 @@ export type MutationVoteArgs = { item_type: Vote_Item_Type; }; +export type MyProfile = BaseUser & { + __typename?: 'MyProfile'; + avatar: Scalars['String']; + bio: Maybe; + email: Maybe; + github: Maybe; + id: Scalars['Int']; + jobTitle: Maybe; + join_date: Scalars['Date']; + lightning_address: Maybe; + linkedin: Maybe; + location: Maybe; + name: Scalars['String']; + nostr_prv_key: Maybe; + nostr_pub_key: Maybe; + role: Maybe; + stories: Array; + twitter: Maybe; + walletsKeys: Array; + website: Maybe; +}; + export enum Post_Type { Bounty = 'Bounty', Question = 'Question', @@ -193,6 +239,20 @@ export type PostComment = { votes_count: Scalars['Int']; }; +export type ProfileDetailsInput = { + avatar: InputMaybe; + bio: InputMaybe; + email: InputMaybe; + github: InputMaybe; + jobTitle: InputMaybe; + lightning_address: InputMaybe; + linkedin: InputMaybe; + location: InputMaybe; + name: InputMaybe; + twitter: InputMaybe; + website: InputMaybe; +}; + export type Project = { __typename?: 'Project'; awards: Array; @@ -224,7 +284,7 @@ export type Query = { getProject: Project; getTrendingPosts: Array; hottestProjects: Array; - me: Maybe; + me: Maybe; newProjects: Array; officialTags: Array; popularTags: Array; @@ -361,21 +421,7 @@ export type Tag = { title: Scalars['String']; }; -export type UpdateProfileInput = { - avatar: InputMaybe; - bio: InputMaybe; - email: InputMaybe; - github: InputMaybe; - jobTitle: InputMaybe; - lightning_address: InputMaybe; - linkedin: InputMaybe; - location: InputMaybe; - name: InputMaybe; - twitter: InputMaybe; - website: InputMaybe; -}; - -export type User = { +export type User = BaseUser & { __typename?: 'User'; avatar: Scalars['String']; bio: Maybe; @@ -388,14 +434,17 @@ export type User = { linkedin: Maybe; location: Maybe; name: Scalars['String']; - nostr_prv_key: Maybe; - nostr_pub_key: Maybe; role: Maybe; stories: Array; twitter: Maybe; website: Maybe; }; +export type UserKeyInputType = { + key: Scalars['String']; + name: Scalars['String']; +}; + export enum Vote_Item_Type { Bounty = 'Bounty', PostComment = 'PostComment', @@ -416,6 +465,12 @@ export type Vote = { payment_request: Scalars['String']; }; +export type WalletKey = { + __typename?: 'WalletKey'; + key: Scalars['String']; + name: Scalars['String']; +}; + export type OfficialTagsQueryVariables = Exact<{ [key: string]: never; }>; @@ -436,7 +491,7 @@ export type SearchProjectsQuery = { __typename?: 'Query', searchProjects: Array< export type MeQueryVariables = Exact<{ [key: string]: never; }>; -export type MeQuery = { __typename?: 'Query', me: { __typename?: 'User', id: number, name: string, avatar: string, join_date: any } | null }; +export type MeQuery = { __typename?: 'Query', me: { __typename?: 'MyProfile', id: number, name: string, avatar: string, join_date: any, jobTitle: string | null, bio: string | null } | null }; export type DonationsStatsQueryVariables = Exact<{ [key: string]: never; }>; @@ -515,19 +570,36 @@ 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; }>; + + +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 | 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 MyProfileAboutQueryVariables = Exact<{ [key: string]: never; }>; + + +export type MyProfileAboutQuery = { __typename?: 'Query', me: { __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 UpdateProfileAboutMutationVariables = Exact<{ + data: InputMaybe; +}>; + + +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 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, nostr_prv_key: string | null, nostr_pub_key: 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 UpdateProfileAboutMutationVariables = Exact<{ - data: InputMaybe; -}>; - - -export type UpdateProfileAboutMutation = { __typename?: 'Mutation', updateProfile: { __typename?: 'User', id: number, name: string, avatar: string, join_date: any, website: string | null, role: string | null, email: string | null, lightning_address: string | null, jobTitle: string | null, twitter: string | null, github: string | null, linkedin: string | null, bio: string | null, location: 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 }> }> } | null }; export type CategoryPageQueryVariables = Exact<{ categoryId: Scalars['Int']; @@ -698,6 +770,8 @@ export const MeDocument = gql` name avatar join_date + jobTitle + bio } } `; @@ -1306,9 +1380,88 @@ export function usePostDetailsLazyQuery(baseOptions?: Apollo.LazyQueryHookOption export type PostDetailsQueryHookResult = ReturnType; export type PostDetailsLazyQueryHookResult = ReturnType; export type PostDetailsQueryResult = Apollo.QueryResult; -export const ProfileDocument = gql` - query profile($profileId: Int!) { - profile(id: $profileId) { +export const MyProfilePreferencesDocument = gql` + query MyProfilePreferences { + me { + id + walletsKeys { + key + name + } + nostr_prv_key + nostr_pub_key + } +} + `; + +/** + * __useMyProfilePreferencesQuery__ + * + * To run a query within a React component, call `useMyProfilePreferencesQuery` and pass it any options that fit your needs. + * When your component renders, `useMyProfilePreferencesQuery` 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 } = useMyProfilePreferencesQuery({ + * variables: { + * }, + * }); + */ +export function useMyProfilePreferencesQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(MyProfilePreferencesDocument, options); + } +export function useMyProfilePreferencesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(MyProfilePreferencesDocument, options); + } +export type MyProfilePreferencesQueryHookResult = ReturnType; +export type MyProfilePreferencesLazyQueryHookResult = ReturnType; +export type MyProfilePreferencesQueryResult = Apollo.QueryResult; +export const UpdateUserPreferencesDocument = gql` + mutation UpdateUserPreferences($walletsKeys: [UserKeyInputType!]) { + updateUserPreferences(userKeys: $walletsKeys) { + id + walletsKeys { + key + name + } + nostr_pub_key + nostr_prv_key + } +} + `; +export type UpdateUserPreferencesMutationFn = Apollo.MutationFunction; + +/** + * __useUpdateUserPreferencesMutation__ + * + * To run a mutation, you first call `useUpdateUserPreferencesMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useUpdateUserPreferencesMutation` 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 [updateUserPreferencesMutation, { data, loading, error }] = useUpdateUserPreferencesMutation({ + * variables: { + * walletsKeys: // value for 'walletsKeys' + * }, + * }); + */ +export function useUpdateUserPreferencesMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(UpdateUserPreferencesDocument, options); + } +export type UpdateUserPreferencesMutationHookResult = ReturnType; +export type UpdateUserPreferencesMutationResult = Apollo.MutationResult; +export type UpdateUserPreferencesMutationOptions = Apollo.BaseMutationOptions; +export const MyProfileAboutDocument = gql` + query MyProfileAbout { + me { id name avatar @@ -1323,61 +1476,48 @@ export const ProfileDocument = gql` linkedin bio location - stories { - id - title - createdAt - tags { - id - title - icon - } - } - nostr_prv_key - nostr_pub_key } } `; /** - * __useProfileQuery__ + * __useMyProfileAboutQuery__ * - * To run a query within a React component, call `useProfileQuery` and pass it any options that fit your needs. - * When your component renders, `useProfileQuery` returns an object from Apollo Client that contains loading, error, and data properties + * 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 } = useProfileQuery({ + * const { data, loading, error } = useMyProfileAboutQuery({ * variables: { - * profileId: // value for 'profileId' * }, * }); */ -export function useProfileQuery(baseOptions: Apollo.QueryHookOptions) { +export function useMyProfileAboutQuery(baseOptions?: Apollo.QueryHookOptions) { const options = {...defaultOptions, ...baseOptions} - return Apollo.useQuery(ProfileDocument, options); + return Apollo.useQuery(MyProfileAboutDocument, options); } -export function useProfileLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { +export function useMyProfileAboutLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { const options = {...defaultOptions, ...baseOptions} - return Apollo.useLazyQuery(ProfileDocument, options); + return Apollo.useLazyQuery(MyProfileAboutDocument, options); } -export type ProfileQueryHookResult = ReturnType; -export type ProfileLazyQueryHookResult = ReturnType; -export type ProfileQueryResult = Apollo.QueryResult; +export type MyProfileAboutQueryHookResult = ReturnType; +export type MyProfileAboutLazyQueryHookResult = ReturnType; +export type MyProfileAboutQueryResult = Apollo.QueryResult; export const UpdateProfileAboutDocument = gql` - mutation updateProfileAbout($data: UpdateProfileInput) { - updateProfile(data: $data) { + mutation updateProfileAbout($data: ProfileDetailsInput) { + updateProfileDetails(data: $data) { id name avatar join_date - website role email - lightning_address jobTitle + lightning_address + website twitter github linkedin @@ -1412,6 +1552,64 @@ export function useUpdateProfileAboutMutation(baseOptions?: Apollo.MutationHookO export type UpdateProfileAboutMutationHookResult = ReturnType; export type UpdateProfileAboutMutationResult = Apollo.MutationResult; export type UpdateProfileAboutMutationOptions = Apollo.BaseMutationOptions; +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 + createdAt + tags { + id + title + icon + } + } + } +} + `; + +/** + * __useProfileQuery__ + * + * To run a query within a React component, call `useProfileQuery` and pass it any options that fit your needs. + * When your component renders, `useProfileQuery` 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 } = useProfileQuery({ + * variables: { + * profileId: // value for 'profileId' + * }, + * }); + */ +export function useProfileQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(ProfileDocument, options); + } +export function useProfileLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(ProfileDocument, options); + } +export type ProfileQueryHookResult = ReturnType; +export type ProfileLazyQueryHookResult = ReturnType; +export type ProfileQueryResult = Apollo.QueryResult; export const CategoryPageDocument = gql` query CategoryPage($categoryId: Int!) { projectsByCategory(category_id: $categoryId) { diff --git a/src/mocks/data/users.ts b/src/mocks/data/users.ts index ac8f844..c292050 100644 --- a/src/mocks/data/users.ts +++ b/src/mocks/data/users.ts @@ -1,12 +1,12 @@ -import { User } from "src/graphql"; +import { MyProfile, User } from "src/graphql"; import { posts } from "./posts"; -export const user: User = { +export const user: User & MyProfile = { id: 123, email: "mtg0987654321@gmail.com", avatar: "https://avatars.dicebear.com/api/bottts/Mtgmtg.svg", bio: "Lorem asiop asklh kluiw wekjhl shkj kljhsva klu khsc klhlkbs mjklwqr kmlk sadlfui mewr qiumnk, asdjomi cskhsdf.", - name: "123123124asdfsadfsa8d7fsadfasdf", + name: "Mtg", github: "MTG2000", jobTitle: "Front-end Web Developer", join_date: new Date(2021).toISOString(), @@ -14,9 +14,19 @@ export const user: User = { linkedin: "https://www.linkedin.com/in/mtg-softwares-dev/", location: "Germany, Berlin", role: "user", - twitter: "john-doe", + twitter: "mtg", website: "https://mtg-dev.tech", stories: posts.stories, nostr_prv_key: "123123124asdfsadfsa8d7fsadfasdf", nostr_pub_key: "123124123123dfsadfsa8d7f11sadfasdf", + walletsKeys: [ + { + key: "1645h234j2421zxvertw", + name: "My Alby wallet key" + }, + { + key: "6643534534534534543", + name: "My Phoenix wallet key" + }, + ] } diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts index a1fb79b..94b5557 100644 --- a/src/mocks/handlers.ts +++ b/src/mocks/handlers.ts @@ -29,6 +29,8 @@ import { MeQuery, ProfileQuery, GetMyDraftsQuery, + MyProfileAboutQuery, + MyProfilePreferencesQuery, } from 'src/graphql' const delay = (ms = 1000) => new Promise((res) => setTimeout(res, ms + Math.random() * 1000)) @@ -195,6 +197,7 @@ export const handlers = [ graphql.query('Me', async (req, res, ctx) => { await delay() + console.log("ME"); return res( ctx.data({ @@ -203,6 +206,27 @@ export const handlers = [ ) }), + + graphql.query('MyProfileAbout', async (req, res, ctx) => { + await delay() + return res( + ctx.data({ + me: me(), + }) + ) + }), + + graphql.query('MyProfilePreferences', async (req, res, ctx) => { + await delay() + return res( + ctx.data({ + me: me(), + }) + ) + }), + + + graphql.query('profile', async (req, res, ctx) => { await delay() diff --git a/src/mocks/resolvers.ts b/src/mocks/resolvers.ts index c3e5ccc..d09f614 100644 --- a/src/mocks/resolvers.ts +++ b/src/mocks/resolvers.ts @@ -1,5 +1,5 @@ import { MOCK_DATA } from "./data"; -import { Query, QueryGetFeedArgs, QueryGetPostByIdArgs } from 'src/graphql' +import { MyProfile, Query, QueryGetFeedArgs, QueryGetPostByIdArgs, User } from 'src/graphql' import { Chance } from "chance"; import { tags } from "./data/tags"; import { hackathons } from "./data/hackathon"; @@ -72,11 +72,16 @@ export function getAllHackathons() { } export function me() { - return MOCK_DATA['user'] + return { + ...MOCK_DATA['user'], + __typename: "MyProfile", + } as MyProfile } + + export function profile() { - return MOCK_DATA['user'] + return { ...MOCK_DATA['user'], __typename: 'User' } as User } export function getMyDrafts(): Query['getMyDrafts'] { diff --git a/src/redux/features/modals.slice.ts b/src/redux/features/modals.slice.ts index c6c3a27..c718a4f 100644 --- a/src/redux/features/modals.slice.ts +++ b/src/redux/features/modals.slice.ts @@ -8,7 +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/AccountCard/LinkingAccountModal"; +import { LinkingAccountModal } from "src/features/Profiles/pages/EditProfilePage/PreferencesTab/LinkingAccountModal"; +import { RemoveWalletKeyModal } from "src/features/Profiles/pages/EditProfilePage/PreferencesTab/RemoveWalletKeyModal"; import { ComponentProps } from "react"; import { generateId } from "src/utils/helperFunctions"; @@ -24,19 +25,27 @@ export enum Direction { export const ALL_MODALS = { + //Projects ProjectDetailsCard, + + // Auth Login_ScanningWalletCard, Login_NativeWalletCard, Login_SuccessCard, Login_ExternalWalletCard, - VoteCard, Claim_GenerateSignatureCard, Claim_CopySignatureCard, Claim_SubmittedCard, Claim_FundWithdrawCard, + + // Misc ConfirmModal, + VoteCard, NoWeblnModal, + + // User Wallets Keys LinkingAccountModal, + RemoveWalletKeyModal, // Text Editor Modals InsertImageModal, diff --git a/src/redux/features/user.slice.ts b/src/redux/features/user.slice.ts index 3cd58d6..24918b4 100644 --- a/src/redux/features/user.slice.ts +++ b/src/redux/features/user.slice.ts @@ -1,12 +1,14 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { MyProfile } from "src/graphql"; interface StoreState { - me: { - id: number; - name: string; - avatar: string; - join_date: string; - } + me: Pick | undefined // fetching user data if exist | null // user not logged in