diff --git a/public/assets/phy/icons/redesign/quiz-bg.svg b/public/assets/phy/icons/redesign/quiz-bg.svg new file mode 100644 index 0000000000..30a77a1dbc --- /dev/null +++ b/public/assets/phy/icons/redesign/quiz-bg.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/assets/phy/icons/redesign/quiz-fg.svg b/public/assets/phy/icons/redesign/quiz-fg.svg new file mode 100644 index 0000000000..069bbe68de --- /dev/null +++ b/public/assets/phy/icons/redesign/quiz-fg.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/app/components/elements/layout/SidebarLayout.tsx b/src/app/components/elements/layout/SidebarLayout.tsx index 3030d2b9d7..0652572ee3 100644 --- a/src/app/components/elements/layout/SidebarLayout.tsx +++ b/src/app/components/elements/layout/SidebarLayout.tsx @@ -1,5 +1,5 @@ import React, { ChangeEvent, RefObject, useEffect, useRef, useState } from "react"; -import { Col, ColProps, RowProps, Input, Offcanvas, OffcanvasBody, OffcanvasHeader, Row } from "reactstrap"; +import { Col, ColProps, RowProps, Input, Offcanvas, OffcanvasBody, OffcanvasHeader, Row, DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown } from "reactstrap"; import partition from "lodash/partition"; import classNames from "classnames"; import { AssignmentDTO, ContentSummaryDTO, IsaacConceptPageDTO, QuestionDTO, QuizAttemptDTO, RegisteredUserDTO } from "../../../../IsaacApiTypes"; @@ -17,6 +17,7 @@ import { Spacer } from "../Spacer"; import { StyledTabPicker } from "../inputs/StyledTabPicker"; import { GroupSelector } from "../../pages/Groups"; import { QuizRubricButton, SectionProgress } from "../quiz/QuizAttemptComponent"; +import { formatISODateOnly } from "../DateString"; export const SidebarLayout = (props: RowProps) => { const { className, ...rest } = props; @@ -674,3 +675,113 @@ export const SignupSidebar = ({activeTab} : {activeTab: number}) => { ; }; + +interface SetQuizzesSidebarProps extends SidebarProps { + titleFilter?: string; + setTitleFilter: React.Dispatch>; +}; + +export const SetQuizzesSidebar = (props: SetQuizzesSidebarProps) => { + const { titleFilter, setTitleFilter } = props; + const deviceSize = useDeviceSize(); + + return + {above["lg"](deviceSize) &&
} +
Search & Filter
+ Title + setTitleFilter(event.target.value)} + placeholder="Search by title" aria-label="Search by title" + /> + ; +}; + +interface ManageQuizzesSidebarProps extends SidebarProps { + manageQuizzesTitleFilter: string; + setManageQuizzesTitleFilter: React.Dispatch>; + quizStartDate: Date | undefined; + setQuizStartDate: React.Dispatch>; + quizSetDateFilterType: string; + setQuizSetDateFilterType: React.Dispatch>; + quizDueDate: Date | undefined; + setQuizDueDate: React.Dispatch>; + quizDueDateFilterType: string; + setQuizDueDateFilterType: React.Dispatch>; + manageQuizzesGroupNameFilter: string; + setManageQuizzesGroupNameFilter: React.Dispatch>; +}; + +export const ManageQuizzesSidebar = (props: ManageQuizzesSidebarProps) => { + const { manageQuizzesTitleFilter, setManageQuizzesTitleFilter, quizStartDate, setQuizStartDate, + quizSetDateFilterType, setQuizSetDateFilterType, quizDueDate, setQuizDueDate, quizDueDateFilterType, + setQuizDueDateFilterType, manageQuizzesGroupNameFilter, setManageQuizzesGroupNameFilter} = props; + + const deviceSize = useDeviceSize(); + + const dateFilterTypeSelector = (dateFilterType: string, setDateFilterType: React.Dispatch>) => + {dateFilterType} + + setDateFilterType('after')}> + after + + setDateFilterType('before')}> + before + + setDateFilterType('on')}> + on + + + ; + + const titleFilterInput =
+ Title + setManageQuizzesTitleFilter(event.target.value)} + placeholder="Search by title" aria-label="Search by title" + /> +
; + + const groupFilterInput =
+ Group + setManageQuizzesGroupNameFilter(event.target.value)} + placeholder="Search by group" aria-label="Search by group" + /> +
; + + const setDateFilterInput =
+
+ Starting + {dateFilterTypeSelector(quizSetDateFilterType, setQuizSetDateFilterType)} +
+ setQuizStartDate(new Date(event.target.value))} + placeholder="Filter by set date" aria-label="Filter by set date" + /> +
; + + const dueDateFilterInput =
+
+ Due + {dateFilterTypeSelector(quizDueDateFilterType, setQuizDueDateFilterType)} +
+ setQuizDueDate(new Date(event.target.value))} + placeholder="Filter by due date" aria-label="Filter by due date" + /> +
; + + return + {above["lg"](deviceSize) &&
} +
Search & Filter
+ {titleFilterInput} + {groupFilterInput} + {setDateFilterInput} + {dueDateFilterInput} + ; +}; diff --git a/src/app/components/elements/list-groups/ListView.tsx b/src/app/components/elements/list-groups/ListView.tsx index 7f11e24ce7..ee25bcc27a 100644 --- a/src/app/components/elements/list-groups/ListView.tsx +++ b/src/app/components/elements/list-groups/ListView.tsx @@ -95,7 +95,7 @@ export const QuizListViewItem = ({item, isQuizSetter, ...rest}: {item: QuizSumma ; return _compareStrings(a?.group, b?.group); const compareCreationDates = (a: AssignedGroup, b: AssignedGroup) => _compareDates(a?.assignment?.creationDate, b?.assignment?.creationDate); @@ -135,11 +139,11 @@ function QuizAssignment({user, assignedGroups, index}: QuizAssignmentProps) { () => markQuizAsCancelled(assignment.id as number) ); - const determineQuizSubjects = (quizSummary?: QuizSummaryDTO) => { - return quizSummary?.tags?.filter(tag => tags.allSubjectTags.map(t => t.id.valueOf()).includes(tag.toLowerCase())).reduce((acc, tag) => acc + `subject-${tag.toLowerCase()} `, ""); + const determineQuizSubject = (quizSummary?: QuizSummaryDTO) => { + return quizSummary?.tags?.filter(tag => tags.allSubjectTags.map(t => t.id.valueOf()).includes(tag.toLowerCase())).reduce((acc, tag) => acc + `${tag.toLowerCase()}`, ""); }; - const subjects = determineQuizSubjects(assignment.quizSummary) || "subject-physics"; + const subject = determineQuizSubject(assignment.quizSummary) || "physics"; const innerTableHeaders : InnerTableHeader[] = [ {title: "Group name", sort: compareGroupNames}, @@ -158,45 +162,61 @@ function QuizAssignment({user, assignedGroups, index}: QuizAssignmentProps) { setIsExpanded(e => !e)} onKeyDown={ifKeyIsEnter(() => setIsExpanded(e => !e))} > - {isPhy && -
-
-
- - {assignedGroups.length} - group{(!assignedGroups || assignedGroups.length != 1) && "s"} - {assignedGroups.length === 0 ? - "No groups have been assigned." - : (`Test assigned to: ` + assignedGroups.map(g => g.group).join(", "))} - - -
-
-
- } - {isAda && - {assignedGroups.length} 
- group{(!assignedGroups || assignedGroups.length != 1) && "s"} - {assignedGroups.length === 0 ? - "No groups have been assigned." - : (`Test assigned to: ` + assignedGroups.map(g => g.group).join(", "))} - - } - {quizTitle} - - - - + {siteSpecific( + <> + {above["md"](deviceSize) && + + } + + {quizTitle} + + + { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + assignment.quizSummary && dispatch(showQuizSettingModal(assignment.quizSummary)); + e.stopPropagation();}}> + Set Test + + + {above["lg"](deviceSize) && + + } + + , + + <> + + {assignedGroups.length} 
+ group{(!assignedGroups || assignedGroups.length != 1) && "s"} + {assignedGroups.length === 0 ? + "No groups have been assigned." + : (`Test assigned to: ` + assignedGroups.map(g => g.group).join(", "))} + + + {quizTitle} + + + + + + )} + {isExpanded && - + @@ -232,8 +252,8 @@ function QuizAssignment({user, assignedGroups, index}: QuizAssignmentProps) { } @@ -363,156 +383,166 @@ const SetQuizzesPageComponent = ({user}: SetQuizzesPageProps) => { ; return - + - - {{ - [siteSpecific("Set Tests", "Available tests")]: - - {undeprecatedQuizzes && <> -

The following tests are available to set to your groups.

- setTitleFilter(event.target.value)} - placeholder="Search by title" aria-label="Search by title" - /> - {undeprecatedQuizzes.length === 0 &&

There are no tests you can set which match your search term.

} - - {siteSpecific( - , - - - {undeprecatedQuizzes.map(quiz => - -
-
- {quiz.title} - {roleVisibilitySummary(quiz)} -
- - - - - - - Preview - - - - - - Actions - - - dispatch(showQuizSettingModal(quiz))} style={{zIndex: '1'}}> + + {activeTab === MANAGE_QUIZ_TAB.set + ? + : } + + + {{ + [siteSpecific("Set Tests", "Available tests")]: + + {undeprecatedQuizzes && <> +

The following tests are available to set to your groups.

+ + {undeprecatedQuizzes.length === 0 &&

There are no tests you can set which match your search term.

} + + {siteSpecific( + , + + {undeprecatedQuizzes.map(quiz => + +
+
+ {quiz.title} + {roleVisibilitySummary(quiz)} +
+ + + + + + + Preview - - - - - )} - )} - } - , - - [siteSpecific("Manage Tests", "Previously set tests")]: - <> -
- -
- {showFilters && (rowFiltersView - ? - - {titleFilterInput} - {setDateFilterInput} - - - {groupFilterInput} - {dueDateFilterInput} - - - : - {titleFilterInput} - {groupFilterInput} - {setDateFilterInput} - {dueDateFilterInput} - ) - } - Tests you have assigned have failed to load, please try refreshing the page.} - thenRender={quizAssignments => { - let quizAssignmentsWithGroupNames: AppQuizAssignment[] = quizAssignments.map(assignment => { - const groupName = persistence.load(KEY.ANONYMISE_GROUPS) === "YES" - ? `Demo Group ${assignment.groupId}` - : groupIdToName[assignment.groupId as number] ?? "Unknown Group"; - return {...assignment, groupName}; - }).reverse(); - if (showFilters) { - const filters = []; - if (manageQuizzesTitleFilter !== "") { - filters.push((assignment : AppQuizAssignment) => assignment.quizSummary?.title?.toLowerCase().includes(manageQuizzesTitleFilter.toLowerCase())); - } - if (manageQuizzesGroupNameFilter !== "") { - filters.push((assignment : AppQuizAssignment) => assignment.groupName?.toLowerCase().includes(manageQuizzesGroupNameFilter.toLowerCase())); - } - if (quizStartDate && !isNaN(quizStartDate.valueOf())) { - filters.push((assignment : AppQuizAssignment) => { - return filterByDate(quizSetDateFilterType, assignment.scheduledStartDate ?? assignment.creationDate, quizStartDate); - }); - } - if (quizDueDate && !isNaN(quizDueDate.valueOf())) { - filters.push((assignment : AppQuizAssignment) => { - return filterByDate(quizDueDateFilterType, assignment.dueDate, quizDueDate); - }); - } - quizAssignmentsWithGroupNames = quizAssignmentsWithGroupNames.filter(filters.reduce((acc, filter) => (assignment) => acc(assignment) && filter(assignment), () => true)); + + + + + Actions + + + dispatch(showQuizSettingModal(quiz))} style={{zIndex: '1'}}> + {siteSpecific("Set Test", "Set test")} + + + + + Preview + + + + + + + )} + )} + } + , + + [siteSpecific("Manage Tests", "Previously set tests")]: + <> + {isAda &&
+ +
} + + {/* Ada filters */} + {showFilters && (rowFiltersView + ? + + {titleFilterInput} + {setDateFilterInput} + + + {groupFilterInput} + {dueDateFilterInput} + + + : + {titleFilterInput} + {groupFilterInput} + {setDateFilterInput} + {dueDateFilterInput} + ) } - // an array of objects, each representing one test and the groups it is assigned to - const quizAssignment: QuizAssignmentProps[] = quizAssignmentsWithGroupNames.reduce((acc, assignment) => { - const existing = acc.find(q => q.assignedGroups.map(a => a.assignment.quizId).includes(assignment.quizId)); - if (existing) { - existing.assignedGroups.push({group: assignment.groupName, assignment: assignment}); - } else { - acc.push({user: user, assignedGroups: [{group: assignment.groupName, assignment: assignment}], index: 0}); - } - return acc; - }, [] as QuizAssignmentProps[]); - - // sort the outermost table by quiz title - quizAssignment.sort((a, b) => a.assignedGroups[0].assignment.quizSummary?.title?.localeCompare(b.assignedGroups[0].assignment.quizSummary?.title ?? "") ?? 0); - - return <> - {quizAssignments.length === 0 &&

You have not set any tests to your groups yet.

} - {quizAssignments.length > 0 &&
-
- - - - {below["xs"](deviceSize) ? <> : below["lg"](deviceSize) ? : } - - - - {quizAssignment.map((g, i) => )} - -
} - ; - }} - /> - - }} - + Tests you have assigned have failed to load, please try refreshing the page.} + thenRender={quizAssignments => { + let quizAssignmentsWithGroupNames: AppQuizAssignment[] = quizAssignments.map(assignment => { + const groupName = persistence.load(KEY.ANONYMISE_GROUPS) === "YES" + ? `Demo Group ${assignment.groupId}` + : groupIdToName[assignment.groupId as number] ?? "Unknown Group"; + return {...assignment, groupName}; + }).reverse(); + + if (showFilters || isPhy) { + const filters = []; + if (manageQuizzesTitleFilter !== "") { + filters.push((assignment : AppQuizAssignment) => assignment.quizSummary?.title?.toLowerCase().includes(manageQuizzesTitleFilter)); + } + if (manageQuizzesGroupNameFilter !== "") { + filters.push((assignment : AppQuizAssignment) => assignment.groupName?.toLowerCase().includes(manageQuizzesGroupNameFilter.toLowerCase())); + } + if (quizStartDate && !isNaN(quizStartDate.valueOf())) { + filters.push((assignment : AppQuizAssignment) => { + return filterByDate(quizSetDateFilterType, assignment.scheduledStartDate ?? assignment.creationDate, quizStartDate); + }); + } + if (quizDueDate && !isNaN(quizDueDate.valueOf())) { + filters.push((assignment : AppQuizAssignment) => { + return filterByDate(quizDueDateFilterType, assignment.dueDate, quizDueDate); + }); + } + quizAssignmentsWithGroupNames = quizAssignmentsWithGroupNames.filter(filters.reduce((acc, filter) => (assignment) => acc(assignment) && filter(assignment), () => true)); + } + + // an array of objects, each representing one test and the groups it is assigned to + const quizAssignment: QuizAssignmentProps[] = quizAssignmentsWithGroupNames.reduce((acc, assignment) => { + const existing = acc.find(q => q.assignedGroups.map(a => a.assignment.quizId).includes(assignment.quizId)); + if (existing) { + existing.assignedGroups.push({group: assignment.groupName, assignment: assignment}); + } else { + acc.push({user: user, assignedGroups: [{group: assignment.groupName, assignment: assignment}], index: 0}); + } + return acc; + }, [] as QuizAssignmentProps[]); + + // sort the outermost table by quiz title + quizAssignment.sort((a, b) => a.assignedGroups[0].assignment.quizSummary?.title?.localeCompare(b.assignedGroups[0].assignment.quizSummary?.title ?? "") ?? 0); + + return <> + {quizAssignments.length === 0 &&

You have not set any tests to your groups yet.

} + {quizAssignments.length > 0 && + {isAda && + + + {below["xs"](deviceSize) ? <> : below["lg"](deviceSize) ? : } + + } + + {quizAssignment.map((g, i) => )} + +
} + ; + }} + /> + + }} + + + ; }; diff --git a/src/scss/common/quiz.scss b/src/scss/common/quiz.scss index 9e7ed54fed..1ff3de1187 100644 --- a/src/scss/common/quiz.scss +++ b/src/scss/common/quiz.scss @@ -169,9 +169,10 @@ content: ""; background-image: url('/assets/common/icons/chevron_down.svg'); background-repeat: no-repeat; - background-position: center; + background-position: left center; width: 20px; height: 10px; + min-width: 1rem; margin-right: 1rem; margin-top: auto; margin-bottom: auto; @@ -181,12 +182,8 @@ } } - button { - min-width: 150px !important; - - &.btn-sm { - min-width: 100px !important; - } + button.btn.btn { + min-width: max-content; } } diff --git a/src/scss/cs/quiz.scss b/src/scss/cs/quiz.scss index 7dbb5d5ac3..bfe2e6dbb1 100644 --- a/src/scss/cs/quiz.scss +++ b/src/scss/cs/quiz.scss @@ -21,3 +21,13 @@ margin-top: -10px; } } + +.set-quiz-table-dropdown { + button { + min-width: 150px !important; + + &.btn-sm { + min-width: 100px !important; + } + } +} diff --git a/src/scss/phy/boards.scss b/src/scss/phy/boards.scss index 2054a4ce5d..89914b7937 100644 --- a/src/scss/phy/boards.scss +++ b/src/scss/phy/boards.scss @@ -53,6 +53,12 @@ text-align: center; } +.board-bubble-info-sm { + @extend .board-bubble-info; + height: 28px; + font-size: 22px; +} + .board-percent-completed { @extend .board-bubble-info; diff --git a/src/scss/phy/button.scss b/src/scss/phy/button.scss index 1de269fe4a..804624e9b7 100644 --- a/src/scss/phy/button.scss +++ b/src/scss/phy/button.scss @@ -90,7 +90,7 @@ color: var(--buttons-light-text); &:hover, &.btn-dropdown.active { - background: var(--buttons-light-hover); + background-color: var(--buttons-light-hover); color: var(--buttons-dark-text); text-decoration: none; } diff --git a/src/scss/phy/color-theme.scss b/src/scss/phy/color-theme.scss index 7ee600efe5..c332961df0 100644 --- a/src/scss/phy/color-theme.scss +++ b/src/scss/phy/color-theme.scss @@ -70,6 +70,7 @@ --buttons-dark-prefix: #{$color-neutral-200}; --buttons-keyline-border: #{$color-neutral-700}; --buttons-keyline-white-hover: #{$color-brand-700}; + --buttons-light-hover: #{$color-brand-500}; --buttons-light-hover-prefix: #{$color-neutral-200}; --nav-primary: #{$color-brand-500}; diff --git a/src/scss/phy/icons.scss b/src/scss/phy/icons.scss index 01271bb002..adb7e4b206 100644 --- a/src/scss/phy/icons.scss +++ b/src/scss/phy/icons.scss @@ -174,6 +174,14 @@ ); } +.page-icon-quiz { + @include svg-icon-layered( + '/assets/phy/icons/redesign/quiz-fg.svg', + '/assets/phy/icons/redesign/quiz-bg.svg', + 75px, 75px, 44px + ); +} + .page-icon-finder { @include svg-icon-layered( '/assets/phy/icons/redesign/page-finder-fg.svg', @@ -246,6 +254,14 @@ ); } +.list-icon-quiz { + @include svg-icon-layered( + '/assets/phy/icons/redesign/quiz-fg.svg', + '/assets/phy/icons/redesign/quiz-bg.svg', + 60px, 60px, 36px + ); +} + .list-icon-events { @include svg-icon-layered( '/assets/phy/icons/redesign/page-event-fg.svg', diff --git a/src/scss/phy/quiz.scss b/src/scss/phy/quiz.scss index b260332c4f..d03b104dd2 100644 --- a/src/scss/phy/quiz.scss +++ b/src/scss/phy/quiz.scss @@ -1,11 +1,27 @@ @import "../common/quiz.scss"; -.quiz-date-filter-type { - button { - min-width: unset; +.quiz-filter-date-span { + font-size: 1rem; + z-index: 5; +} + +.set-quiz-table-inner { + table-layout: auto; + background-color: white; + color: black; + border: 1px solid $color-neutral-200; + + tbody tr { + background-color: $color-neutral-50; + } + + btn { + border: none; } } -.quiz-filter-date-span { - transform: translateY(3px); -} \ No newline at end of file +.manage-quiz-title { + font-size: 1.25rem; + font-family: $secondary-font-medium; + font-weight: bold; +}