Skip to content

Commit 0710dbf

Browse files
Glenn-Chiangleslieyip02jloh02
authored
website: Implement random course picker button (#4039)
* website: Implement random course picker button (#3735) * feat: add searchkit compatiblity * feat: add tooltip * feat: make random course button more subtle * feat: move module random button to bottom of filter list * revert: module divider styles * feat: change button to secondary --------- Co-authored-by: leslie yip <[email protected]> Co-authored-by: Jonathan Loh <[email protected]>
1 parent 0a901bf commit 0710dbf

File tree

5 files changed

+97
-1
lines changed

5 files changed

+97
-1
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import {
2+
PageSizeAccessor,
3+
PaginationAccessor,
4+
SearchkitComponent,
5+
SearchkitComponentProps,
6+
} from 'searchkit';
7+
import { ModuleCode } from 'types/modules';
8+
9+
export type RandomPickerProps = {
10+
getRandomModuleCode: () => Promise<ModuleCode>;
11+
};
12+
13+
interface SearchkitRandomPickerProps extends SearchkitComponentProps {
14+
buttonComponent: React.ElementType<RandomPickerProps>;
15+
}
16+
17+
type State = Record<string, never>;
18+
19+
export default class RandomPicker extends SearchkitComponent<SearchkitRandomPickerProps, State> {
20+
paginationAccessor() {
21+
return this.accessor as PaginationAccessor;
22+
}
23+
24+
// eslint-disable-next-line class-methods-use-this
25+
override defineAccessor() {
26+
return new PaginationAccessor('p');
27+
}
28+
29+
getRandomModuleCode = async (): Promise<ModuleCode> => {
30+
const sizeAccessor = this.searchkit.getAccessorByType(PageSizeAccessor) as PageSizeAccessor;
31+
const totalPages = Math.ceil(this.searchkit.getHitsCount() / sizeAccessor.getSize());
32+
const randomPage = Math.floor(Math.random() * totalPages);
33+
34+
this.paginationAccessor().state = this.paginationAccessor().state.setValue(randomPage);
35+
const { hits } = (await this.searchkit.performSearch()).results.hits;
36+
const randomHit = hits[Math.floor(Math.random() * hits.length)];
37+
/* eslint-disable no-underscore-dangle */
38+
return randomHit._source.moduleCode;
39+
};
40+
41+
override render() {
42+
const { buttonComponent: Button } = this.props;
43+
return <Button getRandomModuleCode={this.getRandomModuleCode} />;
44+
}
45+
}

website/src/views/modules/ModuleFinderSidebar.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
@import "~styles/utils/modules-entry";
22

33
.moduleFilters {
4+
display: flex;
5+
flex-direction: column;
6+
justify-content: center;
47
user-select: none;
58

69
.filterHeader {
@@ -23,6 +26,7 @@
2326

2427
@include media-breakpoint-up(md) {
2528
max-width: 16rem;
29+
padding-right: 0.5rem;
2630
}
2731

2832
@include media-breakpoint-down(sm) {

website/src/views/modules/ModuleFinderSidebar.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,18 @@ import SideMenu, { OPEN_MENU_LABEL } from 'views/components/SideMenu';
1717
import FilterContainer from 'views/components/filters/FilterContainer';
1818
import CheckboxItem from 'views/components/filters/CheckboxItem';
1919
import DropdownListFilters from 'views/components/filters/DropdownListFilters';
20+
import RandomPicker from 'views/components/searchkit/RandomPicker';
2021

2122
import { getSemesterTimetableLessons } from 'selectors/timetables';
2223
import { getSemesterModules } from 'utils/timetables';
2324
import { getModuleSemesterData } from 'utils/modules';
2425
import { notNull } from 'types/utils';
2526

2627
import config from 'config';
27-
import styles from './ModuleFinderSidebar.scss';
2828
import ChecklistFilter, { FilterItem } from '../components/filters/ChecklistFilter';
29+
import ModuleRandomButton from './ModuleRandomButton';
30+
31+
import styles from './ModuleFinderSidebar.scss';
2932

3033
const RESET_FILTER_OPTIONS = { filter: true };
3134

@@ -217,6 +220,8 @@ const ModuleFinderSidebar: React.FC = () => {
217220
containerComponent={FilterContainer}
218221
itemComponent={CheckboxItem}
219222
/>
223+
224+
<RandomPicker buttonComponent={ModuleRandomButton} />
220225
</div>
221226
</SideMenu>
222227
);
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
@import '~styles/utils/modules-entry';
2+
3+
.moduleRandomButton {
4+
composes: btn btn-secondary btn-svg from global;
5+
flex-grow: 1;
6+
margin-bottom: 2rem;
7+
8+
@include media-breakpoint-down(md) {
9+
margin: 0 1rem;
10+
}
11+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Shuffle } from 'react-feather';
2+
import { useDispatch } from 'react-redux';
3+
import { useHistory } from 'react-router-dom';
4+
5+
import { openNotification } from 'actions/app';
6+
import { RandomPickerProps } from 'views/components/searchkit/RandomPicker';
7+
import { modulePage } from 'views/routes/paths';
8+
9+
import styles from './ModuleRandomButton.scss';
10+
11+
const ModuleRandomButton: React.FC<RandomPickerProps> = ({ getRandomModuleCode }) => {
12+
const history = useHistory();
13+
const dispatch = useDispatch();
14+
15+
const handleClick = () => {
16+
getRandomModuleCode()
17+
.then((moduleCode) => history.push(modulePage(moduleCode)))
18+
.catch(() => {
19+
dispatch(openNotification('Failed to fetch a random course.'));
20+
});
21+
};
22+
23+
return (
24+
<button type="button" className={styles.moduleRandomButton} onClick={handleClick}>
25+
<Shuffle className="svg svg-small" />
26+
Random Course
27+
</button>
28+
);
29+
};
30+
31+
export default ModuleRandomButton;

0 commit comments

Comments
 (0)