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"
>
{
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}
+
+
+
+
+ Unlink Discord
+
+
+ >
+ ) : (
+ <>
+
+ Connect to Discord
+
+
-
-
- {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 = () => (
+
+
+
)