diff --git a/__tests__/__snapshots__/storyshots.test.js.snap b/__tests__/__snapshots__/storyshots.test.js.snap index 4b76d7a0e..56c73e1ae 100644 --- a/__tests__/__snapshots__/storyshots.test.js.snap +++ b/__tests__/__snapshots__/storyshots.test.js.snap @@ -12282,7 +12282,7 @@ exports[`Storyshots Components/ProfileImageInfo Profile Image Info 1`] = ` className="card shadow-sm" >
-

Rahul Kalra -

+
-

@noob101 -

+
- -
- + - fakeDiscordUsername - -
+ + fakeDiscordUsername + + +
+ + + +`; + +exports[`Storyshots Components/ProfileImageInfo Profile Image Info With Connect 1`] = ` +
+
+
+ RK +
+
+

+ Rahul Kalra +

+
+ + @noob101 + +
+
+
`; diff --git a/__tests__/pages/profile/__snapshots__/username.test.js.snap b/__tests__/pages/profile/__snapshots__/username.test.js.snap index ff5688ec3..4c47ea4fe 100644 --- a/__tests__/pages/profile/__snapshots__/username.test.js.snap +++ b/__tests__/pages/profile/__snapshots__/username.test.js.snap @@ -175,7 +175,7 @@ exports[`user profile test Should render anonymous users 1`] = ` class="card shadow-sm" >
-

A -

+
-

@fake user -

+
+ class="d-flex ms-auto me-auto mb-4 mt-3 flex-column" + > + +
-

fake user -

+
-

@fake user -

+
+ class="d-flex ms-auto me-auto mb-4 mt-3 flex-column" + > + +
-

fake user -

+
-

@fake user -

+
+ class="d-flex ms-auto me-auto mb-4 mt-3 flex-column" + > + +
-

fake user -

+
-

@fake user -

+
+ class="d-flex ms-auto me-auto mb-4 mt-3 flex-column" + > + +
-

fake user -

+
-

@fake user -

+
+ class="d-flex ms-auto me-auto mb-4 mt-3 flex-column" + > + +
-

fake user -

+
-

@fake user -

+
+
{ const { query } = useRouter() - query['username'] = 'fake user' + query.username = 'fake user' + test('Should render loading spinner if data is not ready', () => { expectLoading() }) @@ -164,13 +169,13 @@ describe('user profile test', () => { } } ] - const { container, findByRole, queryByText } = render( + const { container, findByText, queryByText } = render( ) await waitForElementToBeRemoved(() => queryByText('Loading...')) - await findByRole('heading', { name: /@fake user/i }) + await findByText(/@fake user/i) expect(container).toMatchSnapshot() }) @@ -324,16 +329,358 @@ describe('user profile test', () => { } } ] - const { container, findByRole, queryByText } = render( + const { container, findByText, queryByText } = render( ) await waitForElementToBeRemoved(() => queryByText('Loading...')) - await findByRole('heading', { name: /fakeDiscordUser/i }) + await findByText(/fakeDiscordUser/i) expect(container).toMatchSnapshot() }) + test('should unlink discord from user account', async () => { + expect.assertions(1) + + const session = { + user: { + id: 1, + username: 'fakeusername', + name: 'fake user', + email: 'fake@fakemail.com', + isAdmin: true, + discordUserId: 'fakeDiscordId', + discordUsername: 'fakeDiscordUser', + discordAvatarUrl: 'https://placeimg.com/640/480/any' + }, + submissions: [ + { + id: 1, + status: SubmissionStatus.Passed, + mrUrl: '', + diff: '', + viewCount: 0, + comment: '', + order: 0, + challengeId: 146, + lessonId: 2, + reviewer: { + id: 1, + username: 'fake reviewer' + }, + createdAt: '123', + updatedAt: '123', + comments: null, + user: { + id: 1 + } + }, + { + id: 2, + status: SubmissionStatus.Passed, + mrUrl: '', + diff: '', + viewCount: 0, + comment: '', + order: 0, + challengeId: 145, + lessonId: 2, + reviewer: { + id: 1, + username: 'fake reviewer' + }, + createdAt: '123', + updatedAt: '123', + comments: null, + user: { + id: 1 + } + } + ], + lessonStatus: [ + { + lessonId: 5, + passedAt: new Date(), + starGiven: null, + starsReceived: [ + { + id: 17, + mentorId: 1, + studentId: 6, + lessonId: 5, + student: { + username: 'newbie', + name: 'newbie newbie' + }, + lesson: { + title: 'Foundations of JavaScript', + order: 1 + }, + comment: 'Thanks for your halp!' + } + ] + }, + { + lessonId: 2, + passedAt: new Date(), + starGiven: null, + starsReceived: [ + { + id: 17, + mentorId: 1, + studentId: 6, + lessonId: 2, + student: { + username: 'newbie', + name: 'newbie newbie' + }, + lesson: { + title: 'Variables & Functions', + order: 1 + }, + comment: 'Thanks for your halp!' + } + ] + }, + { + lessonId: 1, + passedAt: new Date(), + starGiven: null, + starsReceived: [ + { + id: 17, + mentorId: 1, + studentId: 6, + lessonId: 2, + student: { + username: 'anonymous', + name: '' + }, + lesson: { + title: 'Variables & Functions', + order: 1 + }, + comment: '' + } + ] + } + ] + } + + const unlinkDiscordMock = { + request: { + query: UNLINK_DISCORD + }, + result: jest.fn(() => ({ + data: { + unlinkDiscord: { + id: 1 + } + } + })) + } + + const mocks = [ + { + request: { query: GET_APP }, + result: { + data: { + session, + lessons: dummyLessonData, + alerts: [] + } + } + }, + { + request: { + query: USER_INFO, + variables: { + username: 'fake user' + } + }, + result: { + data: { + userInfo: session + } + } + }, + unlinkDiscordMock + ] + const { findByText, getByText, getByTestId } = render( + + + + ) + + await waitForElementToBeRemoved(() => getByText('Loading...')) + await findByText(/fakeDiscordUser/i) + + await userEvent.click(getByText(/unlink discord/i)) + await userEvent.click(getByTestId('unlink-discord-btn')) + + await waitFor(() => expect(unlinkDiscordMock.result).toBeCalled()) + }) + + test('should link discord to user account', async () => { + expect.assertions(1) + + const session = { + ...dummySessionData, + submissions: [ + { + id: 1, + status: SubmissionStatus.Passed, + mrUrl: '', + diff: '', + viewCount: 0, + comment: '', + order: 0, + challengeId: 146, + lessonId: 2, + reviewer: { + id: 1, + username: 'fake reviewer' + }, + createdAt: '123', + updatedAt: '123', + comments: null, + user: { + id: 1 + } + }, + { + id: 2, + status: SubmissionStatus.Passed, + mrUrl: '', + diff: '', + viewCount: 0, + comment: '', + order: 0, + challengeId: 145, + lessonId: 2, + reviewer: { + id: 1, + username: 'fake reviewer' + }, + createdAt: '123', + updatedAt: '123', + comments: null, + user: { + id: 1 + } + } + ], + lessonStatus: [ + { + lessonId: 5, + passedAt: new Date(), + starGiven: null, + starsReceived: [ + { + id: 17, + mentorId: 1, + studentId: 6, + lessonId: 5, + student: { + username: 'newbie', + name: 'newbie newbie' + }, + lesson: { + title: 'Foundations of JavaScript', + order: 1 + }, + comment: 'Thanks for your halp!' + } + ] + }, + { + lessonId: 2, + passedAt: new Date(), + starGiven: null, + starsReceived: [ + { + id: 17, + mentorId: 1, + studentId: 6, + lessonId: 2, + student: { + username: 'newbie', + name: 'newbie newbie' + }, + lesson: { + title: 'Variables & Functions', + order: 1 + }, + comment: 'Thanks for your halp!' + } + ] + }, + { + lessonId: 1, + passedAt: new Date(), + starGiven: null, + starsReceived: [ + { + id: 17, + mentorId: 1, + studentId: 6, + lessonId: 2, + student: { + username: 'anonymous', + name: '' + }, + lesson: { + title: 'Variables & Functions', + order: 1 + }, + comment: '' + } + ] + } + ] + } + + const mocks = [ + { + request: { query: GET_APP }, + result: { + data: { + session, + lessons: dummyLessonData, + alerts: [] + } + } + }, + { + request: { + query: USER_INFO, + variables: { + username: 'fake user' + } + }, + result: { + data: { + userInfo: session + } + } + } + ] + + const { findByText, getByText, getByTestId } = render( + + + + ) + + await waitForElementToBeRemoved(() => getByText('Loading...')) + await findByText(/@fake user/i) + + await userEvent.click(getByText(/connect to discord/i)) + await userEvent.click(getByTestId('connect-discord-btn')) + + expect(signIn).toBeCalled() + }) + test('Should render if no stars received', async () => { const session = { ...dummySessionData, @@ -400,13 +747,13 @@ describe('user profile test', () => { } } ] - const { container, findByRole, queryByText } = render( + const { container, findByText, queryByText } = render( ) await waitForElementToBeRemoved(() => queryByText('Loading...')) - await findByRole('heading', { name: /@fake user/i }) + await findByText(/@fake user/i) expect(container).toMatchSnapshot() }) test('Should render anonymous users', async () => { @@ -447,13 +794,13 @@ describe('user profile test', () => { } } ] - const { container, findByRole, queryByText } = render( + const { container, findByText, queryByText } = render( ) await waitForElementToBeRemoved(() => queryByText('Loading...')) - await findByRole('heading', { name: /@fake user/i }) + await findByText(/@fake user/i) expect(container).toMatchSnapshot() }) test('Should render nulled lessons', async () => { @@ -488,7 +835,7 @@ describe('user profile test', () => { ) await waitForElementToBeRemoved(() => screen.queryByText('Loading...')) - await waitFor(() => screen.findByRole('heading', { name: /@fake user/i })) + await waitFor(() => screen.findByText(/@fake user/i)) expect(screen.getAllByText('NaN%')[0]).toBeVisible() }) test('Should render nulled challenges', async () => { @@ -535,13 +882,13 @@ describe('user profile test', () => { } } ] - const { container, findByRole, queryByText } = render( + const { container, findByText, queryByText } = render( ) await waitForElementToBeRemoved(() => queryByText('Loading...')) - await findByRole('heading', { name: /@fake user/i }) + await findByText(/@fake user/i) expect(container).toMatchSnapshot() }) test('Should render nulled submission lessonIds', async () => { @@ -593,13 +940,13 @@ describe('user profile test', () => { } } ] - const { container, findByRole, queryByText } = render( + const { container, findByText, queryByText } = render( ) await waitForElementToBeRemoved(() => queryByText('Loading...')) - await findByRole('heading', { name: /@fake user/i }) + await findByText(/@fake user/i) expect(container).toMatchSnapshot() }) test('Should return error on error', async () => { diff --git a/components/ProfileImageInfo.tsx b/components/ProfileImageInfo.tsx index 1f2f8e669..08db252e4 100644 --- a/components/ProfileImageInfo.tsx +++ b/components/ProfileImageInfo.tsx @@ -1,45 +1,197 @@ -import React from 'react' +import React, { useState } from 'react' import UserInfoImage from './UserInfoImage' -import { UserInfo } from '../@types/user' import Image from 'next/image' import Link from 'next/link' import styles from '../scss/profileImageInfo.module.scss' +import { UserInfo } from '../@types/user' +import { + Modal, + ModalBody, + ModalFooter, + ModalHeader, + ModalTitle +} from 'react-bootstrap' +import { Button } from './theme/Button' +import { signIn } from 'next-auth/react' +import { Button as BsButton } from 'react-bootstrap' +import { useMutation } from '@apollo/client' +import { UNLINK_DISCORD } from '../graphql/queries/unlinkDiscord' +import { useRouter } from 'next/router' + +type DiscordModalProps = { + show: boolean + handleClose: () => void + handleOnClick: () => void + title: string + Body: () => JSX.Element + btnText: string + btnVariant: string + textId: string +} + +const DiscordModal = ({ + show, + handleClose, + handleOnClick, + title, + Body, + btnText, + btnVariant, + textId +}: DiscordModalProps) => { + return ( + + + {title} + + +

+ +

+
+ + + Close + + + {btnText} + + +
+ ) +} + type ProfileImageInfoProps = { user: UserInfo } const ProfileImageInfo: React.FC = ({ user }) => { + const [unlinkDiscord] = useMutation(UNLINK_DISCORD) + const router = useRouter() + + const [show, setShow] = useState(false) + + const handleClose = () => setShow(false) + const handleShow = () => setShow(true) + + const handleConnectClick = () => { + signIn('discord', { callbackUrl: '/discord/success' }) + handleShow() + } + + const handleUnlinkClick = async () => { + try { + handleClose() + await unlinkDiscord() + router.reload() + } catch (e) { + // Calling this function make DiscordBar in storyshots fail with: + // Warning: An update to DiscordBar inside a test was not wrapped in act(...) + // Track it on https://github.com/garageScript/c0d3-app/issues/2257 + // Sentry.captureException(e) + } + } + + const UnlinkDiscordBody = () => ( +

+ You are about to unlink your Discord account. Please be aware that + disconnecting your Discord account will degrade your learning experience + in c0d3 as we use Discord as our community platform +

+ ) + + const ConnectToDiscordBody = () => ( + <> +

+ Please connect your Discord account, or create one if you don't + already have one. Our students and mentors use Discord to communicate + and help each other, give you feedback on your challenges, and sometimes + do virtual hangouts. +

+

+ If you don't want to connect to Discord, then you won't get + any value out of creating an account on C0D3.com. Feel free to browse + our lessons and study on your own! +

+

+ Also, if you would like to share your reasons for not using Discord, + we'd love to hear about it! Your thoughts will help us make more + informed decisions about our community platform and if the reasons are + compelling enough, may inspire us to switch to an alternative. +

+ + ) + return (
-

+

{`${user.firstName} ${user.lastName}`} -

+
-

{'@' + user.username}

+ {`@${user.username}`}
-
+
{user.discordUserId ? ( <> - + + + + {user.discordUsername} + + +
+ + + + ) : ( + <> + + -
- - {user.discordUsername} - -
- ) : null} + )}
) diff --git a/stories/profile/ProfileImageInfo.stories.tsx b/stories/profile/ProfileImageInfo.stories.tsx index 6cc1de371..c5c66aa95 100644 --- a/stories/profile/ProfileImageInfo.stories.tsx +++ b/stories/profile/ProfileImageInfo.stories.tsx @@ -1,3 +1,4 @@ +import { MockedProvider } from '@apollo/client/testing' import * as React from 'react' import ProfileImageInfo from '../../components/ProfileImageInfo' @@ -16,5 +17,13 @@ const user = { } export const _ProfileImageInfo: React.FC = () => ( - + + + +) + +export const _ProfileImageInfoWithConnect: React.FC = () => ( + + + )