diff --git a/package.json b/package.json index b9865e394911d59d2897172ee1c9fcb73499b67a..8ec8a46fd52790580ae8d735528a98fec5e3e9c4 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "react-router-dom": "^5.2.0", "react-scripts": "3.4.1", "react-transition-group": "^4.4.1", + "react-use-localstorage": "^3.5.3", "types": "^0.1.1", "typescript": "~3.7.2" }, diff --git a/public/index.html b/public/index.html index dd99758118c907b49512ad510a4c311233e60e2e..e85b7012e1b603c150e0a373b423528bbe2ff44b 100644 --- a/public/index.html +++ b/public/index.html @@ -41,12 +41,5 @@ To create a production bundle, use `npm run build` or `yarn build`. --> <script src="https://cdnjs.cloudflare.com/ajax/libs/holder/2.9.7/holder.js"></script> - <script> - /* functions to set user settings in console*/ - function setInterfaceSize(size) { - document.documentElement.style.fontSize = `${size}%`; - localStorage.setItem("interfaceSize", size); - } - </script> </body> </html> diff --git a/src/components/App.scss b/src/components/App.scss index 4ea60895db385dd84b75c0014b54de324b26932b..bdb3a88b8c63f5c5dcbe37ebadb1c44f0a15f29f 100644 --- a/src/components/App.scss +++ b/src/components/App.scss @@ -58,3 +58,7 @@ code { border-color: #fff; color: #000; } + +.modal-backdrop{ + z-index: 9000; +} diff --git a/src/components/App.tsx b/src/components/App.tsx index eab3b939da6cd0d3f12587143ff6ad5bbb5c1fb0..05d6844ee7fe210734be71360636d36c1dc309f7 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -9,24 +9,32 @@ import { faChalkboardTeacher, } from "@fortawesome/free-solid-svg-icons"; import StandardView from "./pages/StandardView"; +import { Switch, Route } from "react-router-dom"; +import SignIn from "./pages/SignIn"; +import SettingsModal from "./pages/SettingsModal"; type AppState = { toggledLeft: boolean; toggledRight: boolean; + showSettings: boolean; + fileView: string; }; class App extends React.Component<{}, AppState> { width = window.innerWidth; constructor(props: {}) { super(props); - this.state = { toggledLeft: false, toggledRight: false }; + this.state = { + toggledLeft: false, + toggledRight: false, + showSettings: false, + fileView: localStorage.getItem("fileView") || "folder", + }; } componentDidMount() { - let interfaceSize = localStorage.getItem("interfaceSize"); - if (interfaceSize) { - document.documentElement.style.fontSize = `${interfaceSize}%`; - } + document.documentElement.style.fontSize = `${localStorage.getItem("interfaceSize") || "100"}%`; + window.addEventListener("resize", () => { if (window.innerWidth !== this.width) { @@ -71,7 +79,12 @@ class App extends React.Component<{}, AppState> { toggledRight: true, }); } - } + } + + setFileView(view: string){ + this.setState({fileView: view}); + localStorage.setItem("fileView", view); + } render() { const horizontalBarPages = [ @@ -83,29 +96,46 @@ class App extends React.Component<{}, AppState> { return ( <> - <TopBar - pages={horizontalBarPages} - onFavIconClick={(e) => { - e.preventDefault(); - this.toggleLeftBar(); - }} - onUserIconClick={(e) => { - e.preventDefault(); - this.toggleRightBar(); - }} + <SettingsModal + show={this.state.showSettings} + onHide={() => this.setState({ showSettings: false })} + fileView={this.state.fileView} + onCardViewClick={() => this.setFileView("folder")} + onListViewClick={() => this.setFileView("list")} /> - <StandardView - pages={horizontalBarPages} - toggledLeft={this.state.toggledLeft} - toggledRight={this.state.toggledRight} - onOverlayClick={(e) => { - e.preventDefault(); - this.setState({ toggledLeft: false, toggledRight: false }); - }} - /> + <Switch> + <Route path="/signin"> + <SignIn /> + </Route> + + <Route path="/"> + <TopBar + pages={horizontalBarPages} + onFavIconClick={(e) => { + e.preventDefault(); + this.toggleLeftBar(); + }} + onUserIconClick={(e) => { + e.preventDefault(); + this.toggleRightBar(); + }} + /> + + <StandardView + onSettingsClick={() => this.setState({ showSettings: true })} + toggledLeft={this.state.toggledLeft} + toggledRight={this.state.toggledRight} + onOverlayClick={(e) => { + e.preventDefault(); + this.setState({ toggledLeft: false, toggledRight: false }); + }} + fileView={this.state.fileView} + /> - <BottomBar pages={horizontalBarPages} /> + <BottomBar pages={horizontalBarPages} /> + </Route> + </Switch> </> ); } diff --git a/src/components/atoms/PersonCard/style.module.scss b/src/components/atoms/PersonCard/style.module.scss index 23b3652099a2dab59672a6ff1494aeb0b5336a2d..ff63d91262a903223b16ab83c611204a4439bd69 100644 --- a/src/components/atoms/PersonCard/style.module.scss +++ b/src/components/atoms/PersonCard/style.module.scss @@ -74,6 +74,7 @@ justify-content: center; flex-direction: column; align-items: middle; + margin-top: 30px; } .userInfoBlock { @@ -100,6 +101,7 @@ .userCardContainer { display: flex; padding: 0; + margin-top: 30px; } .userInfoBlock { diff --git a/src/components/organisms/RightBar/index.tsx b/src/components/organisms/RightBar/index.tsx index 4f5b2c11d7e3e6bd87d6b20f99d89aacad7595ca..180b897595050075dd34d5dc8c21c082e332a6a7 100644 --- a/src/components/organisms/RightBar/index.tsx +++ b/src/components/organisms/RightBar/index.tsx @@ -3,14 +3,19 @@ import styles from "./style.module.scss"; import CalendarGroup from "components/molecules/CalendarGroup"; import SideBarTabGroup from "components/molecules/SideBarTabGroup"; import { faCog, faSignOutAlt } from "@fortawesome/free-solid-svg-icons"; + export interface RightBarState { date: Date; } -class RightBar extends React.Component<{}, RightBarState> { +export interface RightBarProps { + onSettingsClick: (event: React.MouseEvent) => void; +} + +class RightBar extends React.Component<RightBarProps, RightBarState> { private timerID: number = 0; - constructor(props: {}) { + constructor(props: RightBarProps) { super(props); this.state = { date: new Date() }; } @@ -33,7 +38,8 @@ class RightBar extends React.Component<{}, RightBarState> { let buttons = [ { title: "Settings", - icon: faCog, + icon: faCog, + onClick: this.props.onSettingsClick, }, { title: "Sign Out", diff --git a/src/components/pages/ModuleResources/components/TopSection/index.tsx b/src/components/pages/ModuleResources/components/TopSection/index.tsx deleted file mode 100644 index 0282349055f615112d21e8617bed8396a684d609..0000000000000000000000000000000000000000 --- a/src/components/pages/ModuleResources/components/TopSection/index.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import React from "react"; -import MyBreadcrumbs from "components/atoms/MyBreadcrumbs"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import Button from "react-bootstrap/Button"; -import styles from "./style.module.scss"; -import { faBorderAll, faList } from "@fortawesome/free-solid-svg-icons"; - -export interface TopSectionProps { - onViewButtonClick: (event: React.MouseEvent) => void; - currentView: string; - scope: string; -} - -const TopSection: React.FC<TopSectionProps> = ({ - onViewButtonClick, - currentView, - scope, -}) => { - return ( - <div - style={{ - display: "flex", - justifyContent: "space-between", - alignItems: "center", - marginTop: scope === "" ? "-0.375rem" : "0", - marginBottom: scope === "" ? "-0.375rem" : "0", - }} - > - <MyBreadcrumbs /> - - {scope === "" ? ( - <Button - className={styles.viewToggleButton} - onClick={onViewButtonClick} - variant="secondary" - > - <FontAwesomeIcon - icon={currentView === "folder" ? faBorderAll : faList} - /> - </Button> - ) : null} - </div> - ); -}; - -export default TopSection; diff --git a/src/components/pages/ModuleResources/components/TopSection/style.module.scss b/src/components/pages/ModuleResources/components/TopSection/style.module.scss deleted file mode 100644 index 0ced3aed00314801f8bd08ea3617f88a34eaf062..0000000000000000000000000000000000000000 --- a/src/components/pages/ModuleResources/components/TopSection/style.module.scss +++ /dev/null @@ -1,25 +0,0 @@ -@import "assets/scss/custom"; - -.viewToggleButton { - background-color: $white !important; - // color: $gray-500 !important; - color: $gray-700 !important; - border-width: 0px !important; - justify-content: space-between !important; - height: 2.25rem !important; - box-shadow: none !important; - border-radius: .5rem; - margin-bottom: 1rem; - font-size: 1.05rem; - padding-top: 0; - padding-bottom: 0; -} - -.viewToggleButton:hover{ - background-color: $gray-100 !important; - color: $gray-700 !important; -} - -.buttonIcon { - margin-top: 0.22rem; -} \ No newline at end of file diff --git a/src/components/pages/ModuleResources/index.tsx b/src/components/pages/ModuleResources/index.tsx index d7cd9cb426d8332f4e3ab39e768b0781e884813f..49136e434dc6d481ef0fb0f668ce88160e250306 100644 --- a/src/components/pages/ModuleResources/index.tsx +++ b/src/components/pages/ModuleResources/index.tsx @@ -1,4 +1,5 @@ import React from "react"; +import Spinner from "react-bootstrap/Spinner"; import { request, download } from "../../../utils/api"; import { api, methods } from "../../../constants/routes"; import SearchBox from "components/molecules/SearchBox"; @@ -6,7 +7,6 @@ import QuickAccessView from "./components/QuickAccessView"; import CurrentDirectoryView from "./components/CurrentDirectoryView"; import FoldersView from "./components/FoldersView"; import ListView from "./components/ListView"; -import TopSection from "./components/TopSection"; import { faFileAlt, faFilePdf, @@ -14,6 +14,7 @@ import { faLink, IconDefinition, } from "@fortawesome/free-solid-svg-icons"; +import MyBreadcrumbs from "components/atoms/MyBreadcrumbs"; export interface Resource { title: string; @@ -32,13 +33,13 @@ export interface Folder { export interface ResourcesProps { year: string; moduleID: string; - scope?: string; + scope?: string; + view: string; } export interface ResourceState { error: any; isLoaded: Boolean; - view: string; resources: Resource[]; searchText: string; } @@ -66,9 +67,8 @@ class ModuleResources extends React.Component<ResourcesProps, ResourceState> { this.state = { error: null, isLoaded: false, - view: "folder", resources: [], - searchText: "", + searchText: "" }; } @@ -77,7 +77,7 @@ class ModuleResources extends React.Component<ResourcesProps, ResourceState> { const onSuccess = (data: { json: () => Promise<any> }) => { let resourceArr: Resource[] = []; - data.json().then((json) => { + data.json().then(json => { for (const key in json) { let resource = json[key]; resourceArr.push({ @@ -93,14 +93,14 @@ class ModuleResources extends React.Component<ResourcesProps, ResourceState> { }); }; const onFailure = (error: { text: () => Promise<any> }) => { - error.text().then((errorText) => { + error.text().then(errorText => { this.setState({ error: errorText, isLoaded: true }); }); }; request(api.MATERIALS_RESOURCES, methods.GET, onSuccess, onFailure, { year: this.props.year, - course: this.moduleCode, + course: this.moduleCode }); } @@ -115,7 +115,7 @@ class ModuleResources extends React.Component<ResourcesProps, ResourceState> { if (indices.length === 1) { // Only one file to download, call single file endpoint let filename = this.state.resources.filter( - (document) => document.id === indices[0] + document => document.id === indices[0] )[0].title; download(api.MATERIALS_RESOURCES_FILE(indices[0]), methods.GET, filename); } else { @@ -123,7 +123,7 @@ class ModuleResources extends React.Component<ResourcesProps, ResourceState> { download(api.MATERIALS_ZIPPED_SELECTION, methods.GET, "materials.zip", { ids: indices, course: this.moduleCode, - year: this.props.year, + year: this.props.year }); } } @@ -146,7 +146,7 @@ class ModuleResources extends React.Component<ResourcesProps, ResourceState> { download(api.MATERIALS_ZIPPED, methods.GET, category + ".zip", { year: this.props.year, course: this.moduleCode, - category: category, + category: category }); } @@ -173,7 +173,7 @@ class ModuleResources extends React.Component<ResourcesProps, ResourceState> { }); }; const onFailure = (error: { text: () => Promise<any> }) => { - error.text().then((errorText) => { + error.text().then(errorText => { console.log(errorText); }); }; @@ -189,7 +189,7 @@ class ModuleResources extends React.Component<ResourcesProps, ResourceState> { let rx = /([a-z]+)\(([^)]+)\)/gi; let match: RegExpExecArray | null; let title = item.title.toLowerCase(); - let tags = item.tags.map((tag) => tag.toLowerCase()); + let tags = item.tags.map(tag => tag.toLowerCase()); let type = item.type.toLowerCase(); while ((match = rx.exec(searchText)) !== null) { @@ -201,7 +201,7 @@ class ModuleResources extends React.Component<ResourcesProps, ResourceState> { break; case "tag": let matchSafe = match as RegExpExecArray; - if (!tags.some((tag) => tag === matchSafe[2])) { + if (!tags.some(tag => tag === matchSafe[2])) { return false; } break; @@ -210,32 +210,37 @@ class ModuleResources extends React.Component<ResourcesProps, ResourceState> { } } let rest = searchText.replace(rx, "").trim(); - if (tags.some((tag) => tag.indexOf(rest) !== -1)) { + if (tags.some(tag => tag.indexOf(rest) !== -1)) { return true; } return title.indexOf(rest) !== -1; } getloadedItems() { - if (!this.state.isLoaded) return <>Loading...</>; + if (!this.state.isLoaded) + return ( + <div + style={{ + position: "absolute", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)" + }} + > + <Spinner animation="border" /> + </div> + ); if (this.state.error) return <> Error retrieving data: {this.state.error} </>; return null; } - toggleView() { - if (this.state.view === "folder") { - this.setState({ view: "list" }); - } else { - this.setState({ view: "folder" }); - } - } render() { let scope = this.props.scope || ""; const view = () => { - switch(this.state.view) { + switch(this.props.view) { case "folder": return ( <> <FoldersView @@ -280,14 +285,10 @@ class ModuleResources extends React.Component<ResourcesProps, ResourceState> { }; return ( <> - <TopSection - onViewButtonClick={() => this.toggleView()} - currentView={this.state.view} - scope={scope} - /> + <MyBreadcrumbs /> <SearchBox searchText={this.state.searchText} - onSearchTextChange={(text) => this.setState({ searchText: text })} + onSearchTextChange={text => this.setState({ searchText: text })} /> {this.getloadedItems() || view()} </> diff --git a/src/components/pages/SettingsModal/index.tsx b/src/components/pages/SettingsModal/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..02201b95397f2750df39878fa189ded805c76f22 --- /dev/null +++ b/src/components/pages/SettingsModal/index.tsx @@ -0,0 +1,85 @@ +import React from "react"; +import Modal from "react-bootstrap/Modal"; +import Col from "react-bootstrap/Col"; +import Form from "react-bootstrap/Form"; +import Row from "react-bootstrap/Row"; +import ButtonGroup from "react-bootstrap/ButtonGroup"; +import Button from "react-bootstrap/Button"; +import useLocalStorage from "react-use-localstorage"; +import styles from "./style.module.scss"; +interface Props { + show: boolean; + onHide: any; + fileView: string; + onCardViewClick: (event: React.MouseEvent) => void; + onListViewClick: (event: React.MouseEvent) => void; +} + +const SettingsModal: React.FC<Props> = ({ + show, + onHide, + fileView, + onCardViewClick, + onListViewClick, +}) => { + const [interfaceSize, setInterfaceSize] = useLocalStorage( + "interfaceSize", + "100" + ); + + return ( + <Modal + style={{ zIndex: "10000" }} + dialogClassName={styles.modal} + show={show} + onHide={onHide} + centered + > + <Modal.Header closeButton> + <Modal.Title>Settings</Modal.Title> + </Modal.Header> + <Modal.Body style={{ minHeight: "60vh" }}> + <h5>Interface</h5> + + <Form> + <Form.Group as={Row}> + <Form.Label column xs="9" sm="10"> + Size + </Form.Label> + <Col xs="3" sm="2"> + <Form.Control + value={interfaceSize} + onChange={(e) => setInterfaceSize(e.target.value)} + onBlur={() => + (document.documentElement.style.fontSize = `${interfaceSize}%`) + } + /> + </Col> + </Form.Group> + + <Form.Group style={{ alignItems: "center" }}> + <Form.Label>File View</Form.Label> + <ButtonGroup style={{ float: "right" }}> + <Button + active={fileView === "card"} + variant="secondary" + onClick={onCardViewClick} + > + Card + </Button> + <Button + active={fileView === "list"} + variant="secondary" + onClick={onListViewClick} + > + List + </Button> + </ButtonGroup> + </Form.Group> + </Form> + </Modal.Body> + </Modal> + ); +}; + +export default SettingsModal; diff --git a/src/components/pages/SettingsModal/style.module.scss b/src/components/pages/SettingsModal/style.module.scss new file mode 100644 index 0000000000000000000000000000000000000000..89bd0266ca2a0231743e35a1aed0537fcbaec301 --- /dev/null +++ b/src/components/pages/SettingsModal/style.module.scss @@ -0,0 +1,11 @@ +@import "assets/scss/custom"; + +@media (min-width: 62rem) { + .modal { + width: 50%; + height: 70%; + max-width: none!important; + max-height: none!important; + + } +} \ No newline at end of file diff --git a/src/components/pages/SignIn/index.tsx b/src/components/pages/SignIn/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2820a46a030ed11cbbb2af009bb11823b2131072 --- /dev/null +++ b/src/components/pages/SignIn/index.tsx @@ -0,0 +1,67 @@ +import React from "react"; +import styles from "./style.module.scss"; +import Navbar from "react-bootstrap/Navbar"; +import Container from "react-bootstrap/Container"; +import logo from "assets/images/logo.svg"; +import cx from "classnames"; +import InputGroup from "react-bootstrap/InputGroup"; +import FormControl from "react-bootstrap/FormControl"; +import Button from "react-bootstrap/Button"; + +const SignIn: React.FC = () => { + return ( + <> + <Navbar + className={styles.navBar} + sticky="top" + expand="lg" + variant="light" + > + <Container style={{ display: "flex", justifyContent: "center" }}> + <img + src={logo} + width="30" + height="30" + className={cx("d-inline-block", "align-center")} + alt="Scientia logo" + /> + </Container> + </Navbar> + + <div className={styles.centered}> + <div style={{ marginRight: "15px", marginLeft: "15px", display: "flex", flexDirection: "column", alignItems: "center" }}> + <h1>Scientia</h1> + <i style={{ color: "gray"}}> + A Unified DoC EdTech Platform + </i> + </div> + <div style={{ marginRight: "15px", marginLeft: "15px", marginTop: "20px" }}> + <p className={styles.inputBarHeading}>Username</p> + <InputGroup className="mb-3"> + <FormControl + className={styles.inputBar} + placeholder="abc123" + aria-label="Username" + aria-describedby="basic-addon1" + /> + </InputGroup> + <p className={styles.inputBarHeading}>Password</p> + <InputGroup className="mb-3"> + <FormControl + className={styles.inputBar} + placeholder="Enter your password" + type="password" + aria-label="Password" + aria-describedby="basic-addon1" + /> + </InputGroup> + <Button variant="secondary" className={styles.inputButton}> + Sign In + </Button> + </div> + </div> + </> + ); +}; + +export default SignIn; diff --git a/src/components/pages/SignIn/style.module.scss b/src/components/pages/SignIn/style.module.scss new file mode 100644 index 0000000000000000000000000000000000000000..f6b1d4522afebfe74d3ba0e5e9441874a83f3da2 --- /dev/null +++ b/src/components/pages/SignIn/style.module.scss @@ -0,0 +1,91 @@ +@import "assets/scss/custom"; + +.navBar { + padding: 0.75rem 1rem; + background: #fff; + border-bottom: 0.0625rem solid #cdd4db; +} + +.centered { + position: fixed; + left: 50%; + transform: translate(-50%, 0%); + margin-top: 75px; + max-width: 420px; + width: 90%; +} + + +$button-background: transparentize($gray-200, 0.75); +$button-border: transparentize($gray-300, 1); + +.inputBar { + border-radius: 0.5rem; + border-width: 1px; + background-color: $button-background; + border-color: $button-background; + transition: 0.2s background-color; + -webkit-transition: 0.2s background-color; + -moz-transition: 0.2s back-ground-color; +} + +.inputBar::placeholder { + color: #acb5bd; + opacity: 1; +} + +.inputBar:focus { + box-shadow: none !important; + border-color: $gray-300; + background-color: $white; +} + +.inputBarHeading { + color: $black; + font-size: 18px; + font-weight: 500; + margin-bottom: 10px; +} + +.inputButton.focus, +.inputButton:focus { + color: #000; + background-color: #f8f9fa; + border-color: #0062cc; + box-shadow: none; +} + +.inputButton { + margin-bottom: 0.625rem; + color: #fff; + background: $black; + font-weight: 500; + font-size: 1.05rem; + letter-spacing: 0; + border-width: 0rem; + line-height: 1.375rem; + height: 2.25rem; + border-radius: 0.5rem !important; + transition: 0.2s background; + -webkit-transition: 0.2s background; + -moz-transition: 0.2s background; + text-align: center; + margin-top: 30px; + width: 100%; +} + +.inputButton.active, +.inputButton:active { + color: #fff; + background: #000 !important; + font-weight: 500; + border-width: 0rem; + height: 2.25rem; + line-height: 1.375rem; +} + +.inputButton:hover { + background: $gray-800; + border-color: #fff; + color: $white; +} diff --git a/src/components/pages/StandardView/index.tsx b/src/components/pages/StandardView/index.tsx index 40c8cea46afafe295eb8202a92e5e54b560bed66..d69c8b49e3f6c922d51bad930fcb55ed17c0ab81 100644 --- a/src/components/pages/StandardView/index.tsx +++ b/src/components/pages/StandardView/index.tsx @@ -22,19 +22,19 @@ import ExamGrading from "../Exams/Grading"; import ExamPastPapers from "../Exams/PastPapers"; interface StandardViewProps { - pages: { - name: string; - path: string; - }[]; toggledLeft: boolean; - toggledRight: boolean; - onOverlayClick: (event: React.MouseEvent<HTMLElement>) => void; + toggledRight: boolean; + fileView: string; + onOverlayClick: (event: React.MouseEvent<HTMLElement>) => void; + onSettingsClick: (event: React.MouseEvent) => void; } const StandardView: React.FC<StandardViewProps> = ({ toggledLeft, toggledRight, - onOverlayClick, + onOverlayClick, + onSettingsClick, + fileView, }: StandardViewProps) => { const [modulesFilter, setModulesFilter] = useState("In Progress"); @@ -88,7 +88,8 @@ const StandardView: React.FC<StandardViewProps> = ({ <ModuleResources year="2021" moduleID={props.match.params.id} - scope={props.match.params.scope} + scope={props.match.params.scope} + view={fileView} /> )} /> @@ -132,7 +133,7 @@ const StandardView: React.FC<StandardViewProps> = ({ <Route path="/" render={() => <Redirect to="/dashboard" />} /> </Switch> </Container> - <RightBar /> + <RightBar onSettingsClick={onSettingsClick} /> </div> ); }; diff --git a/yarn.lock b/yarn.lock index b1bda88aa8b3837e8abc0165738e85a0d675527a..3b13ecfdf7eb857c2fb2fc7a898f26f2ecf6f4e6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9252,6 +9252,11 @@ react-transition-group@^4.4.1: loose-envify "^1.4.0" prop-types "^15.6.2" +react-use-localstorage@^3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/react-use-localstorage/-/react-use-localstorage-3.5.3.tgz#c4bcc097859a2d2879e969f0a57b16345905df82" + integrity sha512-1oNvJmo72G4v5P9ytJZZTb6ywD3UzWBiainTtfbNlb+U08hc+SOD5HqgiLTKUF0MxGcIR9JSnZGmBttNLXaQYA== + react@^16.13.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"