diff --git a/src/app/(protected)/account/profile/_components/ProfileMain.tsx b/src/app/(protected)/account/profile/_components/ProfileMain.tsx deleted file mode 100644 index 6f4e949..0000000 --- a/src/app/(protected)/account/profile/_components/ProfileMain.tsx +++ /dev/null @@ -1,99 +0,0 @@ -/* eslint-disable @typescript-eslint/no-use-before-define */ -import { Col } from 'react-bootstrap'; -import styled from 'styled-components'; -import { Card } from '@/shared/components/Card'; -import { left } from '@/styles/directions'; -import Image from 'next/image'; -import { ProfileCard } from './ProfileBasicComponents'; - -const ProfileMain = () => ( - - - - - - avatar - - - Holly Hammond - Account Manager - holly@colony.com - +42-452-743-233 - - - - - -); - -export default ProfileMain; - -// region STYLES - -const ProfileInformation = styled.div` - padding: 30px 40px; - display: flex; - text-align: ${left}; - justify-content: center; - flex-direction: column; - align-items: center; - - @media (max-width: 1345px) and (min-width: 1200px) { - padding: 30px 15px; - } - - @media screen and (max-width: 360px) { - width: 100%; - } -`; - -const ProfileAvatar = styled.div` - height: 140px; - width: 140px; - overflow: hidden; - border-radius: 50%; - - img { - height: 100%; - } - - @media (max-width: 1345px) and (min-width: 1200px) { - height: 130px; - width: 130px; - } -`; - -const ProfileData = styled.div` - margin-top: 30px; - - @media screen and (max-width: 360px) { - width: 100%; - display: flex; - flex-direction: column; - text-align: center; - padding: 0; - } -`; - -const ProfileName = styled.p` - font-weight: 900; - text-transform: uppercase; - margin: 0; - line-height: 18px; -`; - -const ProfileWork = styled.p` - font-weight: 500; - margin-bottom: 10px; - margin-top: 0; - opacity: 0.6; - line-height: 18px; -`; - -const ProfileContact = styled.p` - margin-top: 0; - margin-bottom: 5px; - line-height: 18px; -`; - -// endregion diff --git a/src/app/(protected)/account/profile/page.tsx b/src/app/(protected)/account/profile/page.tsx index ec1f323..d6cac37 100644 --- a/src/app/(protected)/account/profile/page.tsx +++ b/src/app/(protected)/account/profile/page.tsx @@ -1,18 +1,6 @@ -'use client'; - -import { Container, Row } from 'react-bootstrap'; -import { useTitle } from '@/hooks/useTitle'; -import ProfileMain from './_components/ProfileMain'; +import ProfileMain from 'module/protected/account/profile/ProfileMain'; const Profile = () => { - useTitle('Profile - BeeQuant'); - - return ( - - - - - - ); + return ; }; export default Profile; diff --git a/src/app/(protected)/crypto/exchange/page.tsx b/src/app/(protected)/crypto/exchange/page.tsx index 8c6fb97..3523aff 100644 --- a/src/app/(protected)/crypto/exchange/page.tsx +++ b/src/app/(protected)/crypto/exchange/page.tsx @@ -1,11 +1,8 @@ 'use client'; import { Col, Container, Row } from 'react-bootstrap'; -import { useTitle } from '@/hooks/useTitle'; const CryptoExchanges = () => { - useTitle('Exchanges - BeeQuant'); - return ( diff --git a/src/graphql/codegen/gql.ts b/src/graphql/codegen/gql.ts index 38cac59..b6915f1 100644 --- a/src/graphql/codegen/gql.ts +++ b/src/graphql/codegen/gql.ts @@ -17,7 +17,7 @@ const documents = { types.LoginDocument, '\n mutation Register($input: CreateUserInput!) {\n register(input: $input) {\n code\n message\n data\n }\n }\n': types.RegisterDocument, - '\n query getUserInfo {\n getUserInfo {\n id\n displayName\n }\n }\n': + '\n query getUserInfo {\n getUserInfo {\n id\n displayName\n email\n ref\n }\n }\n': types.GetUserInfoDocument, '\n query getUserById($id: String!) {\n getUserById(id: $id) {\n id\n email\n realName\n displayName\n mobile\n }\n }\n': types.GetUserByIdDocument, @@ -55,8 +55,8 @@ export function gql( * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function gql( - source: '\n query getUserInfo {\n getUserInfo {\n id\n displayName\n }\n }\n' -): (typeof documents)['\n query getUserInfo {\n getUserInfo {\n id\n displayName\n }\n }\n']; + source: '\n query getUserInfo {\n getUserInfo {\n id\n displayName\n email\n ref\n }\n }\n' +): (typeof documents)['\n query getUserInfo {\n getUserInfo {\n id\n displayName\n email\n ref\n }\n }\n']; /** * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/src/graphql/codegen/graphql.ts b/src/graphql/codegen/graphql.ts index d53c2b1..06abeda 100644 --- a/src/graphql/codegen/graphql.ts +++ b/src/graphql/codegen/graphql.ts @@ -230,7 +230,13 @@ export type GetUserInfoQueryVariables = Exact<{ [key: string]: never }>; export type GetUserInfoQuery = { __typename?: 'Query'; - getUserInfo: { __typename?: 'UserType'; id: string; displayName: string }; + getUserInfo: { + __typename?: 'UserType'; + id: string; + displayName: string; + email: string; + ref: string; + }; }; export type GetUserByIdQueryVariables = Exact<{ @@ -375,6 +381,8 @@ export const GetUserInfoDocument = { selections: [ { kind: 'Field', name: { kind: 'Name', value: 'id' } }, { kind: 'Field', name: { kind: 'Name', value: 'displayName' } }, + { kind: 'Field', name: { kind: 'Name', value: 'email' } }, + { kind: 'Field', name: { kind: 'Name', value: 'ref' } }, ], }, }, diff --git a/src/graphql/user.ts b/src/graphql/user.ts index 748b58a..65192bf 100644 --- a/src/graphql/user.ts +++ b/src/graphql/user.ts @@ -5,6 +5,8 @@ export const GET_USER = gql(` getUserInfo { id displayName + email + ref } } `); diff --git a/src/app/(protected)/account/profile/_components/ProfileBasicComponents.tsx b/src/module/protected/account/profile/ProfileBasicComponents.tsx similarity index 100% rename from src/app/(protected)/account/profile/_components/ProfileBasicComponents.tsx rename to src/module/protected/account/profile/ProfileBasicComponents.tsx diff --git a/src/module/protected/account/profile/ProfileMain.test.tsx b/src/module/protected/account/profile/ProfileMain.test.tsx new file mode 100644 index 0000000..3048c34 --- /dev/null +++ b/src/module/protected/account/profile/ProfileMain.test.tsx @@ -0,0 +1,134 @@ +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { MockedProvider } from '@apollo/client/testing'; +import { GET_USER, UPDATE_USER } from '@/graphql/user'; +import { AUTH_TOKEN } from '@/shared/constants/storage'; +import ProfileMain from './ProfileMain'; + +const mocks = [ + { + request: { + query: GET_USER, + }, + result: { + data: { + getUserInfo: { + id: '1332332', + email: 'test@example.com', + ref: 'ref123', + displayName: 'Test User', + }, + }, + }, + }, + { + request: { + query: UPDATE_USER, + variables: { id: '1332332', input: { displayName: 'Updated User' } }, + }, + result: { + data: { + updateUser: { + id: '1332332', + displayName: 'Updated User', + }, + }, + }, + }, +]; + +beforeEach(() => { + localStorage.setItem(AUTH_TOKEN, 'mock-token'); +}); + +test('renders profile information', async () => { + render( + + + + ); + + expect(screen.getByText('Loading...')).toBeInTheDocument(); + + await waitFor(() => { + expect(screen.getByText('Profile')).toBeInTheDocument(); + }); + + expect(screen.getByDisplayValue('Test User')).toBeInTheDocument(); + expect(screen.getByText('test@example.com')).toBeInTheDocument(); + expect(screen.getByText('ref123')).toBeInTheDocument(); +}); + +test('updates display name-valid', async () => { + render( + + + + ); + + await waitFor(() => { + expect(screen.getByText('Profile')).toBeInTheDocument(); + }); + + const input = screen.getByDisplayValue('Test User'); + fireEvent.change(input, { target: { value: 'Updated User' } }); + const button = screen.getByText('Submit'); + fireEvent.click(button); + + await waitFor(() => { + expect(screen.getByDisplayValue('Updated User')).toBeInTheDocument(); + }); +}); + +test('display name validation - minimum length', async () => { + render( + + + + ); + + const input = await screen.findByDisplayValue('Test User'); + fireEvent.change(input, { target: { value: 'Abc' } }); + const button = screen.getByText('Submit'); + fireEvent.click(button); + + const errorMessage = await screen.findByText( + 'Display name must be 4-15 characters long and contain only letters, numbers, hyphens, and underscores.' + ); + expect(errorMessage).toBeInTheDocument(); +}); + +test('display name validation - maximum length', async () => { + render( + + + + ); + + const input = await screen.findByDisplayValue('Test User'); + fireEvent.change(input, { target: { value: 'ThisIsAVeryLongDisplayName1231231231' } }); + const button = screen.getByText('Submit'); + fireEvent.click(button); + + const errorMessage = await screen.findByText( + 'Display name must be 4-15 characters long and contain only letters, numbers, hyphens, and underscores.' + ); + expect(errorMessage).toBeInTheDocument(); +}); + +test('display name validation - invalid characters', async () => { + render( + + + + ); + + const input = await screen.findByDisplayValue('Test User'); + fireEvent.change(input, { target: { value: 'Invalid!' } }); + const button = screen.getByText('Submit'); + fireEvent.click(button); + + const errorMessage = await screen.findByText( + 'Display name must be 4-15 characters long and contain only letters, numbers, hyphens, and underscores.' + ); + expect(errorMessage).toBeInTheDocument(); +}); diff --git a/src/module/protected/account/profile/ProfileMain.tsx b/src/module/protected/account/profile/ProfileMain.tsx new file mode 100644 index 0000000..a1155b4 --- /dev/null +++ b/src/module/protected/account/profile/ProfileMain.tsx @@ -0,0 +1,135 @@ +'use client'; +import { Container, Row } from 'react-bootstrap'; +import { Col } from 'react-bootstrap'; +import { useMutation, useQuery } from '@apollo/client'; +import { useEffect, useState } from 'react'; +import { Card } from '@/shared/components/Card'; +import { useUserContext } from '@/hooks/userHooks'; +import { UPDATE_USER, GET_USER } from '@/graphql/user'; +import { Button } from '@/shared/components/Button'; +import { + ProfileContact, + ProfileData, + ProfileInformation, + ProfileIntro, + ProfileReadOnly, + ProfileText, + Description, + ButtonGroup, + ErrorMessage, +} from './ProfileMainStyleCom'; +import { ProfileCard } from './ProfileBasicComponents'; + +const initialEmail = ''; +const initialRef = ''; +const initialDisplayName = ''; + +const ProfileMain = () => { + const { store, setStore } = useUserContext(); + const [userId, setUserId] = useState(''); + const [msg, setMsg] = useState(''); + const { loading, data } = useQuery(GET_USER); + const [updateUser] = useMutation(UPDATE_USER); + const validateUpdatedDisplayName = (name: string) => { + const regex = /^[a-zA-Z0-9-_]+$/; + return name.length >= 4 && name.length <= 15 && regex.test(name); + }; + const handleUpdateUser = () => { + if (!validateUpdatedDisplayName(disName)) { + setMsg( + 'Display name must be 4-15 characters long and contain only letters, numbers, hyphens, and underscores.' + ); + return; + } + setMsg(''); + updateUser({ + variables: { + id: userId, + input: { + displayName: disName, + }, + }, + }).then(() => { + setStore(() => { + const updatedStore = { + ...store, + displayName: disName, + }; + + return updatedStore; + }); + }); + }; + + const [disEmail, setDisEmail] = useState(initialEmail); + const [disRef, setDisRef] = useState(initialRef); + const [disName, setDisplayName] = useState(initialDisplayName); + useEffect(() => { + if (!loading && data) { + const { id, email, ref, displayName } = data.getUserInfo; + setUserId(id); + setDisEmail(email); + setDisRef(ref); + setDisplayName(displayName || initialDisplayName); + setStore((prevStore: Record) => { + const updatedStore = { + ...prevStore, + displayName, + email, + ref, + }; + return updatedStore; + }); + } + }, [loading, data]); + + if (loading) { + return

Loading...

; + } + + return ( + + + + + + + + <>Profile + + + <>Update your account information + + + + Display Name + setDisplayName(e.target.value)} + /> + + + Email + {disEmail} + + + Reference + {disRef} + + + + + + {msg && {msg}} + + + + + + + ); +}; +export default ProfileMain; diff --git a/src/module/protected/account/profile/ProfileMainStyleCom.tsx b/src/module/protected/account/profile/ProfileMainStyleCom.tsx new file mode 100644 index 0000000..f892f0c --- /dev/null +++ b/src/module/protected/account/profile/ProfileMainStyleCom.tsx @@ -0,0 +1,96 @@ +import styled from 'styled-components'; +import { colorText, colorBackgroundBody, colorBackground } from '@/styles/palette'; +import { left } from '@/styles/directions'; + +export const ButtonGroup = styled.div` + display: flex; + justify-content: flex-start; + margin-left: 168px; + margin-top: 30px; +`; + +export const ProfileInformation = styled.div` + padding: 30px 20px; + display: flex; + text-align: ${left}; + justify-content: center; + flex-direction: column; + align-items: left; + + @media (max-width: 1345px) and (min-width: 1200px) { + padding: 30px 15px; + } + + @media screen and (max-width: 360px) { + width: 100%; + } +`; + +export const ProfileData = styled.div` + margin-top: 30px; + + @media screen and (max-width: 360px) { + width: 100%; + display: flex; + flex-direction: column; + text-align: center; + padding: 0; + } +`; + +export const ProfileReadOnly = styled.p` + flex: 1; + padding: 8px; + border: 1px solid ${colorBackgroundBody}; + margin: 0; + color: ${colorText}; + background-color: ${colorBackgroundBody}; +`; + +export const ProfileContact = styled.div` + display: flex; + margin-top: 10px; + margin-bottom: 5px; + line-height: 18px; + align-items: center; + input { + flex: 1; + padding: 8px; + border: 1px solid ${colorBackgroundBody}; + margin: 0; + color: ${colorText}; + background-color: ${colorBackground}; + } +`; + +export const Description = styled.p` + text-align: left; + margin-left: 0px; + margin-right: 20px; + width: 150px; +`; + +export const ProfileIntro = styled.p` + font-weight: 900; + text-transform: uppercase; + text-align: left; + margin-bottom: 4px; + line-height: 18px; +`; +export const ProfileText = styled.p` + text-align: left; + margin-bottom: 5px; + line-height: 18px; +`; + +export const ErrorMessage = styled.div` + background-color: #f8d7da; + color: #721c24; + padding: 10px; + margin-left: auto; + margin-right: auto; + border: 1px; + solid #f5c6cb; + border-radius: 5px; + text-align: center; +`; diff --git a/tsconfig.json b/tsconfig.json index 79332c5..45a37de 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,52 +2,22 @@ "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, - "lib": [ - "ES2020", - "DOM", - "DOM.Iterable" - ], + "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", - "types": [ - "vite/client", - "jest", - "react", - "react-dom", - "node" - ], + "types": ["vite/client", "jest", "react", "react-dom", "node"], "skipLibCheck": true, "baseUrl": "src", "paths": { - "@/styles/*": [ - "styles/*" - ], - "@/config/*": [ - "config/*" - ], - "@/shared/*": [ - "shared/*" - ], - "@/graphql/*": [ - "graphql/*" - ], - "@/utils/*": [ - "shared/utils/*" - ], - "@/hooks/*": [ - "hooks/*" - ], - "@/constants/*": [ - "shared/constants/*" - ], - "@/containers/*": [ - "containers/*" - ], - "@/routes/*": [ - "routes/*" - ], - "@/components/*": [ - "shared/components/*" - ] + "@/styles/*": ["styles/*"], + "@/config/*": ["config/*"], + "@/shared/*": ["shared/*"], + "@/graphql/*": ["graphql/*"], + "@/utils/*": ["shared/utils/*"], + "@/hooks/*": ["hooks/*"], + "@/constants/*": ["shared/constants/*"], + "@/containers/*": ["containers/*"], + "@/routes/*": ["routes/*"], + "@/components/*": ["shared/components/*"] }, /* Bundler mode */ "moduleResolution": "bundler", @@ -73,13 +43,6 @@ } ] }, - "include": [ - "./src", - "./dist/types/**/*.ts", - "./next-env.d.ts", - ".next/types/**/*.ts" - ], - "exclude": [ - "./node_modules" - ] + "include": ["./src", "./dist/types/**/*.ts", "./next-env.d.ts", ".next/types/**/*.ts"], + "exclude": ["./node_modules"] }