diff --git a/src/components/common/WhatsNew/whatsNew.tsx b/src/components/common/WhatsNew/whatsNew.tsx index 05097ca1..bf1040a6 100644 --- a/src/components/common/WhatsNew/whatsNew.tsx +++ b/src/components/common/WhatsNew/whatsNew.tsx @@ -1,4 +1,3 @@ -import InfoIcon from '@mui/icons-material/Info'; import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; import { IconButton, Paper, Popover, Tooltip } from '@mui/material'; import React, { useEffect, useState } from 'react'; @@ -218,17 +217,16 @@ export function WhatsNewButton() { return ( <> - + - - + {unreadCount > 0 && ( - + )} diff --git a/src/components/dashboard/Tutorial/Tutorial.tsx b/src/components/dashboard/Tutorial/Tutorial.tsx new file mode 100644 index 00000000..a56a2e7d --- /dev/null +++ b/src/components/dashboard/Tutorial/Tutorial.tsx @@ -0,0 +1,216 @@ +import type { ReactJSXElement } from '@emotion/react/types/jsx-namespace'; +import CloseIcon from '@mui/icons-material/Close'; +import { Backdrop, Button, IconButton, Popover } from '@mui/material'; +import React, { useEffect, useState } from 'react'; + +type TutorialPopupProps = { + element: Element; + open: boolean; + incrementStep: () => void; + close: () => void; + title: string; + buttonText: string; + anchorOrigin: { + vertical: 'center' | 'bottom' | 'top'; + horizontal: 'center' | 'left' | 'right'; + }; + transformOrigin: { + vertical: 'center' | 'bottom' | 'top'; + horizontal: 'center' | 'left' | 'right'; + }; + children: ReactJSXElement | string; +}; + +const TutorialPopup = ({ + element, + open, + incrementStep, + close, + title, + buttonText, + anchorOrigin, + transformOrigin, + children, +}: TutorialPopupProps) => { + useEffect(() => { + if (open) { + element.classList.add('tutorial-raise'); + if (element.tagName === 'TR' || element.tagName === 'TD') { + element.classList.add('tutorial-table'); + } + //Wait to scroll untill classes applies + setTimeout( + () => element.scrollIntoView({ behavior: 'smooth', block: 'nearest' }), + 0, + ); + } else { + element.classList.remove('tutorial-raise'); + element.classList.remove('tutorial-table'); + } + return () => { + element.classList.remove('tutorial-raise'); + element.classList.remove('tutorial-table'); + }; + }, [open, element]); + + return ( + +
+
+

{title}

+ + + +
+
{children}
+ +
+
+ ); +}; + +type StepTemplate = { + id: string; + element?: Element; + title: string; + content: ReactJSXElement | string; + anchorOrigin: { + vertical: 'center' | 'bottom' | 'top'; + horizontal: 'center' | 'left' | 'right'; + }; + transformOrigin: { + vertical: 'center' | 'bottom' | 'top'; + horizontal: 'center' | 'left' | 'right'; + }; +}; +type Step = StepTemplate & { element: Element }; + +const stepsTemplate: StepTemplate[] = [ + { + id: 'search', + title: 'Search', + content: + "Search for any number of courses and professors and we'll find all the combinations of classes they teach.", + anchorOrigin: { vertical: 'bottom', horizontal: 'center' }, + transformOrigin: { vertical: 'top', horizontal: 'center' }, + }, + { + id: 'RHS', + title: 'Overviews and Compare', + content: 'See an overview of your search on this side.', + anchorOrigin: { vertical: 'top', horizontal: 'left' }, + transformOrigin: { vertical: 'bottom', horizontal: 'right' }, + }, + { + id: 'result', + title: 'Results', + content: + 'See the average grade for a course and Rate My Professors score here.', + anchorOrigin: { vertical: 'bottom', horizontal: 'center' }, + transformOrigin: { vertical: 'top', horizontal: 'center' }, + }, + { + id: 'actions', + title: 'More Information & Compare', + content: ( + <> +

Open a result for more detailed information.

+

Click the checkbox to add an item to the compare tab.

+ + ), + anchorOrigin: { vertical: 'bottom', horizontal: 'center' }, + transformOrigin: { vertical: 'top', horizontal: 'center' }, + }, + { + id: 'filters', + title: 'Filters', + content: 'Use the filters to show more specific data.', + anchorOrigin: { vertical: 'bottom', horizontal: 'center' }, + transformOrigin: { vertical: 'top', horizontal: 'center' }, + }, + { + id: 'LHS', + title: "That's All!", + content: + 'Try searching for a class you need to take and looking through the results.', + anchorOrigin: { vertical: 'top', horizontal: 'right' }, + transformOrigin: { vertical: 'bottom', horizontal: 'left' }, + }, +]; + +type TutorialProps = { + open: boolean; + close: () => void; +}; + +const Tutorial = ({ open, close }: TutorialProps) => { + const [steps, setSteps] = useState([]); + const [place, setPlace] = useState(0); + + useEffect(() => { + // For each element, set anchor based on `data-tutorial-id` + const elements = document.querySelectorAll('[data-tutorial-id]'); + if (!elements.length) { + close(); + return; + } + const newSteps = [...stepsTemplate]; + elements.forEach((element) => { + if (element.checkVisibility()) { + const id = element.getAttribute('data-tutorial-id') as string; + const foundStep = newSteps.findIndex((step) => step.id === id); + if (foundStep !== -1) { + newSteps[foundStep].element = element; + } + } + }); + setSteps( + newSteps.filter((step) => typeof step.element !== 'undefined') as Step[], + ); + setPlace(0); + }, [open, close]); + + return ( + <> + ({ zIndex: theme.zIndex.modal })} open={open} /> + {steps.map(({ content, ...otherProps }, index) => ( + { + if (place === steps.length - 1) { + close(); + return; + } + setPlace(place + 1); + }} + close={close} + buttonText={index === steps.length - 1 ? 'Done' : 'Next'} + {...otherProps} + > + {content} + + ))} + + ); +}; + +export default Tutorial; diff --git a/src/components/navigation/Carousel/Carousel.tsx b/src/components/navigation/Carousel/Carousel.tsx index 7fce1b9f..d3a7c37a 100644 --- a/src/components/navigation/Carousel/Carousel.tsx +++ b/src/components/navigation/Carousel/Carousel.tsx @@ -72,7 +72,7 @@ const Carousel = ({ names, children, compareLength }: CarouselProps) => { }); }; - const isSmallScreen = useMediaQuery('(max-width: 600px)'); + const isSmallScreen = useMediaQuery('(max-width: 640px)'); const [open, setOpen] = useState(false); useEffect(() => setOpen(!isSmallScreen), [isSmallScreen]); useEffect(() => { diff --git a/src/components/navigation/TopMenu/TopMenu.tsx b/src/components/navigation/TopMenu/TopMenu.tsx index 09b0ea77..3917c830 100644 --- a/src/components/navigation/TopMenu/TopMenu.tsx +++ b/src/components/navigation/TopMenu/TopMenu.tsx @@ -1,12 +1,14 @@ -import { Share } from '@mui/icons-material'; +import HelpOutlineIcon from '@mui/icons-material/HelpOutline'; +import ShareIcon from '@mui/icons-material/Share'; import { IconButton, Snackbar, Tooltip } from '@mui/material'; import Image from 'next/image'; import Link from 'next/link'; import { useRouter } from 'next/router'; -import React, { useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import Background from '@/../public/background.png'; import WhatsNew from '@/components/common/WhatsNew/whatsNew'; +import Tutorial from '@/components/dashboard/Tutorial/Tutorial'; import SearchBar from '@/components/search/SearchBar/SearchBar'; /** @@ -53,6 +55,25 @@ export function TopMenu({ resultsLoading, setResultsLoading }: TopMenuProps) { alert(url); } + const [openTutorial, setOpenTutorial] = useState(false); + const closeTutorial = useCallback(() => setOpenTutorial(false), []); + const [tutorialHint, setTutorialHint] = useState(false); + //Open if not already closed (based on localStorage) + useEffect(() => { + const previous = localStorage.getItem('tutorialHint'); + let ask = previous === null; + if (previous !== null) { + const parsed = JSON.parse(previous); + if (parsed !== null && parsed.value !== 'opened') { + ask = true; + } + } + if (ask) { + setTutorialHint(true); + } + }, []); + const cacheIndex = 0; //Increment this to open the popup for all users on next deployment + return ( <>
@@ -76,30 +97,68 @@ export function TopMenu({ resultsLoading, setResultsLoading }: TopMenuProps) { className="order-last basis-full sm:order-none sm:basis-[32rem] shrink" input_className="[&>.MuiInputBase-root]:bg-white [&>.MuiInputBase-root]:dark:bg-haiti" /> - -
- -
- - - { - let url = window.location.href; - if ( - router.query && - Object.keys(router.query).length === 0 && - Object.getPrototypeOf(router.query) === Object.prototype - ) { - url = 'https://trends.utdnebula.com/'; +
+
+ +
+
+
- - - + /> +
+ + { + setTutorialHint(false); + localStorage.setItem( + 'tutorialHint', + JSON.stringify({ + value: 'opened', + cacheIndex: cacheIndex, + }), + ); + setOpenTutorial(true); + }} + > + + + +
+
+ + { + let url = window.location.href; + if ( + router.query && + Object.keys(router.query).length === 0 && + Object.getPrototypeOf(router.query) === Object.prototype + ) { + url = 'https://trends.utdnebula.com/'; + } + shareLink(url); + }} + > + + + +
setOpenCopied(false)} message="Copied!" /> + ); } diff --git a/src/components/search/Filters/Filters.tsx b/src/components/search/Filters/Filters.tsx index b965a062..63fa2c09 100644 --- a/src/components/search/Filters/Filters.tsx +++ b/src/components/search/Filters/Filters.tsx @@ -132,7 +132,7 @@ const Filters = ({ } return ( -
+
{/* min letter grade dropdown*/} +
void; removeFromCompare: (arg0: SearchQuery) => void; color?: string; + showTutorial: boolean; }; function Row({ @@ -100,6 +101,7 @@ function Row({ addToCompare, removeFromCompare, color, + showTutorial, }: RowProps) { const [open, setOpen] = useState(false); const canOpen = @@ -160,8 +162,12 @@ function Row({ if (canOpen) setOpen(!open); }} // opens/closes the card by clicking anywhere on the row className={canOpen ? 'cursor-pointer' : ''} + data-tutorial-id={showTutorial && 'result'} > - +
}; rmp: { [key: string]: GenericFetchedData }; @@ -308,6 +315,7 @@ type SearchResultsTableProps = { const SearchResultsTable = ({ resultsLoading, + numSearches, includedResults, grades, rmp, @@ -539,7 +547,7 @@ const SearchResultsTable = ({ {resultsLoading === 'done' - ? sortedResults.map((result) => ( + ? sortedResults.map((result, index) => ( )) : Array(10) diff --git a/src/pages/dashboard/index.tsx b/src/pages/dashboard/index.tsx index e81f188e..4e7adaf2 100644 --- a/src/pages/dashboard/index.tsx +++ b/src/pages/dashboard/index.tsx @@ -585,6 +585,7 @@ export const Dashboard: NextPage = (props: Props): React.ReactNode => { const searchResultsTable = ( = (props: Props): React.ReactNode => {
- {carousel} - {searchResultsTable} +
{carousel}
+
{searchResultsTable}
- + {searchResultsTable} = (props: Props): React.ReactNode => { ref={panelRRef} minSize={30} defaultSize={50} + data-tutorial-id="RHS" >
{carousel} diff --git a/src/styles/globals.css b/src/styles/globals.css index 265750b2..7d3a5fc0 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -27,4 +27,13 @@ body, .bg-darken { background-color: rgba(0, 0, 0, 0.6); } + + .tutorial-raise { + position: relative; + z-index: calc(var(--mui-zIndex-modal) + 1); + scroll-margin: 10rem; + } + .tutorial-table { + background-color: var(--mui-palette-background-paper); + } }