Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/components/basket/export-options/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ 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();
const [isLoading, setIsLoading] = useState(true);
const imageDisclosure = useDisclosure();
const calendarDisclosure = useDisclosure();
const crnDisclosure = useDisclosure();
const linkDisclosure = useDisclosure();

// Enable after data loads
useEffect(() => {
Expand Down Expand Up @@ -61,6 +63,7 @@ const ExportOptions = observer(() => {
Share & Export
</MenuButton>
<MenuList>
<MenuItem onClick={linkDisclosure.onOpen}>Link</MenuItem>
<MenuItem onClick={imageDisclosure.onOpen}>Image</MenuItem>
<MenuItem onClick={calendarDisclosure.onOpen}>Calendar</MenuItem>
<MenuItem onClick={handleCSVExport}>CSV</MenuItem>
Expand All @@ -70,6 +73,9 @@ const ExportOptions = observer(() => {
)}
</Menu>
</Box>
<ExportLink
isOpen={linkDisclosure.isOpen}
onClose={linkDisclosure.onClose}/>

<ExportImage
isOpen={imageDisclosure.isOpen}
Expand Down
111 changes: 111 additions & 0 deletions src/components/basket/export-options/link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import {Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalHeader,
ModalOverlay,
IconButton,
useToast,
Tooltip,
Stack,
HStack,
Box,
} from '@chakra-ui/react';
import {CopyIcon} from '@chakra-ui/icons';
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;
onClose: () => 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) {
try {
await navigator.clipboard.writeText(url);

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 (
<>
<Modal
isOpen={isOpen}
size='3xl'
autoFocus={false}
onClose={onClose}
>
<ModalOverlay/>
<ModalContent>
<ModalHeader>Share Link</ModalHeader>
<ModalCloseButton/>
<ModalBody>
<Stack spacing={4}>
<Box overflow='hidden' textOverflow='ellipsis' whiteSpace='nowrap'>
<Link href={url}>
{url}
</Link>
</Box>
<HStack w='full' justifyContent='end'>
<Tooltip label='copy link'>
<IconButton aria-label='copy link' icon={<CopyIcon />} onClick={async () => {
await handleLinkCopy();
}}/>
</Tooltip>
</HStack>
</Stack>
</ModalBody>
</ModalContent>
</Modal>
</>
);
});

export default ExportLink;
112 changes: 112 additions & 0 deletions src/components/basket/import/import.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import React from 'react';
import {
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalHeader,
ModalOverlay,
IconButton,
Tooltip,
Stack,
HStack,
Text,
} 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 {type BasketData} from '../export-options/link';
import BasketTable from '../table';

type ImportBasketProps = {
basketData: BasketData;
isOpen: boolean;
onClose: () => void;
};

const ImportBasket = observer(({basketData, isOpen, onClose}: ImportBasketProps) => {
const {allBasketsState} = useStore();
const {apiState} = useStore();

const partialBasket: Partial<BasketState> = {
id: '0',
name: basketData.name,
forTerm: basketData.term,
sectionIds: basketData.sections,
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`);
}

return `${SEMESTER_DISPLAY_MAPPING[term.semester]} ${term.year}`;
};

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);

onClose();
};

return (
<>
{
apiState.hasDataForTrackedEndpoints
&& <Modal
isOpen={isOpen}
size='3xl'
autoFocus={false}
onClose={onClose}
>
<ModalOverlay/>
<ModalContent>
<ModalHeader>Import Basket - {createdBasket.name}</ModalHeader>
<ModalCloseButton/>
<ModalBody>
<Stack spacing={4}>
<Text>
Term: {getTermDisplayName(createdBasket.forTerm)}
</Text>
<BasketTable basket={createdBasket} isForCapture={true}/>
<HStack w='full' justifyContent='end'>
<Tooltip label='import basket'>
<IconButton aria-label='copy link' icon={<CheckIcon />} onClick={() => {
importBasket();
}}/>
</Tooltip>
</HStack>
</Stack>
</ModalBody>
</ModalContent>
</Modal>
}
</>
);
});

export default ImportBasket;
9 changes: 7 additions & 2 deletions src/components/basket/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
36 changes: 35 additions & 1 deletion src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
import ImportBasket from 'src/components/basket/import/import';
import {instanceOf} from 'prop-types';

const isFirstRender = typeof window === 'undefined';

const MainContent = observer(() => {
const [numberOfScrolledColumns, setNumberOfScrolledColumns] = useState(0);
const courseTableContainerRef = useRef<HTMLDivElement>(null);
const {apiState} = useStore();
const router = useRouter();
const {isOpen, onOpen, onClose} = useDisclosure();
const [importBasketData, setImportBasketData] = useState<BasketData | undefined>(undefined);

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]);

const handleScrollToTop = useCallback(() => {
if (courseTableContainerRef.current) {
Expand Down Expand Up @@ -80,6 +111,9 @@ const MainContent = observer(() => {
</Box>
</ScrollTopDetector>
</Box>
{ importBasketData !== undefined
&& <ImportBasket basketData={importBasketData} isOpen={isOpen} onClose={closeImport}/>
}
</>
);
});
Expand Down
Loading