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 ( -
-
{text}
-
-
-
-
-
-
-
- ); - } -} - -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 ( +
+
{text}
+
+
+
+
+
+
+
+ ); +}; + +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')} -
-
- {CONTACT} -
-
- © 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')} +
+
+ {CONTACT} +
+
+ © 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) => ( -
-
{e.score}
-
{e.name}
-
- ))} -
- ); - } -} - -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) => ( +
+
{e.score}
+
{e.name}
+
+ ))} +
+ ); +}; +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 = () => {