diff --git a/src/IsaacAppTypes.tsx b/src/IsaacAppTypes.tsx index f4c5cb47fa..55f4e75586 100644 --- a/src/IsaacAppTypes.tsx +++ b/src/IsaacAppTypes.tsx @@ -322,7 +322,22 @@ export interface ActiveModal { overflowVisible?: boolean; } -export enum BoardOrder { +export type ProgressSortOrder = number | "name" | "totalQuestionPartPercentage" | "totalQuestionPercentage"; + +export enum QuizzesBoardOrder { + "title" = "title", + "-title" = "-title", + "setBy" = "setBy", + "-setBy" = "-setBy", + "dueDate" = "dueDate", + "-dueDate" = "-dueDate", + "setDate" = "setDate", + "-setDate" = "-setDate", + "startDate" = "startDate", + "-startDate" = "-startDate", +} + +export enum AssignmentBoardOrder { "created" = "created", "-created" = "-created", "visited" = "visited", diff --git a/src/app/components/elements/CardGrid.tsx b/src/app/components/elements/CardGrid.tsx index 0c1f78e5ff..651a9ef3d6 100644 --- a/src/app/components/elements/CardGrid.tsx +++ b/src/app/components/elements/CardGrid.tsx @@ -4,12 +4,12 @@ import { below, useDeviceSize } from '../../services'; export const CardGrid = (props: ContainerProps) => { const deviceSize = useDeviceSize(); - const width = deviceSize === 'xs' ? 1 : below['lg'](deviceSize) ? 2 : 3; + const width = deviceSize === 'xs' ? 1 : below['md'](deviceSize) ? 2 : 3; if (!props.children || !Array.isArray(props.children)) return null; const rows = []; for (let i = 0; i < Math.ceil(props.children.length / width); i++) { - rows.push( + rows.push( {props.children.slice(i * width, (i + 1) * width)} ); } @@ -17,4 +17,4 @@ export const CardGrid = (props: ContainerProps) => { return {rows} ; -}; \ No newline at end of file +}; diff --git a/src/app/components/elements/CollapsibleContainer.tsx b/src/app/components/elements/CollapsibleContainer.tsx new file mode 100644 index 0000000000..aed50abd2f --- /dev/null +++ b/src/app/components/elements/CollapsibleContainer.tsx @@ -0,0 +1,28 @@ +import classNames from "classnames"; +import React, { useLayoutEffect, useRef, useState } from "react"; + +export interface CollapsibleContainerProps extends React.HTMLAttributes { + expanded: boolean; +} + +export const CollapsibleContainer = (props: CollapsibleContainerProps) => { + const {expanded, ...rest} = props; + const [expandedHeight, setExpandedHeight] = useState(0); + + const divRef = useRef(null); + + useLayoutEffect(() => { + if (expanded) { + setExpandedHeight(divRef?.current ? [...divRef.current.children].map(c => + c.getAttribute("data-targetHeight") ? parseInt(c.getAttribute("data-targetHeight") as string) : c.clientHeight + ).reduce((a, b) => a + b, 0) : 0); + } + }, [expanded, props.children]); + + return
; +}; diff --git a/src/app/components/elements/CollapsibleList.tsx b/src/app/components/elements/CollapsibleList.tsx index fbed44923d..812b03c852 100644 --- a/src/app/components/elements/CollapsibleList.tsx +++ b/src/app/components/elements/CollapsibleList.tsx @@ -41,7 +41,7 @@ export const CollapsibleList = (props: CollapsibleListProps) => { {title && {title}} {(props.numberSelected ?? 0) > 0 - && } + && }
diff --git a/src/app/components/elements/Gameboards.tsx b/src/app/components/elements/Gameboards.tsx index d5cf00a744..af1c231156 100644 --- a/src/app/components/elements/Gameboards.tsx +++ b/src/app/components/elements/Gameboards.tsx @@ -1,6 +1,6 @@ import React from "react"; import { Card, CardBody, Button, Row, Col, Table, UncontrolledTooltip, Spinner } from "reactstrap"; -import { BoardOrder, Boards } from "../../../IsaacAppTypes"; +import { AssignmentBoardOrder, Boards } from "../../../IsaacAppTypes"; import { isPhy, siteSpecific, difficultiesOrdered, difficultyShortLabelMap, isAda, BoardViews, BoardCreators, BoardCompletions, matchesAllWordsInAnyOrder, formatBoardOwner, boardCompletionSelection } from "../../services"; import { SortItemHeader } from "./SortableItemHeader"; import { BoardCard } from "./cards/BoardCard"; @@ -21,8 +21,8 @@ export interface GameboardsTableProps { setBoardCreator: (creator: BoardCreators) => void; boardCompletion: BoardCompletions; setBoardCompletion: (boardCompletion: BoardCompletions) => void; - boardOrder: BoardOrder; - setBoardOrder: (boardOrder: BoardOrder) => void; + boardOrder: AssignmentBoardOrder; + setBoardOrder: (boardOrder: AssignmentBoardOrder) => void; } export interface GameboardsCardsProps { @@ -55,7 +55,7 @@ const CSTable = (props: GameboardsTableProps) => { } = props; const tableHeader = - + colSpan={isPhy ? 1 : 4} className={siteSpecific("", "w-100")} defaultOrder={AssignmentBoardOrder.title} reverseOrder={AssignmentBoardOrder["-title"]} currentOrder={boardOrder} setOrder={setBoardOrder} alignment="start"> {siteSpecific("Board name", "Quiz name")} @@ -66,13 +66,13 @@ const CSTable = (props: GameboardsTableProps) => { {isAda && Creator} - + defaultOrder={AssignmentBoardOrder.created} reverseOrder={AssignmentBoardOrder["-created"]} currentOrder={boardOrder} setOrder={setBoardOrder}> Created - + defaultOrder={AssignmentBoardOrder.attempted} reverseOrder={AssignmentBoardOrder["-attempted"]} currentOrder={boardOrder} setOrder={setBoardOrder}> Attempted - + defaultOrder={AssignmentBoardOrder.correct} reverseOrder={AssignmentBoardOrder["-correct"]} currentOrder={boardOrder} setOrder={setBoardOrder}> Correct {siteSpecific( diff --git a/src/app/components/elements/SortableItemHeader.tsx b/src/app/components/elements/SortableItemHeader.tsx index bf7801e9ef..b2b411ef03 100644 --- a/src/app/components/elements/SortableItemHeader.tsx +++ b/src/app/components/elements/SortableItemHeader.tsx @@ -1,12 +1,10 @@ import React, { ComponentProps } from "react"; -import { BoardOrder } from "../../../IsaacAppTypes"; -import { isDefined, siteSpecific, SortOrder } from "../../services"; +import { isDefined, siteSpecific } from "../../services"; import { Spacer } from "./Spacer"; +import { NonUndefined } from "@reduxjs/toolkit/dist/query/tsHelpers"; +import classNames from "classnames"; -export type ProgressSortOrder = number | "name" | "totalQuestionPartPercentage" | "totalQuestionPercentage"; -type Order = BoardOrder | SortOrder | ProgressSortOrder; - -function toggleSort( +function toggleSort( defaultOrder: T, reverseOrder: T, currentOrder: T, @@ -18,7 +16,7 @@ function toggleSort( } } -function sortClass( +function sortClass( defaultOrder: T, reverseOrder: T, currentOrder: T, @@ -35,7 +33,7 @@ function sortClass( } } -export interface SortItemHeaderProps extends ComponentProps<"th"> { +export interface SortItemHeaderProps extends ComponentProps<"th"> { defaultOrder: T, reverseOrder: T, currentOrder: T, @@ -46,7 +44,7 @@ export interface SortItemHeaderProps extends ComponentProps<"th alignment?: "start" | "center" | "end", } -export const SortItemHeader = (props: SortItemHeaderProps) => { +export const SortItemHeader = (props: SortItemHeaderProps>) => { const { defaultOrder, reverseOrder, @@ -68,7 +66,10 @@ export const SortItemHeader = (props: SortItemHeaderProps) = ; - return + return toggleSort(defaultOrder, reverseOrder, currentOrder, setOrder))} + >
{props.children} {justify === "justify-content-start" && } diff --git a/src/app/components/elements/modals/QuestionSearchModal.tsx b/src/app/components/elements/modals/QuestionSearchModal.tsx index b0ce7df52a..ecc5c077a4 100644 --- a/src/app/components/elements/modals/QuestionSearchModal.tsx +++ b/src/app/components/elements/modals/QuestionSearchModal.tsx @@ -255,10 +255,10 @@ export const QuestionSearchModal = ( - className={siteSpecific("w-40", "w-30")} setOrder={sortableTableHeaderUpdateState(questionsSort, setQuestionsSort, "title")} - defaultOrder={SortOrder.ASC as SortOrder} + defaultOrder={SortOrder.ASC} reverseOrder={SortOrder.DESC} currentOrder={questionsSort['title']} alignment="start" diff --git a/src/app/components/elements/quiz/QuizProgressCommon.tsx b/src/app/components/elements/quiz/QuizProgressCommon.tsx index 11883527b5..4efafd4d52 100644 --- a/src/app/components/elements/quiz/QuizProgressCommon.tsx +++ b/src/app/components/elements/quiz/QuizProgressCommon.tsx @@ -1,12 +1,12 @@ import React, {useContext, useLayoutEffect, useMemo, useRef, useState} from "react"; import {Button} from "reactstrap"; -import {AssignmentProgressPageSettingsContext} from "../../../../IsaacAppTypes"; +import {AssignmentProgressPageSettingsContext, ProgressSortOrder} from "../../../../IsaacAppTypes"; import {isAuthorisedFullAccess, siteSpecific, TODAY} from "../../../services"; import {Link} from "react-router-dom"; import orderBy from "lodash/orderBy"; import { IsaacSpinner } from "../../handlers/IsaacSpinner"; import { closeActiveModal, openActiveModal, useAppDispatch, useReturnQuizToStudentMutation } from "../../../state"; -import { ProgressSortOrder, SortItemHeader } from "../SortableItemHeader"; +import { SortItemHeader } from "../SortableItemHeader"; import { AssignmentProgressDTO } from "../../../../IsaacApiTypes"; export const ICON = siteSpecific( @@ -133,9 +133,9 @@ export function ResultsTable({assignmentId, , [semiSortedProgress, reverseOrder, sortOrder]); const tableHeaderFooter = - + className="student-name" defaultOrder={"name"} reverseOrder={"name"} currentOrder={sortOrder} setOrder={toggleSort} reversed={reverseOrder}/> {questions.map((q, index) => - key={q.id} className={`${isSelected(q)}`} defaultOrder={index} reverseOrder={index} currentOrder={sortOrder} setOrder={toggleSort} @@ -147,14 +147,14 @@ export function ResultsTable({assignmentId, )} {isAssignment ? <> - className="total-column left" defaultOrder={"totalQuestionPartPercentage"} reverseOrder={"totalQuestionPartPercentage"} currentOrder={sortOrder} setOrder={toggleSort} reversed={reverseOrder}> Total Parts - className="total-column right" defaultOrder={"totalQuestionPercentage"} reverseOrder={"totalQuestionPercentage"} @@ -162,7 +162,7 @@ export function ResultsTable({assignmentId, Total Qs : - defaultOrder={"totalQuestionPartPercentage"} reverseOrder={"totalQuestionPartPercentage"} currentOrder={sortOrder} setOrder={toggleSort} reversed={reverseOrder} diff --git a/src/app/components/elements/svg/FilterCount.tsx b/src/app/components/elements/svg/FilterCount.tsx index dfc4d77022..dffe9eb9b5 100644 --- a/src/app/components/elements/svg/FilterCount.tsx +++ b/src/app/components/elements/svg/FilterCount.tsx @@ -3,13 +3,19 @@ import {Circle} from "./Circle"; import { siteSpecific } from "../../../services"; import { Hexagon } from "./Hexagon"; -const filterIconWidth = 25; +export interface FilterCountProps extends React.SVGProps { + count: number; + widthPx?: number; +} + +export const FilterCount = (props: FilterCountProps) => { + const {count, widthPx, ...rest} = props; + const filterIconWidth = widthPx || 25; -export const FilterCount = ({count}: {count: number}) => { return {`${count} filters selected`} diff --git a/src/app/components/elements/tables/TableLinks.tsx b/src/app/components/elements/tables/TableLinks.tsx new file mode 100644 index 0000000000..6eca60b2b8 --- /dev/null +++ b/src/app/components/elements/tables/TableLinks.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import classNames from "classnames"; + +interface TdLinkProps extends React.HTMLAttributes { + to: string | undefined; +} + +interface TrLinkProps extends React.HTMLAttributes { + to: string | undefined; +} + +// an anywhere between a and a ; +}; + +// this wrapper exists such that you only need specify the link prop once per row. +// just drop in place of a . +export const TrLink = ({to, children, ...rest}: TrLinkProps) => { + return + {React.Children.map(children, child => React.isValidElement(child) && child.type === "td" ? : child)} + ; +}; diff --git a/src/app/components/pages/AssignmentSchedule.tsx b/src/app/components/pages/AssignmentSchedule.tsx index af9bafaacc..4bf492c2c6 100644 --- a/src/app/components/pages/AssignmentSchedule.tsx +++ b/src/app/components/pages/AssignmentSchedule.tsx @@ -46,7 +46,7 @@ import { TODAY, useDeviceSize } from "../../services"; -import {AppGroup, AssignmentScheduleContext, BoardOrder, ValidAssignmentWithListingDate} from "../../../IsaacAppTypes"; +import {AppGroup, AssignmentScheduleContext, AssignmentBoardOrder, ValidAssignmentWithListingDate} from "../../../IsaacAppTypes"; import {calculateHexagonProportions, Hexagon} from "../elements/svg/Hexagon"; import classNames from "classnames"; import {currentYear, DateInput} from "../elements/inputs/DateInput"; @@ -522,7 +522,7 @@ type AssignmentsGroupedByDate = [number, [number, [number, ValidAssignmentWithLi export const AssignmentSchedule = ({user}: {user: RegisteredUserDTO}) => { const assignmentsSetByMeQuery = useGetMySetAssignmentsQuery(undefined); const { data: assignmentsSetByMe } = assignmentsSetByMeQuery; - const { data: gameboards } = useGetGameboardsQuery({startIndex: 0, limit: BoardLimit.All, sort: BoardOrder.created}); + const { data: gameboards } = useGetGameboardsQuery({startIndex: 0, limit: BoardLimit.All, sort: AssignmentBoardOrder.created}); const { data: groups } = useGetGroupsQuery(false); const [viewBy, setViewBy] = useState<"startDate" | "dueDate">("startDate"); diff --git a/src/app/components/pages/MyGameboards.tsx b/src/app/components/pages/MyGameboards.tsx index 3fe78d1463..aa4d0e3656 100644 --- a/src/app/components/pages/MyGameboards.tsx +++ b/src/app/components/pages/MyGameboards.tsx @@ -8,7 +8,7 @@ import { Input, Label, Row} from 'reactstrap'; -import {BoardOrder} from "../../../IsaacAppTypes"; +import {AssignmentBoardOrder} from "../../../IsaacAppTypes"; import {GameboardDTO, RegisteredUserDTO} from "../../../IsaacApiTypes"; import {TitleAndBreadcrumb} from "../elements/TitleAndBreadcrumb"; import { @@ -33,8 +33,8 @@ export interface GameboardsDisplaySettingsProps { switchViewAndClearSelected: (e: React.ChangeEvent) => void, boardLimit: BoardLimit, setBoardLimit: (limit: BoardLimit) => void, - boardOrder: BoardOrder, - setBoardOrder: (order: BoardOrder) => void, + boardOrder: AssignmentBoardOrder, + setBoardOrder: (order: AssignmentBoardOrder) => void, showFilters: boolean, setShowFilters: React.Dispatch>, } @@ -57,8 +57,8 @@ const GameboardsDisplaySettings = ({boardView, switchViewAndClearSelected, board diff --git a/src/app/components/pages/SetAssignments.tsx b/src/app/components/pages/SetAssignments.tsx index 24b65a3c90..19fe7087dc 100644 --- a/src/app/components/pages/SetAssignments.tsx +++ b/src/app/components/pages/SetAssignments.tsx @@ -63,7 +63,7 @@ import { } from "../../services"; import {IsaacSpinner, Loading} from "../handlers/IsaacSpinner"; import {GameboardDTO, RegisteredUserDTO, UserGroupDTO} from "../../../IsaacApiTypes"; -import {BoardAssignee, BoardOrder, Boards} from "../../../IsaacAppTypes"; +import {BoardAssignee, AssignmentBoardOrder, Boards} from "../../../IsaacAppTypes"; import {BoardCard} from "../elements/cards/BoardCard"; import classNames from "classnames"; import {StyledSelect} from "../elements/inputs/StyledSelect"; @@ -246,8 +246,8 @@ interface SetAssignmentsTableProps { setBoardTitleFilter: (title: string) => void; boardCreator: BoardCreators; setBoardCreator: (creator: BoardCreators) => void; - boardOrder: BoardOrder; - setBoardOrder: (boardOrder: BoardOrder) => void; + boardOrder: AssignmentBoardOrder; + setBoardOrder: (boardOrder: AssignmentBoardOrder) => void; groupsByGameboard: {[p: string]: BoardAssignee[]}; openAssignModal: (board: GameboardDTO) => void; } @@ -263,7 +263,7 @@ const PhyTable = (props: SetAssignmentsTableProps) => { const tableHeader = - + defaultOrder={AssignmentBoardOrder.title} reverseOrder={AssignmentBoardOrder["-title"]} currentOrder={boardOrder} setOrder={setBoardOrder} alignment="start"> Board name - + defaultOrder={AssignmentBoardOrder.visited} reverseOrder={AssignmentBoardOrder["-visited"]} currentOrder={boardOrder} setOrder={setBoardOrder}> Last viewed @@ -348,7 +348,7 @@ const CSTable = (props: SetAssignmentsTableProps) => { const tableHeader = - + colSpan={2} defaultOrder={AssignmentBoardOrder.title} reverseOrder={AssignmentBoardOrder["-title"]} currentOrder={boardOrder} setOrder={setBoardOrder}> Quiz name - + defaultOrder={AssignmentBoardOrder.visited} reverseOrder={AssignmentBoardOrder["-visited"]} currentOrder={boardOrder} setOrder={setBoardOrder}> Last viewed @@ -575,8 +575,8 @@ export const SetAssignments = () => { diff --git a/src/app/components/pages/quizzes/MyQuizzes.tsx b/src/app/components/pages/quizzes/MyQuizzes.tsx index 204f372183..c4eaa7ab14 100644 --- a/src/app/components/pages/quizzes/MyQuizzes.tsx +++ b/src/app/components/pages/quizzes/MyQuizzes.tsx @@ -1,213 +1,305 @@ -import React, { useEffect, useState } from "react"; +import React, { ReactNode, useCallback, useEffect, useState } from "react"; import { useGetAttemptedFreelyByMeQuery, useGetQuizAssignmentsAssignedToMeQuery } from "../../../state"; import {Link, RouteComponentProps, useHistory, withRouter} from "react-router-dom"; -import * as RS from "reactstrap"; import {ShowLoading} from "../../handlers/ShowLoading"; -import {QuizAssignmentDTO, QuizAttemptDTO, QuizSummaryDTO, RegisteredUserDTO} from "../../../../IsaacApiTypes"; +import {RegisteredUserDTO} from "../../../../IsaacApiTypes"; import {TitleAndBreadcrumb} from "../../elements/TitleAndBreadcrumb"; import {formatDate} from "../../elements/DateString"; -import {AppQuizAssignment} from "../../../../IsaacAppTypes"; +import {QuizzesBoardOrder} from "../../../../IsaacAppTypes"; import { + above, + convertAssignmentToQuiz, + convertAttemptToQuiz, + DisplayableQuiz, extractTeacherName, - isAttempt, - isEventLeaderOrStaff, - isFound, + isPhy, isTutorOrAbove, - partitionCompleteAndIncompleteQuizzes, - siteSpecific + QuizStatus, + selectOnChange, + siteSpecific, + useDeviceSize } from "../../../services"; import {Spacer} from "../../elements/Spacer"; import {Tabs} from "../../elements/Tabs"; -import {useGetAvailableQuizzesQuery} from "../../../state"; import {PageFragment} from "../../elements/PageFragment"; import { CardGrid } from "../../elements/CardGrid"; -import partition from "lodash/partition"; +import { SortItemHeader } from "../../elements/SortableItemHeader"; +import { Card, CardBody, Button, Table, Container, Alert, Row, Col, Label, Input } from "reactstrap"; +import orderBy from "lodash/orderBy"; +import classNames from "classnames"; +import StyledToggle from "../../elements/inputs/StyledToggle"; +import { TrLink } from "../../elements/tables/TableLinks"; +import { StyledDropdown } from "../../elements/inputs/DropdownInput"; +import { StyledSelect } from "../../elements/inputs/StyledSelect"; +import { CollapsibleContainer } from "../../elements/CollapsibleContainer"; +import { FilterCount } from "../../elements/svg/FilterCount"; -interface MyQuizzesPageProps extends RouteComponentProps { +export interface QuizzesPageProps extends RouteComponentProps { user: RegisteredUserDTO; } -type Quiz = AppQuizAssignment | QuizAttemptDTO; - interface QuizAssignmentProps { - item: Quiz; -} - -enum Status { - Unstarted, Started, Complete + quiz: DisplayableQuiz; } -const todaysDate = new Date(new Date().setHours(0, 0, 0, 0)); - -function QuizItem({item}: QuizAssignmentProps) { - const assignment = isAttempt(item) ? null : item; - const attempt = isAttempt(item) ? item : assignment?.attempt; - const status: Status = !attempt ? Status.Unstarted : !attempt.completedDate ? Status.Started : Status.Complete; - const assignmentStartDate = assignment?.scheduledStartDate ?? assignment?.creationDate; - const overdue = (status !== Status.Complete && assignment?.dueDate) ? (todaysDate > assignment.dueDate) : false; +function QuizItem({quiz}: QuizAssignmentProps) { + const assignmentStartDate = quiz.startDate ?? quiz.creationDate; return
- - -

{item.quizSummary?.title || item.quizId }

- - {assignment - ? assignment.dueDate &&

Due date: {formatDate(assignment.dueDate)}

- : attempt && siteSpecific( -

Freely {status === Status.Started ? "attempting" : "attempted"}

, -

{status === Status.Started ? "Attempting" : "Attempted"} independently

+ + +

{quiz.title || quiz.id }

+ + {quiz.isAssigned + ? quiz.dueDate &&

Due date: {formatDate(quiz.dueDate)}

+ : quiz.attempt && siteSpecific( +

Freely {quiz.status === QuizStatus.Started ? "attempting" : "attempted"}

, +

{quiz.status === QuizStatus.Started ? "Attempting" : "Attempted"} independently

) } - {assignment &&

+ {quiz.isAssigned &&

Set: {formatDate(assignmentStartDate)} - {assignment.assignerSummary && <> by {extractTeacherName(assignment.assignerSummary)}} + {quiz.assignerSummary && <> by {extractTeacherName(quiz.assignerSummary)}}

} - {attempt &&

- {status === Status.Complete ? - `Completed: ${formatDate(attempt.completedDate)}` - : `Started: ${formatDate(attempt.startDate)}` + {quiz.attempt &&

+ {quiz.status === QuizStatus.Complete ? + `Completed: ${formatDate(quiz.attempt.completedDate)}` + : `Started: ${formatDate(quiz.attempt.startDate)}` }

}
- {assignment ? <> - {status === Status.Unstarted && !overdue && + {quiz.isAssigned ? <> + {quiz.status === QuizStatus.NotStarted && } + {quiz.status === QuizStatus.Started && } + {quiz.status === QuizStatus.Overdue && } + {quiz.status === QuizStatus.Complete && ( + )} - : attempt && <> - {status === Status.Started && + : quiz.attempt && <> + {quiz.status === QuizStatus.Started && } + {quiz.status === QuizStatus.Complete && ( + )} }
-
-
+ +
; } interface AssignmentGridProps { - quizzes: Quiz[]; - empty: string; + quizzes: DisplayableQuiz[]; + emptyMessage: ReactNode; } -function QuizGrid({quizzes, empty}: AssignmentGridProps) { +function QuizGrid({quizzes, emptyMessage}: AssignmentGridProps) { return <> - {quizzes.length === 0 &&

{empty}

} + {quizzes.length === 0 &&

{emptyMessage}

} {quizzes.length > 0 && - {quizzes.map(item => )} + {quizzes.map(quiz => )} } ; } -const MyQuizzesPageComponent = ({user}: MyQuizzesPageProps) => { +// To avoid the chaos of QuizProgressCommon, this and PracticeQuizTable are **separate components**. Despite this repeating some code, please don't try to merge them. +const AssignedQuizTable = ({quizzes, boardOrder, setBoardOrder, emptyMessage}: {quizzes: DisplayableQuiz[], boardOrder: QuizzesBoardOrder, setBoardOrder: (order: QuizzesBoardOrder) => void, emptyMessage: ReactNode}) => { + + return
is illegal, so a row can't be wrapped in a . instead, each contains a link. +export const TdLink = ({to, children, ...rest}: TdLinkProps) => { + return {to ? {children} : <>{children}}
Groups @@ -274,7 +274,7 @@ const PhyTable = (props: SetAssignmentsTableProps) => { Challenge: {difficultiesOrdered.slice(2).map(d => difficultyShortLabelMap[d]).join(", ")} Manage
Groups @@ -359,7 +359,7 @@ const CSTable = (props: SetAssignmentsTableProps) => { Creator Manage
+ + + + + + + + + + defaultOrder={QuizzesBoardOrder.title} reverseOrder={QuizzesBoardOrder["-title"]} currentOrder={boardOrder} setOrder={setBoardOrder} alignment="start">Title + defaultOrder={QuizzesBoardOrder.setBy} reverseOrder={QuizzesBoardOrder["-setBy"]} currentOrder={boardOrder} setOrder={setBoardOrder} alignment="start">Set by + defaultOrder={QuizzesBoardOrder.dueDate} reverseOrder={QuizzesBoardOrder["-dueDate"]} currentOrder={boardOrder} setOrder={setBoardOrder} alignment="start">Due Date + defaultOrder={QuizzesBoardOrder.setDate} reverseOrder={QuizzesBoardOrder["-setDate"]} currentOrder={boardOrder} setOrder={setBoardOrder} alignment="start">Set Date + + + + {quizzes.map(quiz => { + return + + + + + + ; + })} + {quizzes.length === 0 && + + } + +
{/* chevrons */} +
+
+ {quiz.title || quiz.id}
+ {quiz.status === QuizStatus.Overdue && Overdue} + {quiz.status === QuizStatus.Started && Started} + {quiz.status === QuizStatus.NotStarted && Not started} + {quiz.status === QuizStatus.Complete && <> + Completed · + {quiz.quizFeedbackMode === "NONE" ? No feedback available + : Feedback available + } + } +
+
{quiz.assignerSummary && extractTeacherName(quiz.assignerSummary)}{quiz.dueDate && formatDate(quiz.dueDate)}{quiz.setDate && formatDate(quiz.setDate)}
{emptyMessage}
; +}; + +const PracticeQuizTable = ({quizzes, boardOrder, setBoardOrder, emptyMessage}: {quizzes: DisplayableQuiz[], boardOrder: QuizzesBoardOrder, setBoardOrder: (order: QuizzesBoardOrder) => void, emptyMessage: ReactNode}) => { + return + + + + + + + + defaultOrder={QuizzesBoardOrder.title} reverseOrder={QuizzesBoardOrder["-title"]} currentOrder={boardOrder} setOrder={setBoardOrder} alignment="start">Title + defaultOrder={QuizzesBoardOrder.startDate} reverseOrder={QuizzesBoardOrder["-startDate"]} currentOrder={boardOrder} setOrder={setBoardOrder} alignment="start">Start Date + + + + {quizzes.map(quiz => { + return + + + + ; + })} + {quizzes.length === 0 && + + } + +
{/* chevrons */} +
+
+ {quiz.title || quiz.id} + {quiz.status === QuizStatus.Complete && Completed} +
+
{formatDate(quiz.startDate)}
{emptyMessage}
; +}; + +interface QuizFiltersProps { + setShowCompleted: (show: boolean) => void; + setQuizCreator: (creator: string) => void; + setQuizTitleFilter: (title: string) => void; + quizStatuses: QuizStatus[]; + setQuizStatuses: React.Dispatch>; + showFilters: boolean; +} + +const QuizFilters = ({setShowCompleted, setQuizTitleFilter, setQuizCreator, quizStatuses, setQuizStatuses, showFilters}: QuizFiltersProps) => { + return + + + + + + + + + + + + + + ; +}; + +const MyQuizzesPageComponent = ({user}: QuizzesPageProps) => { - const {data: quizzes} = useGetAvailableQuizzesQuery(0); const {data: quizAssignments} = useGetQuizAssignmentsAssignedToMeQuery(); const {data: freeAttempts} = useGetAttemptedFreelyByMeQuery(); + const [displayMode, setDisplayMode] = useState<"table" | "cards">("table"); + const [boardOrder, setBoardOrder] = useState(QuizzesBoardOrder.dueDate); + const [showCompleted, setShowCompleted] = useState(false); + + const deviceSize = useDeviceSize(); + + const [showFilters, setShowFilters] = useState(false); + const [quizTitleFilter, setQuizTitleFilter] = useState(""); + const [quizCreatorFilter, setQuizCreator] = useState(""); + const [quizStatusFilter, setQuizStatuses] = useState([QuizStatus.NotStarted, QuizStatus.Started]); + + const sortQuizzesByOrder = useCallback((quizzes: DisplayableQuiz[]) => { + // if we're in table mode, sort by the order set by the user via the columns (boardOrder). + // if we're in cards mode, sort by the default order: due date, then set date, then title. + return displayMode === "table" ? orderBy( + quizzes, + [boardOrder.valueOf().charAt(0) === "-" ? boardOrder.valueOf().slice(1) : boardOrder, "title"], + [boardOrder.valueOf().charAt(0) === "-" ? "desc" : "asc", "asc"] + ) : orderBy(quizzes, [ + (q) => q.dueDate, + (q) => q.setDate, + (q) => q.title ?? "" + ], ["asc", "asc", "asc"]); + }, [boardOrder, displayMode]); + const pageHelp = Use this page to see tests you need to take and your test results.
You can also take some tests freely whenever you want to test your knowledge.
; - function sortCurrentQuizzes(a : QuizAssignmentDTO, b : QuizAssignmentDTO) { - // Compare by due date (or lack of due date) if possible - if (a.dueDate && b.dueDate) { - if (a.dueDate < b.dueDate) { - return -1; - } - if (a.dueDate > b.dueDate) { - return 1; - } - } - else if (a.dueDate) { - return -1; - } - else if (b.dueDate) { - return 1; - } - // Otherwise compare by set date - if (a.creationDate && b.creationDate) { - if (a.creationDate < b.creationDate) { - return -1; - } - if (a.creationDate > b.creationDate) { - return 1; - } - } - return 0; - } - - function sortCompletedQuizzes(a : QuizAssignmentDTO, b : QuizAssignmentDTO) { - // Compare by completion date; if incomplete (i.e. overdue), use due date instead - const aDate = a.attempt?.completedDate ?? a.dueDate ?? 0; - const bDate = b.attempt?.completedDate ?? b.dueDate ?? 0; - if (aDate < bDate) { - return -1; - } - if (aDate > bDate) { - return 1; - } - return 0; - } - - const [completedQuizzes, incompleteQuizzes] = quizAssignments ? partitionCompleteAndIncompleteQuizzes(quizAssignments) : [[], []]; - const [overdueQuizzes, currentQuizzes] = partition(incompleteQuizzes, a => a.dueDate ? todaysDate > a.dueDate : false); - const sortedCurrentQuizzes = [...currentQuizzes].sort(sortCurrentQuizzes); - - const [completedFreeAttempts, currentFreeAttempts] = partitionCompleteAndIncompleteQuizzes(freeAttempts ?? []); - const sortedCurrentFreeAttempts = [...currentFreeAttempts].sort(sortCurrentQuizzes); - - const completedOrOverdueQuizzes = [ - ...isFound(overdueQuizzes) ? overdueQuizzes : [], - ...isFound(completedQuizzes) ? completedQuizzes : [], - ...isFound(completedFreeAttempts) ? completedFreeAttempts : [] - ]; - const sortedCompletedOrOverdueQuizzes = [...completedOrOverdueQuizzes].sort(sortCompletedQuizzes); - - const showQuiz = (quiz: QuizSummaryDTO) => { - switch (user.role) { - case "STUDENT": - // Tutors should see the same tests as students can - // eslint-disable-next-line no-fallthrough - case "TUTOR": - return (quiz.hiddenFromRoles && !quiz.hiddenFromRoles?.includes("STUDENT")) || quiz.visibleToStudents; - case "TEACHER": - return (quiz.hiddenFromRoles && !quiz.hiddenFromRoles?.includes("TEACHER")) ?? true; - default: - return true; - } + const quizMatchesFilters = (quiz: DisplayableQuiz | undefined) : quiz is DisplayableQuiz => { + if (!quiz) return false; + const titleMatches = !quizTitleFilter || (quiz.title?.toLowerCase().includes(quizTitleFilter.toLowerCase()) ?? true); + const creatorMatches = !quizCreatorFilter || (extractTeacherName(quiz.assignerSummary)?.toLowerCase().includes(quizCreatorFilter.toLowerCase()) ?? true); + const statusMatches = !quizStatusFilter || (!!quiz.status && quizStatusFilter.includes(quiz.status)); + return titleMatches && creatorMatches && statusMatches; }; - // If the user is event admin or above, and the quiz is hidden from teachers, then show that - // If the user is teacher or above, show if the quiz is visible to students - const roleVisibilitySummary = (quiz: QuizSummaryDTO) => <> - {isEventLeaderOrStaff(user) && quiz.hiddenFromRoles && quiz.hiddenFromRoles?.includes("TEACHER") &&
hidden from teachers
} - {isTutorOrAbove(user) && ((quiz.hiddenFromRoles && !quiz.hiddenFromRoles?.includes("STUDENT")) || quiz.visibleToStudents) &&
visible to students
} - ; + // quizAssignments are quizzes; they have a start date, due date, assignee, etc. They can only be completed once, i.e. have a single attempt inside the object. + // freeAttempts is a list of attempts at a quiz, i.e. they are not quizzes themselves. We want to display them the same, though, so we must sort this type discrepancy out first. + const [assignedQuizzes, practiceQuizzes] = [quizAssignments?.map(convertAssignmentToQuiz).filter(quizMatchesFilters) ?? [], freeAttempts?.map(convertAttemptToQuiz).filter(quizMatchesFilters) ?? []]; + + const sortedAssignedQuizzes = sortQuizzesByOrder(assignedQuizzes); + const sortedPracticeQuizzes = sortQuizzesByOrder(practiceQuizzes); const tabAnchors = ["#in-progress", "#completed", "#practice"]; @@ -222,82 +314,118 @@ const MyQuizzesPageComponent = ({user}: MyQuizzesPageProps) => { if (location.hash && anchorMap[location.hash as keyof typeof anchorMap]) { setTabOverride(anchorMap[location.hash as keyof typeof anchorMap]); } - if (location.search.includes("filter")) { - setFilterText(new URLSearchParams(location.search).get("filter") || ""); - } }, [anchorMap]); - const [filterText, setFilterText] = useState(""); - const [copied, setCopied] = useState(false); + // TODO: revert to StyledToggle when the component is more widely used (post-redesign) - return + // const displayModeToggle =
+ // Display mode + // setDisplayMode(d => d === "table" ? "cards" : "table")} + // /> + const displayModeToggle =
+ Display in + setDisplayMode(d => d === "table" ? "cards" : "table")}> + + + +
; + + const pastTestsToggle =
+ Past tests +
+ { + const target = !showCompleted; + setShowCompleted(target); + setQuizStatuses(s => target ? [...s, QuizStatus.Complete, QuizStatus.Overdue] : s.filter(status => ![QuizStatus.Complete, QuizStatus.Overdue].includes(status))); + }} + /> +
+
; + + // +!! converts a string to 0 if null or empty and 1 otherwise + const filterCount = +!!quizTitleFilter + +!!quizCreatorFilter + quizStatusFilter.length; + + const filtersToggle = +
+ )} + + } + + ; +}; + +export const PracticeQuizzes = withRouter(PracticeQuizzesComponent); diff --git a/src/app/components/site/cs/RoutesCS.tsx b/src/app/components/site/cs/RoutesCS.tsx index ef4b35f597..28171955e1 100644 --- a/src/app/components/site/cs/RoutesCS.tsx +++ b/src/app/components/site/cs/RoutesCS.tsx @@ -33,6 +33,7 @@ import {ExamSpecificationsDirectory} from "../../pages/ExamSpecificationsDirecto import { StudentResources } from "../../pages/StudentResources"; import { TeacherResources } from "../../pages/TeacherResources"; import { CSProjects } from "../../pages/CSProjects"; +import { PracticeQuizzes } from "../../pages/quizzes/PracticeQuizzes"; const Equality = lazy(() => import('../../pages/Equality')); const EventDetails = lazy(() => import('../../pages/EventDetails')); @@ -67,6 +68,8 @@ export const RoutesCS = [ // Student test pages , + , + // Quiz (test) pages , , diff --git a/src/app/components/site/phy/NavigationBarPhy.tsx b/src/app/components/site/phy/NavigationBarPhy.tsx index 00f05ffa85..15d1b4c256 100644 --- a/src/app/components/site/phy/NavigationBarPhy.tsx +++ b/src/app/components/site/phy/NavigationBarPhy.tsx @@ -47,6 +47,7 @@ export const NavigationBarPhy = () => { GCSE Resources A Level Resources Question Finder + Practice Tests Concepts Glossary diff --git a/src/app/components/site/phy/RoutesPhy.tsx b/src/app/components/site/phy/RoutesPhy.tsx index abf44e9163..576d37c41c 100644 --- a/src/app/components/site/phy/RoutesPhy.tsx +++ b/src/app/components/site/phy/RoutesPhy.tsx @@ -33,7 +33,6 @@ import {TeacherRequest} from "../../pages/TeacherRequest"; import { RegistrationStart } from "../../pages/RegistrationStart"; import {EmailAlterHandler} from "../../handlers/EmailAlterHandler"; import {News} from "../../pages/News"; -import {QuestionFinder} from "../../pages/QuestionFinder"; import { RegistrationAgeCheck } from "../../pages/RegistrationAgeCheck"; import { RegistrationAgeCheckFailed } from "../../pages/RegistrationAgeCheckFailed"; import { RegistrationAgeCheckParentalConsent } from "../../pages/RegistrationAgeCheckParentalConsent"; @@ -42,6 +41,7 @@ import { RegistrationTeacherConnect } from "../../pages/RegistrationTeacherConne import { RegistrationSuccess } from "../../pages/RegistrationSuccess"; import { RegistrationSetPreferences } from "../../pages/RegistrationSetPreferences"; import { RegistrationGroupInvite } from "../../pages/RegistrationGroupInvite"; +import { PracticeQuizzes } from "../../pages/quizzes/PracticeQuizzes"; const Equality = lazy(() => import('../../pages/Equality')); const EventDetails = lazy(() => import('../../pages/EventDetails')); @@ -71,6 +71,7 @@ export const RoutesPhy = [ // Student test pages , , + , // Quiz (test) pages , diff --git a/src/app/services/gameboards.tsx b/src/app/services/gameboards.tsx index c4dc9c1ba4..9b8df6a6d5 100644 --- a/src/app/services/gameboards.tsx +++ b/src/app/services/gameboards.tsx @@ -13,7 +13,7 @@ import { siteSpecific, stagesOrdered } from "./"; -import {BoardOrder, Boards, NOT_FOUND_TYPE, NumberOfBoards} from "../../IsaacAppTypes"; +import {AssignmentBoardOrder, Boards, NOT_FOUND_TYPE, NumberOfBoards} from "../../IsaacAppTypes"; import { selectors, useAppDispatch, @@ -204,7 +204,7 @@ export enum BoardLimit { "All" = "ALL" } -export const BOARD_ORDER_NAMES: {[key in BoardOrder]: string} = { +export const BOARD_ORDER_NAMES: {[key in AssignmentBoardOrder]: string} = { "created": "Date Created Ascending", "-created": "Date Created Descending", "visited": "Date Visited Ascending", @@ -218,11 +218,11 @@ export const BOARD_ORDER_NAMES: {[key in BoardOrder]: string} = { }; const BOARD_SORT_FUNCTIONS = { - [BoardOrder.visited]: (b: GameboardDTO) => b.lastVisited?.valueOf(), - [BoardOrder.created]: (b: GameboardDTO) => b.creationDate?.valueOf(), - [BoardOrder.title]: (b: GameboardDTO) => b.title, - [BoardOrder.attempted]: (b: GameboardDTO) => b.percentageAttempted, - [BoardOrder.correct]: (b: GameboardDTO) => b.percentageCorrect + [AssignmentBoardOrder.visited]: (b: GameboardDTO) => b.lastVisited?.valueOf(), + [AssignmentBoardOrder.created]: (b: GameboardDTO) => b.creationDate?.valueOf(), + [AssignmentBoardOrder.title]: (b: GameboardDTO) => b.title, + [AssignmentBoardOrder.attempted]: (b: GameboardDTO) => b.percentageAttempted, + [AssignmentBoardOrder.correct]: (b: GameboardDTO) => b.percentageCorrect }; const parseBoardLimitAsNumber: (limit: BoardLimit) => NumberOfBoards = (limit: BoardLimit) => @@ -235,7 +235,7 @@ export const useGameboards = (initialView: BoardViews, initialLimit: BoardLimit) const [ loadGameboards ] = useLazyGetGameboardsQuery(); const boards = useAppSelector(selectors.boards.boards); - const [boardOrder, setBoardOrder] = useState(BoardOrder.visited); + const [boardOrder, setBoardOrder] = useState(AssignmentBoardOrder.visited); const [boardView, setBoardView] = useState(initialView); const [boardLimit, setBoardLimit] = useState(initialLimit); const [boardTitleFilter, setBoardTitleFilter] = useState(""); @@ -321,4 +321,4 @@ export const useGameboards = (initialView: BoardViews, initialLimit: BoardLimit) boardLimit, setBoardLimit, boardTitleFilter, setBoardTitleFilter }; -} \ No newline at end of file +}; diff --git a/src/app/services/quiz.ts b/src/app/services/quiz.ts index cec3daa33b..6187f48d1c 100644 --- a/src/app/services/quiz.ts +++ b/src/app/services/quiz.ts @@ -37,7 +37,8 @@ import { QuizAttemptDTO, QuizFeedbackMode, QuizSummaryDTO, - RegisteredUserDTO + RegisteredUserDTO, + UserSummaryDTO } from "../../IsaacApiTypes"; import partition from "lodash/partition"; import {skipToken} from "@reduxjs/toolkit/query"; @@ -271,13 +272,88 @@ export function getQuizAssignmentCSVDownloadLink(assignmentId: number) { return `${API_PATH}/quiz/assignment/${assignmentId}/download`; } -type QuizAttemptOrAssignment = (QuizAttemptDTO | QuizAssignmentDTO); +// type QuizAttemptOrAssignment = (QuizAttemptDTO | QuizAssignmentDTO); -export function isAttempt(a: QuizAttemptOrAssignment): a is QuizAttemptDTO { - return !('groupId' in a); +// export function isAttempt(a: QuizAttemptOrAssignment): a is QuizAttemptDTO { +// return !('groupId' in a); +// } + +// export function partitionCompleteAndIncompleteQuizzes(assignmentsAndAttempts: QuizAssignmentDTO[]): [QuizAssignmentDTO[], QuizAssignmentDTO[]]; +// export function partitionCompleteAndIncompleteQuizzes(assignmentsAndAttempts: QuizAttemptOrAssignment[]): [QuizAttemptOrAssignment[], QuizAttemptOrAssignment[]] { +// return partition(assignmentsAndAttempts, a => isDefined(isAttempt(a) ? a.completedDate : a.attempt?.completedDate)); +// } + +export function partitionCompleteAndIncompleteQuizzes(assignmentsAndAttempts: QuizAssignmentDTO[]): [QuizAssignmentDTO[], QuizAssignmentDTO[]] { + return partition(assignmentsAndAttempts, a => isDefined(a.attempt?.completedDate)); +} + +export enum QuizStatus { + Overdue = "Overdue", + NotStarted = "Not started", + Started = "Started", + Complete = "Complete", } -export function partitionCompleteAndIncompleteQuizzes(assignmentsAndAttempts: QuizAssignmentDTO[]): [QuizAssignmentDTO[], QuizAssignmentDTO[]]; -export function partitionCompleteAndIncompleteQuizzes(assignmentsAndAttempts: QuizAttemptOrAssignment[]): [QuizAttemptOrAssignment[], QuizAttemptOrAssignment[]] { - return partition(assignmentsAndAttempts, a => isDefined(isAttempt(a) ? a.completedDate : a.attempt?.completedDate)); +const todaysDate = new Date(new Date().setHours(0, 0, 0, 0)); + +// Assigned quizzes (QuizAssignmentDTO) and single attempts at practice quizzes (QuizAttemptDTO) are considered the same thing for display purposes +export interface DisplayableQuiz { + id: string | number; + isAssigned: boolean; + title?: string; + creationDate?: Date; + startDate?: Date; + setDate?: Date; + dueDate?: Date; + completedDate?: Date; + attempt?: QuizAttemptDTO; + assignerSummary?: UserSummaryDTO; + quizFeedbackMode?: QuizFeedbackMode; + link?: string; + status?: QuizStatus; +}; + +export function convertAssignmentToQuiz(assignment: QuizAssignmentDTO): DisplayableQuiz | undefined { + if (!assignment.id) { + return undefined; + } + const status = assignment.attempt?.completedDate ? QuizStatus.Complete + : (assignment.dueDate && todaysDate > assignment.dueDate) ? QuizStatus.Overdue + : (assignment.attempt) ? QuizStatus.Started + : QuizStatus.NotStarted; + + return { + id: assignment.id, + isAssigned: true, + title: assignment.quizSummary?.title, + creationDate: assignment.creationDate, + setDate: assignment.scheduledStartDate ?? assignment.creationDate, + startDate: assignment.attempt?.startDate, + dueDate: assignment.dueDate, + completedDate: assignment.attempt?.completedDate, + attempt: assignment.attempt, + quizFeedbackMode: assignment.quizFeedbackMode, + assignerSummary: assignment.assignerSummary, + link: status === QuizStatus.Complete ? (assignment.quizFeedbackMode !== "NONE" ? `/test/attempt/${assignment.attempt?.id}/feedback` : undefined) + : status === QuizStatus.Overdue ? undefined + : `/test/assignment/${assignment.id}`, + status: status + }; +} + +export function convertAttemptToQuiz(attempt: QuizAttemptDTO): DisplayableQuiz | undefined { + if (!attempt.id) { + return undefined; + } + return { + id: attempt.id, + isAssigned: false, + title: attempt.quizSummary?.title, + startDate: attempt.startDate, + completedDate: attempt.completedDate, + attempt: attempt, + quizFeedbackMode: attempt.feedbackMode, + link: attempt.completedDate ? `/test/attempt/${attempt.id}/feedback` : `/test/attempt/${attempt.id}`, + status: attempt.completedDate ? QuizStatus.Complete : QuizStatus.Started, + }; } diff --git a/src/app/state/slices/api/gameboardApi.ts b/src/app/state/slices/api/gameboardApi.ts index 969908d428..5a4bff0c4c 100644 --- a/src/app/state/slices/api/gameboardApi.ts +++ b/src/app/state/slices/api/gameboardApi.ts @@ -1,5 +1,5 @@ import {isaacApi} from "./baseApi"; -import {BoardOrder, Boards, NumberOfBoards} from "../../../../IsaacAppTypes"; +import {AssignmentBoardOrder, Boards, NumberOfBoards} from "../../../../IsaacAppTypes"; import {GameboardDTO, GameboardListDTO, IsaacWildcard} from "../../../../IsaacApiTypes"; import {onQueryLifecycleEvents} from "./utils"; import {isPhy, QUESTION_CATEGORY, siteSpecific} from "../../../services"; @@ -8,7 +8,7 @@ import {logAction} from "../../actions/logging"; export const gameboardApi = isaacApi.injectEndpoints({ endpoints: (build) => ({ - getGameboards: build.query({ + getGameboards: build.query({ query: ({startIndex, limit, sort}) => ({ url: "/gameboards/user_gameboards", params: {"start_index": startIndex, limit, sort} diff --git a/src/scss/common/elements.scss b/src/scss/common/elements.scss index 80befbb51e..915e15f0ed 100644 --- a/src/scss/common/elements.scss +++ b/src/scss/common/elements.scss @@ -296,6 +296,8 @@ iframe.email-html { .collapsible-body { transition: max-height 0.3s ease-in-out, height 0.3s ease-in-out; + // https://stackoverflow.com/q/6421966; x-overflow must be visible (for e.g. box shadows), y-overflow would preferably be entirely hidden but can't be. + overflow: visible clip; // height must be animated alongside max-height to prevent jumping if the inner content changes height during the animation max-height: 0; } diff --git a/src/scss/common/gameboard.scss b/src/scss/common/gameboard.scss index b05ed293c6..198000f583 100644 --- a/src/scss/common/gameboard.scss +++ b/src/scss/common/gameboard.scss @@ -146,6 +146,7 @@ content: url(/assets/common/icons/chevron_down.svg); filter: invert(1); transform: rotate(-90deg); + transition: transform 0.2s ease-in-out; display: block; width: 100%; height: 100%; @@ -153,4 +154,4 @@ &.selected::after { transform: rotate(0deg); } -} \ No newline at end of file +} diff --git a/src/scss/common/icons.scss b/src/scss/common/icons.scss index 2537b791e3..4f3c4eca25 100644 --- a/src/scss/common/icons.scss +++ b/src/scss/common/icons.scss @@ -46,3 +46,7 @@ polygon.fill-secondary { @include icon-dropdown(90deg); } } + +img[aria-disabled="true"] { + opacity: 0.5; +} diff --git a/src/scss/common/quiz.scss b/src/scss/common/quiz.scss index 3bb64e15c2..9e7ed54fed 100644 --- a/src/scss/common/quiz.scss +++ b/src/scss/common/quiz.scss @@ -206,4 +206,35 @@ background: url("/assets/action-done.svg") no-repeat center content-box; transform: none; } -} \ No newline at end of file +} + +.my-quizzes-table { + @extend .my-gameboard-table; + min-width: 694px; + thead > tr { + @extend .my-gameboard-table-header; + > th { + padding: 0.5rem 1rem; + } + > th:not(:last-child) { + cursor: pointer; + } + } + tbody > tr { + @include td-link-padding(0.8rem 1rem); + + &:not(.overdue) { + cursor: pointer; + } + &.overdue { + background: rgba($gray-160, 0.09); + } + &.completed { + background: rgba(map-get($theme-colors, "success"), 0.09); + } + + > td { + background: transparent; + } + } +} diff --git a/src/scss/common/table.scss b/src/scss/common/table.scss index 1b59f4ec33..ccd6d8a388 100644 --- a/src/scss/common/table.scss +++ b/src/scss/common/table.scss @@ -1,3 +1,27 @@ .table-row-dragging { - display: table; + display: table; +} + +.td-link { + vertical-align: middle; + + &:has(> a) { + display: contents; + + > a { + @extend td; + display: table-cell; + vertical-align: middle; + text-decoration: none !important; + } + } +} + +@mixin td-link-padding ($padding: 0.8rem 1rem) { + > td:not(.td-link), > td.td-link > a, td.td-link:not(:has(> a)) { + padding: $padding; + } + > td.td-link:has(> a) { + padding: 0; + } }