From 4172559368e8fee12b126441c876988953decf9f Mon Sep 17 00:00:00 2001 From: Dylan Hojnoski Date: Tue, 5 Mar 2024 18:36:13 -0500 Subject: [PATCH 1/8] have going to link semester working --- .../basket/export-options/index.tsx | 6 ++ src/components/basket/export-options/link.tsx | 94 +++++++++++++++++++ src/pages/index.tsx | 31 ++++++ 3 files changed, 131 insertions(+) create mode 100644 src/components/basket/export-options/link.tsx diff --git a/src/components/basket/export-options/index.tsx b/src/components/basket/export-options/index.tsx index 4832deb..cff2657 100644 --- a/src/components/basket/export-options/index.tsx +++ b/src/components/basket/export-options/index.tsx @@ -17,6 +17,7 @@ import WrappedFontAwesomeIcon from 'src/components/wrapped-font-awesome-icon'; import ExportImage from './image'; import ExportCalendar from './calendar'; import CRNScript from './crn-script'; +import ExportLink from './link'; const ExportOptions = observer(() => { const {allBasketsState: {currentBasket}, apiState} = useStore(); @@ -24,6 +25,7 @@ const ExportOptions = observer(() => { const imageDisclosure = useDisclosure(); const calendarDisclosure = useDisclosure(); const crnDisclosure = useDisclosure(); + const linkDisclosure = useDisclosure(); // Enable after data loads useEffect(() => { @@ -61,6 +63,7 @@ const ExportOptions = observer(() => { Share & Export + Link Image Calendar CSV @@ -70,6 +73,9 @@ const ExportOptions = observer(() => { )} + void; +}; + +export type BasketData = { + term: IPotentialFutureTerm; + name: string; + sections: string[]; + courses: string[]; + searchQueries: string[]; +}; + +const ExportLink = observer(({isOpen, onClose}: ExportLinkProps) => { + const {allBasketsState: {currentBasket}} = useStore(); + const toast = useToast(); + let url = ''; + + if (currentBasket) { + const basketData: BasketData = {term: currentBasket.forTerm, + name: currentBasket.name, + sections: currentBasket.sections.map(element => element.id), + courses: currentBasket.courses.map(element => element.id), + searchQueries: currentBasket.searchQueries}; + + // Get json data + const jsonString: string = JSON.stringify(basketData); + url = window.location.toString() + '?basket=' + encodeURIComponent(jsonString); + } + + const handleLinkCopy = async () => { + if (url.length > 0) { + await navigator.clipboard.writeText(url); + + toast({ + title: 'Link Copied', + status: 'success', + duration: 500, + }); + } + }; + + return ( + <> + + + + Share Link + + + + + {url} + + + + } onClick={async () => { + await handleLinkCopy(); + }}/> + + + + + + + + ); +}); + +export default ExportLink; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 011f70d..6365a8e 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -9,12 +9,43 @@ import useStore from 'src/lib/state/context'; import Basket from 'src/components/basket'; import ScrollTopDetector from 'src/components/scroll-top-detector'; import CoursesSearchBar from 'src/components/search-bar/courses'; +import {useRouter} from 'next/router'; +import {type BasketData} from 'src/components/basket/export-options/link'; const isFirstRender = typeof window === 'undefined'; const MainContent = observer(() => { const [numberOfScrolledColumns, setNumberOfScrolledColumns] = useState(0); const courseTableContainerRef = useRef(null); + const {allBasketsState, apiState} = useStore(); + const router = useRouter(); + + let openBasket = false; + // Check if there is a basket in the query parameter + if (router.query.basket) { + const basketData: BasketData = JSON.parse(router.query.basket.toString()) as BasketData; + void router.replace('/'); + const newBasket = allBasketsState.addBasket(basketData.term); + + newBasket.setName(basketData.name); + + for (const element of basketData.sections) { + newBasket.addSection(element); + } + + for (const element of basketData.courses) { + newBasket.addCourse(element); + } + + for (const element of basketData.searchQueries) { + newBasket.addSearchQuery(element); + } + + allBasketsState.setSelectedBasket(newBasket.id); + + apiState.setSelectedTerm(basketData.term); + openBasket = true; + } const handleScrollToTop = useCallback(() => { if (courseTableContainerRef.current) { From 7fa8765812f8ced8940da037519075985d12de86 Mon Sep 17 00:00:00 2001 From: Max Isom Date: Tue, 5 Mar 2024 21:45:51 -0800 Subject: [PATCH 2/8] Trigger workflows? From 688f1e9cc375c2c571ae125bd94b635147f89a1c Mon Sep 17 00:00:00 2001 From: Dylan Hojnoski Date: Sat, 19 Oct 2024 20:29:08 -0400 Subject: [PATCH 3/8] Added null check --- src/pages/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 4179e96..493ae2c 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -22,7 +22,7 @@ const MainContent = observer(() => { let openBasket = false; // Check if there is a basket in the query parameter - if (router.query.basket) { + if (router?.query.basket) { const basketData: BasketData = JSON.parse(router.query.basket.toString()) as BasketData; void router.replace('/'); const newBasket = allBasketsState.addBasket(basketData.term); From 569dd732f85c23bbb8f6908fa561ecadec45b374 Mon Sep 17 00:00:00 2001 From: Dylan Hojnoski Date: Wed, 23 Oct 2024 12:48:07 -0400 Subject: [PATCH 4/8] Created modal to confirm import of basket --- src/components/basket/export-options/link.tsx | 3 +- src/components/basket/import/import.tsx | 112 ++++++++++++++++++ src/components/basket/table.tsx | 9 +- src/pages/index.tsx | 41 +++---- 4 files changed, 135 insertions(+), 30 deletions(-) create mode 100644 src/components/basket/import/import.tsx diff --git a/src/components/basket/export-options/link.tsx b/src/components/basket/export-options/link.tsx index dcfbe67..361e3c5 100644 --- a/src/components/basket/export-options/link.tsx +++ b/src/components/basket/export-options/link.tsx @@ -1,5 +1,4 @@ -import { - Modal, +import {Modal, ModalBody, ModalCloseButton, ModalContent, diff --git a/src/components/basket/import/import.tsx b/src/components/basket/import/import.tsx new file mode 100644 index 0000000..d1a9db3 --- /dev/null +++ b/src/components/basket/import/import.tsx @@ -0,0 +1,112 @@ +import { + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalHeader, + ModalOverlay, + IconButton, + Tooltip, + Stack, + HStack, +} from '@chakra-ui/react'; +import {CheckIcon} from '@chakra-ui/icons'; +import {observer} from 'mobx-react-lite'; +import useStore from 'src/lib/state/context'; +import {BasketState} from 'src/lib/state/basket'; +import {type IPotentialFutureTerm} from 'src/lib/types'; +import toTitleCase from 'src/lib/to-title-case'; +import {SEMESTER_DISPLAY_MAPPING} from 'src/lib/constants'; +import BasketTable from '../table'; +import {type BasketData} from '../export-options/link'; + +type ImportBasketProps = { + basketData: BasketData; + isOpen: boolean; + onClose: () => void; +}; + +const ImportBasket = observer(({basketData, isOpen, onClose}: ImportBasketProps) => { + const {allBasketsState} = useStore(); + const {apiState} = useStore(); + + const partialBasket: Partial = { + id: '0', + name: basketData.name, + forTerm: basketData.term, + sectionIds: basketData.sections, + courseIds: basketData.courses, + searchQueries: basketData.searchQueries, + }; + const getTermDisplayName = (term: IPotentialFutureTerm) => { + if (term.isFuture) { + return toTitleCase(`Future ${term.semester.toLowerCase()} Semester`); + } + + return `${SEMESTER_DISPLAY_MAPPING[term.semester]} ${term.year}`; + }; + + const createdBasket = new BasketState(apiState, basketData.term, basketData.name, partialBasket); + + console.log(createdBasket); + + const importBasket = () => { + const newBasket = allBasketsState.addBasket(basketData.term); + + newBasket.setName(basketData.name); + + for (const element of basketData.sections) { + newBasket.addSection(element); + } + + for (const element of basketData.courses) { + newBasket.addCourse(element); + } + + for (const element of basketData.searchQueries) { + newBasket.addSearchQuery(element); + } + + allBasketsState.setSelectedBasket(newBasket.id); + + apiState.setSelectedTerm(basketData.term); + onClose(); + }; + + return ( + <> + { + apiState.hasDataForTrackedEndpoints + && + + + Import Basket - {createdBasket.name} + + + + + Term: {getTermDisplayName(createdBasket.forTerm)} + + + + + } onClick={() => { + importBasket(); + }}/> + + + + + + + } + + ); +}); + +export default ImportBasket; diff --git a/src/components/basket/table.tsx b/src/components/basket/table.tsx index 2b76e04..9fbdf85 100644 --- a/src/components/basket/table.tsx +++ b/src/components/basket/table.tsx @@ -241,10 +241,15 @@ type BasketTableProps = { onClose?: () => void; isForCapture?: boolean; tableProps?: TableProps; + basket?: BasketState; }; -const BodyWithData = observer(({onClose, isForCapture}: BasketTableProps) => { - const {allBasketsState: {currentBasket}, uiState} = useStore(); +const BodyWithData = observer(({onClose, isForCapture, basket}: BasketTableProps) => { + let {allBasketsState: {currentBasket}, uiState} = useStore(); + + if (basket) { + currentBasket = basket; + } const handleSearch = (query: string) => { uiState.setSearchValue(query); diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 493ae2c..11eca41 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,6 +1,6 @@ import React, {useCallback, useRef, useState, useEffect} from 'react'; import Head from 'next/head'; -import {Box, Divider} from '@chakra-ui/react'; +import {Box, Divider, useDisclosure} from '@chakra-ui/react'; import {observer} from 'mobx-react-lite'; import {NextSeo} from 'next-seo'; import CoursesTable from 'src/components/courses-table'; @@ -11,41 +11,27 @@ import ScrollTopDetector from 'src/components/scroll-top-detector'; import CoursesSearchBar from 'src/components/search-bar/courses'; import {useRouter} from 'next/router'; import {type BasketData} from 'src/components/basket/export-options/link'; +import ImportBasket from 'src/components/basket/import/import'; const isFirstRender = typeof window === 'undefined'; const MainContent = observer(() => { const [numberOfScrolledColumns, setNumberOfScrolledColumns] = useState(0); const courseTableContainerRef = useRef(null); - const {allBasketsState, apiState} = useStore(); + const {apiState} = useStore(); const router = useRouter(); + const {isOpen, onOpen, onClose} = useDisclosure(); + const [importBasketData, setImportBasketData] = useState(undefined); - let openBasket = false; - // Check if there is a basket in the query parameter - if (router?.query.basket) { - const basketData: BasketData = JSON.parse(router.query.basket.toString()) as BasketData; - void router.replace('/'); - const newBasket = allBasketsState.addBasket(basketData.term); - - newBasket.setName(basketData.name); - - for (const element of basketData.sections) { - newBasket.addSection(element); - } - - for (const element of basketData.courses) { - newBasket.addCourse(element); - } + // Wait for data to be loaded to import basket + useEffect(() => { + if (apiState.hasDataForTrackedEndpoints && router?.query.basket) { + setImportBasketData(JSON.parse(router.query.basket.toString()) as BasketData); + void router.replace('/'); - for (const element of basketData.searchQueries) { - newBasket.addSearchQuery(element); + onOpen(); } - - allBasketsState.setSelectedBasket(newBasket.id); - - apiState.setSelectedTerm(basketData.term); - openBasket = true; - } + }, [apiState.hasDataForTrackedEndpoints]); const handleScrollToTop = useCallback(() => { if (courseTableContainerRef.current) { @@ -111,6 +97,9 @@ const MainContent = observer(() => { + { importBasketData !== undefined + && + } ); }); From d60fc9b419494e90513c890865cee38b99dec73e Mon Sep 17 00:00:00 2001 From: Dylan Hojnoski Date: Wed, 23 Oct 2024 12:55:55 -0400 Subject: [PATCH 5/8] Fix test --- src/components/basket/import/import.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/basket/import/import.tsx b/src/components/basket/import/import.tsx index d1a9db3..45ffb36 100644 --- a/src/components/basket/import/import.tsx +++ b/src/components/basket/import/import.tsx @@ -1,3 +1,4 @@ +import React from 'react'; import { Modal, ModalBody, From fe92c95dc18d5ac0703fc547cb5a80252bd9316e Mon Sep 17 00:00:00 2001 From: Dylan Hojnoski Date: Wed, 23 Oct 2024 13:51:25 -0400 Subject: [PATCH 6/8] Fixed data not showing up when in the wrong term and modal opening when changing semesters --- src/components/basket/import/import.tsx | 17 ++++++++-------- src/pages/index.tsx | 26 +++++++++++++++++++------ 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/src/components/basket/import/import.tsx b/src/components/basket/import/import.tsx index 45ffb36..03c7d0a 100644 --- a/src/components/basket/import/import.tsx +++ b/src/components/basket/import/import.tsx @@ -10,6 +10,7 @@ import { Tooltip, Stack, HStack, + Text, } from '@chakra-ui/react'; import {CheckIcon} from '@chakra-ui/icons'; import {observer} from 'mobx-react-lite'; @@ -18,8 +19,8 @@ import {BasketState} from 'src/lib/state/basket'; import {type IPotentialFutureTerm} from 'src/lib/types'; import toTitleCase from 'src/lib/to-title-case'; import {SEMESTER_DISPLAY_MAPPING} from 'src/lib/constants'; -import BasketTable from '../table'; import {type BasketData} from '../export-options/link'; +import BasketTable from '../table'; type ImportBasketProps = { basketData: BasketData; @@ -39,6 +40,9 @@ const ImportBasket = observer(({basketData, isOpen, onClose}: ImportBasketProps) courseIds: basketData.courses, searchQueries: basketData.searchQueries, }; + + const createdBasket = new BasketState(apiState, basketData.term, basketData.name, partialBasket); + const getTermDisplayName = (term: IPotentialFutureTerm) => { if (term.isFuture) { return toTitleCase(`Future ${term.semester.toLowerCase()} Semester`); @@ -47,10 +51,6 @@ const ImportBasket = observer(({basketData, isOpen, onClose}: ImportBasketProps) return `${SEMESTER_DISPLAY_MAPPING[term.semester]} ${term.year}`; }; - const createdBasket = new BasketState(apiState, basketData.term, basketData.name, partialBasket); - - console.log(createdBasket); - const importBasket = () => { const newBasket = allBasketsState.addBasket(basketData.term); @@ -70,7 +70,6 @@ const ImportBasket = observer(({basketData, isOpen, onClose}: ImportBasketProps) allBasketsState.setSelectedBasket(newBasket.id); - apiState.setSelectedTerm(basketData.term); onClose(); }; @@ -90,9 +89,9 @@ const ImportBasket = observer(({basketData, isOpen, onClose}: ImportBasketProps) - - Term: {getTermDisplayName(createdBasket.forTerm)} - + + Term: {getTermDisplayName(createdBasket.forTerm)} + diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 11eca41..ded37a3 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -12,6 +12,7 @@ import CoursesSearchBar from 'src/components/search-bar/courses'; import {useRouter} from 'next/router'; import {type BasketData} from 'src/components/basket/export-options/link'; import ImportBasket from 'src/components/basket/import/import'; +import {instanceOf} from 'prop-types'; const isFirstRender = typeof window === 'undefined'; @@ -23,12 +24,25 @@ const MainContent = observer(() => { const {isOpen, onOpen, onClose} = useDisclosure(); const [importBasketData, setImportBasketData] = useState(undefined); - // Wait for data to be loaded to import basket - useEffect(() => { - if (apiState.hasDataForTrackedEndpoints && router?.query.basket) { - setImportBasketData(JSON.parse(router.query.basket.toString()) as BasketData); - void router.replace('/'); + if (router?.query.basket && importBasketData === undefined) { + const parsedBasket = JSON.parse(router.query.basket.toString()) as BasketData; + setImportBasketData(parsedBasket); + void router.replace('/'); + + // Change term to basket term so that it can get the data for it + if (parsedBasket !== undefined) { + apiState.setSelectedTerm(parsedBasket.term); + } + } + const closeImport = () => { + setImportBasketData(undefined); + onClose(); + }; + + // Wait for data to be loaded to open import basket + useEffect(() => { + if (apiState.hasDataForTrackedEndpoints && importBasketData !== null) { onOpen(); } }, [apiState.hasDataForTrackedEndpoints]); @@ -98,7 +112,7 @@ const MainContent = observer(() => { { importBasketData !== undefined - && + && } ); From d1987a18bcb6bd89db9df3f9ec7896cd1f1c9e2e Mon Sep 17 00:00:00 2001 From: Dylan Hojnoski Date: Fri, 13 Dec 2024 11:52:44 -0500 Subject: [PATCH 7/8] Shortened link preview --- src/components/basket/export-options/link.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/basket/export-options/link.tsx b/src/components/basket/export-options/link.tsx index 361e3c5..912e183 100644 --- a/src/components/basket/export-options/link.tsx +++ b/src/components/basket/export-options/link.tsx @@ -9,6 +9,7 @@ import {Modal, Tooltip, Stack, HStack, + Box, } from '@chakra-ui/react'; import {CopyIcon} from '@chakra-ui/icons'; import {observer} from 'mobx-react-lite'; @@ -72,9 +73,11 @@ const ExportLink = observer(({isOpen, onClose}: ExportLinkProps) => { - - {url} - + + + {url} + + } onClick={async () => { From e4ee800fec9fb134db8c5e668db759991bcc652e Mon Sep 17 00:00:00 2001 From: Dylan Hojnoski Date: Fri, 13 Dec 2024 12:24:52 -0500 Subject: [PATCH 8/8] Copy link on modal open --- src/components/basket/export-options/link.tsx | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/components/basket/export-options/link.tsx b/src/components/basket/export-options/link.tsx index 912e183..e5bbad1 100644 --- a/src/components/basket/export-options/link.tsx +++ b/src/components/basket/export-options/link.tsx @@ -16,6 +16,7 @@ import {observer} from 'mobx-react-lite'; import useStore from 'src/lib/state/context'; import {type IPotentialFutureTerm} from 'src/lib/types'; import Link from 'next/link'; +import {useEffect} from 'react'; type ExportLinkProps = { isOpen: boolean; @@ -49,16 +50,30 @@ const ExportLink = observer(({isOpen, onClose}: ExportLinkProps) => { const handleLinkCopy = async () => { if (url.length > 0) { - await navigator.clipboard.writeText(url); + try { + await navigator.clipboard.writeText(url); - toast({ - title: 'Link Copied', - status: 'success', - duration: 500, - }); + toast({ + title: 'Link Copied', + status: 'success', + duration: 500, + }); + } catch (error) { + console.error('Failed to copy link to clipboard:', error); + } } }; + useEffect(() => { + const copyOnOpen = async () => { + if (isOpen) { + await handleLinkCopy(); + } + }; + + void copyOnOpen(); + }, [isOpen]); + return ( <>