Skip to content

Commit 5fb511c

Browse files
authored
Merge pull request #46 from DylanHojnoski/share-url
2 parents e04f0d8 + e4ee800 commit 5fb511c

File tree

5 files changed

+271
-3
lines changed

5 files changed

+271
-3
lines changed

src/components/basket/export-options/index.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@ import WrappedFontAwesomeIcon from 'src/components/wrapped-font-awesome-icon';
1717
import ExportImage from './image';
1818
import ExportCalendar from './calendar';
1919
import CRNScript from './crn-script';
20+
import ExportLink from './link';
2021

2122
const ExportOptions = observer(() => {
2223
const {allBasketsState: {currentBasket}, apiState} = useStore();
2324
const [isLoading, setIsLoading] = useState(true);
2425
const imageDisclosure = useDisclosure();
2526
const calendarDisclosure = useDisclosure();
2627
const crnDisclosure = useDisclosure();
28+
const linkDisclosure = useDisclosure();
2729

2830
// Enable after data loads
2931
useEffect(() => {
@@ -61,6 +63,7 @@ const ExportOptions = observer(() => {
6163
Share & Export
6264
</MenuButton>
6365
<MenuList>
66+
<MenuItem onClick={linkDisclosure.onOpen}>Link</MenuItem>
6467
<MenuItem onClick={imageDisclosure.onOpen}>Image</MenuItem>
6568
<MenuItem onClick={calendarDisclosure.onOpen}>Calendar</MenuItem>
6669
<MenuItem onClick={handleCSVExport}>CSV</MenuItem>
@@ -70,6 +73,9 @@ const ExportOptions = observer(() => {
7073
)}
7174
</Menu>
7275
</Box>
76+
<ExportLink
77+
isOpen={linkDisclosure.isOpen}
78+
onClose={linkDisclosure.onClose}/>
7379

7480
<ExportImage
7581
isOpen={imageDisclosure.isOpen}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import {Modal,
2+
ModalBody,
3+
ModalCloseButton,
4+
ModalContent,
5+
ModalHeader,
6+
ModalOverlay,
7+
IconButton,
8+
useToast,
9+
Tooltip,
10+
Stack,
11+
HStack,
12+
Box,
13+
} from '@chakra-ui/react';
14+
import {CopyIcon} from '@chakra-ui/icons';
15+
import {observer} from 'mobx-react-lite';
16+
import useStore from 'src/lib/state/context';
17+
import {type IPotentialFutureTerm} from 'src/lib/types';
18+
import Link from 'next/link';
19+
import {useEffect} from 'react';
20+
21+
type ExportLinkProps = {
22+
isOpen: boolean;
23+
onClose: () => void;
24+
};
25+
26+
export type BasketData = {
27+
term: IPotentialFutureTerm;
28+
name: string;
29+
sections: string[];
30+
courses: string[];
31+
searchQueries: string[];
32+
};
33+
34+
const ExportLink = observer(({isOpen, onClose}: ExportLinkProps) => {
35+
const {allBasketsState: {currentBasket}} = useStore();
36+
const toast = useToast();
37+
let url = '';
38+
39+
if (currentBasket) {
40+
const basketData: BasketData = {term: currentBasket.forTerm,
41+
name: currentBasket.name,
42+
sections: currentBasket.sections.map(element => element.id),
43+
courses: currentBasket.courses.map(element => element.id),
44+
searchQueries: currentBasket.searchQueries};
45+
46+
// Get json data
47+
const jsonString: string = JSON.stringify(basketData);
48+
url = window.location.toString() + '?basket=' + encodeURIComponent(jsonString);
49+
}
50+
51+
const handleLinkCopy = async () => {
52+
if (url.length > 0) {
53+
try {
54+
await navigator.clipboard.writeText(url);
55+
56+
toast({
57+
title: 'Link Copied',
58+
status: 'success',
59+
duration: 500,
60+
});
61+
} catch (error) {
62+
console.error('Failed to copy link to clipboard:', error);
63+
}
64+
}
65+
};
66+
67+
useEffect(() => {
68+
const copyOnOpen = async () => {
69+
if (isOpen) {
70+
await handleLinkCopy();
71+
}
72+
};
73+
74+
void copyOnOpen();
75+
}, [isOpen]);
76+
77+
return (
78+
<>
79+
<Modal
80+
isOpen={isOpen}
81+
size='3xl'
82+
autoFocus={false}
83+
onClose={onClose}
84+
>
85+
<ModalOverlay/>
86+
<ModalContent>
87+
<ModalHeader>Share Link</ModalHeader>
88+
<ModalCloseButton/>
89+
<ModalBody>
90+
<Stack spacing={4}>
91+
<Box overflow='hidden' textOverflow='ellipsis' whiteSpace='nowrap'>
92+
<Link href={url}>
93+
{url}
94+
</Link>
95+
</Box>
96+
<HStack w='full' justifyContent='end'>
97+
<Tooltip label='copy link'>
98+
<IconButton aria-label='copy link' icon={<CopyIcon />} onClick={async () => {
99+
await handleLinkCopy();
100+
}}/>
101+
</Tooltip>
102+
</HStack>
103+
</Stack>
104+
</ModalBody>
105+
</ModalContent>
106+
</Modal>
107+
</>
108+
);
109+
});
110+
111+
export default ExportLink;
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import React from 'react';
2+
import {
3+
Modal,
4+
ModalBody,
5+
ModalCloseButton,
6+
ModalContent,
7+
ModalHeader,
8+
ModalOverlay,
9+
IconButton,
10+
Tooltip,
11+
Stack,
12+
HStack,
13+
Text,
14+
} from '@chakra-ui/react';
15+
import {CheckIcon} from '@chakra-ui/icons';
16+
import {observer} from 'mobx-react-lite';
17+
import useStore from 'src/lib/state/context';
18+
import {BasketState} from 'src/lib/state/basket';
19+
import {type IPotentialFutureTerm} from 'src/lib/types';
20+
import toTitleCase from 'src/lib/to-title-case';
21+
import {SEMESTER_DISPLAY_MAPPING} from 'src/lib/constants';
22+
import {type BasketData} from '../export-options/link';
23+
import BasketTable from '../table';
24+
25+
type ImportBasketProps = {
26+
basketData: BasketData;
27+
isOpen: boolean;
28+
onClose: () => void;
29+
};
30+
31+
const ImportBasket = observer(({basketData, isOpen, onClose}: ImportBasketProps) => {
32+
const {allBasketsState} = useStore();
33+
const {apiState} = useStore();
34+
35+
const partialBasket: Partial<BasketState> = {
36+
id: '0',
37+
name: basketData.name,
38+
forTerm: basketData.term,
39+
sectionIds: basketData.sections,
40+
courseIds: basketData.courses,
41+
searchQueries: basketData.searchQueries,
42+
};
43+
44+
const createdBasket = new BasketState(apiState, basketData.term, basketData.name, partialBasket);
45+
46+
const getTermDisplayName = (term: IPotentialFutureTerm) => {
47+
if (term.isFuture) {
48+
return toTitleCase(`Future ${term.semester.toLowerCase()} Semester`);
49+
}
50+
51+
return `${SEMESTER_DISPLAY_MAPPING[term.semester]} ${term.year}`;
52+
};
53+
54+
const importBasket = () => {
55+
const newBasket = allBasketsState.addBasket(basketData.term);
56+
57+
newBasket.setName(basketData.name);
58+
59+
for (const element of basketData.sections) {
60+
newBasket.addSection(element);
61+
}
62+
63+
for (const element of basketData.courses) {
64+
newBasket.addCourse(element);
65+
}
66+
67+
for (const element of basketData.searchQueries) {
68+
newBasket.addSearchQuery(element);
69+
}
70+
71+
allBasketsState.setSelectedBasket(newBasket.id);
72+
73+
onClose();
74+
};
75+
76+
return (
77+
<>
78+
{
79+
apiState.hasDataForTrackedEndpoints
80+
&& <Modal
81+
isOpen={isOpen}
82+
size='3xl'
83+
autoFocus={false}
84+
onClose={onClose}
85+
>
86+
<ModalOverlay/>
87+
<ModalContent>
88+
<ModalHeader>Import Basket - {createdBasket.name}</ModalHeader>
89+
<ModalCloseButton/>
90+
<ModalBody>
91+
<Stack spacing={4}>
92+
<Text>
93+
Term: {getTermDisplayName(createdBasket.forTerm)}
94+
</Text>
95+
<BasketTable basket={createdBasket} isForCapture={true}/>
96+
<HStack w='full' justifyContent='end'>
97+
<Tooltip label='import basket'>
98+
<IconButton aria-label='copy link' icon={<CheckIcon />} onClick={() => {
99+
importBasket();
100+
}}/>
101+
</Tooltip>
102+
</HStack>
103+
</Stack>
104+
</ModalBody>
105+
</ModalContent>
106+
</Modal>
107+
}
108+
</>
109+
);
110+
});
111+
112+
export default ImportBasket;

src/components/basket/table.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,10 +241,15 @@ type BasketTableProps = {
241241
onClose?: () => void;
242242
isForCapture?: boolean;
243243
tableProps?: TableProps;
244+
basket?: BasketState;
244245
};
245246

246-
const BodyWithData = observer(({onClose, isForCapture}: BasketTableProps) => {
247-
const {allBasketsState: {currentBasket}, uiState} = useStore();
247+
const BodyWithData = observer(({onClose, isForCapture, basket}: BasketTableProps) => {
248+
let {allBasketsState: {currentBasket}, uiState} = useStore();
249+
250+
if (basket) {
251+
currentBasket = basket;
252+
}
248253

249254
const handleSearch = (query: string) => {
250255
uiState.setSearchValue(query);

src/pages/index.tsx

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, {useCallback, useRef, useState, useEffect} from 'react';
22
import Head from 'next/head';
3-
import {Box, Divider} from '@chakra-ui/react';
3+
import {Box, Divider, useDisclosure} from '@chakra-ui/react';
44
import {observer} from 'mobx-react-lite';
55
import {NextSeo} from 'next-seo';
66
import CoursesTable from 'src/components/courses-table';
@@ -9,12 +9,43 @@ import useStore from 'src/lib/state/context';
99
import Basket from 'src/components/basket';
1010
import ScrollTopDetector from 'src/components/scroll-top-detector';
1111
import CoursesSearchBar from 'src/components/search-bar/courses';
12+
import {useRouter} from 'next/router';
13+
import {type BasketData} from 'src/components/basket/export-options/link';
14+
import ImportBasket from 'src/components/basket/import/import';
15+
import {instanceOf} from 'prop-types';
1216

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

1519
const MainContent = observer(() => {
1620
const [numberOfScrolledColumns, setNumberOfScrolledColumns] = useState(0);
1721
const courseTableContainerRef = useRef<HTMLDivElement>(null);
22+
const {apiState} = useStore();
23+
const router = useRouter();
24+
const {isOpen, onOpen, onClose} = useDisclosure();
25+
const [importBasketData, setImportBasketData] = useState<BasketData | undefined>(undefined);
26+
27+
if (router?.query.basket && importBasketData === undefined) {
28+
const parsedBasket = JSON.parse(router.query.basket.toString()) as BasketData;
29+
setImportBasketData(parsedBasket);
30+
void router.replace('/');
31+
32+
// Change term to basket term so that it can get the data for it
33+
if (parsedBasket !== undefined) {
34+
apiState.setSelectedTerm(parsedBasket.term);
35+
}
36+
}
37+
38+
const closeImport = () => {
39+
setImportBasketData(undefined);
40+
onClose();
41+
};
42+
43+
// Wait for data to be loaded to open import basket
44+
useEffect(() => {
45+
if (apiState.hasDataForTrackedEndpoints && importBasketData !== null) {
46+
onOpen();
47+
}
48+
}, [apiState.hasDataForTrackedEndpoints]);
1849

1950
const handleScrollToTop = useCallback(() => {
2051
if (courseTableContainerRef.current) {
@@ -80,6 +111,9 @@ const MainContent = observer(() => {
80111
</Box>
81112
</ScrollTopDetector>
82113
</Box>
114+
{ importBasketData !== undefined
115+
&& <ImportBasket basketData={importBasketData} isOpen={isOpen} onClose={closeImport}/>
116+
}
83117
</>
84118
);
85119
});

0 commit comments

Comments
 (0)