diff --git a/src/components/Attributes.jsx b/src/components/Attributes.jsx
deleted file mode 100644
index 3a7e925..0000000
--- a/src/components/Attributes.jsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import { appBoundClassNames as classNames } from '../common/boundClassNames';
-
-class Attributes extends Component {
- render() {
- const { t, entries, fixedWidthName, longName, longInfo } = this.props;
-
- return (
-
- {entries.map((e) => (
-
-
- {e.name}
-
- {e.onInfoClick ? (
-
- {e.info}
-
- ) : (
-
{e.info}
- )}
-
- ))}
-
- );
- }
-}
-
-Attributes.propTypes = {
- entries: PropTypes.arrayOf(
- PropTypes.shape({
- name: PropTypes.string.isRequired,
- info: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
- onMouseOver: PropTypes.func,
- onMouseOut: PropTypes.func,
- onInfoClick: PropTypes.func,
- isInfoClickDisabled: PropTypes.bool,
- }),
- ).isRequired,
- fixedWidthName: PropTypes.bool,
- longName: PropTypes.bool,
- longInfo: PropTypes.bool,
-};
-
-export default withTranslation()(Attributes);
diff --git a/src/components/Attributes.tsx b/src/components/Attributes.tsx
new file mode 100644
index 0000000..b8362f5
--- /dev/null
+++ b/src/components/Attributes.tsx
@@ -0,0 +1,62 @@
+import React from 'react';
+import { appBoundClassNames as classNames } from '../common/boundClassNames';
+import { useTranslation } from 'react-i18next';
+
+interface AttributeEntry {
+ name: string;
+ info: string | React.ReactNode;
+ onMouseOver?: () => void;
+ onMouseOut?: () => void;
+ onInfoClick?: () => void;
+ isInfoClickDisabled?: boolean;
+}
+
+interface AttributesProps {
+ entries: AttributeEntry[];
+ fixedWidthName?: boolean;
+ longName?: boolean;
+ longInfo?: boolean;
+}
+
+const Attributes = ({
+ entries,
+ fixedWidthName = false,
+ longName = false,
+ longInfo = false,
+}: AttributesProps) => {
+ const { t } = useTranslation();
+
+ return (
+
+ {entries.map((e) => (
+
+
+ {e.name}
+
+ {e.onInfoClick ? (
+
+ {e.info}
+
+ ) : (
+
{e.info}
+ )}
+
+ ))}
+
+ );
+};
+
+export default Attributes;
diff --git a/src/components/BetaPopup.jsx b/src/components/BetaPopup.jsx
deleted file mode 100644
index 204538e..0000000
--- a/src/components/BetaPopup.jsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import { appBoundClassNames as classNames } from '../common/boundClassNames';
-
-import CloseButton from './CloseButton';
-
-class BetaPopup extends Component {
- constructor(props) {
- super(props);
-
- this.state = {
- isOpen: true,
- };
- }
-
- close = () => {
- this.setState({
- isOpen: false,
- });
- };
-
- render() {
- const { isOpen } = this.state;
- const { title, content, link } = this.props;
-
- if (!isOpen) {
- return null;
- }
-
- return (
-
-
-
-
{title}
-
- {content.map((l) => (
-
{l}
- ))}
-
-
-
-
- );
- }
-}
-
-BetaPopup.propTypes = {
- title: PropTypes.string.isRequired,
- content: PropTypes.arrayOf(PropTypes.string).isRequired,
- link: PropTypes.string.isRequired,
-};
-
-export default withTranslation()(BetaPopup);
diff --git a/src/components/BetaPopup.tsx b/src/components/BetaPopup.tsx
new file mode 100644
index 0000000..02a6081
--- /dev/null
+++ b/src/components/BetaPopup.tsx
@@ -0,0 +1,47 @@
+import { useState } from 'react';
+import { appBoundClassNames as classNames } from '../common/boundClassNames';
+
+import CloseButton from './CloseButton';
+
+interface BetaPopupProps {
+ title: string;
+ content: string[];
+ link: string;
+}
+
+const BetaPopup = ({ title, content, link }: BetaPopupProps) => {
+ const [isOpen, setIsOpen] = useState(true);
+
+ const close = () => {
+ setIsOpen(false);
+ };
+
+ if (!isOpen) {
+ return null;
+ }
+
+ return (
+
+
+
+
{title}
+
+ {content.map((l, index) => (
+
{l}
+ ))}
+
+
+
+
+ );
+};
+
+export default BetaPopup;
diff --git a/src/components/CloseButton.jsx b/src/components/CloseButton.jsx
deleted file mode 100644
index e1b342c..0000000
--- a/src/components/CloseButton.jsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-
-import { appBoundClassNames as classNames } from '../common/boundClassNames';
-
-class CloseButton extends Component {
- render() {
- const { onClick } = this.props;
-
- return (
-
-
-
- );
- }
-}
-
-CloseButton.propTypes = {
- onClick: PropTypes.func.isRequired,
-};
-
-export default CloseButton;
diff --git a/src/components/CloseButton.tsx b/src/components/CloseButton.tsx
new file mode 100644
index 0000000..2f76e6c
--- /dev/null
+++ b/src/components/CloseButton.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import { appBoundClassNames as classNames } from '../common/boundClassNames';
+
+interface CloseButtonProps {
+ onClick: () => void;
+}
+
+const CloseButton = ({ onClick }: CloseButtonProps) => {
+ return (
+
+
+
+ );
+};
+
+export default CloseButton;
diff --git a/src/components/CountController.jsx b/src/components/CountController.jsx
deleted file mode 100644
index 9aa7877..0000000
--- a/src/components/CountController.jsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import { appBoundClassNames as classNames } from '../common/boundClassNames';
-
-class CountController extends Component {
- render() {
- const { count, updateCount } = this.props;
-
- return (
-
-
{
- if (count > 0) {
- updateCount(count - 1);
- }
- }}
- />
- {count}
- {
- updateCount(count + 1);
- }}
- />
-
- );
- }
-}
-
-CountController.propTypes = {
- count: PropTypes.number,
- updateCount: PropTypes.func,
-};
-
-export default withTranslation()(CountController);
diff --git a/src/components/CountController.tsx b/src/components/CountController.tsx
new file mode 100644
index 0000000..c8f1a54
--- /dev/null
+++ b/src/components/CountController.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import { appBoundClassNames as classNames } from '../common/boundClassNames';
+
+interface CountControllerProps {
+ count: number;
+ updateCount: (newCount: number) => void;
+}
+
+const CountController: React.FC = ({ count, updateCount }) => {
+ return (
+
+
{
+ if (count > 0) {
+ updateCount(count - 1);
+ }
+ }}
+ />
+ {count}
+ {
+ updateCount(count + 1);
+ }}
+ />
+
+ );
+};
+
+export default CountController;
diff --git a/src/components/CourseStatus.jsx b/src/components/CourseStatus.jsx
deleted file mode 100644
index 92d5163..0000000
--- a/src/components/CourseStatus.jsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import { appBoundClassNames as classNames } from '../common/boundClassNames';
-
-class CourseStatus extends Component {
- render() {
- const { entries } = this.props;
-
- return (
-
- {entries.map((e) => (
-
-
{e.name}
-
- {e.info.map((k) => (
-
-
{k.name}
-
- {k.controller}
-
- ))}
-
-
- ))}
-
- );
- }
-}
-
-CourseStatus.propTypes = {
- entries: PropTypes.arrayOf(
- PropTypes.shape({
- name: PropTypes.string.isRequired,
- info: PropTypes.arrayOf(
- PropTypes.exact({
- name: PropTypes.string.isRequired,
- controller: PropTypes.any.isRequired,
- onMouseOver: PropTypes.func,
- onMouseOut: PropTypes.func,
- }),
- ).isRequired,
- }),
- ).isRequired,
-};
-
-export default withTranslation()(CourseStatus);
diff --git a/src/components/CourseStatus.tsx b/src/components/CourseStatus.tsx
new file mode 100644
index 0000000..b6af40c
--- /dev/null
+++ b/src/components/CourseStatus.tsx
@@ -0,0 +1,45 @@
+import React from 'react';
+import { appBoundClassNames as classNames } from '../common/boundClassNames';
+
+interface CourseInfo {
+ name: string;
+ controller: React.ReactNode;
+ onMouseOver?: () => void;
+ onMouseOut?: () => void;
+}
+
+interface CourseEntry {
+ name: string;
+ info: CourseInfo[];
+}
+
+interface CourseStatusProps {
+ entries: CourseEntry[];
+}
+
+const CourseStatus: React.FC = ({ entries }) => {
+ return (
+
+ {entries.map((e) => (
+
+
{e.name}
+
+ {e.info.map((k) => (
+
+
{k.name}
+
+ {k.controller}
+
+ ))}
+
+
+ ))}
+
+ );
+};
+
+export default CourseStatus;
diff --git a/src/components/CreditBar.jsx b/src/components/CreditBar.jsx
deleted file mode 100644
index a266fb6..0000000
--- a/src/components/CreditBar.jsx
+++ /dev/null
@@ -1,115 +0,0 @@
-import React, { Component } from 'react';
-import { withTranslation } from 'react-i18next';
-import PropTypes from 'prop-types';
-
-import { appBoundClassNames as classNames } from '../common/boundClassNames';
-import { ItemFocusFrom } from '@/shapes/enum';
-
-class CreditBar extends Component {
- render() {
- const {
- takenCredit,
- plannedCredit,
- totalCredit,
- focusedCredit,
- colorIndex,
- isCategoryFocused,
- focusFrom,
- } = this.props;
-
- const getWidth = (credit) => {
- if (totalCredit === 0) {
- return 100;
- }
- return (credit / totalCredit) * 100;
- };
-
- const focusPosition =
- focusedCredit === 0
- ? 0
- : focusFrom === ItemFocusFrom.LIST || focusFrom === ItemFocusFrom.ADDING
- ? 3
- : focusFrom === ItemFocusFrom.TABLE_TAKEN
- ? 1
- : 2;
-
- const Tag = isCategoryFocused ? 'span' : React.Fragment;
- const text = (
- <>
- {takenCredit}
- {focusPosition === 1 && {`(${focusedCredit})`}}
- {' \u2192 '}
- {takenCredit + plannedCredit}
- {focusPosition === 2 && {`(${focusedCredit})`}}
- {focusPosition === 3 && {`+${focusedCredit}`}}
- {' / '}
- {totalCredit}
- >
- );
-
- const widths = [
- getWidth(takenCredit - (focusPosition === 1 ? focusedCredit : 0)),
- getWidth(focusPosition === 1 ? focusedCredit : 0),
- getWidth(plannedCredit - (focusPosition === 2 ? focusedCredit : 0)),
- getWidth(focusPosition === 2 || focusPosition === 3 ? focusedCredit : 0),
- ];
-
- return (
-
- );
- }
-}
-
-CreditBar.propTypes = {
- takenCredit: PropTypes.number.isRequired,
- plannedCredit: PropTypes.number.isRequired,
- totalCredit: PropTypes.number.isRequired,
- focusedCredit: PropTypes.number.isRequired,
- colorIndex: PropTypes.number.isRequired,
- isCategoryFocused: PropTypes.bool.isRequired,
- focusFrom: PropTypes.oneOf(Object.values(ItemFocusFrom)).isRequired,
-};
-
-export default withTranslation()(CreditBar);
diff --git a/src/components/CreditBar.tsx b/src/components/CreditBar.tsx
new file mode 100644
index 0000000..64d9af9
--- /dev/null
+++ b/src/components/CreditBar.tsx
@@ -0,0 +1,108 @@
+import React from 'react';
+import { appBoundClassNames as classNames } from '../common/boundClassNames';
+import { ItemFocusFrom } from '@/shapes/enum';
+
+interface CreditBarProps {
+ takenCredit: number;
+ plannedCredit: number;
+ totalCredit: number;
+ focusedCredit: number;
+ colorIndex: number;
+ isCategoryFocused: boolean;
+ focusFrom: ItemFocusFrom;
+}
+
+const CreditBar: React.FC = ({
+ takenCredit,
+ plannedCredit,
+ totalCredit,
+ focusedCredit,
+ colorIndex,
+ isCategoryFocused,
+ focusFrom,
+}) => {
+ const getWidth = (credit: number) => {
+ if (totalCredit === 0) {
+ return 100;
+ }
+ return (credit / totalCredit) * 100;
+ };
+
+ const focusPosition =
+ focusedCredit === 0
+ ? 0
+ : focusFrom === ItemFocusFrom.LIST || focusFrom === ItemFocusFrom.ADDING
+ ? 3
+ : focusFrom === ItemFocusFrom.TABLE_TAKEN
+ ? 1
+ : 2;
+
+ const Tag = isCategoryFocused ? 'span' : React.Fragment;
+ const text = (
+ <>
+ {takenCredit}
+ {focusPosition === 1 && {`(${focusedCredit})`}}
+ {' \u2192 '}
+ {takenCredit + plannedCredit}
+ {focusPosition === 2 && {`(${focusedCredit})`}}
+ {focusPosition === 3 && {`+${focusedCredit}`}}
+ {' / '}
+ {totalCredit}
+ >
+ );
+
+ const widths = [
+ getWidth(takenCredit - (focusPosition === 1 ? focusedCredit : 0)),
+ getWidth(focusPosition === 1 ? focusedCredit : 0),
+ getWidth(plannedCredit - (focusPosition === 2 ? focusedCredit : 0)),
+ getWidth(focusPosition === 2 || focusPosition === 3 ? focusedCredit : 0),
+ ];
+
+ return (
+
+ );
+};
+
+export default CreditBar;
diff --git a/src/components/Divider.jsx b/src/components/Divider.jsx
deleted file mode 100644
index 7946d0e..0000000
--- a/src/components/Divider.jsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-
-import { appBoundClassNames as classNames } from '../common/boundClassNames';
-
-class Divider extends Component {
- static Orientation = {
- HORIZONTAL: 'HORIZONTAL',
- VERTICAL: 'VERTICAL',
- };
-
- render() {
- const { orientation, isVisible, gridArea } = this.props;
-
- const orientationOnDesktop =
- typeof orientation === 'string' ? orientation : orientation.desktop;
- const orientationOnMobile = typeof orientation === 'string' ? orientation : orientation.mobile;
- const isVisibleOnDesktop = typeof isVisible === 'boolean' ? isVisible : isVisible.desktop;
- const isVisibleOnMobile = typeof isVisible === 'boolean' ? isVisible : isVisible.mobile;
-
- return (
-
- );
- }
-}
-
-const orientationType = PropTypes.oneOf([
- Divider.Orientation.HORIZONTAL,
- Divider.Orientation.VERTICAL,
-]);
-
-Divider.propTypes = {
- orientation: PropTypes.oneOfType([
- orientationType,
- PropTypes.shape({
- desktop: orientationType.isRequired,
- mobile: orientationType.isRequired,
- }),
- ]).isRequired,
- isVisible: PropTypes.oneOfType([
- PropTypes.bool,
- PropTypes.shape({
- desktop: PropTypes.bool.isRequired,
- mobile: PropTypes.bool.isRequired,
- }),
- ]).isRequired,
- gridArea: PropTypes.string,
-};
-
-export default Divider;
diff --git a/src/components/Divider.tsx b/src/components/Divider.tsx
new file mode 100644
index 0000000..9eb46d9
--- /dev/null
+++ b/src/components/Divider.tsx
@@ -0,0 +1,44 @@
+import { appBoundClassNames as classNames } from '../common/boundClassNames';
+import { Orientation } from '@/shapes/enum';
+
+interface OrientationConfig {
+ desktop: Orientation;
+ mobile: Orientation;
+}
+
+interface VisibilityConfig {
+ desktop: boolean;
+ mobile: boolean;
+}
+
+interface DividerProps {
+ orientation: Orientation | OrientationConfig;
+ isVisible: boolean | VisibilityConfig;
+ gridArea?: string;
+}
+
+const Divider = ({ orientation, isVisible, gridArea }: DividerProps) => {
+ const orientationOnDesktop = typeof orientation === 'string' ? orientation : orientation.desktop;
+ const orientationOnMobile = typeof orientation === 'string' ? orientation : orientation.mobile;
+ const isVisibleOnDesktop = typeof isVisible === 'boolean' ? isVisible : isVisible.desktop;
+ const isVisibleOnMobile = typeof isVisible === 'boolean' ? isVisible : isVisible.mobile;
+
+ return (
+
+ );
+};
+
+export default Divider;
diff --git a/src/components/OtlplusPlaceholder.jsx b/src/components/OtlplusPlaceholder.jsx
deleted file mode 100644
index c79cf62..0000000
--- a/src/components/OtlplusPlaceholder.jsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import React, { Component } from 'react';
-import { Link } from 'react-router-dom';
-import { withTranslation } from 'react-i18next';
-
-import { appBoundClassNames as classNames } from '../common/boundClassNames';
-import { CONTACT } from '../common/constants';
-
-class OtlplusPlaceholder extends Component {
- render() {
- const { t } = this.props;
-
- return (
-
-
OTL PLUS
-
- {t('ui.menu.credit')}
- |
- {t('ui.menu.licences')}
- |
- {t('ui.menu.privacy')}
-
-
-
- © 2016,
-
SPARCS
- OTL Team
-
-
- );
- }
-}
-
-OtlplusPlaceholder.propTypes = {};
-
-export default withTranslation()(OtlplusPlaceholder);
diff --git a/src/components/OtlplusPlaceholder.tsx b/src/components/OtlplusPlaceholder.tsx
new file mode 100644
index 0000000..7bbb204
--- /dev/null
+++ b/src/components/OtlplusPlaceholder.tsx
@@ -0,0 +1,30 @@
+import { Link } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+import { appBoundClassNames as classNames } from '../common/boundClassNames';
+import { CONTACT } from '../common/constants';
+
+const OtlplusPlaceholder = () => {
+ const { t } = useTranslation();
+ return (
+
+
OTL PLUS
+
+ {t('ui.menu.credit')}
+ |
+ {t('ui.menu.licences')}
+ |
+ {t('ui.menu.privacy')}
+
+
+
+ © 2016,
+
SPARCS
+ OTL Team
+
+
+ );
+};
+
+export default OtlplusPlaceholder;
diff --git a/src/components/PlannerOverlay.jsx b/src/components/PlannerOverlay.tsx
similarity index 66%
rename from src/components/PlannerOverlay.jsx
rename to src/components/PlannerOverlay.tsx
index 94f737e..82e6a25 100644
--- a/src/components/PlannerOverlay.jsx
+++ b/src/components/PlannerOverlay.tsx
@@ -1,20 +1,33 @@
import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
import { appBoundClassNames as classNames } from '../common/boundClassNames';
+interface Option {
+ label: string;
+ onClick: () => void;
+ isSmall?: boolean;
+ isDisabled?: boolean;
+}
+
+interface PlannerOverlayProps {
+ yearIndex: number;
+ semesterIndex: -1 | 0 | 1;
+ tableSize: number;
+ cellWidth: number;
+ cellHeight: number;
+ isPlannerWithSummer: boolean;
+ isPlannerWithWinter: boolean;
+ options: Option[];
+}
+
const PlannerOverlay = ({
- t,
yearIndex,
semesterIndex,
tableSize,
cellWidth,
cellHeight,
isPlannerWithSummer,
- isPlannerWithWinter,
options,
-}) => {
+}: PlannerOverlayProps) => {
const verticalBase = 17 + (isPlannerWithSummer ? 15 : 0) + cellHeight * tableSize;
const getTop = () => {
@@ -66,22 +79,4 @@ const PlannerOverlay = ({
);
};
-PlannerOverlay.propTypes = {
- yearIndex: PropTypes.number.isRequired,
- semesterIndex: PropTypes.oneOf([-1, 0, 1]).isRequired,
- tableSize: PropTypes.number.isRequired,
- cellWidth: PropTypes.number.isRequired,
- cellHeight: PropTypes.number.isRequired,
- isPlannerWithSummer: PropTypes.bool.isRequired,
- isPlannerWithWinter: PropTypes.bool.isRequired,
- options: PropTypes.arrayOf(
- PropTypes.exact({
- label: PropTypes.string.isRequired,
- onClick: PropTypes.func.isRequired,
- isSmall: PropTypes.bool,
- isDisabled: PropTypes.bool,
- }),
- ).isRequired,
-};
-
-export default withTranslation()(React.memo(PlannerOverlay));
+export default React.memo(PlannerOverlay);
diff --git a/src/components/Scores.jsx b/src/components/Scores.jsx
deleted file mode 100644
index d62e264..0000000
--- a/src/components/Scores.jsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-
-import { appBoundClassNames as classNames } from '../common/boundClassNames';
-
-class Scores extends Component {
- render() {
- const { entries, big } = this.props;
-
- return (
-
- {entries.map((e) => (
-
- ))}
-
- );
- }
-}
-
-Scores.propTypes = {
- entries: PropTypes.arrayOf(
- PropTypes.shape({
- name: PropTypes.string.isRequired,
- score: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
- onMouseOver: PropTypes.func,
- onMouseOut: PropTypes.func,
- }),
- ).isRequired,
- big: PropTypes.bool,
-};
-
-export default Scores;
diff --git a/src/components/Scores.tsx b/src/components/Scores.tsx
new file mode 100644
index 0000000..8f4c67f
--- /dev/null
+++ b/src/components/Scores.tsx
@@ -0,0 +1,28 @@
+import React from 'react';
+import { appBoundClassNames as classNames } from '../common/boundClassNames';
+
+interface Entry {
+ name: string;
+ score: string | React.ReactNode;
+ onMouseOver?: () => void;
+ onMouseOut?: () => void;
+}
+
+interface ScoresProps {
+ entries: Entry[];
+ big?: boolean;
+}
+
+const Scores = ({ entries, big }: ScoresProps) => {
+ return (
+
+ {entries.map((e) => (
+
+ ))}
+
+ );
+};
+export default Scores;
diff --git a/src/components/Scroller.jsx b/src/components/Scroller.jsx
deleted file mode 100644
index 8725bf8..0000000
--- a/src/components/Scroller.jsx
+++ /dev/null
@@ -1,144 +0,0 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-
-import Scrollbar from 'react-scrollbars-custom';
-import { appBoundClassNames as classNames } from '../common/boundClassNames';
-
-class Scroller extends Component {
- constructor(props) {
- super(props);
-
- this.state = {
- isMouseIn: false,
- isScrolling: false,
- };
- }
-
- render() {
- const { onScroll, children, noScrollX, noScrollY, expandTop, expandBottom } = this.props;
- const { isScrolling, isMouseIn } = this.state;
-
- return (
- {
- this.setState({ isMouseIn: true });
- }}
- onMouseLeave={async () => {
- this.setState({ isMouseIn: false });
- }}
- onScroll={() => {
- this.setState({ isScrolling: true });
- if (onScroll) {
- onScroll();
- }
- }}
- onScrollStop={async () => {
- // eslint-disable-next-line no-promise-executor-return
- await new Promise((r) => setTimeout(r, 400));
- this.setState({ isScrolling: false });
- }}
- minimalThumbSize={24}
- noScrollX={noScrollX}
- noScrollY={noScrollY}>
- {children}
-
- );
- }
-}
-
-Scroller.propTypes = {
- onScroll: PropTypes.func,
- noScrollX: PropTypes.bool,
- noScrollY: PropTypes.bool,
- expandTop: PropTypes.number,
- expandBottom: PropTypes.number,
-};
-
-Scroller.defaultProps = {
- noScrollX: true,
- noScrollY: false,
- expandTop: 0,
- expandBottom: 12,
-};
-
-export default Scroller;
diff --git a/src/components/Scroller.tsx b/src/components/Scroller.tsx
new file mode 100644
index 0000000..2f0eb51
--- /dev/null
+++ b/src/components/Scroller.tsx
@@ -0,0 +1,126 @@
+import React, { useState } from 'react';
+import Scrollbar from 'react-scrollbars-custom';
+import { appBoundClassNames as classNames } from '../common/boundClassNames';
+
+interface ScrollerProps {
+ onScroll?: () => void;
+ noScrollX?: boolean;
+ noScrollY?: boolean;
+ expandTop?: number;
+ expandBottom?: number;
+ children: React.ReactNode;
+}
+
+const Scroller = ({
+ onScroll,
+ children,
+ noScrollX = true,
+ noScrollY = false,
+ expandTop = 0,
+ expandBottom = 12,
+}: ScrollerProps) => {
+ const [isMouseIn, setIsMouseIn] = useState(false);
+ const [isScrolling, setIsScrolling] = useState(false);
+
+ const handleScroll = () => {
+ setIsScrolling(true);
+ if (onScroll) {
+ onScroll();
+ }
+ };
+
+ const handleScrollStop = async () => {
+ // Adding delay before resetting scrolling state
+ await new Promise((resolve) => setTimeout(resolve, 400));
+ setIsScrolling(false);
+ };
+
+ return (
+ setIsMouseIn(true)}
+ onMouseLeave={() => setIsMouseIn(false)}
+ onScroll={handleScroll}
+ onScrollStop={handleScrollStop}
+ minimalThumbSize={24}
+ noScrollX={noScrollX}
+ noScrollY={noScrollY}>
+ {children}
+
+ );
+};
+
+export default Scroller;
diff --git a/src/components/SearchFilter.jsx b/src/components/SearchFilter.jsx
deleted file mode 100644
index 2376bc1..0000000
--- a/src/components/SearchFilter.jsx
+++ /dev/null
@@ -1,83 +0,0 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-
-import { appBoundClassNames as classNames } from '../common/boundClassNames';
-
-import SearchFilterEntity from './SearchFilterEntity';
-
-const VALUE_INDEX = 0;
-const LABEL_INDEX = 1;
-const DIMMED_INDEX = 2;
-
-class SearchFilter extends Component {
- _isChecked = (value) => {
- const { checkedValues } = this.props;
- return checkedValues.has(value);
- };
-
- _handleValueCheckedChange = (value, isChecked) => {
- const { isRadio, options, checkedValues, updateCheckedValues } = this.props;
-
- if (isRadio) {
- updateCheckedValues(new Set([value]));
- } else if (isChecked) {
- if (value === 'ALL') {
- updateCheckedValues(new Set(['ALL']));
- } else {
- const checkedValuesCopy = new Set(checkedValues);
- checkedValuesCopy.add(value);
- checkedValuesCopy.delete('ALL');
- updateCheckedValues(checkedValuesCopy);
- }
- } else {
- // eslint-disable-next-line no-lonely-if
- if (value === 'ALL') {
- // Pass
- } else {
- const checkedValuesCopy = new Set(checkedValues);
- checkedValuesCopy.delete(value);
- if (checkedValuesCopy.size === 0 && options.some((o) => o[VALUE_INDEX] === 'ALL')) {
- checkedValuesCopy.add('ALL');
- }
- updateCheckedValues(checkedValuesCopy);
- }
- }
- };
-
- render() {
- const { inputName, titleName, options, checkedValues, isRadio } = this.props;
-
- const mapCircle = (o) => (
- this._handleValueCheckedChange(e.target.value, e.target.checked)}
- isChecked={checkedValues.has(o[VALUE_INDEX])}
- />
- );
-
- return (
-
-
{titleName}
-
{options.map(mapCircle)}
-
- );
- }
-}
-
-SearchFilter.propTypes = {
- updateCheckedValues: PropTypes.func.isRequired,
- inputName: PropTypes.string.isRequired,
- titleName: PropTypes.string.isRequired,
- options: PropTypes.arrayOf(
- PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.bool])),
- ).isRequired,
- checkedValues: PropTypes.instanceOf(Set).isRequired,
- isRadio: PropTypes.bool,
-};
-
-export default SearchFilter;
diff --git a/src/components/SearchFilter.tsx b/src/components/SearchFilter.tsx
new file mode 100644
index 0000000..83d9128
--- /dev/null
+++ b/src/components/SearchFilter.tsx
@@ -0,0 +1,82 @@
+import { appBoundClassNames as classNames } from '../common/boundClassNames';
+import SearchFilterEntity from './SearchFilterEntity';
+
+const VALUE_INDEX = 0;
+const LABEL_INDEX = 1;
+const DIMMED_INDEX = 2;
+
+interface Option {
+ [VALUE_INDEX]: string;
+ [LABEL_INDEX]: string;
+ [DIMMED_INDEX]?: boolean;
+}
+
+interface SearchFilterProps {
+ updateCheckedValues: (values: Set) => void;
+ inputName: string;
+ titleName: string;
+ options: Option[];
+ checkedValues: Set;
+ isRadio?: boolean;
+}
+
+const SearchFilter = ({
+ inputName,
+ titleName,
+ options,
+ checkedValues,
+ updateCheckedValues,
+ isRadio = false,
+}: SearchFilterProps) => {
+ const isChecked = (value: string) => {
+ return checkedValues.has(value);
+ };
+
+ const handleValueCheckedChange = (value: string, isChecked: boolean) => {
+ if (isRadio) {
+ updateCheckedValues(new Set([value]));
+ } else if (isChecked) {
+ if (value === 'ALL') {
+ updateCheckedValues(new Set(['ALL']));
+ } else {
+ const checkedValuesCopy = new Set(checkedValues);
+ checkedValuesCopy.add(value);
+ checkedValuesCopy.delete('ALL');
+ updateCheckedValues(checkedValuesCopy);
+ }
+ } else {
+ if (value === 'ALL') {
+ // Pass
+ } else {
+ const checkedValuesCopy = new Set(checkedValues);
+ checkedValuesCopy.delete(value);
+ if (checkedValuesCopy.size === 0 && options.some((o) => o[VALUE_INDEX] === 'ALL')) {
+ checkedValuesCopy.add('ALL');
+ }
+ updateCheckedValues(checkedValuesCopy);
+ }
+ }
+ };
+
+ const mapCircle = (o: Option) => (
+ handleValueCheckedChange(e.target.value, e.target.checked)}
+ isChecked={isChecked(o[VALUE_INDEX])}
+ />
+ );
+
+ return (
+
+
{titleName}
+
{options.map(mapCircle)}
+
+ );
+};
+
+export default SearchFilter;
diff --git a/src/components/SearchFilterEntity.jsx b/src/components/SearchFilterEntity.jsx
deleted file mode 100644
index c111ed0..0000000
--- a/src/components/SearchFilterEntity.jsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-
-import { appBoundClassNames as classNames } from '../common/boundClassNames';
-
-class SearchFilterEntity extends Component {
- render() {
- const { value, name, label, isRadio, isDimmed, onChange, isChecked } = this.props;
- const isAll = value === 'ALL';
- const inputId = `${name}-${value}`;
- return (
-
- );
- }
-}
-
-SearchFilterEntity.propTypes = {
- value: PropTypes.string.isRequired,
- name: PropTypes.string.isRequired,
- label: PropTypes.string.isRequired,
- isRadio: PropTypes.bool,
- isDimmed: PropTypes.bool,
- onChange: PropTypes.func.isRequired,
- isChecked: PropTypes.bool.isRequired,
-};
-
-export default SearchFilterEntity;
diff --git a/src/components/SearchFilterEntity.tsx b/src/components/SearchFilterEntity.tsx
new file mode 100644
index 0000000..af94fb5
--- /dev/null
+++ b/src/components/SearchFilterEntity.tsx
@@ -0,0 +1,46 @@
+import React from 'react';
+import { appBoundClassNames as classNames } from '../common/boundClassNames';
+
+interface SearchFilterEntityProps {
+ value: string;
+ name: string;
+ label: string;
+ isRadio?: boolean;
+ isDimmed?: boolean;
+ onChange: (e: React.ChangeEvent) => void;
+ isChecked: boolean;
+}
+
+const SearchFilterEntity: React.FC = ({
+ value,
+ name,
+ label,
+ isRadio = false,
+ isDimmed = false,
+ onChange,
+ isChecked,
+}) => {
+ const isAll = value === 'ALL';
+ const inputId = `${name}-${value}`;
+
+ return (
+
+ );
+};
+
+export default SearchFilterEntity;
diff --git a/src/components/sections/dictionary/coursedetail/CourseDetailSection.jsx b/src/components/sections/dictionary/coursedetail/CourseDetailSection.jsx
index ae1af5a..c144e29 100644
--- a/src/components/sections/dictionary/coursedetail/CourseDetailSection.jsx
+++ b/src/components/sections/dictionary/coursedetail/CourseDetailSection.jsx
@@ -24,7 +24,7 @@ import { addCourseRead } from '../../../../redux/actions/dictionary/list';
import courseFocusShape from '../../../../shapes/state/dictionary/CourseFocusShape';
import userShape from '../../../../shapes/model/session/UserShape';
import OtlplusPlaceholder from '../../../OtlplusPlaceholder';
-
+import { Orientation } from '@/shapes/enum';
class CourseDetailSection extends Component {
constructor(props) {
super(props);
@@ -148,11 +148,11 @@ class CourseDetailSection extends Component {
-
+
-
+
-
+
>
diff --git a/src/components/sections/dictionary/courselist/CourseSearchSubSection.jsx b/src/components/sections/dictionary/courselist/CourseSearchSubSection.jsx
index 150e0bc..42ec241 100644
--- a/src/components/sections/dictionary/courselist/CourseSearchSubSection.jsx
+++ b/src/components/sections/dictionary/courselist/CourseSearchSubSection.jsx
@@ -240,7 +240,7 @@ class CourseSearchSubSection extends Component {
{t('ui.button.cancel')}
-
+
);
diff --git a/src/components/sections/planner/courselist/CourseSearchSubSection.jsx b/src/components/sections/planner/courselist/CourseSearchSubSection.jsx
index fbbe59d..a892c77 100644
--- a/src/components/sections/planner/courselist/CourseSearchSubSection.jsx
+++ b/src/components/sections/planner/courselist/CourseSearchSubSection.jsx
@@ -23,6 +23,7 @@ import {
getTermOptions,
} from '../../../../common/searchOptions';
import { performSearchCourses } from '../../../../common/commonOperations';
+import { Orientation } from '@/shapes/enum';
class CourseSearchSubSection extends Component {
constructor(props) {
@@ -235,7 +236,7 @@ class CourseSearchSubSection extends Component {
{t('ui.button.cancel')}
-
+
);
diff --git a/src/components/sections/planner/coursemanage/CourseCustomizeSubSection.jsx b/src/components/sections/planner/coursemanage/CourseCustomizeSubSection.jsx
index e27527a..244f364 100644
--- a/src/components/sections/planner/coursemanage/CourseCustomizeSubSection.jsx
+++ b/src/components/sections/planner/coursemanage/CourseCustomizeSubSection.jsx
@@ -10,7 +10,8 @@ import Scroller from '../../../Scroller';
// import Divider from '../../../Divider';
import SearchFilter from '../../../SearchFilter';
// import CourseStatus from '../../../CourseStatus';
-// import CountController from '../../../CountController';
+// import CountController from '../../../CountController'
+// import { Orientation } from '@/shapes/enum';
import { getSemesterName } from '../../../../utils/semesterUtils';
import { getCourseOfItem, getSemesterOfItem } from '../../../../utils/itemUtils';
@@ -246,7 +247,7 @@ class CourseCustomizeSubSection extends Component {
isRadio={true}
/>
{/* TODO: Implement credit customization */}
- {/*
+ {/*
-
+
)}
@@ -164,8 +165,8 @@ class CourseManageSection extends Component {
-
+
);
diff --git a/src/components/sections/timetable/timetableandinfos/ShareSubSection.jsx b/src/components/sections/timetable/timetableandinfos/ShareSubSection.jsx
index 9e7a381..21e24f3 100644
--- a/src/components/sections/timetable/timetableandinfos/ShareSubSection.jsx
+++ b/src/components/sections/timetable/timetableandinfos/ShareSubSection.jsx
@@ -16,6 +16,7 @@ import timetableShape, {
import userShape from '../../../../shapes/model/session/UserShape';
import Divider from '../../../Divider';
+import { Orientation } from '@/shapes/enum';
class ShareSubSection extends Component {
render() {
@@ -64,7 +65,7 @@ class ShareSubSection extends Component {
@@ -103,7 +104,7 @@ class ShareSubSection extends Component {
diff --git a/src/components/sections/write-reviews/reviewsleft/TakenLecturesSubSection.jsx b/src/components/sections/write-reviews/reviewsleft/TakenLecturesSubSection.jsx
index c0e7a23..4f84634 100644
--- a/src/components/sections/write-reviews/reviewsleft/TakenLecturesSubSection.jsx
+++ b/src/components/sections/write-reviews/reviewsleft/TakenLecturesSubSection.jsx
@@ -21,6 +21,7 @@ import { getSemesterName } from '../../../../utils/semesterUtils';
import userShape from '../../../../shapes/model/session/UserShape';
import lectureShape from '../../../../shapes/model/subject/LectureShape';
+import { Orientation } from '@/shapes/enum';
class TakenLecturesSubSection extends Component {
focusLectureWithClick = (lecture) => {
@@ -71,9 +72,7 @@ class TakenLecturesSubSection extends Component {
{targetSemesters.map((s, i) => (
- {i !== 0 ? (
-
- ) : null}
+ {i !== 0 ? : null}
{`${s.year} ${getSemesterName(
s.semester,
)}`}
diff --git a/src/components/sections/write-reviews/reviewsright/LectureReviewsSubSection.jsx b/src/components/sections/write-reviews/reviewsright/LectureReviewsSubSection.jsx
index c702773..f0a3329 100644
--- a/src/components/sections/write-reviews/reviewsright/LectureReviewsSubSection.jsx
+++ b/src/components/sections/write-reviews/reviewsright/LectureReviewsSubSection.jsx
@@ -22,6 +22,7 @@ import { updateReview as UpdateLatestReview } from '../../../../redux/actions/wr
import userShape from '../../../../shapes/model/session/UserShape';
import reviewsFocusShape from '../../../../shapes/state/write-reviews/ReviewsFocusShape';
+import { Orientation } from '@/shapes/enum';
class LectureReviewsSubSection extends Component {
componentDidMount() {
@@ -121,7 +122,7 @@ class LectureReviewsSubSection extends Component {
pageFrom="Write Reviews"
updateOnSubmit={this.updateOnReviewSubmit}
/>
-
+
{`${t('ui.title.relatedReviews')} - ${
reviewsFocus.lecture[t('js.property.title')]
}`}
diff --git a/src/pages/AccountPage.tsx b/src/pages/AccountPage.tsx
index e1e43cf..f741149 100644
--- a/src/pages/AccountPage.tsx
+++ b/src/pages/AccountPage.tsx
@@ -7,6 +7,7 @@ import MyInfoSubSection from '@/components/sections/account/MyInfoSubSection';
import AcademicInfoSubSection from '@/components/sections/account/AcademicInfoSubSection';
import FavoriteDepartmentsSubSection from '@/components/sections/account/FavoriteDepartmentsSubSection';
import { API_URL } from '@/const';
+import { Orientation } from '@/shapes/enum';
const AccountPage = () => {
const { t } = useTranslation();
@@ -16,11 +17,11 @@ const AccountPage = () => {
-
+
-
+
-
+
{/* TODO: Implement ShareSubSection */}
{/*
diff --git a/src/pages/WriteReviewsPage.jsx b/src/pages/WriteReviewsPage.jsx
index 489cc73..58a1484 100644
--- a/src/pages/WriteReviewsPage.jsx
+++ b/src/pages/WriteReviewsPage.jsx
@@ -28,6 +28,7 @@ import reviewsFocusShape from '../shapes/state/write-reviews/ReviewsFocusShape';
import OtlplusPlaceholder from '../components/OtlplusPlaceholder';
import { useLocation } from 'react-router';
import { parseQueryString } from '@/common/utils/parseQueryString';
+import { Orientation } from '@/shapes/enum';
class WriteReviewsPage extends Component {
componentDidMount() {
@@ -94,9 +95,9 @@ class WriteReviewsPage extends Component {