diff --git a/src/assets/scss/global.scss b/src/assets/scss/global.scss index 575583219de3847a51fd3faec10f6d18872bb29e..2f9b6ca2b61c5ea7a9fd462cf53b735800d90085 100644 --- a/src/assets/scss/global.scss +++ b/src/assets/scss/global.scss @@ -20,52 +20,4 @@ $teal-tag-background: transparentize($teal-100, 0.5); .tagTeal { color: $teal-700; background: $teal-tag-background; -} - -// Section header with checkbox settings -.sectionHeaderContainer { - display: flex; - justify-content: space-between; - margin-top: 1.875rem; - align-items: center; -} - -.sectionHeader { - font-weight: 500; - font-size: 20px; - text-transform: uppercase; -} - -.sectionHeaderButton { - background-color: $white; - color: $gray-500; - border-width: 0px; - border-radius: 8px; - margin-left: 20px; - justify-content: space-between; - height: 2.25rem; - transition: 0.2s background-color; - -webkit-transition: 0.2s background-color; - -moz-transition: 0.2s background-color; - font-size: 1.05rem; -} - -.buttonIcon { - margin-top: 0.22rem; -} - -.sectionHeaderButton:global(.active), -.sectionHeaderButton:active { - background: $gray-400 !important; - font-weight: 500; - text-align: left; - border-width: 0rem; - height: 2.25rem; - line-height: 1.375rem; -} - -.sectionHeaderButton:hover, .sectionHeaderButton:focus { - background-color: $gray-200; - color: $gray-700 !important; - box-shadow: none !important; } \ No newline at end of file diff --git a/src/components/molecules/CategoryHeader/index.tsx b/src/components/molecules/CategoryHeader/index.tsx index 8aa7ac14d5d3e54a9681c76d2337822495ceda02..25082f656fd78f03e580e6e00df29cc7bc123db4 100644 --- a/src/components/molecules/CategoryHeader/index.tsx +++ b/src/components/molecules/CategoryHeader/index.tsx @@ -3,20 +3,16 @@ import styles from "./style.module.scss"; import Button from "react-bootstrap/Button"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { IconDefinition } from "@fortawesome/free-solid-svg-icons"; +import { faDownload } from "@fortawesome/free-solid-svg-icons"; export interface CategoryHeaderProps { heading: string; - onSelectAllClick: (event: React.MouseEvent) => void; - selectAllIcon: IconDefinition; - checkBoxColor: string; + onDownloadClick: (event: React.MouseEvent) => void; } const CategoryHeader: React.FC<CategoryHeaderProps> = ({ heading, - onSelectAllClick, - selectAllIcon, - checkBoxColor + onDownloadClick, }: CategoryHeaderProps) => { return ( <> @@ -25,17 +21,19 @@ const CategoryHeader: React.FC<CategoryHeaderProps> = ({ {heading} </span> <div className={styles.sectionHeaderButtonGroup}> - <Button - style={{ color: checkBoxColor }} - className={styles.sectionHeaderButton} - onClick={onSelectAllClick} - variant="secondary" - > - <FontAwesomeIcon - className={styles.buttonIcon} - icon={selectAllIcon} - /> - </Button> + <span id="download-button"> + <Button + variant="secondary" + className={styles.sectionHeaderButton} + onClick={onDownloadClick} + > + <FontAwesomeIcon + className={styles.buttonIcon} + icon={faDownload} + /> + Download Section + </Button> + </span> </div> </div> </> diff --git a/src/components/molecules/CategoryHeader/style.module.scss b/src/components/molecules/CategoryHeader/style.module.scss index e2a1338b52a22d9c9a0018fc9f01e0dbdac30737..35998888521e09f5abcdd730b4e854d7fcc1df9b 100644 --- a/src/components/molecules/CategoryHeader/style.module.scss +++ b/src/components/molecules/CategoryHeader/style.module.scss @@ -1,2 +1,48 @@ @import "assets/scss/custom"; -@import "assets/scss/global"; + +.sectionHeaderContainer { + display: flex; + justify-content: space-between; + margin-top: 1.875rem; + align-items: center; +} + +.sectionHeader { + font-size: 20px; + text-transform: uppercase; +} + +.sectionHeaderButton { + background-color: $white; + color: $gray-500; + border-width: 0px; + border-radius: 8px; + margin-left: 20px; + justify-content: space-between; + height: 2.25rem; + transition: 0.2s background-color; + -webkit-transition: 0.2s background-color; + -moz-transition: 0.2s background-color; + font-size: 1.05rem; +} + +.buttonIcon { + margin-top: 0.22rem; + margin-right: 0.22rem; +} + +.sectionHeaderButton:global(.active), +.sectionHeaderButton:active { + background: $gray-400 !important; + font-weight: 500; + text-align: left; + border-width: 0rem; + height: 2.25rem; + line-height: 1.375rem; +} + +.sectionHeaderButton:hover, .sectionHeaderButton:focus { + background-color: $gray-200; + color: $gray-700 !important; + box-shadow: none !important; +} diff --git a/src/components/molecules/CategoryList/index.tsx b/src/components/molecules/CategoryList/index.tsx index e61877f0d4fc60bbb4018dff1179bed81f0a9a78..f74bd06753d9955caedf960adba01424d5cc13d3 100644 --- a/src/components/molecules/CategoryList/index.tsx +++ b/src/components/molecules/CategoryList/index.tsx @@ -6,9 +6,14 @@ import Row from "react-bootstrap/esm/Row"; import Col from "react-bootstrap/esm/Col"; import Badge from "react-bootstrap/Badge"; import { faSquare, faCheckSquare } from "@fortawesome/free-regular-svg-icons"; +import { + faFileAlt, + IconDefinition, + faFilePdf, + faFileVideo, +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { SelectionProps } from "components/molecules/SelectionView"; -import { resourceTypeToIcon } from "../../pages/ModuleResources" const CategoryList: React.FC<{ select: SelectionProps }> = ({ select, @@ -16,17 +21,29 @@ const CategoryList: React.FC<{ select: SelectionProps }> = ({ return ( <> {select.selectionItems.map(({ title, type, tags, id }) => { + let normalIcon: IconDefinition; if (type === undefined || tags === undefined) return null; + switch (type) { + case "pdf": + normalIcon = faFilePdf; + break; + case "video": + normalIcon = faFileVideo; + break; + default: + normalIcon = faFileAlt; + break; + } let icon = select.isAnySelected() || select.state.isHoveringOver[id] ? select.state.isSelected[id] ? faCheckSquare : faSquare - : resourceTypeToIcon(type); + : normalIcon; return ( <Row - style={{ marginTop: "10px", marginLeft: "0px", marginRight: "0px", cursor: "pointer" }} + style={{ marginTop: "10px", marginLeft: "-10px", marginRight: "-10px", cursor: "pointer" }} onClick={() => select.handleCardClick(id)} onMouseOver={() => select.handleMouseOver(id)} onMouseOut={() => select.handleMouseOut(id)} @@ -45,9 +62,9 @@ const CategoryList: React.FC<{ select: SelectionProps }> = ({ </Badge> ))} </Col> - <Col md="auto" className="px-0"> + <Col md="auto"> <FontAwesomeIcon - style={{ fontSize: "1.125rem", marginRight: "0.5rem" }} + style={{ fontSize: "1.125rem" }} icon={icon} onClick={(e) => { e.stopPropagation(); diff --git a/src/components/molecules/CurrentDirectoryRow/index.tsx b/src/components/molecules/CurrentDirectoryRow/index.tsx index a606063adb53214ab0a4f5cfe1353f7c29fbc05f..e3d51b5cde7f0e9a928bde5f23c07b06335465a0 100644 --- a/src/components/molecules/CurrentDirectoryRow/index.tsx +++ b/src/components/molecules/CurrentDirectoryRow/index.tsx @@ -4,8 +4,13 @@ import Row from "react-bootstrap/esm/Row"; import Col from "react-bootstrap/esm/Col"; import FileCard from "components/atoms/FileCard"; import { faSquare, faCheckSquare } from "@fortawesome/free-regular-svg-icons"; +import { + faFileAlt, + IconDefinition, + faFilePdf, + faFileVideo, +} from "@fortawesome/free-solid-svg-icons"; import { SelectionProps } from "components/molecules/SelectionView"; -import { resourceTypeToIcon } from "../../pages/ModuleResources" const CurrentDirectoryRow: React.FC<{ select: SelectionProps }> = ({ select, @@ -15,8 +20,19 @@ const CurrentDirectoryRow: React.FC<{ select: SelectionProps }> = ({ style={{ marginTop: "10px", marginLeft: "-10px", marginRight: "-10px" }} > {select.selectionItems.map(({ title, type, tags, id }) => { + let normalIcon: IconDefinition; if (type === undefined || tags === undefined) return null; - + switch (type) { + case "pdf": + normalIcon = faFilePdf; + break; + case "video": + normalIcon = faFileVideo; + break; + default: + normalIcon = faFileAlt; + break; + } return ( <Col xs={6} @@ -41,7 +57,7 @@ const CurrentDirectoryRow: React.FC<{ select: SelectionProps }> = ({ ? select.state.isSelected[id] ? faCheckSquare : faSquare - : resourceTypeToIcon(type) + : normalIcon } onClick={() => select.handleCardClick(id)} onIconClick={(e) => { diff --git a/src/components/molecules/QuickAccessRow/index.tsx b/src/components/molecules/QuickAccessRow/index.tsx index 34d1ab888ae6ed2fb977fcc3a21affab7ec39b2b..884f21a2065eccb18b9476d379b33b4d6cea4544 100644 --- a/src/components/molecules/QuickAccessRow/index.tsx +++ b/src/components/molecules/QuickAccessRow/index.tsx @@ -6,8 +6,13 @@ import Row from "react-bootstrap/esm/Row"; import Col from "react-bootstrap/esm/Col"; import FileCard from "components/atoms/FileCard"; import { faSquare, faCheckSquare } from "@fortawesome/free-regular-svg-icons"; +import { + faFileAlt, + IconDefinition, + faFilePdf, + faFileVideo, +} from "@fortawesome/free-solid-svg-icons"; import { SelectionProps } from "components/molecules/SelectionView"; -import { resourceTypeToIcon } from "../../pages/ModuleResources" const QuickAccessRow: React.FC<{ select: SelectionProps }> = ({ select, @@ -22,8 +27,19 @@ const QuickAccessRow: React.FC<{ select: SelectionProps }> = ({ )} > {select.selectionItems.map(({ title, type, tags, id }) => { + let normalIcon: IconDefinition; if (type === undefined || tags === undefined) return null; - + switch (type) { + case "pdf": + normalIcon = faFilePdf; + break; + case "video": + normalIcon = faFileVideo; + break; + default: + normalIcon = faFileAlt; + break; + } return ( <Col xs={7} @@ -48,7 +64,7 @@ const QuickAccessRow: React.FC<{ select: SelectionProps }> = ({ ? select.state.isSelected[id] ? faCheckSquare : faSquare - : resourceTypeToIcon(type) + : normalIcon } onClick={() => select.handleCardClick(id)} onIconClick={(e) => { diff --git a/src/components/molecules/SelectionView/components/SectionHeader/style.module.scss b/src/components/molecules/SelectionView/components/SectionHeader/style.module.scss index 9fee92a7869d578f2c303a30b06ad6dccc395e4b..c74607afbf4941c77272d62edad3f60081f3c864 100644 --- a/src/components/molecules/SelectionView/components/SectionHeader/style.module.scss +++ b/src/components/molecules/SelectionView/components/SectionHeader/style.module.scss @@ -1,5 +1,55 @@ @import "assets/scss/custom"; -@import "assets/scss/global"; + +.sectionHeaderContainer { + display: flex; + justify-content: space-between; + margin-top: 1.875rem; + align-items: center; +} + +.sectionHeader { + font-weight: 500; + font-size: 20px; + cursor: pointer; +} + +.sectionHeaderButton { + background-color: $white; + color: $gray-500; + border-width: 0px; + border-radius: 8px; + margin-left: 20px; + justify-content: space-between; + height: 2.25rem; + transition: 0.2s background-color; + -webkit-transition: 0.2s background-color; + -moz-transition: 0.2s background-color; + font-size: 1.05rem; +} + +.buttonIcon { + margin-top: 0.22rem; +} + +.buttonCheckbox :global(.form-check-input) { + margin: 0px; +} + +.sectionHeaderButton:global(.active), +.sectionHeaderButton:active { + background: $gray-400 !important; + font-weight: 500; + text-align: left; + border-width: 0rem; + height: 2.25rem; + line-height: 1.375rem; +} + +.sectionHeaderButton:hover, .sectionHeaderButton:focus { + background-color: $gray-200; + color: $gray-700 !important; + box-shadow: none !important; +} .alert-enter { opacity: 0; diff --git a/src/components/molecules/SelectionView/index.tsx b/src/components/molecules/SelectionView/index.tsx index b920facb16fd1cf09109fd1c39afcc733d133810..ffdda456edd2397b2e7b8665b9b5bf19e6e1990f 100644 --- a/src/components/molecules/SelectionView/index.tsx +++ b/src/components/molecules/SelectionView/index.tsx @@ -15,7 +15,6 @@ type idBooleanMap = { [key: number]: boolean }; export interface SelectionProps { selectionItems: SelectionItem[]; state: MyState; - setIsSelected: (selection: idBooleanMap) => void; isAnySelected: () => boolean; handleCardClick: (id: number) => void; handleIconClick: (id: number) => void; @@ -127,7 +126,6 @@ class SelectionView extends React.Component<MyProps, MyState> { let selection: SelectionProps = { selectionItems: this.props.selectionItems, state: this.state, - setIsSelected: (selection) => this.setState({ isSelected: selection }), isAnySelected: () => this.isAnySelected(), handleCardClick: (id: number) => this.handleCardClick(id), handleIconClick: (id: number) => this.handleIconClick(id), diff --git a/src/components/pages/ModuleResources/components/FoldersView.tsx b/src/components/pages/ModuleResources/components/FoldersView.tsx index 48455eb46769339c5a6f0c334dc2f8c1b21cfd4e..44dfdb87971315a42bf9802471dc24675559ac9e 100644 --- a/src/components/pages/ModuleResources/components/FoldersView.tsx +++ b/src/components/pages/ModuleResources/components/FoldersView.tsx @@ -1,5 +1,4 @@ import React from "react"; -import { Folder } from "../index"; import SelectionView, { SelectionProps, } from "components/molecules/SelectionView"; @@ -7,17 +6,18 @@ import FoldersRow from "components/molecules/FoldersRow"; import { useHistory, useLocation } from "react-router-dom"; export interface FoldersViewProps { - folders: Folder[]; + folders: { + title: string; + id: number; + }[]; scope: string; searchText: string; - handleFolderDownload: (ids: number[]) => void; } const FoldersView: React.FC<FoldersViewProps> = ({ folders, scope, searchText, - handleFolderDownload }) => { let history = useHistory(); let location = useLocation(); @@ -31,7 +31,7 @@ const FoldersView: React.FC<FoldersViewProps> = ({ return ( <SelectionView heading="Folders" - onDownloadClick={handleFolderDownload} + onDownloadClick={() => {}} onItemClick={handleFolderClick} selectionItems={folders} render={(select: SelectionProps) => <FoldersRow select={select} />} diff --git a/src/components/pages/ModuleResources/components/ListView.tsx b/src/components/pages/ModuleResources/components/ListView.tsx index 8ac1e21676047034d081e59469fc98d553ad2a4e..dc8d5045657187c20bee8de81443d0175db45652 100644 --- a/src/components/pages/ModuleResources/components/ListView.tsx +++ b/src/components/pages/ModuleResources/components/ListView.tsx @@ -1,14 +1,16 @@ import React from "react"; -import { Resource, Folder } from "../index"; +import { Resource } from "../index"; import SelectionView, { SelectionProps, } from "components/molecules/SelectionView"; import CategoryList from "components/molecules/CategoryList"; import CategoryHeader from "components/molecules/CategoryHeader"; -import { faSquare, faCheckSquare } from "@fortawesome/free-regular-svg-icons"; export interface ListViewProps { - folders: Folder[]; + folders: { + title: string; + id: number; + }[]; resources: Resource[]; searchText: string; onDownloadClick: (identifiers: number[]) => void; @@ -21,7 +23,8 @@ const ListView: React.FC<ListViewProps> = ({ folders, resources, searchText, - onDownloadClick, + onDownloadClick, + onSectionDownloadClick, onItemClick, includeInSearchResult }) => { @@ -44,36 +47,14 @@ const ListView: React.FC<ListViewProps> = ({ let categorySelect : SelectionProps = { selectionItems: select.selectionItems.filter(res => res.folder === title), state: select.state, - setIsSelected: select.setIsSelected, isAnySelected: select.isAnySelected, handleCardClick: select.handleCardClick, handleIconClick: select.handleIconClick, handleMouseOver: select.handleMouseOver, handleMouseOut: select.handleMouseOut } - - function isAllSelected() : boolean { - let isSelected = categorySelect.state.isSelected; - return categorySelect.selectionItems.every(item => isSelected[item.id]); - } - - function onSelectAllClick() { - let setValue = !isAllSelected(); - let isSelected = JSON.parse(JSON.stringify(select.state.isSelected)); - let items = categorySelect.selectionItems; - for (let item in items) { - isSelected[items[item].id] = setValue; - } - select.setIsSelected(isSelected); - } - return (<> - <CategoryHeader - heading={title} - onSelectAllClick={onSelectAllClick} - selectAllIcon={isAllSelected() ? faCheckSquare : faSquare} - checkBoxColor={select.isAnySelected() ? "#495057" : "#e9ecef"} - /> + <CategoryHeader heading={title} onDownloadClick={() => onSectionDownloadClick(title)}/> <CategoryList select={categorySelect} /> </>) })} diff --git a/src/components/pages/ModuleResources/index.tsx b/src/components/pages/ModuleResources/index.tsx index d7cd9cb426d8332f4e3ab39e768b0781e884813f..2312c4980b7c9ee0ee31ad7845193c5a8fb87ec0 100644 --- a/src/components/pages/ModuleResources/index.tsx +++ b/src/components/pages/ModuleResources/index.tsx @@ -7,13 +7,6 @@ import CurrentDirectoryView from "./components/CurrentDirectoryView"; import FoldersView from "./components/FoldersView"; import ListView from "./components/ListView"; import TopSection from "./components/TopSection"; -import { - faFileAlt, - faFilePdf, - faFileVideo, - faLink, - IconDefinition, -} from "@fortawesome/free-solid-svg-icons"; export interface Resource { title: string; @@ -21,12 +14,6 @@ export interface Resource { tags: string[]; folder: string; id: number; - path?: string; -} - -export interface Folder { - title: string; - id: number; } export interface ResourcesProps { @@ -43,19 +30,6 @@ export interface ResourceState { searchText: string; } -export function resourceTypeToIcon(type: string): IconDefinition { - switch (type) { - case "pdf": - return faFilePdf; - case "video": - return faFileVideo; - case "link": - return faLink; - default: - return faFileAlt; - } -} - class ModuleResources extends React.Component<ResourcesProps, ResourceState> { moduleCode = this.props.moduleID.startsWith("CO") ? this.props.moduleID.slice(2) @@ -86,7 +60,6 @@ class ModuleResources extends React.Component<ResourcesProps, ResourceState> { tags: resource.tags, folder: resource.category, id: resource.id, - path: resource.path, } as Resource); } this.setState({ resources: resourceArr, isLoaded: true }); @@ -104,13 +77,6 @@ class ModuleResources extends React.Component<ResourcesProps, ResourceState> { }); } - // Gets the unique categories/folders that have been assigned for the resources - folders(): Folder[] { - return Array.from(new Set<string>( - this.state.resources.map((res: Resource) => res.folder)) - ).map((title, id) => ({ title: title, id: id })); - } - handleFileDownload(indices: number[]) { if (indices.length === 1) { // Only one file to download, call single file endpoint @@ -128,20 +94,6 @@ class ModuleResources extends React.Component<ResourcesProps, ResourceState> { } } - handleFolderDownload(ids: number[]) { - let categories = this.folders().filter(folder => folder.id in ids) - .map(folder => folder.title); - if (categories.length === 1) { - this.handleSectionDownload(categories[0]); - } else { - // No endpoint for multiple category download, reuse zipped selection instead - let resourceIds = this.state.resources - .filter(resource => resource.folder in categories) - .map(resource => resource.id); - this.handleFileDownload(resourceIds); - } - } - handleSectionDownload(category: string) { download(api.MATERIALS_ZIPPED, methods.GET, category + ".zip", { year: this.props.year, @@ -150,17 +102,7 @@ class ModuleResources extends React.Component<ResourcesProps, ResourceState> { }); } - handleResourceClick(id: number) { - let resource = this.state.resources.find(resource => resource.id === id); - if (resource === undefined) { - return; - } - if (resource.type === "link") { - window.open(resource.path, "_blank"); - return; - } - - // Resource is of file type, get from Materials + handleFileClick(id: number) { const onSuccess = (data: any) => { // TODO: Try to navigate straight to the endpoint url instead of creating an object url data.blob().then((blob: any) => { @@ -234,48 +176,58 @@ class ModuleResources extends React.Component<ResourcesProps, ResourceState> { render() { let scope = this.props.scope || ""; - const view = () => { - switch(this.state.view) { - case "folder": return ( - <> - <FoldersView - folders={this.folders()} - scope={scope} - searchText={this.state.searchText} - handleFolderDownload={(ids) => this.handleFolderDownload(ids)} - /> - - <CurrentDirectoryView - resources={this.state.resources} - scope={scope} - searchText={this.state.searchText} - onDownloadClick={(ids) => this.handleFileDownload(ids)} - onItemClick={(id) => this.handleResourceClick(id)} - includeInSearchResult={this.includeInSearchResult} - /> + let folders: { title: string; id: number }[] = Array.from( + new Set<string>(this.state.resources.map((res: Resource) => res.folder)) + ).map((title: string, id: number) => ({ + title: title, + id: id, + })); - <QuickAccessView - resources={this.state.resources} - scope={scope} - searchText={this.state.searchText} - onDownloadClick={(ids) => this.handleFileDownload(ids)} - onItemClick={(id) => this.handleResourceClick(id)} - /> - </> - ); - case "list": return ( - <> - <ListView - folders={this.folders()} - resources={this.state.resources} - searchText={this.state.searchText} - onDownloadClick={(ids) => this.handleFileDownload(ids)} - onSectionDownloadClick={(category) => this.handleSectionDownload(category)} - onItemClick={(id) => this.handleResourceClick(id)} - includeInSearchResult={this.includeInSearchResult} - /> - </> - ); + const view = () => { + switch (this.state.view) { + case "folder": + return ( + <> + <FoldersView + folders={folders} + scope={scope} + searchText={this.state.searchText} + /> + + <CurrentDirectoryView + resources={this.state.resources} + scope={scope} + searchText={this.state.searchText} + onDownloadClick={(ids) => this.handleFileDownload(ids)} + onItemClick={(id) => this.handleFileClick(id)} + includeInSearchResult={this.includeInSearchResult} + /> + + <QuickAccessView + resources={this.state.resources} + scope={scope} + searchText={this.state.searchText} + onDownloadClick={(ids) => this.handleFileDownload(ids)} + onItemClick={(id) => this.handleFileClick(id)} + /> + </> + ); + case "list": + return ( + <> + <ListView + folders={folders} + resources={this.state.resources} + searchText={this.state.searchText} + onDownloadClick={(ids) => this.handleFileDownload(ids)} + onSectionDownloadClick={(category) => + this.handleSectionDownload(category) + } + onItemClick={(id) => this.handleFileClick(id)} + includeInSearchResult={this.includeInSearchResult} + /> + </> + ); } }; return (