diff --git a/.eslintrc.js b/.eslintrc.js index 3ffa0d73..f4507d7e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,7 +1,7 @@ module.exports = { - plugins: ['unicorn', 'cypress'], + plugins: ['unicorn'], env: { - 'cypress/globals': true, + jest: true, }, extends: ['next', 'next/core-web-vitals', 'semistandard', 'prettier'], rules: { @@ -16,11 +16,5 @@ module.exports = { 'react-hooks/rules-of-hooks': 'warn', 'react/jsx-no-comment-textnodes': 'warn', 'react/no-children-prop': 'warn', - 'cypress/no-assigning-return-values': 'error', - 'cypress/no-unnecessary-waiting': 'error', - 'cypress/assertion-before-screenshot': 'warn', - 'cypress/no-force': 'warn', - 'cypress/no-async-tests': 'error', - 'cypress/no-pause': 'error', }, } diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a9833331..e45b39e7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,6 +6,7 @@ on: jobs: build-storybook: + needs: [format, lint] runs-on: ubuntu-latest steps: - name: Checkout diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml index 657bcae6..4b657943 100644 --- a/.github/workflows/patch.yml +++ b/.github/workflows/patch.yml @@ -13,7 +13,7 @@ jobs: steps: - uses: actions/checkout@v2 with: - token: ${{ secrets.GH_ORG_PAT }} + token: ${{ secrets.GITHUB_TOKEN }} - uses: actions/setup-node@v2 with: node-version: '16.x' diff --git a/.gitignore b/.gitignore index c8ea5469..f0d8ac02 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,5 @@ build-storybook.log .env .env.* !.env.example -data \ No newline at end of file +data +public/videos/ diff --git a/.storybook/manager.js b/.storybook/manager.js new file mode 100644 index 00000000..baf80b25 --- /dev/null +++ b/.storybook/manager.js @@ -0,0 +1,6 @@ +import { addons } from '@storybook/manager-api'; +import theme from './theme'; + +addons.setConfig({ + theme: theme, +}); diff --git a/.storybook/theme.js b/.storybook/theme.js new file mode 100644 index 00000000..eb437465 --- /dev/null +++ b/.storybook/theme.js @@ -0,0 +1,9 @@ +import { create } from '@storybook/theming/create' + +export default create({ + base: 'light', + brandTitle: 'Glance UI', + brandUrl: 'https://odu-emse.github.io/Glance-Frontend', + brandImage: 'https://raw.githubusercontent.com/odu-emse/Glance-Frontend/dfcceb573bbd40592e48d87f660489c66866838f/public/images/GLANCE_1.png', + brandTarget: '_self', +}) \ No newline at end of file diff --git a/README.md b/README.md index 457f7c4e..209cfab5 100644 --- a/README.md +++ b/README.md @@ -1,110 +1,35 @@ -# EMSE - Asynchronous Learning Management Platform | UI +

+ GLANCE. Education at a glance. +

-Hello! +--- -This repository contains all the files that are required for the operation of the AMLP user interface. The UI is a web application that facilitates interactions with the API. The UI is built using ReactJS and is ran in a Docker container. +## Setting up your enviorment -This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). +If you are tasked with working on the Glance project, you can follow these steps to setup your development enviorment. -## Pre-requisites +1. This project is built on **NodeJS v16** so you will need to make sure your computer has this version installed and setup. https://nodejs.org/en/blog/release/v16.16.0 +2. Clone the repository to your computer and then promptly run `npm install` to build the dependancies. +3. Create the `.env` file using the provided `.env.example` as reference. You must provide all of the settings. +4. Run `npm run dev` to start the testing server. Otherwise run `npm run storybook` to run the Storybook pane. -- Docker - > Docker is an open platform for developing, shipping, and running applications. Docker enables you to separate your applications from your infrastructure so you can deliver software quickly. With Docker, you can manage your infrastructure in the same ways you manage your applications. By taking advantage of Docker’s methodologies for shipping, testing, and deploying code quickly, you can significantly reduce the delay between writing code and running it in production. - - [Install Docker for your OS](https://docs.docker.com/desktop/) -- make - > Make is a tool which controls the generation of executables and other non-source files of a program from the program's source files. Make gets its knowledge of how to build your program from a file called the makefile, which lists each of the non-source files and how to compute it from other files. When you write a program, you should write a makefile for it, so that it is possible to use Make to build and install the program. - - [Install GNU win32 on Windows](http://gnuwin32.sourceforge.net +--- -## Environmental Variables +## Contributing to the project -After cloning the repository, create a .env file with the appropriate variables that you received from your supervisor or through documentation. This file will contain the necessary variables like, our JWT configuration and our edge functions' URL. +All of our tickets are managed via [YouTrack](https://emse.myjetbrains.com/youtrack/agiles/120-2/current). -```shell -$ cd emsePortal && touch .env -``` +When creating a new branch it should be named the ticket number prefixed with the word ALMP. Ex. ALMP-123, ALMP-32, ALMP-345 -## Caveats when working on UI +Pull requests should describe the changes which have been made in the title and description. -To run the UI, first please make sure you have all the necessary Pre-Requisite are installed. Without these, the application will not be able to run or compile. +All new work is to be commited to the `development` branch and **NOT** `main`. -Second, you should verify if you are working across the entire stack (need to modify both the UI and the API to complete your issue), or is it specific for only the UI. If the later, you should only run commands that have `dev` in the name. If you require to work across the entire stack, please follow the set-up steps in the API's README.md file, and use all the `local` commands. +Please keep your branches up to date with the upstream branch. -As a short hand explanation, `local` commands expect you to have the API running on your machine, while `dev` commands reach out to our staging server API. +--- -## Common Commands +## Style Guide -### Start container - -To start up the application using our staging API, use the following command: - -```shell -$ make up-dev -``` - -If you want to run the application using your local instance of the API, use the following command: - -```shell -$ make up-local -``` - -### Build image - -While currently there isn't a huge difference in what these two commands do, they are here for future scaling support and to make it easier to switch between the two. In short, if you are using the staging API, you should use the `make build-dev` command, otherwise, use the `make build-local` command. Stick to either one throughout your entire development process, to avoid duplicate images and containers being built. - -```shell -$ make build-dev -``` - -```shell -$ make build-local -``` - -### Remove image - -```shell -$ make rm-dev -``` - -```shell -$ make rm-local -``` - -### Enter container - -This command is useful when you want to look into the Docker container and it's current files that are shared across your own OS. - -```shell -$ make enter-dev -``` - -```shell -$ make enter-local -``` - -## Helper Commands - -### Clean system - -This command should only be used if you know what you are doing. It will remove all images, containers, cached files, and other files that are not needed for Docker's basic operation. - -```shell -$ make prune -``` - -### List containers - -```shell -$ make rncn -``` - -### List images - -```shell -$ make img -``` - -### Stop all containers - -```shell -$ make down -``` +The front end is disctated by the style guide. You can access this document here: +[Glance EMSE Style Guide](https://www.figma.com/file/vTRSf0PF69Gc6w3VC6UKnJ/Style-Guide?node-id=0%3A1&t=Wjq1q3ai1KiWNRsh-1) diff --git a/components/common/accordion/accordion.stories.tsx b/components/common/accordion/accordion.stories.tsx deleted file mode 100644 index c9e27d30..00000000 --- a/components/common/accordion/accordion.stories.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react' -import * as React from 'react' -import { Accordion } from './accordion' - -export default { - title: 'Common/Accordion', - component: Accordion, - argTypes: { - title: { control: 'text' }, - lessons: { control: 'LessonLinkProps[]' }, - }, -} as ComponentMeta - -const Template: ComponentStory = (args) => ( - -) - -export const Topic1: ComponentStory = Template.bind({}) -Topic1.storyName = 'Default' -Topic1.args = { - title: 'Topic 1', - lessons: [ - { - label: 'Module1', - url: 'lesonlink/module-1', - checked: true, - }, - { - label: 'Module2', - url: 'lessonlink/module-2', - checked: false, - }, - { - label: 'Module3', - url: 'lessonlink/module-3', - checked: true, - }, - ], -} - -export const Topic2: ComponentStory = Template.bind({}) -Topic2.storyName = 'More elements' -Topic2.args = { - title: 'Topic 2', - lessons: [ - { - label: 'Module 1', - url: 'lesonlink/module-1', - }, - { - label: 'Module 2', - url: 'lessonlink/module-2', - }, - { - label: 'Module 3', - url: 'lessonlink/module-3', - }, - { - label: 'Module 4', - url: 'lessonlink/module-4', - }, - { - label: 'Module 5', - url: 'lessonlink/module-5', - }, - { - label: 'Module 6', - url: 'lessonlink/module-6', - }, - { - label: 'Module 7', - url: 'lessonlink/module-7', - }, - { - label: 'Module 8', - url: 'lessonlink/module-8', - }, - ], -} -export const Topic3: ComponentStory = Template.bind({}) -Topic3.storyName = 'All completed' -Topic3.args = { - title: 'Topic 3', - lessons: [ - { - label: 'Module1', - url: 'lesonlink/module-1', - checked: true, - }, - { - label: 'Module2', - url: 'lessonlink/module-2', - checked: true, - }, - { - label: 'Module3', - url: 'lessonlink/module-3', - checked: true, - }, - ], -} diff --git a/components/common/accordion/accordion.tsx b/components/common/accordion/accordion.tsx deleted file mode 100644 index eb00236c..00000000 --- a/components/common/accordion/accordion.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import * as React from 'react' - -import { LessonLink } from '../forms/inputs/lesson_link/lesson_link' -import type { LessonLinkProps } from '../forms/inputs/lesson_link/lesson_link' - -export const Accordion: React.FC = ({ - title = 'Topic 1', - lessons = [ - { - label: 'Module1', - url: 'lesonlink/module-1', - checked: true, - }, - { - label: 'Module2', - url: 'lessonlink/module-2', - checked: false, - }, - { - label: 'Module3', - url: 'lessonlink/module-3', - checked: true, - }, - ], -}): React.ReactElement => { - return ( -
-
- -
-
-
-

- {lessons?.length && - lessons.map((lesson, lessonIndex) => ( - - ))} -

-
-
-
- ) -} - -export type AccordionProps = { - /** - * A descriptive label for the title - */ - title: string - /** - * Utilizing Atom Element "Lesson link" properties in this component - */ - lessons: LessonLinkProps[] - /** - * String that has been used here are dynamically used from the Lessonlink Component - */ -} diff --git a/components/common/assignment/assignment_panel.tsx b/components/common/assignment/assignment_panel.tsx index 8d55071a..223a898a 100644 --- a/components/common/assignment/assignment_panel.tsx +++ b/components/common/assignment/assignment_panel.tsx @@ -59,7 +59,7 @@ export const AssignmentPanel = ({ ? moduleInformation.software.map( (software, sfwIndex) => (
  • {software}
  • - ) + ), ) : null} @@ -89,7 +89,7 @@ export const AssignmentPanel = ({ {mod.moduleTitle} -{' '} {mod.assignmentTitle} - ) + ), ) : null} diff --git a/components/common/button/button.stories.tsx b/components/common/button/button.stories.tsx index 7bd83dde..78724ed1 100644 --- a/components/common/button/button.stories.tsx +++ b/components/common/button/button.stories.tsx @@ -1,26 +1,24 @@ import React from 'react' -import { ComponentStory, ComponentMeta } from '@storybook/react' +import { ComponentStory } from '@storybook/react' import { Button } from './button' export default { - title: 'Common/Button', + title: 'Common/Buttons/Button', component: Button, - argTypes: { - backgroundColor: { control: 'color' }, - }, -} as ComponentMeta +} const Template: ComponentStory = (args) => + ) } @@ -64,19 +31,11 @@ type ButtonProps = { /** * A boolean that determines whether the button is the principal call/action on the page */ - variant?: 'primary' | 'secondary' | 'transparent' | 'white' - /** - * An enum that determines the shape of the button - */ - shape?: 'regular' | 'pill' - /** - * A boolean that determines whether the button is representing a loading state - */ - loading?: boolean + variant?: 'primary' | 'secondary' /** * An enum that defines the button's size */ - size?: 'small' | 'base' | 'large' | 1 + size: 'small' | 'medium' | 'large' /** * An enum that defines the button's type */ @@ -103,19 +62,11 @@ Button.propTypes = { /** * Is this the principal call to action on the page? */ - variant: PropTypes.oneOf(['primary', 'secondary', 'transparent']), - /** - * Is this the principal call to action on the page? - */ - shape: PropTypes.oneOf(['regular', 'pill']), - /** - * Is the button representing a loading state? - */ - loading: PropTypes.bool, + variant: PropTypes.oneOf(['primary', 'secondary']), /** * How large should the button be? */ - size: PropTypes.oneOf(['small', 'base', 'large', 1]), + size: PropTypes.oneOf(['small', 'medium', 'large']), /** * Button contents */ @@ -135,18 +86,12 @@ Button.propTypes = { /** * What type of button is this? */ - type: PropTypes.oneOf(['button', 'submit', 'reset']).isRequired, - /** - * Indicates the className - based on selection of a property named 'size', className is atlered - */ - className: PropTypes.string, + //type: PropTypes.oneOf(['button', 'submit', 'reset']).isRequired, } Button.defaultProps = { variant: 'primary', - shape: 'regular', - loading: false, - size: 'base', + size: 'medium', onClick: undefined, children: 'Click Here', disabled: false, diff --git a/components/common/charts/radar/radar.stories.tsx b/components/common/charts/radar/radar.stories.tsx deleted file mode 100644 index d705d837..00000000 --- a/components/common/charts/radar/radar.stories.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import * as React from 'react' -import { Radar } from 'recharts' -import { RadarComponent, RadarProps } from './radar' -import { ComponentMeta, ComponentStory } from '@storybook/react' - -export default { - title: 'Common/Charts/Radar Chart', - component: RadarComponent, - args: { - legend: true, - width: 600, - height: 350, - aspectLock: false, - }, - argTypes: { - width: { - control: { - type: 'range', - min: 250, - max: 1135, - }, - }, - height: { - control: { - type: 'range', - min: 250, - max: 1135, - }, - }, - fillColor: { - control: { - type: 'color', - defaultValue: '#e74d3c', - }, - }, - strokeColor: { - control: { - type: 'color', - defaultValue: '#e74d3c', - }, - }, - fillOpacity: { - control: { - type: 'range', - min: 0, - max: 1, - step: 0.1, - }, - }, - children: { - control: false, - }, - data: { - control: false, - }, - }, -} as ComponentMeta - -const Template: ComponentStory = (args: RadarProps) => { - return ( -
    - -
    - ) -} - -export const SingleChildrenRadar = Template.bind({}) -SingleChildrenRadar.args = { - dataKey: 'subject', - fillOpacity: 0.5, - data: [ - { - subject: 'ENMA 600', - A: 50, - }, - { - subject: 'ENMA 601', - A: 48, - }, - { - subject: 'ENMA 603', - A: 52, - }, - { - subject: 'ENMA 604', - A: 99, - }, - { - subject: 'ENMA 614', - A: 62, - }, - { - subject: 'ENMA 715', - A: 25, - }, - ], - fillColor: '#e74d3c', - strokeColor: '#e74d3c', - children: , -} -export const MultipleChildrenRadar = Template.bind({}) -/** - * This idiom has multiple children - */ -MultipleChildrenRadar.args = { - ...SingleChildrenRadar.args, - data: [ - { - subject: 'ENMA 600', - A: 50, - B: 98, - C: 150, - fullMark: 150, - }, - { - subject: 'ENMA 601', - A: 48, - B: 87, - C: 130, - fullMark: 150, - }, - { - subject: 'ENMA 603', - A: 52, - B: 71, - C: 120, - fullMark: 150, - }, - { - subject: 'ENMA 604', - A: 99, - B: 120, - C: 150, - fullMark: 150, - }, - { - subject: 'ENMA 614', - A: 62, - B: 70, - C: 120, - fullMark: 150, - }, - { - subject: 'ENMA 715', - A: 25, - B: 55, - C: 130, - fullMark: 150, - }, - ], - children: ( - <> - - - - - ), -} diff --git a/components/common/charts/radar/radar.tsx b/components/common/charts/radar/radar.tsx deleted file mode 100644 index e2a1ac3b..00000000 --- a/components/common/charts/radar/radar.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import * as React from 'react' -import { - Legend, - PolarAngleAxis, - PolarGrid, - PolarRadiusAxis, - RadarChart, - ResponsiveContainer, - Tooltip, -} from 'recharts' - -export const RadarComponent: React.FC = ({ - data, - width, - height, - tooltip, - legend, - aspectLock, - outerRadius, - centerYCoor, - centerXCoor, - children, - fillColor, - strokeColor, - fillOpacity, - dataKey, - radiusAxis, - radiusAxisAngle, -}) => { - const matrixChildren: any[] = [] - - if ( - typeof children !== 'string' && - typeof children !== 'number' && - children && - !children && - children.props.children?.length > 1 - ) { - React.Children.map(children.props.children, (child) => { - matrixChildren.push(child) - }) - } - return ( - - - - - {radiusAxis && ( - - )} - {matrixChildren?.length > 0 - ? matrixChildren.map((child, childIndex) => { - return React.cloneElement(child, { - key: childIndex, - fill: fillColor, - stroke: strokeColor, - fillOpacity, - }) - }) - : React.cloneElement(children, { - fill: fillColor, - fillOpacity, - stroke: strokeColor, - })} - {tooltip && } - {legend && ( - - )} - - - ) -} - -export type RadarProps = { - /** - * The data to be displayed in the chart. The data should be in the form of an array of objects, each object should have a `dataKey` property which is exactly referenced in the `dataKey` prop being passed in. - * @type Array - */ - data: RadarPoint[] - /** - * The width of the chart in pixel value. - * @type number - */ - width: number - /** - * The height of the chart in pixel value, unless the aspect ratio is locked, in which case it is treated as a percentage of the parent element. - * @type number - */ - height: number - /** - * If true, the idiom will have the tooltip hover interaction enabled. - * @default false - */ - tooltip?: boolean - /** - * If true, the idiom will include a legend - * @default false - */ - legend?: boolean - /** - * If true, the aspect ratio of the chart will be locked. The idiom will also treat the height property as a percentage, rather than a px value. - * @default false - */ - aspectLock?: boolean - outerRadius?: number - /** - * The Center Y coordinate of the radar chart in percentage - */ - centerYCoor?: number - /** - * The Center X coordinate of the radar chart in percentage. - */ - centerXCoor?: number - /** - * The `` component that you wish to render. - */ - children: React.ReactNode | React.ReactNode[] | any - /** - * The fill color of the chart. - */ - fillColor: string - /** - * The stroke color of the idiom. - */ - strokeColor: string - /** - * The property from the data object that will be used to create the corner of the chart. - * @default subject - */ - dataKey: string - /** - * The opacity of the fill color that the idiom will possess. - * @default 0.5 - */ - fillOpacity: number - /** - * If true, the radius axis will be displayed. - * @default true - */ - radiusAxis?: boolean - /** - * The radius axis' angle, if visible. - * @default 90 - */ - radiusAxisAngle?: number -} - -export type RadarPoint = { - cx?: number - cy?: number - angle?: number - radius?: number - value?: number - payload?: any - name?: string - subject?: string - A?: number - B?: number - C?: number - D?: number - fullMark?: number -} diff --git a/components/common/chat/bubble_message/bubble_message.tsx b/components/common/chat/bubble_message/bubble_message.tsx index 952a0955..3158f37a 100644 --- a/components/common/chat/bubble_message/bubble_message.tsx +++ b/components/common/chat/bubble_message/bubble_message.tsx @@ -16,7 +16,7 @@ export const BubbleMessage = ({ message.map( ( { message, user, timestamp }, - index + index, ) => ( <>
  • // {moment( - timestamp + timestamp, ).format('hh:mm A')} @@ -70,7 +70,7 @@ export const BubbleMessage = ({
  • )} - ) + ), )} diff --git a/components/common/community/social_card/social_card.stories.tsx b/components/common/community/social_card/social_card.stories.tsx deleted file mode 100644 index ca1781d5..00000000 --- a/components/common/community/social_card/social_card.stories.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import * as React from 'react' -import { SocialCard } from './social_card' -import type { ComponentMeta, ComponentStory } from '@storybook/react' - -export default { - title: 'Common/Community/Social Card', - component: SocialCard, -} as ComponentMeta - -const Template: ComponentStory = (args) => ( - -) -export const Primary: ComponentStory = Template.bind({}) -Primary.args = { - timestamp: 1664376815, - content: - 'AWESOME hiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii', - likes: 20, - comments: 50, - user: { - firstName: 'Avantika', - lastName: 'Mittapally', - role: 'Advisor', - image: 'https://www.creative-tim.com/learning-lab/tailwind-starter-kit/img/team-4-470x470.png', - title: 'Chair of Department', - office: 'ESB 2101', - department: 'Engineering Management & Systems Engineering', - }, -} -export const Secondary: ComponentStory = Template.bind({}) -Secondary.storyName = 'Longer Content' -Secondary.args = { - ...Primary.args, - content: - 'Lorem, ipsum dolor sit amet consectetur adipisicing elit. Quae error iure officiis exercitationem, commodi ab reiciendis eum ex veritatis placeat amet architecto itaque cumque blanditiis numquam repellat, necessitatibus natus nihil! Lorem, ipsum dolor sit amet consectetur adipisicing elit. Quae error iure officiis exercitationem, commodi ab reiciendis eum ex veritatis placeat amet architecto itaque cumque blanditiis numquam repellat, necessitatibus natus nihil!', -} -export const TA = Template.bind({}) -TA.storyName = 'TA user' -TA.description = 'TA user here' -TA.args = { - ...Secondary.args, - user: { - ...Secondary.args.user, - role: 'TA', - }, -} -export const Prof = Template.bind({}) -Prof.storyName = 'Professor user' -Prof.args = { - ...Secondary.args, - user: { - ...Secondary.args.user, - role: 'Prof', - }, -} diff --git a/components/common/community/social_card/social_card.tsx b/components/common/community/social_card/social_card.tsx deleted file mode 100644 index f7ded962..00000000 --- a/components/common/community/social_card/social_card.tsx +++ /dev/null @@ -1,213 +0,0 @@ -import moment from 'moment' -import * as React from 'react' -import { AiFillLike } from 'react-icons/ai' -import { - FaBookmark, - FaCommentDots, - FaGraduationCap, - FaRegSmile, - FaStar, -} from 'react-icons/fa' -import { MdAttachFile } from 'react-icons/md' -export const SocialCard: React.FC = ({ - timestamp, - content, - likes, - comments, - user, -}): React.ReactElement => { - const [isClicked, setIsClicked] = React.useState(false) - - return ( - // removed items-center justify-center min-h-screen for community layout -
    - {/* {modify md:w-9/12 to md:w-screen for better fit community page} */} -
    -
    -
    -
    -
    - user profile image setIsClicked(!isClicked)} - /> - {isClicked && ( - - )} -
    -
    - -
    -
    - - {user.role} - {' '} - - {user.firstName} {user.lastName} -
    -
    - {moment.unix(timestamp).fromNow()} |{' '} - {user.department} - {user.office} -
    -
    -
    - -
    - {user.role === 'Prof' && ( - - )} - - {user.role === 'Advisor' && ( - - )} - - {user.role === 'TA' && ( - - )} -
    -
    - -
    -
    - {content} -
    -
    -
    -
    -
    -
    - - {likes} Likes -
    -
    - - {comments} comments -
    -
    -
    -
    - {/* Comment and attachments */} -
    -
    - - -
    - - {/* Comment icons */} -
    - - -
    -
    -
    -
    - ) -} - -export type SocialCardProps = { - /** - * Assigning the below declared type "UserAccountprops" to user - */ - user: UserAccountProps - /** - * A descriptive label to display the timestamp of the user's comment - */ - timestamp: number - /** - * A descriptive label to display the content of the user's comment - */ - content: string - /** - * A descriptive label to display the number of likes of the user's comment - */ - likes: number - /** - * A descriptive label to display the number of comments of the user's comment - */ - comments: number -} - -export type UserAccountProps = { - /** - * A descriptive label to display the user's Last Name - */ - lastName: string - /** - * A descriptive label to display the user's First Name - */ - firstName: string - /** - * A descriptive label to display the title - */ - title: string - /** - * A descriptive label to display the name of the office user belongs - */ - office: string - /** - * A descriptive label to display the name of the department user belongs - */ - department: string - /** - * A descriptive label to display the user's role(advisor/TA) - */ - role: string - /** - * A descriptive label to display the user's profile picture - */ - image: string -} - -// LessonLink.defaultProps ={ -// url : "../components", -// checked : false, -// label: "Introduction" -// } diff --git a/components/common/community/threads/comments/comments_hierarchy.stories.tsx b/components/common/community/threads/comments/comments_hierarchy.stories.tsx index 94e5118b..e2c4f242 100644 --- a/components/common/community/threads/comments/comments_hierarchy.stories.tsx +++ b/components/common/community/threads/comments/comments_hierarchy.stories.tsx @@ -11,7 +11,7 @@ export default { } as ComponentMeta const Template: ComponentStory = ( - args: CommentsHierarchyProps + args: CommentsHierarchyProps, ) => { const { data, error } = useSWR( { @@ -40,7 +40,7 @@ const Template: ComponentStory = ( } `, }, - gqlFetcher + gqlFetcher, ) if (!data) return

    Loading...

    diff --git a/components/common/community/threads/comments/comments_hierarchy.tsx b/components/common/community/threads/comments/comments_hierarchy.tsx index b0016740..d81c4d82 100644 --- a/components/common/community/threads/comments/comments_hierarchy.tsx +++ b/components/common/community/threads/comments/comments_hierarchy.tsx @@ -1,6 +1,7 @@ import * as React from 'react' import { Thread } from '../thread/thread' import { ThreadType } from '../../../../../types' +import { ReactNode } from 'react' /** * This function generates an array of DOM thread components based on a parent comment tree. @@ -9,22 +10,24 @@ import { ThreadType } from '../../../../../types' * @param {Object} parentComment - The parent comment tree. * @returns {Array} An array of DOM thread components. */ -const commentGen = (parentComment) => { +const commentGen = (parentComment: ThreadType): Array | [] => { if (parentComment.comments === undefined) return [] const threads = [] for (const comment of parentComment.comments) { const subThreads = commentGen(comment) threads.push( -
    +
    {subThreads} -
    +
    , ) } return threads diff --git a/components/common/community/threads/markdown/markdown_container.tsx b/components/common/community/threads/markdown/markdown_container.tsx new file mode 100644 index 00000000..d04a0e8a --- /dev/null +++ b/components/common/community/threads/markdown/markdown_container.tsx @@ -0,0 +1,18 @@ +import React from 'react' +import { markdownConfig } from '@/utils/markdown.config' +import remarkGfm from 'remark-gfm' +import remarkMath from 'remark-math' +import rehypeKatex from 'rehype-katex' +import ReactMarkdown from 'react-markdown' + +const MarkdownContainer = ({ children }) => ( + + {children} + +) + +export default MarkdownContainer diff --git a/components/common/community/threads/thread/thread.stories.tsx b/components/common/community/threads/thread/thread.stories.tsx index 64addeee..e10bf85c 100644 --- a/components/common/community/threads/thread/thread.stories.tsx +++ b/components/common/community/threads/thread/thread.stories.tsx @@ -70,10 +70,10 @@ const useThreadData = (threadId: string, usrprofileId: string | number) => { id: threadId, }, }, - gqlFetcher + gqlFetcher, ) const isUpvoted = data?.thread[0].upvotes.some( - (upvote) => upvote.id === usrprofileId + (upvote) => upvote.id === usrprofileId, ) return { data, error, isUpvoted } diff --git a/components/common/community/threads/thread/thread.tsx b/components/common/community/threads/thread/thread.tsx index 81ea386c..873d8ed5 100644 --- a/components/common/community/threads/thread/thread.tsx +++ b/components/common/community/threads/thread/thread.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useState, useEffect } from 'react' +import React, { useContext, useState, useEffect, createRef } from 'react' import { GoArrowUp, GoCommentDiscussion } from 'react-icons/go' import { TbShare } from 'react-icons/tb' import { IconContext } from 'react-icons' @@ -7,54 +7,44 @@ import GlobalUserContext from '@/contexts/global_user_context' import useSWR from 'swr' import gqlFetcher, { client } from '@/utils/gql_fetcher' import { gql } from 'graphql-request' +import { TextArea } from '@/common/forms/inputs/text_area/text_area' +import { ThreadType } from '../../../../../types' +import MarkdownContainer from '@/common/community/threads/markdown/markdown_container' +import { Button } from '@/components/common/button/button' +import router, { Router } from 'next/router' export const Thread: React.FC = ({ title, body, - // upvotes, id, userProfile, + topics = [], children, isUpvoted: initialIsUpvoted, + upvotesProp = [], commentCount = 0, viewCutOff = false, showAuthor = true, }) => { const [isClicked, setIsClicked] = useState(false) const [isUpvoted, setIsUpvoted] = useState(initialIsUpvoted) + const [addComment, setAddComment] = useState(false) + const [commentBody, setCommentBody] = useState('') + const [upvotes, setUpvotes] = useState(upvotesProp?.length || 0) const { user } = useContext(GlobalUserContext) - const [upvotes, setUpvotes] = useState(0) - const { data } = useSWR( - { - query: gql` - query GetThread($input: ID!) { - thread(input: { id: $input }) { - id - title - body - upvotes { - openID - id - } - } - } - `, - variables: { input: id }, - }, - gqlFetcher - ) + const currentThread = createRef() + + const { mutate } = useSWR({}, gqlFetcher) useEffect(() => { - if (data) { - const initialIsUpvoted = data.thread[0].upvotes.some( - (upvote) => upvote.id === user.id + if (upvotesProp) { + const initialIsUpvoted = upvotesProp.some( + (upvote) => upvote.id === user.id, ) setIsUpvoted(initialIsUpvoted) - setUpvotes(data.thread[0].upvotes.length) + setUpvotes(upvotesProp.length) } - }, [data, user.id]) - - const { mutate } = useSWR({}, gqlFetcher) + }, [user.id, upvotesProp]) const upvoteThread = (threadId) => { mutate(async () => { @@ -68,7 +58,7 @@ export const Thread: React.FC = ({ `, { input: threadId, - } + }, ) }, false) .then(() => { @@ -92,7 +82,7 @@ export const Thread: React.FC = ({ `, { input: threadId, - } + }, ) }, false) .then(() => { @@ -103,6 +93,34 @@ export const Thread: React.FC = ({ console.log(err) }) } + const addCommentToThread = (threadId, commentBody, author) => { + mutate(async () => { + await client.request( + gql` + mutation AddCommentToThread( + $threadID: ID! + $commentBody: String! + $commentAuthor: ID! + ) { + addCommentToThread( + parentThreadID: $threadID + data: { body: $commentBody, author: $commentAuthor } + ) { + id + body + } + } + `, + { + threadID: threadId, + commentBody, + commentAuthor: author, + }, + ) + }, false).catch((err) => { + console.log(err) + }) + } let url: string return ( @@ -110,33 +128,51 @@ export const Thread: React.FC = ({
    {showAuthor && ( -
    +
    user profile image -

    +

    {userProfile.firstName} {userProfile.lastName}

    )} {title &&

    {title}

    } -

    {body.slice(0, 150)}

    + {topics.length > 0 && ( +
    + {topics.map((topic, topicIndex) => { + return ( +

    + {topic} +

    + ) + })} +
    + )} +

    + {body.slice(0, 150)} +

    {viewCutOff && ( - -

    + )} +
    -

    + {addComment && ( +
    + - + icon={false} + /> -
    diff --git a/components/common/community/watched_threads/watched_threads.tsx b/components/common/community/watched_threads/watched_threads.tsx index 5621e65e..63609f44 100644 --- a/components/common/community/watched_threads/watched_threads.tsx +++ b/components/common/community/watched_threads/watched_threads.tsx @@ -1,36 +1,6 @@ import { ThreadType } from '../../../../types' import Link from 'next/link' -export const WatchedThreads = ({ - threads, - title = 'Watched Threads', -}: { - threads: Array - title?: string -}) => { - return ( - <> -

    {title}

    - {threads.map((thread, threadIndex) => ( - - {thread.parentLesson.collection.module.moduleName.length > - 30 - ? `${thread.parentLesson.collection.module.moduleName.substring( - 0, - 30 - )}...` - : thread.parentLesson.collection.module.moduleName} - - ))} - - ) -} - export const WatchedSidebarList = ({ title, threads, diff --git a/components/common/community/watched_threads_sidebar/watched_threads_sidebar.tsx b/components/common/community/watched_threads_sidebar/watched_threads_sidebar.tsx index b537ed82..78aaa28a 100644 --- a/components/common/community/watched_threads_sidebar/watched_threads_sidebar.tsx +++ b/components/common/community/watched_threads_sidebar/watched_threads_sidebar.tsx @@ -12,26 +12,7 @@ const WatchedThreadSidebar: React.FC = ({ handle, children, }) => { - return ( - - ) + return } export default WatchedThreadSidebar diff --git a/components/common/content_types/video/video_player/video_player.tsx b/components/common/content_types/video/video_player/video_player.tsx index c65cbd38..1187ff9a 100644 --- a/components/common/content_types/video/video_player/video_player.tsx +++ b/components/common/content_types/video/video_player/video_player.tsx @@ -41,7 +41,7 @@ export const VideoPlayer: React.FC = ({ const mouseTimeout = useRef() const handleTimeUpdate = ( - event: SyntheticEvent + event: SyntheticEvent, ) => { if (!progressBar.current) return @@ -138,7 +138,7 @@ export const VideoPlayer: React.FC = ({ (card.timestamp / videoPlayer.current!.duration) * 100, 0, - 100 + 100, ) + '%', }} > diff --git a/components/common/forms/inputs/input/Oldinput.stories.tsx b/components/common/forms/inputs/input/Oldinput.stories.tsx new file mode 100644 index 00000000..0c533f6c --- /dev/null +++ b/components/common/forms/inputs/input/Oldinput.stories.tsx @@ -0,0 +1,127 @@ +import * as React from 'react' +import { useState } from 'react' +import { Input, InputProps } from './Oldinput' +import { ComponentMeta, ComponentStory } from '@storybook/react' + +export default { + title: 'Common/Forms/Inputs/Old_Input', + component: Input, + argTypes: { + onChange: { + control: false, + }, + role: { + control: 'text', + }, + description: { + control: false, + }, + }, +} as ComponentMeta + +const Template: ComponentStory = (args: InputProps) => { + const [value] = useState(args.defaultValue ?? '') + const [options, setOptions] = useState([]) + + return ( + { + const isVisable = value.length > 0 + + if (isVisable) { + setOptions((prev) => [ + 'result1', + 'result2', + 'result3', + 'result4', + ]) + } else { + setOptions((prev) => []) + } + }} + options={options} + defaultValue={value} + /> + ) +} + +export const Default = Template.bind({}) +Default.args = { + label: 'Label', + name: 'text-input', + type: 'text', + ariaLabel: 'Text input field', +} +export const Email = Template.bind({}) +Email.args = { + label: 'Email address', + name: 'email', + role: 'input', + type: 'email', +} + +// Email.play = async ({ canvasElement }) => { +// const canvas = within(canvasElement) +// +// const label = canvas.getByText('Email address') +// userEvent.type(canvas.getByRole('input'), 'email@provider.com') +// expect(label.classList.contains('peer-focus:text-blue-600')) +// } + +export const Descriptive = Template.bind({}) +Descriptive.args = { + ...Email.args, + description: ( + <> + For more information on how your data is stored and accessed + throughout the application, please visit our{' '} + + Privacy Policy + + . + + ), +} + +export const Disabled = Template.bind({}) +Disabled.args = { + ...Email.args, + disabled: true, +} +// TODO: Rewrite this test to use cypress + +// Disabled.play = async ({ canvasElement }) => { +// const canvas = within(canvasElement) +// +// const input = canvas.getByRole('input') +// userEvent.type(input, 'email@provider.com') +// expect(input.textContent).toBe('') +// expect(input).toBeDisabled() +// } + +export const ErrorState = Template.bind({}) +ErrorState.args = { + ...Email.args, + error: true, +} + +export const ErrorStateWithDescription = Template.bind({}) +ErrorStateWithDescription.args = { + ...Email.args, + error: true, + description: + 'The error occurred while we were processing your request. Please try again and contact your system administrator if this issue persists.', +} + +export const Search = Template.bind({}) +Search.args = { + label: 'Search', + name: 'floating_search', + role: 'search', + type: 'search', + // options: ['Items', 'Items1'] +} diff --git a/components/common/forms/inputs/input/Oldinput.tsx b/components/common/forms/inputs/input/Oldinput.tsx new file mode 100644 index 00000000..7f5cd560 --- /dev/null +++ b/components/common/forms/inputs/input/Oldinput.tsx @@ -0,0 +1,182 @@ +import * as React from 'react' +import { BiSearch } from 'react-icons/bi' +import { dropdownOption } from '../select/select' + +export const Input = ({ + label, + name, + role, + onChange, + description, + required = false, + type, + ariaLabel, + defaultValue, + disabled = false, + error = false, + options, + placeholder = 'Enter here', + className = '', + icon = false, +}: InputProps) => { + const classes = [ + className, + 'block py-2.5 px-0 w-full text-sm text-gray-900 bg-transparent appearance-none dark:text-white focus:outline-none focus:ring-0 peer', + className.includes('border') ? '' : 'border-0 border-b-2', + error + ? 'border-red-500 dark:border-red-400 focus:border-red-600 dark:focus:border-red-500' + : 'dark:focus:border-blue-500 focus:border-blue-600 dark:border-gray-600 border-gray-300', + disabled ? 'cursor-not-allowed' : '', + ].join(' ') + return ( + <> +
    + onChange(event.target.value)} + disabled={disabled} + /> + {options && + options.map((option, optionIndex) => ( + + ))} + + + {type === 'search' && icon && ( + + )} + {description && ( +

    + {description} +

    + )} +
    + + ) +} + +export type InputProps = { + /** + * The label for the input. This will be the floating element that is displayed above the input. + */ + label: string + /** + * The name of the input. This is used to identify the input when it is submitted. This value is also used by the label element to identify the `htmlFor` attribute. + */ + name: string + /** + * The role attribute on the input allows the browser to gather knowledge about the purpose of the input. This improves accessibility, device adaptation, and cross browser synchronicity. + */ + role?: string + /** + * The on Change event handler for the input. This is used to update the value of the input. + * @param value The value of the input event. + */ + onChange: (e: string) => void + /** + * The description value is used to give users additional information about the either the input or the error that happened. This is used to provide additional context to the user, under the input element. This is just supplementary information, so it's visual hierarchy should not interfere with the input element's. + */ + description?: string | React.ReactNode | React.ReactElement + /** + * The required attribute is used to indicate weather the input element is required or not. + */ + required?: boolean + /** + * The input type determines the way browsers conduct their validation and on a device by device basis it can change the keyboard behavior for the user. + */ + type: + | 'text' + | 'email' + | 'password' + | 'search' + | 'url' + | 'tel' + | 'number' + | 'file' + /** + * The default value of the input. This is used to set the value of the input when the page is first loaded. + */ + defaultValue?: string + /** + * The disabled attribute is used to indicate weather the input element is disabled or not. + */ + disabled?: boolean + /** + * The error value is used to render different styles of the input element based on the current error state that is passed in as a parameter. + */ + error?: boolean + /** + * The aria-label attribute is used to provide a label for the input element. This is used to provide additional context to the user, under the input element. This is just supplementary information, so its visual hierarchy should not interfere with the input element's. + */ + ariaLabel?: string + + options?: dropdownOption[] | string[] + /** + * The placeholder value is used to provide a placeholder for the input element. This is used to provide additional context to the user, under the input element. + */ + placeholder?: string + /** + * The className value is used to provide a custom class name to the input element. + */ + className?: string + /** + * The icon boolean is used to determine weather or not to render the search icon on the input element. + */ + icon?: boolean +} diff --git a/components/common/forms/inputs/input/input.stories.tsx b/components/common/forms/inputs/input/input.stories.tsx index 6c16b8e8..87276a72 100644 --- a/components/common/forms/inputs/input/input.stories.tsx +++ b/components/common/forms/inputs/input/input.stories.tsx @@ -4,7 +4,7 @@ import { Input, InputProps } from './input' import { ComponentMeta, ComponentStory } from '@storybook/react' export default { - title: 'Common/Forms/Inputs/Input', + title: 'Common/Forms/Inputs/Inputs', component: Input, argTypes: { onChange: { @@ -125,3 +125,28 @@ Search.args = { type: 'search', // options: ['Items', 'Items1'] } +export const Radio = Template.bind({}) +Radio.args = { + type: 'radio', + name: 'radio', + content: 'Choice1', +} +export const Checkbox = Template.bind({}) +Checkbox.args = { + type: 'checkbox', + name: 'checkbox', + content: 'Choice1', +} +export const NumericalInput = Template.bind({}) +NumericalInput.args = { + type: 'number', + label: 'Type in your age', + name: 'number', +} +export const RangeSlider = Template.bind({}) +RangeSlider.args = { + type: 'range', + name: 'range', + min: 10, + max: 50, +} diff --git a/components/common/forms/inputs/input/input.tsx b/components/common/forms/inputs/input/input.tsx index 7f2c75b7..cfbe45e8 100644 --- a/components/common/forms/inputs/input/input.tsx +++ b/components/common/forms/inputs/input/input.tsx @@ -1,108 +1,116 @@ -import * as React from 'react' -import { BiSearch } from 'react-icons/bi' -import { dropdownOption } from '../select/select' +import React, { useState } from 'react' export const Input = ({ label, name, role, onChange, - description, required = false, type, - ariaLabel, - defaultValue, disabled = false, + description, error = false, - options, placeholder = 'Enter here', className = '', + length, + content, + min, + max, + value, + checked = false, }: InputProps) => { + const [isChecked, setIsChecked] = useState(checked) + const inputShape = type === 'radio' ? 'rounded-full' : 'rounded-sm' + const handleInputChange = () => { + if (!disabled) setIsChecked(!isChecked) + } + const inputLength = + length === 'short' + ? 'w-1/6' + : length === 'normal' + ? 'w-1/3' + : length === 'long' + ? 'w-1/2' + : 'w-full' const classes = [ className, - 'block py-2.5 px-0 w-full text-sm text-gray-900 bg-transparent appearance-none dark:text-white focus:outline-none focus:ring-0 peer', - className.includes('border') ? '' : 'border-0 border-b-2', - error - ? 'border-red-500 dark:border-red-400 focus:border-red-600 dark:focus:border-red-500' - : 'dark:focus:border-blue-500 focus:border-blue-600 dark:border-gray-600 border-gray-300', - disabled ? 'cursor-not-allowed' : '', + 'block appearance-none focus:outline-none focus:ring-0 peer', + error ? 'border-red-500 focus:border-red-600' : 'border-wgray', + disabled + ? 'cursor-not-allowed' + : 'focus:border-royalblue hover:border-royalblue', + length ? `${inputLength}` : '', + type === 'radio' || type === 'checkbox' ? 'sr-only' : 'border-2', + type === 'range' ? 'h-1 cursor-ew-resize' : '', ].join(' ') + return ( <> -
    +
    + {label && ( +

    + +

    + )} onChange(event.target.value)} disabled={disabled} + min={type === 'range' ? min : undefined} + max={type === 'range' ? max : undefined} + value={type === 'range' ? value : undefined} + onChange={(event) => { + if (type === 'checkbox' || type === 'radio') { + handleInputChange() + } + onChange(event.target.value) + }} + // onChange={handleInputChange} /> - {options && - options.map((option, optionIndex) => ( - - ))} - - - {type === 'search' && ( - - )} + {isChecked && ( +
    + )} +
    + ) : null} + {type === 'radio' || type === 'checkbox' ? ( + + ) : null} {description && (

    - {description} + {error ? `Wrong ${type}` : 'This is a test message'}

    )}
    @@ -148,10 +156,9 @@ export type InputProps = { | 'tel' | 'number' | 'file' - /** - * The default value of the input. This is used to set the value of the input when the page is first loaded. - */ - defaultValue?: string + | 'radio' + | 'checkbox' + | 'range' /** * The disabled attribute is used to indicate weather the input element is disabled or not. */ @@ -164,8 +171,6 @@ export type InputProps = { * The aria-label attribute is used to provide a label for the input element. This is used to provide additional context to the user, under the input element. This is just supplementary information, so its visual hierarchy should not interfere with the input element's. */ ariaLabel?: string - - options?: dropdownOption[] | string[] /** * The placeholder value is used to provide a placeholder for the input element. This is used to provide additional context to the user, under the input element. */ @@ -174,4 +179,28 @@ export type InputProps = { * The className value is used to provide a custom class name to the input element. */ className?: string + /** + * An enum that specifies the length of the input field + */ + length: 'short' | 'normal' | 'long' | 'full' + /** + * Content string for radio input + */ + content?: string + /** + * A boolean determines whether the input box is checked or not + */ + checked?: boolean + /** + * The `min` attribute is used to specify the minimum value for a range slider + */ + min?: number + /** + * The `max` attribute is used to specify the maximum value for a range slider + */ + max?: number + /** + * The `value` attribute is used to define the initial value of a range slider + */ + value?: number } diff --git a/components/common/forms/inputs/text_area/text_area.stories.tsx b/components/common/forms/inputs/text_area/text_area.stories.tsx index 5a207354..c3db2668 100644 --- a/components/common/forms/inputs/text_area/text_area.stories.tsx +++ b/components/common/forms/inputs/text_area/text_area.stories.tsx @@ -40,7 +40,7 @@ Primary.args = { id: 'text-area', role: 'textbox', name: 'text-area', - rows: 1, + rows: 2, placeholder: 'This is a placeholder text for the text area component.', disabled: false, maxLength: 1000, diff --git a/components/common/forms/inputs/text_area/text_area.tsx b/components/common/forms/inputs/text_area/text_area.tsx index 84982dd1..205ce501 100644 --- a/components/common/forms/inputs/text_area/text_area.tsx +++ b/components/common/forms/inputs/text_area/text_area.tsx @@ -1,7 +1,6 @@ import * as React from 'react' -import { useRef } from 'react' +import { useRef, useEffect } from 'react' import useAutosizeTextArea from './use_autosize_text_area' -import { IoSend } from 'react-icons/io5' export const TextArea: React.FC = ({ handle = () => null, @@ -9,7 +8,7 @@ export const TextArea: React.FC = ({ id = 'text-area', role = 'textbox', name = 'text-area', - rows = 1, + rows = 2, placeholder = '', disabled = false, maxLength = 1000, @@ -18,25 +17,27 @@ export const TextArea: React.FC = ({ wrap = 'soft', autofocus = false, label = '', + onChange, defaultValue = '', className = '', + icon = true, }): React.ReactElement => { const textAreaRef = useRef(null) useAutosizeTextArea(textAreaRef.current, value) - const classes = [ className, - 'w-full bg-white placeholder:italic border border-slate-400 shadow-md rounded-md py-2 pl-3 pr-10 focus:outline-2 focus:outline-dashed focus:ring-0 disabled:opacity-50 disabled:cursor-not-allowed', + 'w-full bg-white placeholder:italic border border-2 border-wgray shadow-md focus:outline-none focus:ring-0 peer rounded-sm py-2 pl-3 pr-10 disabled:opacity-50 disabled:cursor-not-allowed', value.length === maxLength - ? 'border-red-400 focus:border-red-500 focus:outline-red-400 focus:ring-red-400' - : ' focus:outline-blue-400', + ? 'border-red-400 focus:border-red-500 hover:border-red-400' + : 'focus:border-royalblue', + !disabled && value.length !== maxLength ? 'hover:border-royalblue' : '', ].join(' ') return (
    -
    - ) -} - -type UserProfileProps = { - user: UserAccountProps - isCurrentUser: boolean -} -type UserAccountProps = { - lastName: string - firstname: string - email: string - image: string - planOfStudy: planOfStudyProps -} -type planOfStudyProps = { - status: string - term: string -}[] diff --git a/components/common/pages/sidebar/hamburger.tsx b/components/common/pages/sidebar/hamburger.tsx deleted file mode 100644 index 8250b3e7..00000000 --- a/components/common/pages/sidebar/hamburger.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import * as React from 'react' - -export const Hamburger = ({ onClick }: HamburgerProps) => { - return ( - - ) -} - -type HamburgerProps = { - onClick: () => void -} diff --git a/components/common/pages/sidebar/leftsidebar.stories.tsx b/components/common/pages/sidebar/leftsidebar.stories.tsx deleted file mode 100644 index 6b0b2c17..00000000 --- a/components/common/pages/sidebar/leftsidebar.stories.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import * as React from 'react' -import { Sidebar } from './sidebar' -import type { ComponentMeta, ComponentStory } from '@storybook/react' - -export default { - title: 'Common/Pages/Layouts/Left Sidebar', - component: Sidebar, -} as ComponentMeta - -const Template: ComponentStory = (args) => { - const [open, setOpen] = React.useState(true) - return ( -
    - -
    - ) -} - -export const Default: ComponentStory = Template.bind({}) -Default.args = { - icon: 'https://www.creative-tim.com/learning-lab/tailwind-starter-kit/img/team-4-470x470.png', -} diff --git a/components/common/pages/sidebar/logo.tsx b/components/common/pages/sidebar/logo.tsx deleted file mode 100644 index e313bb1f..00000000 --- a/components/common/pages/sidebar/logo.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import * as React from 'react' -import { Link } from '../../links/link/link' - -export const Logo = ({ extended }: LogoProps) => { - return ( -
    - - ODU Logo - -
    - ) -} - -type LogoProps = { - extended: boolean -} diff --git a/components/common/pages/sidebar/sidebar.tsx b/components/common/pages/sidebar/sidebar.tsx deleted file mode 100644 index bddd6814..00000000 --- a/components/common/pages/sidebar/sidebar.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import * as React from 'react' -import { SidebarItem } from './sidebar_item/sidebar_item' -import { IoIosArrowBack, IoIosArrowForward } from 'react-icons/io' - -export type SidebarProps = { - userSession: any - isLoading: boolean - icon: string - open: boolean - handle: (open: boolean) => void -} - -export const Sidebar: React.FC = ({ - userSession, - isLoading, - open, - handle, -}) => { - return ( -
    - - -
    -
    - ) -} diff --git a/components/common/pages/sidebar/sidebar_item/sidebar_item.tsx b/components/common/pages/sidebar/sidebar_item/sidebar_item.tsx deleted file mode 100644 index f80c1329..00000000 --- a/components/common/pages/sidebar/sidebar_item/sidebar_item.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react' -import Link from 'next/link' - -export type SidebarItemProps = { - value: string - href: string - icon: string - collapsed: boolean -} - -export const SidebarItem: React.FC = ({ - value, - href, - icon, - collapsed, -}) => { - return ( - -
    - - {!collapsed ?

    {value}

    : null} -
    - - ) -} diff --git a/components/common/pages/sidebar/sidebar_lessons/sidebar_lessons.tsx b/components/common/pages/sidebar/sidebar_lessons/sidebar_lessons.tsx deleted file mode 100644 index f28f95f1..00000000 --- a/components/common/pages/sidebar/sidebar_lessons/sidebar_lessons.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import * as React from 'react' -import Link from 'next/link' - -export const SidebarLessons = ({ open, handle }): React.ReactElement => { - const lessons = [ - 'Operations Research', - 'Supply Chain Management', - 'Linear Regression', - 'Nonlinear Algebra', - 'OR Quiz 1', - ] - - return ( -
    - -
    - ) -} - -export type SidebarLessonsProps = {} diff --git a/components/common/quiz/quiz_attempt_list.tsx b/components/common/quiz/quiz_attempt_list.tsx new file mode 100644 index 00000000..341a0756 --- /dev/null +++ b/components/common/quiz/quiz_attempt_list.tsx @@ -0,0 +1,46 @@ +import React from 'react' +import { QuizResult } from '@/types/graphql' +import QuizAttemptRow from '@/common/quiz/quiz_attempt_row' + +function QuizAttemptList({ + moduleId, + instanceId, + result, +}: { + moduleId: string + instanceId: string + result: QuizResult[] +}) { + return ( +
    +
    + +

    + Attempt +

    +

    Date

    +

    time

    +

    + Score +

    +
    + {result + .sort( + (a, b) => + new Date(b.submittedAt).valueOf() - + new Date(a.submittedAt).valueOf(), + ) + .map((result, index) => ( + + ))} +
    + ) +} + +export default QuizAttemptList diff --git a/components/common/quiz/quiz_attempt_row.tsx b/components/common/quiz/quiz_attempt_row.tsx new file mode 100644 index 00000000..2983e83b --- /dev/null +++ b/components/common/quiz/quiz_attempt_row.tsx @@ -0,0 +1,41 @@ +import React from 'react' +import Link from 'next/link' +import moment from 'moment' +import { QuizResult } from '@/types/graphql' + +function QuizAttemptRow({ + moduleId, + instanceId, + result, + row, +}: { + moduleId: string + instanceId: string + result: QuizResult + row: number +}) { + return ( +
    + {row === 0 ? ( +

    + Latest +

    + ) : ( + + )} + + Attempt {row + 1} + +

    {moment(result.submittedAt).format('MM/DD/YYYY hh:mm A')}

    +

    XX minutes

    +

    + {result.score} + out of {result.quizInstance.quiz.totalPoints} +

    +
    + ) +} + +export default QuizAttemptRow diff --git a/components/common/quiz/quiz_header.tsx b/components/common/quiz/quiz_header.tsx new file mode 100644 index 00000000..8108a78f --- /dev/null +++ b/components/common/quiz/quiz_header.tsx @@ -0,0 +1,38 @@ +import React from 'react' +import { Quiz } from '@/types/graphql' + +function QuizHeader({ data }: { data: Quiz }) { + return ( +
    +
    +
    + +

    + Time Limit     + {data.timeLimit ? `${data.timeLimit} Minutes` : 'None'} +

    +
    + +

    + Question     + {data.numQuestions} +

    +
    + +

    + Points     + {data.totalPoints} +

    +
    +
    +
    + +

    + INSTRUCTIONS +

    +

    {data.instructions}

    +
    + ) +} + +export default QuizHeader diff --git a/components/common/quiz/quiz_questions.stories.tsx b/components/common/quiz/quiz_questions.stories.tsx index 60d527bc..71a5e60c 100644 --- a/components/common/quiz/quiz_questions.stories.tsx +++ b/components/common/quiz/quiz_questions.stories.tsx @@ -9,7 +9,7 @@ export default { } as ComponentMeta const Template: ComponentStory = ( - args: QuizQuestionProps + args: QuizQuestionProps, ) => export const Default = Template.bind({}) diff --git a/components/common/quiz/quiz_questions.tsx b/components/common/quiz/quiz_questions.tsx index f0bb62a1..d84276e9 100644 --- a/components/common/quiz/quiz_questions.tsx +++ b/components/common/quiz/quiz_questions.tsx @@ -1,9 +1,22 @@ +import React, { useState } from 'react' + export const QuizQuestion = ({ questionNumber, question, questionType, options, -}: QuizQuestionProps) => { + updateAnswer, +}) => { + const [selectedOption, setSelectedOption] = useState(null) + + const handleChange = (e) => { + setSelectedOption(e.target.value) + const selectedAnswer = options.find( + (option) => option.text === e.target.value, + ) + updateAnswer(questionNumber, selectedAnswer.id) + } + return (
    @@ -11,27 +24,21 @@ export const QuizQuestion = ({
      {options.map((option, index) => ( -
      +
      ))} @@ -43,6 +50,9 @@ export const QuizQuestion = ({ export type QuizQuestionProps = { question: string questionNumber: number - questionType?: string | boolean - options: Array + questionType?: string + options: Array<{ + id: string + text: string + }> } diff --git a/components/common/tabs_panel/tabs_panel.tsx b/components/common/tabs_panel/tabs_panel.tsx index f3d05c04..f695fa0a 100644 --- a/components/common/tabs_panel/tabs_panel.tsx +++ b/components/common/tabs_panel/tabs_panel.tsx @@ -69,7 +69,7 @@ export const TabsPanel = ({ moduleInfo }: TabsProps) => {
    - ) + ), )} diff --git a/components/common/user/account_sidebar.tsx b/components/common/user/account_sidebar.tsx new file mode 100644 index 00000000..23da5006 --- /dev/null +++ b/components/common/user/account_sidebar.tsx @@ -0,0 +1,217 @@ +import React from 'react' +import { Button } from '@/components/common/button/button' +import Link from 'next/link' +import { Session } from 'next-auth' +import { UserAccount } from '@/common/community/threads/thread/thread' +import { InstructorProfile, User } from '@/types/index' + +interface AccountSidebarProps { + verifyEdit: (openID: string) => boolean + isEditMode: boolean + sessionUser: Session + setEditMode: (isEditMode: boolean) => void + setUpdatedProfile: React.Dispatch> + updateSocial: ( + openID: string, + accountID: string, + socialInput: { + github?: string | null + linkedin?: string | null + portfolio?: string | null + facebook?: string | null + twitter?: string | null + }, + userInput: { + id: string + openID: string + biography?: string | null + phoneNumber?: string | null + }, + instructorInput?: InstructorProfile, + ) => void + userOpenID: string + contextAccount: + | (Omit & { + id: string + }) + | null + updatedProfile: User + setInstructorMode: React.Dispatch> + instructorMode: boolean + isInstructor: true | false + defaultUserData: User + instructorDetails: InstructorProfile +} + +function AccountSidebar({ + verifyEdit, + isEditMode, + sessionUser, + setEditMode, + setUpdatedProfile, + updateSocial, + userOpenID, + contextAccount, + updatedProfile, + setInstructorMode, + instructorMode, + isInstructor, + defaultUserData, + instructorDetails, +}: AccountSidebarProps) { + return ( + + ) +} + +export default AccountSidebar diff --git a/components/common/user/editable_field.tsx b/components/common/user/editable_field.tsx new file mode 100644 index 00000000..f2d2be3b --- /dev/null +++ b/components/common/user/editable_field.tsx @@ -0,0 +1,147 @@ +import React from 'react' +import { Input, InputProps } from '@/common/forms/inputs/input/input' +import { + TextArea, + TextAreaProps, +} from '@/common/forms/inputs/text_area/text_area' +import { + FaFacebook, + FaGithub, + FaLink, + FaLinkedin, + FaTwitter, +} from 'react-icons/fa' +import Link from 'next/link' + +interface EditableFieldProps { + /** + * The type of input field to display + */ + type: 'text' | 'area' + /** + * The details of the input field + */ + inputDetails: TextAreaProps | InputProps + /** + * Whether the field is in edit mode + */ + isEditing: boolean + /** + * The header to display when not in edit mode + */ + header?: string | null + /* + * The platform to display an icon for + */ + platform?: string + /** + * The url to link the header to + */ + headerURL?: string | null + /** + * determines whether the text displayed will be uppercase with a h4 tag or lowercase with a p tag + */ + isHeader?: boolean +} + +function EditableField({ + type, + isEditing, + inputDetails, + header, + platform = null, + headerURL = null, + isHeader = true, +}: EditableFieldProps) { + if (type === 'text') { + const details = inputDetails as InputProps + if (isEditing) { + return ( + + ) + } + if ( + !details.defaultValue || + typeof details.defaultValue === 'undefined' + ) + return null + return ( +
    + {headerURL ? ( + + + +

    + {header} +

    +
    + + ) : ( + <> + + {isHeader ? ( +

    + {header} +

    + ) : ( +

    + {header} +

    + )} + + )} +
    + ) + } + if (type === 'area') { + const details = inputDetails as TextAreaProps + if (isEditing) { + return ( + - - - - -
    - {data.thread.length > 0 && - data.thread.map( - ( - { - data1, - author, - createdAt, - upvotes, - instructorProfile, - comments, - body, - }, - index - ) => ( - <> - - - ) - )} -
    -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    - -
    - -
    -
    - - - -
    -
    -
    - -
    -
    -

    Groups

    -
    -
      - {groupsProps - ? groupsProps.map( - (group, groupIndex) => ( -
    • - {`${group.groupsName} -
      -

      - { - group.groupsName - } -

      -

      - {`${group.groupsMemberCount} members`} -

      -
      - -
    • - ) - ) - : null} -
    -
    -
    -
    -
    -

    - Polls & Surveys -

    -
    -
      - {pollSurveysProps - ? pollSurveysProps.map( - (poll, pollIndex) => ( -
    • -
      -

      - { - poll.pollSurveyName - } -

      -

      - {`Takes about ` + - poll.timestamp + - ` `} - {poll.timestamp === - 1 - ? 'minute' - : 'minutes'} -

      -
      - -
    • - ) - ) - : null} -
    -
    -
    -
    -
    -
    -

    - Popular challenges -

    -
    -
      - {challengesProps - ? challengesProps.map( - ( - challenge, - challengeIndex - ) => ( -
    • -
      -

      - { - challenge.challengesName - } -

      -

      - {`Added by ` + - challenge.challengesUserCount.toLocaleString() + - ` `} - {challenge.challengesUserCount === - 1 - ? 'user' - : 'users'} -

      -
      - -
    • - ) - ) - : null} -
    -
    -
    -
    -
    -
    -

    - Most Recent Chats -

    -
    -
      - {contactProps - ? contactProps.map( - (contact, contactIndex) => ( -
    • -
      - {`${contact.contactFirstName} - -
      - -
      -

      - {contact.contactTitle === - undefined - ? '' - : contact.contactTitle + - ` `} - {contact.contactFirstName + - ` ` + - contact.contactLastName} -

      -
      - -
    • - ) - ) - : null} -
    -
    -
    -
    -
    -
    - - - ) -} - -export type CommunityPageProps = { - socialCardProps: SocialCardProps - userAccountProps: UserAccountProps - inputProps: InputProps - groupsProps: GroupsProps[] - pollSurveysProps: PollSurveysProps[] - contactProps: ContactProps[] - challengesProps: ChallengesProps[] -} - -export type GroupsProps = { - groupsProfileImage: string - groupsName: string - groupsMemberCount: number -} - -export type ChallengesProps = { - challengesName: string - challengesUserCount: number -} - -export type PollSurveysProps = { - pollSurveyName: string - timestamp: number -} - -export type ContactProps = { - contactTitle?: string - contactFirstName: string - contactLastName: string - contactProfileImage: string - contactStatus: string -} diff --git a/components/pages/errors/page_not_found/page_not_found.stories.tsx b/components/pages/errors/page_not_found/page_not_found.stories.tsx index 6e25e548..11726515 100644 --- a/components/pages/errors/page_not_found/page_not_found.stories.tsx +++ b/components/pages/errors/page_not_found/page_not_found.stories.tsx @@ -10,7 +10,7 @@ export default { } as ComponentMeta const Template: ComponentStory = ( - args: PageNotFoundProps + args: PageNotFoundProps, ) => { return } diff --git a/components/pages/errors/request_failed/request_failed.stories.tsx b/components/pages/errors/request_failed/request_failed.stories.tsx new file mode 100644 index 00000000..e02d3b0e --- /dev/null +++ b/components/pages/errors/request_failed/request_failed.stories.tsx @@ -0,0 +1,32 @@ +import * as React from 'react' +import RequestFailed from './request_failed' +import type { RequestFailedProps } from './request_failed' +import { Meta, StoryFn } from '@storybook/react' + +export default { + title: 'Pages/Errors/Request Failed', + component: RequestFailed, + argTypes: { + title: { + control: { + type: 'text', + }, + }, + subtitle: { + control: { + type: 'text', + }, + }, + }, +} as Meta + +const Template: StoryFn = (args: RequestFailedProps) => { + return +} + +export const Primary: StoryFn = Template.bind({}) +Primary.storyName = 'Default' +Primary.args = { + title: 'Error', + subtitle: 'Could not retrieve user information!', +} diff --git a/components/pages/errors/request_failed/request_failed.tsx b/components/pages/errors/request_failed/request_failed.tsx new file mode 100644 index 00000000..5ba90ab1 --- /dev/null +++ b/components/pages/errors/request_failed/request_failed.tsx @@ -0,0 +1,26 @@ +import React from 'react' +import { Button } from '@/components/common/button/button' +import { useRouter } from 'next/router' + +const RequestFailed = ({ title, subtitle }: RequestFailedProps) => { + const router = useRouter() + return ( +
    +

    {title}

    +

    {subtitle}

    + +
    + ) +} + +export type RequestFailedProps = { + title: string + subtitle: string +} + +export default RequestFailed diff --git a/components/pages/modules/module/lessons/lesson/content_type/content_loader.tsx b/components/pages/modules/module/lessons/lesson/content_type/content_loader.tsx index a57682be..fb679bba 100644 --- a/components/pages/modules/module/lessons/lesson/content_type/content_loader.tsx +++ b/components/pages/modules/module/lessons/lesson/content_type/content_loader.tsx @@ -1,29 +1,20 @@ import dynamic from 'next/dynamic' -export const ContentLoader = ({ type, data }) => { +export const ContentLoader = ({ + type, + data, +}: { + type: any | undefined + data: any +}) => { console.log('ContentLoader:', type, data) - - //Temp data - // data = [ - // { - // "type": "VIDEO", - // "link": "/videos/hello.mp4", - // "primary": false - // }, - // { - // "type": "CAPTIONS", - // "link": "/video/captions.ttt", - // "primary": false - // }, - // { - // "type": "TRANSCRIPT", - // "link": "/video/trascript.txt", - // "primary": false - // }, - // ] - - //Temp type - //type = "VIDEO" + if (type === undefined) + return ( +
    +				Error: Could not load content. Content `type` is undefined.
    +				Please contact the system adminsitrator to resolve this issue.
    +			
    + ) const Content = dynamic(() => import(`./${type.toLowerCase()}`), { ssr: false, diff --git a/components/pages/modules/module/lessons/lesson/content_type/pdf.tsx b/components/pages/modules/module/lessons/lesson/content_type/pdf.tsx index ed8f355e..41e5492c 100644 --- a/components/pages/modules/module/lessons/lesson/content_type/pdf.tsx +++ b/components/pages/modules/module/lessons/lesson/content_type/pdf.tsx @@ -1,5 +1,4 @@ const PDFContent = ({ data }) => { - //console.log("p",data) let pdfLink = '' for (let i = 0; i < data.length; i++) { if (data[i].type === 'PDF') { @@ -8,7 +7,7 @@ const PDFContent = ({ data }) => { } return ( -
    +
    { + const router = useRouter() + const { moduleId } = router.query + const [answers, setAnswers] = useState([]) + const { user } = useContext(GlobalUserContext) + + const updateAnswer = (questionNumber, answerId) => { + setAnswers((prevAnswers) => { + const newAnswers = [...prevAnswers] + newAnswers[questionNumber - 1] = answerId + return newAnswers + }) + } + const { data: quizData, error } = useSWR( + { + query: getQuizById, + variables: { quizID: data[0].link }, + }, + gqlFetcher, + ) as { + data: { + quiz: Array + } + error: Error + } + const { mutate } = useSWR({}, gqlFetcher) + + const [instanceId, setInstanceId] = useState( + () => localStorage.getItem('quizInstanceId') || null, + ) + + useEffect(() => { + if (instanceId) { + localStorage.setItem('quizInstanceId', instanceId) + } else { + localStorage.removeItem('quizInstanceId') + } + }, [instanceId]) + + const takeQuizFunction = async () => { + try { + const result = await client.request( + gql` + mutation TakeQuizRequest($id: ID!) { + createQuizInstance(quizID: $id) { + id + } + } + `, + { id: data[0].link }, + ) + + // Extract the id from the response and update the state + const id = result.createQuizInstance.id + setInstanceId(id) + } catch (error) { + console.error('Error while creating quiz instance:', error) + } + } + + const { data: quizResponse } = useSWR( + instanceId + ? { + query: createQuizInstance, + variables: { id: instanceId }, + } + : null, + gqlFetcher, + ) as { + data: { + quizInstance: Array + } + error: Error + } + const submitQuiz = (instanceId, answers) => { + mutate(async () => { + await client.request( + gql` + mutation SubmitQuiz($input: QuizSubmission!) { + submitQuiz(input: $input) { + id + } + } + `, + { + input: { + student: user.id, + quizInstance: instanceId, + answers, + }, + }, + ) + }) + .then(() => { + localStorage.removeItem('quizInstanceId') + router.push( + `/modules/${ + moduleId as string + }/result?instanceId=${instanceId}`, + ) + }) + .catch((error) => { + console.error('Error while submitting quiz:', error) + }) + } + + if (error) { + return
    Error: {error.message}
    + } + + if (!quizData) { + return
    Loading...
    + } + + return ( + // map the QuizQuestion after using the data +
    + + {quizResponse ? ( + <> + {quizResponse.quizInstance[0].questions.map( + (question, index) => ( + ({ + id: answer.id, + text: answer.text, + }))} + updateAnswer={updateAnswer} + questionType={''} + /> + ), + )} +
    + +
    + + ) : ( +
    + +

    {instanceId}

    +
    + )} +
    + ) +} + +export default QuizContent diff --git a/components/pages/modules/module/lessons/lesson/content_type/text.tsx b/components/pages/modules/module/lessons/lesson/content_type/text.tsx new file mode 100644 index 00000000..328d285b --- /dev/null +++ b/components/pages/modules/module/lessons/lesson/content_type/text.tsx @@ -0,0 +1,19 @@ +import MarkdownContainer from '@/components/common/community/threads/markdown/markdown_container' + +const TextContent = ({ data }) => { + //console.log("t",data) + let text = '' + for (let i = 0; i < data.length; i++) { + if (data[i].type === 'TEXT') { + text = data[i].link + } + } + + return ( +
    + {text} +
    + ) +} + +export default TextContent diff --git a/components/pages/modules/module/lessons/lesson/content_type/video.tsx b/components/pages/modules/module/lessons/lesson/content_type/video.tsx index d858f960..b367a666 100644 --- a/components/pages/modules/module/lessons/lesson/content_type/video.tsx +++ b/components/pages/modules/module/lessons/lesson/content_type/video.tsx @@ -9,7 +9,7 @@ const VideoContent = ({ data }) => { for (let i = 0; i < data.length; i++) { if (data[i].type === 'VIDEO') { videolink = data[i].link - } else if (data[i].type === 'CAPTIONS') { + } else if (data[i].type === 'CAPTION') { videoCaptions = data[i].link } else if (data[i].type === 'TRANSCRIPT') { videoTranscript = data[i].link diff --git a/components/pages/modules/module/lessons/lesson/module_item/module_item.tsx b/components/pages/modules/module/lessons/lesson/module_item/module_item.tsx index 86d57ec3..2da5f810 100644 --- a/components/pages/modules/module/lessons/lesson/module_item/module_item.tsx +++ b/components/pages/modules/module/lessons/lesson/module_item/module_item.tsx @@ -1,62 +1,62 @@ import Link from 'next/link' - import { HiChevronRight } from 'react-icons/hi' +import { Module } from '@/types/graphql' +import React from 'react' -export const ModuleItem = ({ data, role }) => { - // Use the link below once DB schema is updated - // const link = `/modules/${data.id}/sections/${data?.headSection}/lessons/${data?.sections[data?.headSection]?.headLesson}` +export const ModuleItem = ({ + data, + expanded, + handleExpansion, + selected, +}: ModuleItemProps) => { return ( - -
    -
    -
    -

    MODULE {data.moduleNumber}

    -

    //

    -

    {role}

    -
    -
    -

    {data.moduleName}

    -
    +
    +
    handleExpansion(!expanded)} + > +
    +

    + SECTION {data.collections[0].section.sectionNumber} +

    + / +

    {data.collections[0].name}

    + / +

    + MODULE {data.prefix && data.prefix} + {data.number} +

    -
    +
    +

    + {data.name} +

    +
    +
    + + -
    -
    - + + +
    ) } export type ModuleItemProps = { - /** - * Boolean that determines if the course module is completed or not - * @type boolean - * @default false - */ - data: { - id: String - moduleName: String - moduleNumber: Number - intro: String - createdAt: String - description: String - duration: Number - keywords: Array - numSlides: Number - feedback: String | null - parentModules: Array | null - members: Array | null - } + data: Module role: String + expanded: boolean + handleExpansion: React.Dispatch> + selected: boolean } diff --git a/components/pages/user/user_profile/user_profile.stories.tsx b/components/pages/user/user_profile/user_profile.stories.tsx new file mode 100644 index 00000000..4c520894 --- /dev/null +++ b/components/pages/user/user_profile/user_profile.stories.tsx @@ -0,0 +1,107 @@ +import * as React from 'react' +import { UserProfile, UserProfileProps } from './user_profile' + +export default { + title: 'Pages/User/User Profile', + component: UserProfile, + argTypes: { + user: { + control: { + type: 'object', + }, + }, + userOpenID: { + control: { + type: 'text', + }, + }, + contextAccount: { + control: { + type: 'object', + }, + }, + updateSocial: { + control: { + type: 'object', + }, + }, + verifyEdit: { + control: { + type: 'object', + }, + }, + }, +} + +const Template = (args) => + +export const Primary = Template.bind({}) +Primary.args = { + user: { + firstName: 'John', + lastName: 'Doe', + avatar: 'https://avatars.githubusercontent.com/u/1024025?v=4', + id: '1', + biography: 'lorem', + openID: '1', + phoneNumber: '1234567890', + dob: '1990-01-01', + email: 'fake@example.com', + social: { + id: '1', + twitter: 'https://twitter.com/hashtag/lorem', + instagram: 'https://www.instagram.com/lorem/', + facebook: 'https://www.facebook.com/lorem', + website: 'https://lorem.com', + linkedin: 'https://www.linkedin.com/in/lorem', + }, + }, + userOpenID: '1', + contextAccount: { + id: '1', + firstName: 'John', + lastName: 'Doe', + openID: '1', + }, + updateSocial( + openID: string, + accountID: string, + socialInput: { + github?: string | null + linkedin?: string | null + portfolio?: string | null + facebook?: string | null + twitter?: string | null + }, + userInput: { + id: string + openID: string + biography?: string | null + phoneNumber?: string | null + }, + ): void {}, + verifyEdit(): boolean { + return false + }, + sessionUser: { + id: '1', + user: { + email: 'fake@example.com', + name: 'John Doe', + image: 'https://avatars.githubusercontent.com/u/1024025?v=4', + }, + expires: '2021-08-01T00:00:00.000Z', + openId: '1', + idToken: '1', + }, +} as UserProfileProps +Primary.storyName = 'User Profile - logged out' + +export const Secondary = Template.bind({}) +Secondary.args = { + ...Primary.args, + verifyEdit(): boolean { + return true + }, +} +Secondary.storyName = 'User Profile - logged in' diff --git a/components/pages/user/user_profile/user_profile.tsx b/components/pages/user/user_profile/user_profile.tsx new file mode 100644 index 00000000..c8f00546 --- /dev/null +++ b/components/pages/user/user_profile/user_profile.tsx @@ -0,0 +1,151 @@ +import * as React from 'react' +import { useState } from 'react' +import { InstructorProfile, User } from '@/types/index' +import { Session } from 'next-auth' +import { UserAccount } from '@/common/community/threads/thread/thread' +import AccountSidebar from '@/common/user/account_sidebar' +import StudentContent from '@/common/user/student_content' +import InstructorContent from '@/common/user/instructor_content' +import Head from 'next/head' + +export const UserProfile = ({ + user, + sessionUser, + verifyEdit, + updateSocial, + userOpenID, + contextAccount, + isInstructor, + instructorDetails, + setInstructorMode, + instructorMode, +}: UserProfileProps) => { + const [updatedProfile, setUpdatedProfile] = useState< + | (User & { + instructorProfile?: InstructorProfile + }) + | null + >(user) + const [updatedInstructorProfile, setUpdatedInstructorProfile] = + useState(instructorDetails) + const [isEditMode, setEditMode] = useState(false) + + return ( +
    + + + {updatedProfile.firstName} {updatedProfile.lastName} | + Profile | GLANCE + + +

    + {updatedProfile?.firstName} {updatedProfile?.lastName} +

    +
    + + {!isInstructor ? ( + + ) : ( + + )} +
    +
    + ) +} + +export type UserProfileProps = { + /** + * The user account coming from the DB + */ + user: User + /** + * The user account coming from the session + */ + sessionUser: Session + /** + * Function to verify if the user is allowed to edit the profile + * @param userToEdit + * @returns boolean - true if the user is allowed to edit the profile + */ + verifyEdit: (userToEdit: string) => boolean + /** + * Function to update the social media links, user biography, and phone number + * @param openID - the user's openID from the session + * @param accountID - the user's accountID from the DB + * @param socialInput - the social media links + * @param userInput - the user's biography and phone number + */ + updateSocial: ( + openID: string, + accountID: string, + socialInput: { + github?: string | null + linkedin?: string | null + portfolio?: string | null + facebook?: string | null + twitter?: string | null + }, + userInput: { + id: string + openID: string + biography?: string | null + phoneNumber?: string | null + }, + ) => void + /** + * The user's openID from the session + */ + userOpenID: string + /** + * The user's account from context with the ID field required + */ + contextAccount: + | (Omit & { + id: string + }) + | null + /** + * Boolean to determine if the user is an instructor + */ + isInstructor: boolean + /** + * The instructor profile data from the DB + */ + instructorDetails: InstructorProfile + /** + * Function to turn on and off instructor mode + */ + setInstructorMode: React.Dispatch> + /** + * Boolean to determine if the user is in instructor mode or student mode + */ + instructorMode: boolean +} diff --git a/components/pages/user_page/user_page.stories.tsx b/components/pages/user_page/user_page.stories.tsx deleted file mode 100644 index 95db3aad..00000000 --- a/components/pages/user_page/user_page.stories.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import * as React from 'react' -import { UserPage } from './user_page' -import type { UserPageProps } from './user_page' - -import { ComponentMeta, ComponentStory } from '@storybook/react' - -export default { - title: 'Pages/User Page', - component: UserPage, - argTypes: {}, -} as ComponentMeta - -const Template: ComponentStory = (args: UserPageProps) => { - return -} - -export const Primary: ComponentStory = Template.bind({}) -Primary.storyName = 'Default' -Primary.args = {} diff --git a/components/pages/user_page/user_page.tsx b/components/pages/user_page/user_page.tsx deleted file mode 100644 index 0863ccc8..00000000 --- a/components/pages/user_page/user_page.tsx +++ /dev/null @@ -1,392 +0,0 @@ -import React, { useState } from 'react' -import Image from 'next/image' -import useAuth from '@/hooks/use_auth' -import gqlFetcher from '../../../utils/gql_fetcher' -import useSWR from 'swr' -import { gql } from 'graphql-request' -import { useRouter } from 'next/router' - -export const UserPage = () => { - const [isInstructor] = useState(true) - const [showInstructor, setShowInstructor] = useState(false) - const [showModal, setShowModal] = useState(false) - const { jwt: token, user } = useAuth() - const router = useRouter() - - const { data, error } = useSWR( - { - query: gql` - { - user(id: "${user?.sub}") { - id - openID - picURL - firstName - lastName - dob - email - plan{ - id - modules{ - enrolledAt - role - module{ - id - moduleName - moduleNumber - } - } - assignmentResults { - id - submittedAt - result - gradedBy { - firstName - lastName - email - instructorProfile { - id - title - officeLocation - officeHours - contactPolicy - phone - background - researchInterest - } - } - assignment { - id - name - dueAt - } - } - } - instructorProfile{ - id - title - officeLocation - officeHours - contactPolicy - phone - background - researchInterest - } - } - } - `, - token, - }, - gqlFetcher - ) - - if (error) { - console.log(error) - // throw new Error(error); - } - if (!data) { - return
    Loading...
    - } - - return router.query.user !== user?.sub ? ( -
    - Show the profile of the user with id {router.query.user} -
    - ) : ( -
    - -
    -

    - Profile -

    -
    -
    - - -
    -
    - -
    - {/*
    - -
    */} - {showInstructor && ( - <> -
    - -
    -
    - -
    -
    - -
    -
    -