Skip to content

Commit

Permalink
Merge pull request #46 from DylanHojnoski/share-url
Browse files Browse the repository at this point in the history
  • Loading branch information
codetheweb authored Jan 3, 2025
2 parents e04f0d8 + e4ee800 commit 5fb511c
Show file tree
Hide file tree
Showing 5 changed files with 271 additions and 3 deletions.
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

0 comments on commit 5fb511c

Please sign in to comment.