Skip to content

Redesign: practice tests #1402

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
May 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
9b4e3bf
Filter practice tests on subject-specific pages
axlewin Apr 15, 2025
625ae0e
Add "no relevant tests" message
axlewin Apr 15, 2025
d345c6c
Remove assigner filter from My Practice Tests
axlewin Apr 15, 2025
016820a
Add subject/field filters to practice tests
axlewin Apr 16, 2025
f7dc047
Restyle practice tests to match designs
axlewin Apr 16, 2025
43b55f3
Don't show grey background if no quizzes found
axlewin Apr 16, 2025
7bc8c19
Add share/print buttons
axlewin Apr 16, 2025
57b9ba1
Correct total quizzes count
axlewin Apr 16, 2025
f42a580
Only show one "no quizzes found" message
axlewin Apr 16, 2025
9e1b309
Use subject colours on subject filters
axlewin Apr 17, 2025
0f4a4da
Add stage selector to practice tests
axlewin Apr 24, 2025
33e2e94
Merge branch 'redesign-2024' of https://github.com/isaacphysics/isaac…
axlewin Apr 24, 2025
e936189
Merge branch 'redesign-2024' of https://github.com/isaacphysics/isaac…
axlewin Apr 24, 2025
9a20c48
Remove unused import
axlewin Apr 24, 2025
882270a
Make practice test filters consistent with concepts/QF
axlewin Apr 25, 2025
d5cf1d8
Hide filter headings if no filters available
axlewin Apr 25, 2025
110486d
Use `StyledTabPicker`s on subject-specific practice test pages
axlewin Apr 25, 2025
b2a2e24
Overhaul ListView typing to correctly allow spread params
jacbn Apr 29, 2025
a9a3d88
Use ListView in PracticeQuizzes, with `/view` link in place of `/prev…
jacbn Apr 29, 2025
b8ea243
Remove unused imports and params
jacbn Apr 29, 2025
6ac8feb
Improve styling of practice quizzes on Ada after move to ListView
jacbn Apr 29, 2025
d8a0a42
Update VRT baselines
actions-user Apr 29, 2025
b235cff
Merge pull request #1412 from isaacphysics/vrt/redesign/practice-quizzes
jacbn Apr 30, 2025
fd79de8
Merge branch 'redesign-2024' into redesign/practice-quizzes
jacbn Apr 30, 2025
fb428be
Merge branch 'redesign-2024' into redesign/practice-quizzes
jacbn Apr 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 137 additions & 9 deletions src/app/components/elements/layout/SidebarLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import React, { ChangeEvent, RefObject, useEffect, useRef, useState } from "react";
import React, { ChangeEvent, Dispatch, RefObject, SetStateAction, useEffect, useRef, useState } from "react";
import { Col, ColProps, RowProps, Input, Offcanvas, OffcanvasBody, OffcanvasHeader, Row, DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown, Form, Label } from "reactstrap";
import partition from "lodash/partition";
import classNames from "classnames";
import { AssignmentDTO, ContentSummaryDTO, GameboardDTO, GameboardItem, IsaacBookIndexPageDTO, IsaacConceptPageDTO, QuestionDTO, QuizAssignmentDTO, QuizAttemptDTO, RegisteredUserDTO, Stage } from "../../../../IsaacApiTypes";
import { above, ACCOUNT_TAB, ACCOUNT_TABS, AUDIENCE_DISPLAY_FIELDS, below, BOARD_ORDER_NAMES, BoardCompletions, BoardCreators, BoardLimit, BoardSubjects, BoardViews, confirmThen, determineAudienceViews, EventStageMap, EventStatusFilter, EventTypeFilter, filterAssignmentsByStatus, filterAudienceViewsByProperties, getDistinctAssignmentGroups, getDistinctAssignmentSetters, getHumanContext, getThemeFromContextAndTags, HUMAN_STAGES, ifKeyIsEnter, isAda, isDefined, PHY_NAV_SUBJECTS, isTeacherOrAbove, QuizStatus, siteSpecific, TAG_ID, tags, STAGE, useDeviceSize, LearningStage, HUMAN_SUBJECTS, ArrayElement, isFullyDefinedContext, isSingleStageContext, Item, stageLabelMap, extractTeacherName, determineGameboardSubjects, PATHS, getQuestionPlaceholder } from "../../../services";
import { above, ACCOUNT_TAB, ACCOUNT_TABS, AUDIENCE_DISPLAY_FIELDS, below, BOARD_ORDER_NAMES, BoardCompletions, BoardCreators, BoardLimit, BoardSubjects, BoardViews, confirmThen, determineAudienceViews, EventStageMap,
EventStatusFilter, EventTypeFilter, filterAssignmentsByStatus, filterAudienceViewsByProperties, getDistinctAssignmentGroups, getDistinctAssignmentSetters, getHumanContext, getThemeFromContextAndTags, HUMAN_STAGES,
ifKeyIsEnter, isAda, isDefined, PHY_NAV_SUBJECTS, isTeacherOrAbove, QuizStatus, siteSpecific, TAG_ID, tags, STAGE, useDeviceSize, LearningStage, HUMAN_SUBJECTS, ArrayElement, isFullyDefinedContext, isSingleStageContext,
Item, stageLabelMap, extractTeacherName, determineGameboardSubjects, PATHS, getQuestionPlaceholder, getFilteredStageOptions } from "../../../services";
import { StageAndDifficultySummaryIcons } from "../StageAndDifficultySummaryIcons";
import { selectors, useAppSelector, useGetQuizAssignmentsAssignedToMeQuery } from "../../../state";
import { Link, useHistory, useLocation } from "react-router-dom";
Expand Down Expand Up @@ -541,9 +544,132 @@ export const QuestionFinderSidebar = (props: QuestionFinderSidebarProps) => {
</ContentSidebar>;
};

export const PracticeQuizzesSidebar = (props: SidebarProps) => {
// TODO
return <ContentSidebar {...props}/>;
interface PracticeQuizzesSidebarProps extends SidebarProps {
filterText: string;
setFilterText: Dispatch<SetStateAction<string>>;
filterTags?: Tag[];
setFilterTags: Dispatch<SetStateAction<Tag[]>>;
tagCounts: Record<string, number>;
filterStages?: Stage[];
setFilterStages: Dispatch<SetStateAction<Stage[] | undefined>>;
stageCounts: Record<string, number>;
}

export const PracticeQuizzesSidebar = (props: PracticeQuizzesSidebarProps) => {
const { filterText, setFilterText, filterTags, setFilterTags, tagCounts, filterStages, setFilterStages, stageCounts, ...rest } = props;
const pageContext = useAppSelector(selectors.pageContext.context);
const fields = pageContext?.subject ? tags.getDirectDescendents(pageContext.subject as TAG_ID) : [];

const updateFilterTags = (tag: Tag) => {
if (filterTags?.includes(tag)) {
setFilterTags(filterTags.filter(t => t !== tag));
}
else {
setFilterTags([...(filterTags ?? []), tag]);
}
};

const updateFilterStages = (stage: Stage) => {
if (filterStages?.includes(stage)) {
setFilterStages(filterStages.filter(s => s !== stage));
} else {
setFilterStages([...(filterStages ?? []), stage]);
}
};

// Clear stage filters on subject change, since previous stages may not be visible to deselect
useEffect(() => {
setFilterStages(undefined);
}, [filterTags]);

return <ContentSidebar {...rest}>
<div className="section-divider"/>
<h5>Search practice tests</h5>
<Input type="search" placeholder="e.g. Challenge" value={filterText} className="search--filter-input my-3"
onChange={(e: ChangeEvent<HTMLInputElement>) => setFilterText(e.target.value)} />

{!pageContext?.subject && Object.keys(PHY_NAV_SUBJECTS).filter(s => tagCounts[s] > 0).length > 0 && <>
<div className="section-divider"/>
<h5>Filter by subject</h5>
<ul>
{Object.keys(PHY_NAV_SUBJECTS).filter(s => tagCounts[s] > 0).map((subject, i) => {
const subjectTag = tags.getById(subject as TAG_ID);
const descendentTags = tags.getDirectDescendents(subjectTag.id);
const isSelected = filterTags?.includes(subjectTag) || descendentTags.some(tag => filterTags?.includes(tag));
const isPartial = descendentTags.some(tag => filterTags?.includes(tag)) && descendentTags.some(tag => !filterTags?.includes(tag));
return <li key={i} className={classNames("ps-2", {"checkbox-region": isSelected})}>
<FilterCheckbox
checkboxStyle="button" color="theme" data-bs-theme={subject} tag={subjectTag} conceptFilters={filterTags as Tag[]}
setConceptFilters={setFilterTags} tagCounts={tagCounts} dependentTags={descendentTags} incompatibleTags={descendentTags}
partiallySelected={descendentTags.some(tag => filterTags?.includes(tag))}
className={classNames({"icon-checkbox-off": !isSelected, "icon icon-checkbox-partial-alt": isSelected && isPartial, "icon-checkbox-selected": isSelected && !isPartial})}
/>
{isSelected && <ul className="ms-3 ps-2">
{descendentTags.filter(tag => tagCounts[tag.id] > 0)
.map((tag, j) => <li key={j}>
<FilterCheckbox
checkboxStyle="button" color="theme" bsSize="sm" data-bs-theme={subject} tag={tag} conceptFilters={filterTags as Tag[]}
setConceptFilters={setFilterTags} tagCounts={tagCounts} incompatibleTags={[subjectTag]}
/>
</li>)}
</ul>}
</li>;
})}
</ul>
</>}

{pageContext?.subject && fields.filter(tag => tagCounts[tag.id] > 0).length > 0 && <>
<div className="section-divider"/>
<h5>Filter by topic</h5>
<ul>
{fields.filter(tag => tagCounts[tag.id] > 0)
.map((tag, j) => <li key={j} >
<StyledTabPicker checkboxTitle={tag.title} checked={filterTags?.includes(tag)}
count={tagCounts[tag.id]} onInputChange={() => updateFilterTags(tag)}/>
</li>)}
</ul>
</>}

{!isSingleStageContext(pageContext) && getFilteredStageOptions().filter(s => stageCounts[s.label] > 0).length > 0 && <>
<div className="section-divider"/>
<h5>Filter by stage</h5>
<ul>
{getFilteredStageOptions().filter(s => stageCounts[s.label] > 0).map((stage, i) =>
<li key={i}>
<StyledCheckbox checked={filterStages?.includes(stage.value)}
label={<>{stage.label} {tagCounts && <span className="text-muted">({stageCounts[stage.label]})</span>}</>}
color="theme" data-bs-theme={filterTags?.length === 1 ? filterTags[0].id : undefined}
onChange={() => {updateFilterStages(stage.value);}}/>
</li>)}
</ul>
</>}

{isFullyDefinedContext(pageContext) && <>
<div className="section-divider"/>
<div className="sidebar-help">
<p>The practice tests shown here have been filtered to only show those that are relevant to {getHumanContext(pageContext)}.</p>
<p>If you want to explore our full range of practice tests, you can view the main practice tests page:</p>
<AffixButton size="md" color="keyline" tag={Link} to="/practice_tests" affix={{
affix: "icon-right",
position: "suffix",
type: "icon"
}}>
Browse all practice tests
</AffixButton>
</div>
</>}
<div className="section-divider"/>
<div className="sidebar-help">
<p>You can see all of the tests that you have in progress or have completed in your My Isaac:</p>
<AffixButton size="md" color="keyline" tag={Link} to="/tests" affix={{
affix: "icon-right",
position: "suffix",
type: "icon"
}}>
My tests
</AffixButton>
</div>
</ContentSidebar>;
};

export const LessonsAndRevisionSidebar = (props: SidebarProps) => {
Expand Down Expand Up @@ -1188,10 +1314,12 @@ export const MyQuizzesSidebar = (props: MyQuizzesSidebarProps) => {
{statusOptions.map(state => <QuizStatusCheckbox
key={state} status={state} count={undefined} statusFilter={quizStatusFilter} setStatusFilter={setQuizStatusFilter}
/>)}
<h5 className="my-3">Filter by assigner</h5>
<Input type="select" onChange={e => setQuizCreatorFilter(e.target.value)}>
{["All", ...getDistinctAssignmentSetters(quizzes)].map(setter => <option key={setter} value={setter}>{setter}</option>)}
</Input>
{activeTab === 1 && <>
<h5 className="my-3">Filter by assigner</h5>
<Input type="select" onChange={e => setQuizCreatorFilter(e.target.value)}>
{["All", ...getDistinctAssignmentSetters(quizzes)].map(setter => <option key={setter} value={setter}>{setter}</option>)}
</Input>
</>}
<div className="section-divider mt-4"/>
<h5 className="mb-3">Display</h5>
<StyledDropdown value={displayMode} onChange={() => setDisplayMode(d => d === "table" ? "cards" : "table")}>
Expand Down
29 changes: 13 additions & 16 deletions src/app/components/elements/list-groups/AbstractListViewItem.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { Link } from "react-router-dom";
import React from "react";
import React, { ReactNode } from "react";
import { StageAndDifficultySummaryIcons } from "../StageAndDifficultySummaryIcons";
import { ViewingContext} from "../../../../IsaacAppTypes";
import classNames from "classnames";
import { Button, Col, ListGroupItem, ListGroupItemProps, Row } from "reactstrap";
import { Spacer } from "../Spacer";
import { Button, Col, ListGroupItem, ListGroupItemProps } from "reactstrap";
import { CompletionState } from "../../../../IsaacApiTypes";
import { below, isPhy, Subject, useDeviceSize } from "../../../services";
import { below, isPhy, siteSpecific, Subject, useDeviceSize } from "../../../services";
import { PhyHexIcon } from "../svg/PhyHexIcon";
import { TitleIconProps } from "../PageTitle";
import { Markup } from "../markup";
Expand Down Expand Up @@ -44,14 +43,12 @@ const LinkTags = ({linkTags}: {linkTags: {tag: string, url?: string}[];}) => {
</>;
};

const QuizLinks = (props: React.HTMLAttributes<HTMLSpanElement> & {previewQuizUrl: string, quizButton: JSX.Element}) => {
const QuizLinks = (props: React.HTMLAttributes<HTMLSpanElement> & {previewQuizUrl?: string, quizButton?: ReactNode}) => {
const { previewQuizUrl, quizButton, ...rest } = props;
return <span {...rest} className={classNames(rest.className, "d-flex")}>
<Spacer/>
<Button to={previewQuizUrl} color="keyline" tag={Link} className="set-quiz-button-md">
Preview
</Button>
<span style={{minWidth: "20px"}}/>
return <span {...rest} className={classNames(rest.className, "d-flex justify-content-end gap-3")}>
{previewQuizUrl && <Button to={previewQuizUrl} color={siteSpecific("keyline", "secondary")} tag={Link} className="set-quiz-button-md">
{previewQuizUrl.includes("/preview/") ? "Preview" : "View test"}
</Button>}
{quizButton}
</span>;
};
Expand All @@ -62,7 +59,7 @@ export interface ListViewTagProps {
}

export interface AbstractListViewItemProps extends ListGroupItemProps {
title: string;
title?: string;
icon?: TitleIconProps;
subject?: Subject;
subtitle?: string;
Expand All @@ -82,7 +79,7 @@ export interface AbstractListViewItemProps extends ListGroupItemProps {

export const AbstractListViewItem = ({icon, title, subject, subtitle, breadcrumb, status, tags, supersededBy, linkTags, quizTag, url, audienceViews, previewQuizUrl, quizButton, isCard, fullWidth, ...rest}: AbstractListViewItemProps) => {
const deviceSize = useDeviceSize();
const isQuiz: boolean = (previewQuizUrl && quizButton) ? true : false;
const isQuiz: boolean = !!(previewQuizUrl || quizButton);

fullWidth = fullWidth || below["sm"](deviceSize) || ((status || audienceViews || previewQuizUrl || quizButton) ? false : true);
const colWidths = fullWidth ? [12,12,12,12,12] : isQuiz ? [12,6,6,6,6] : [12,8,7,6,7];
Expand All @@ -96,7 +93,7 @@ export const AbstractListViewItem = ({icon, title, subject, subtitle, breadcrumb
</div>
<div className="align-content-center">
<div className="d-flex">
<span className="question-link-title"><Markup encoding="latex">{title}</Markup></span>
<span className={classNames("link-title", {"question-link-title": isPhy || !isQuiz})}><Markup encoding="latex">{title}</Markup></span>
{quizTag && <span className="quiz-level-1-tag ms-sm-2">{quizTag}</span>}
{isPhy && <div className="d-flex flex-column justify-self-end">
{supersededBy && <a
Expand Down Expand Up @@ -124,7 +121,7 @@ export const AbstractListViewItem = ({icon, title, subject, subtitle, breadcrumb
{linkTags && <div className="d-flex py-3 flex-wrap">
<LinkTags linkTags={linkTags}/>
</div>}
{previewQuizUrl && quizButton && fullWidth && <div className="d-flex d-md-none align-items-center">
{isQuiz && fullWidth && <div className="d-flex d-md-none align-items-center">
<QuizLinks previewQuizUrl={previewQuizUrl} quizButton={quizButton}/>
</div>}
</div>
Expand All @@ -137,7 +134,7 @@ export const AbstractListViewItem = ({icon, title, subject, subtitle, breadcrumb
{audienceViews && <Col md={4} lg={5} xl={4} xxl={3} className="d-none d-md-flex justify-content-end">
<StageAndDifficultySummaryIcons audienceViews={audienceViews} stack spacerWidth={5} className={classNames({"list-view-border": audienceViews.length > 0})}/>
</Col>}
{previewQuizUrl && quizButton && <Col md={6} className="d-none d-md-flex align-items-center justify-content-end">
{isQuiz && <Col md={6} className="d-none d-md-flex align-items-center justify-content-end">
<QuizLinks previewQuizUrl={previewQuizUrl} quizButton={quizButton}/>
</Col>}
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ export const ContentSummaryListGroupItem = ({item, search, showBreadcrumb, noCar
<div className={classNames("flex-fill", {"py-3 pe-3 align-content-center": isAda, "d-flex": isAda && !stack, "d-md-flex": isPhy})}>
<div className={"align-self-center " + titleClasses}>
<div className="d-flex">
<Markup encoding={"latex"} className={classNames( "question-link-title", {"text-theme": isPhy})}>
<Markup encoding={"latex"} className={classNames( "link-title question-link-title", {"text-theme": isPhy})}>
{title ?? ""}
</Markup>
{isPhy && typeLabel && <span className={"small text-muted align-self-end d-none d-md-inline ms-2 mb-1"}>
Expand Down
Loading
Loading