From be5fd1c97d5ccd240893b743567207191c7b9828 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Wed, 3 Jul 2024 15:25:08 +0100 Subject: [PATCH 01/61] Prefer namespace-independent RS imports --- src/app/components/pages/QuestionFinder.tsx | 135 ++++++++++---------- 1 file changed, 68 insertions(+), 67 deletions(-) diff --git a/src/app/components/pages/QuestionFinder.tsx b/src/app/components/pages/QuestionFinder.tsx index ad70238c1b..47cc389c2a 100644 --- a/src/app/components/pages/QuestionFinder.tsx +++ b/src/app/components/pages/QuestionFinder.tsx @@ -7,7 +7,6 @@ import { useAppDispatch, useAppSelector } from "../../state"; -import * as RS from "reactstrap"; import debounce from "lodash/debounce"; import {MultiValue} from "react-select"; import { @@ -52,6 +51,7 @@ import classNames from "classnames"; import queryString from "query-string"; import { PageFragment } from "../elements/PageFragment"; import {RenderNothing} from "../elements/RenderNothing"; +import { Button, Card, CardBody, CardHeader, Col, Container, Form, Input, Label, Row, UncontrolledTooltip } from "reactstrap"; const selectStyle = { className: "basic-multi-select", classNamePrefix: "select", @@ -311,50 +311,51 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { ; - return + return - - - - + + + + + Specify your search criteria and we will find questions related to your chosen filter(s). - - - - - Stage + + + + + searchStages.includes(o.value))} options={getFilteredStageOptions()} onChange={selectOnChange(setSearchStages, true)} /> - - - Difficulty + + + - - {isAda && - Exam Board + + {isAda && + searchExamBoards.includes(o.value))} options={getFilteredExamBoardOptions({byStages: searchStages})} onChange={(s: MultiValue>) => selectOnChange(setSearchExamBoards, true)(s)} /> - } - - - - Topic + } + + + + {siteSpecific( , { }} /> )} - - - - {isPhy && - Book + + + + {isPhy && + { }} options={bookOptions} /> - } - - - {isPhy && isStaff(user) && - - setSearchFastTrack(e.target.checked)} />{' '}Show FastTrack questions - - } - - - - Search - } + + + {isPhy && isStaff(user) && +
+ +
+ } +
+ + + + handleSearch(e.target.value)} /> -
-
+ + - {user && isLoggedIn(user) && - - + {user && isLoggedIn(user) && +
+
{ label={

Revision mode

} /> - + Revision mode hides your previous answers, so you can practice questions that you have answered before. - +
{ disabled={revisionMode} />
- - - } - - - - - + + +
} + + + + +

Results

-
- - + + + {[searchQuery, searchTopics, searchBook, searchStages, searchDifficulties, searchExamBoards].every(v => v.length === 0) && selections.every(v => v.length === 0) ? @@ -444,9 +445,9 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { (displayQuestions?.length ? <> ({...q, correct: revisionMode ? undefined : q.correct}) as ContentSummaryDTO)}/> - - - + + + + {displayQuestions && (totalQuestions ?? 0) > displayQuestions.length &&
{`${totalQuestions} questions match your criteria.`}
@@ -467,7 +468,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { No results found) } - - - ; + + + ; }); From 62b6c328b01b84126767f3878084493bec4f8cdd Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Thu, 4 Jul 2024 11:04:45 +0100 Subject: [PATCH 02/61] StyledCheckbox: colours, minor code cleanup --- .../elements/inputs/StyledCheckbox.tsx | 7 +++++-- src/scss/common/checkbox.scss | 19 ++++++++++++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/app/components/elements/inputs/StyledCheckbox.tsx b/src/app/components/elements/inputs/StyledCheckbox.tsx index f7c2cad34a..455fc3c143 100644 --- a/src/app/components/elements/inputs/StyledCheckbox.tsx +++ b/src/app/components/elements/inputs/StyledCheckbox.tsx @@ -8,6 +8,9 @@ import classNames from "classnames"; // A custom checkbox, dealing with mouse and keyboard input. Pass `onChange((e : ChangeEvent) => void)`, `checked: bool`, and `label: Element` as required as props to use. export const StyledCheckbox = (props : InputProps) => { + + const {label, ignoreLabelHover, className, ...rest} = props; + const [checked, setChecked] = useState(props.checked ?? false); const id = useMemo(() => {return (props.id ?? "") + "-" + v4();}, [props.id]); const onCheckChange = (e: React.ChangeEvent) => { @@ -23,14 +26,14 @@ export const StyledCheckbox = (props : InputProps) => { return
{checked &&
} - onCheckChange(e)} // If the user toggles with a keyboard, this does not change the state of the checkbox, so we need to do it manually (with modification to `target` // as this is a keyboard event, not a change event). We also prevent default to avoid submitting the outer form. onKeyDown={(e) => ifKeyIsEnter(() => {onCheckChange({...e, target: {...e.currentTarget, checked: !e.currentTarget.checked}}); e.preventDefault();})(e)} />
- {props.label &&
; }; \ No newline at end of file diff --git a/src/scss/common/checkbox.scss b/src/scss/common/checkbox.scss index 0e3f1757a6..8f34226506 100644 --- a/src/scss/common/checkbox.scss +++ b/src/scss/common/checkbox.scss @@ -13,7 +13,7 @@ appearance: none; width: 100%; height: 100%; - border: 0.15em solid #000; + border: 0.1em solid #000; border-radius: 0.25em; margin: 0; outline: none; @@ -28,6 +28,16 @@ background: #333; border-color: #333; } + + &[color="primary"] { + background: $primary; + border-color: $primary; + + &:hover, &:disabled { + background: darken($primary, 10%); + border-color: darken($primary, 10%); + } + } } &:not(.checked):hover, &:not(.checked):disabled { @@ -78,5 +88,12 @@ width: fit-content; margin: 0; line-height: normal; + + &.hover-override + div input[type="checkbox"]:not(:checked) { + &:hover { + background: #f8f8f8; + border-color: #666; + } + } } } \ No newline at end of file From b219880213e2a7ccf377fe33951738e43bd32482 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Thu, 4 Jul 2024 11:06:44 +0100 Subject: [PATCH 03/61] Genericise dropdown chevron styles --- .../markup/portals/InlineDropZones.tsx | 2 +- src/scss/common/icons.scss | 21 +++++++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/app/components/elements/markup/portals/InlineDropZones.tsx b/src/app/components/elements/markup/portals/InlineDropZones.tsx index e95586ba92..756a6c2795 100644 --- a/src/app/components/elements/markup/portals/InlineDropZones.tsx +++ b/src/app/components/elements/markup/portals/InlineDropZones.tsx @@ -110,7 +110,7 @@ function InlineDropRegion({id, index, emptyWidth, emptyHeight, rootElement}: {id {isDefined(isCorrect) &&
{isCorrect ? "✔" : "✘"}
} - {!item && expand dropdown} + {!item && expand dropdown}
diff --git a/src/scss/common/icons.scss b/src/scss/common/icons.scss index 43416e37a6..2537b791e3 100644 --- a/src/scss/common/icons.scss +++ b/src/scss/common/icons.scss @@ -22,14 +22,27 @@ polygon.fill-secondary { fill: $secondary; } -.icon-dropdown { +.dropzone-dropdown { position: absolute; align-self: center; right: 5px; + @extend .icon-dropdown-180; +} + +@mixin icon-dropdown($deg) { + transform: rotate($deg); + -webkit-transform: rotate($deg); + -ms-transform: rotate($deg); +} + +.icon-dropdown-180 { + &.active { + @include icon-dropdown(180deg); + } +} +.icon-dropdown-90 { &.active { - transform: rotate(180deg); - -webkit-transform: rotate(180deg); - -ms-transform: rotate(180deg); + @include icon-dropdown(90deg); } } From 539eac0e09b5e86dbabd38d93937a2b00fc278cc Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Thu, 4 Jul 2024 11:07:19 +0100 Subject: [PATCH 04/61] Initial QF redesign; layout and filters --- .../components/elements/CollapsibleList.tsx | 39 +++++ src/app/components/pages/QuestionFinder.tsx | 133 +++++++++++++++++- src/scss/common/_utils.scss | 8 ++ src/scss/common/elements.scss | 8 ++ 4 files changed, 185 insertions(+), 3 deletions(-) create mode 100644 src/app/components/elements/CollapsibleList.tsx diff --git a/src/app/components/elements/CollapsibleList.tsx b/src/app/components/elements/CollapsibleList.tsx new file mode 100644 index 0000000000..346b9ab9fd --- /dev/null +++ b/src/app/components/elements/CollapsibleList.tsx @@ -0,0 +1,39 @@ +import React, { useEffect, useRef, useState } from "react"; +import { Col, Row } from "reactstrap"; +import { Spacer } from "./Spacer"; +import classNames from "classnames"; + +export interface CollapsibleListProps { + title?: string; + expanded?: boolean; // initial expanded state only + children?: React.ReactNode; +} + +export const CollapsibleList = (props: CollapsibleListProps) => { + + const [expanded, setExpanded] = useState(props.expanded || false); + const [expandedHeight, setExpandedHeight] = useState(0); + const listRef = useRef(null); + + useEffect(() => { + if (expanded) { + setExpandedHeight(listRef?.current ? [...listRef.current.children].map(c => c.clientHeight).reduce((a, b) => a + b, 0) : 0); + } + }, [expanded]); + + return + + + + {/* TODO:
*/} + +
+ {props.children} +
+
+ ; +}; \ No newline at end of file diff --git a/src/app/components/pages/QuestionFinder.tsx b/src/app/components/pages/QuestionFinder.tsx index 47cc389c2a..87446d77a7 100644 --- a/src/app/components/pages/QuestionFinder.tsx +++ b/src/app/components/pages/QuestionFinder.tsx @@ -51,7 +51,8 @@ import classNames from "classnames"; import queryString from "query-string"; import { PageFragment } from "../elements/PageFragment"; import {RenderNothing} from "../elements/RenderNothing"; -import { Button, Card, CardBody, CardHeader, Col, Container, Form, Input, Label, Row, UncontrolledTooltip } from "reactstrap"; +import { Button, Card, CardBody, CardHeader, Col, Container, Dropdown, DropdownMenu, DropdownToggle, Form, Input, Label, Row, UncontrolledTooltip } from "reactstrap"; +import { CollapsibleList } from "../elements/CollapsibleList"; const selectStyle = { className: "basic-multi-select", classNamePrefix: "select", @@ -317,7 +318,133 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { - + + + + handleSearch(e.target.value)} + /> + {/* TODO: add magnifying glass symbol to end of input */} + + + + + + + + + Filter by + + + + + {getFilteredStageOptions().map((stage, index) => ( +
+ setSearchStages(s => s.includes(stage.value) ? s.filter(v => v !== stage.value) : [...s, stage.value])} + label={{stage.label}} + /> +
+ ))} +
+ {isAda && + {getFilteredExamBoardOptions().map((board, index) => ( +
+ setSearchExamBoards(s => s.includes(board.value) ? s.filter(v => v !== board.value) : [...s, board.value])} + label={{board.label}} + /> +
+ ))} +
} + {isAda && + {/* TODO */} + } + + {/* TODO */} + + +
+ {/* TODO: merge in LLM branch and add "Show LLM qs" here */} + { + setRevisionMode(r => !r); + debouncedRevisionModeUpdate(); + }} + label={} + /> +
+
+
+
+ + + {/* TODO: update styling of question block */} + + + + + Showing 30 of 1235. + + + + + {[searchQuery, searchTopics, searchBook, searchStages, searchDifficulties, searchExamBoards].every(v => v.length === 0) && + selections.every(v => v.length === 0) ? + Please select filters : + (displayQuestions?.length ? + <> + ({...q, correct: revisionMode ? undefined : q.correct}) as ContentSummaryDTO)}/> + + + + + + {displayQuestions && (totalQuestions ?? 0) > displayQuestions.length && +
+ {`${totalQuestions} questions match your criteria.`}
+ Not found what you're looking for? Try refining your search filters. +
} + : + No results found) + } +
+
+
+ +
+ + + {/* Previous finder. TODO: remove */} + + {/* @@ -469,6 +596,6 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { } - +
*/} ; }); diff --git a/src/scss/common/_utils.scss b/src/scss/common/_utils.scss index 0103d24bee..79378ae123 100644 --- a/src/scss/common/_utils.scss +++ b/src/scss/common/_utils.scss @@ -27,4 +27,12 @@ .overflow-x-auto { overflow-x: auto !important; +} + +.h-min-content { + height: min-content !important; +} + +.h-max-content { + height: max-content !important; } \ No newline at end of file diff --git a/src/scss/common/elements.scss b/src/scss/common/elements.scss index ecbf135ba5..589d48bd27 100644 --- a/src/scss/common/elements.scss +++ b/src/scss/common/elements.scss @@ -294,3 +294,11 @@ iframe.email-html { // } // } //} + +.collapsible-head img { + transition: transform 0.1s ease; +} + +.collapsible-body { + transition: max-height 0.3s ease; +} \ No newline at end of file From f934829359c3dda13c87bce63dc0672bdf07fc0c Mon Sep 17 00:00:00 2001 From: Skye Purchase Date: Wed, 24 Jul 2024 13:50:11 +0100 Subject: [PATCH 05/61] Implement Ada filter structure --- public/assets/common/icons/completed.svg | 3 + public/assets/common/icons/filter-icon.svg | 3 + public/assets/common/icons/incorrect.svg | 3 + public/assets/common/icons/not-started.svg | 3 + .../components/elements/CollapsibleList.tsx | 9 +- .../elements/inputs/StyledCheckbox.tsx | 6 +- .../elements/svg/DifficultyIcons.tsx | 16 +- src/app/components/pages/QuestionFinder.tsx | 178 +++++++++++++++--- src/scss/common/checkbox.scss | 19 +- src/scss/common/elements.scss | 4 + 10 files changed, 199 insertions(+), 45 deletions(-) create mode 100644 public/assets/common/icons/completed.svg create mode 100644 public/assets/common/icons/filter-icon.svg create mode 100644 public/assets/common/icons/incorrect.svg create mode 100644 public/assets/common/icons/not-started.svg diff --git a/public/assets/common/icons/completed.svg b/public/assets/common/icons/completed.svg new file mode 100644 index 0000000000..26372d00fb --- /dev/null +++ b/public/assets/common/icons/completed.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/common/icons/filter-icon.svg b/public/assets/common/icons/filter-icon.svg new file mode 100644 index 0000000000..4b8e5d7984 --- /dev/null +++ b/public/assets/common/icons/filter-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/common/icons/incorrect.svg b/public/assets/common/icons/incorrect.svg new file mode 100644 index 0000000000..7322443cbf --- /dev/null +++ b/public/assets/common/icons/incorrect.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/common/icons/not-started.svg b/public/assets/common/icons/not-started.svg new file mode 100644 index 0000000000..1fcd42970d --- /dev/null +++ b/public/assets/common/icons/not-started.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/app/components/elements/CollapsibleList.tsx b/src/app/components/elements/CollapsibleList.tsx index 346b9ab9fd..0f8d8dd5ac 100644 --- a/src/app/components/elements/CollapsibleList.tsx +++ b/src/app/components/elements/CollapsibleList.tsx @@ -6,6 +6,7 @@ import classNames from "classnames"; export interface CollapsibleListProps { title?: string; expanded?: boolean; // initial expanded state only + subList?: boolean; children?: React.ReactNode; } @@ -21,10 +22,12 @@ export const CollapsibleList = (props: CollapsibleListProps) => { } }, [expanded]); + const title = props.title && props.subList ? props.title : {props.title}; + return - @@ -36,4 +39,4 @@ export const CollapsibleList = (props: CollapsibleListProps) => {
; -}; \ No newline at end of file +}; diff --git a/src/app/components/elements/inputs/StyledCheckbox.tsx b/src/app/components/elements/inputs/StyledCheckbox.tsx index bfa19bacd8..b72efb6c60 100644 --- a/src/app/components/elements/inputs/StyledCheckbox.tsx +++ b/src/app/components/elements/inputs/StyledCheckbox.tsx @@ -17,14 +17,14 @@ export const StyledCheckbox = (props : InputProps) => { props.onChange && props.onChange(e); setChecked(e.target.checked); }; - + // if `checked` is changed externally, reflect this here useEffect(() => { setChecked(props.checked ?? false); }, [props.checked]); return
-
+
{checked &&
} onCheckChange(e)} @@ -36,4 +36,4 @@ export const StyledCheckbox = (props : InputProps) => { {label &&
; -}; \ No newline at end of file +}; diff --git a/src/app/components/elements/svg/DifficultyIcons.tsx b/src/app/components/elements/svg/DifficultyIcons.tsx index 9b94a57a75..546eba2381 100644 --- a/src/app/components/elements/svg/DifficultyIcons.tsx +++ b/src/app/components/elements/svg/DifficultyIcons.tsx @@ -10,13 +10,17 @@ import {Circle} from "./Circle"; const difficultyIconWidth = 25; const difficultyIconXPadding = 1.5; const yPadding = 2; -const difficultyCategories = ["P", "C"]; const difficultyCategoryLevels = siteSpecific([1, 2, 3], [1, 2]); const miniHexagon = calculateHexagonProportions(difficultyIconWidth / 2, 0); const miniSquare = {width: difficultyIconWidth, height: difficultyIconWidth}; -interface DifficultyIconShapeProps {difficultyCategory: string; difficultyCategoryLevel: number; active: boolean} -function SingleDifficultyIconShape({difficultyCategory, difficultyCategoryLevel, active}: DifficultyIconShapeProps) { +interface DifficultyIconShapeProps { + difficultyCategory: string; + difficultyCategoryLevel: number; + active: boolean; + blank?: boolean; +} +function SingleDifficultyIconShape({difficultyCategory, difficultyCategoryLevel, active, blank}: DifficultyIconShapeProps) { // FIXME the calculations here need refactoring, had to rush them to get it done return {difficultyCategory === "P" ? @@ -28,13 +32,13 @@ function SingleDifficultyIconShape({difficultyCategory, difficultyCategoryLevel, } {
- {difficultyCategory} + {blank ? "" : difficultyCategory}
}
; } -export function DifficultyIcons({difficulty} : {difficulty : Difficulty}) { +export function DifficultyIcons({difficulty, blank} : {difficulty: Difficulty, blank?: boolean}) { const difficultyLabel = difficultyShortLabelMap[difficulty]; const difficultyCategory = difficultyLabel[0]; const difficultyLevel = parseInt(difficultyLabel[1]); @@ -50,7 +54,7 @@ export function DifficultyIcons({difficulty} : {difficulty : Difficulty}) { const active = difficultyCategoryLevel <= difficultyLevel; return ; })} diff --git a/src/app/components/pages/QuestionFinder.tsx b/src/app/components/pages/QuestionFinder.tsx index 54830fa563..f9f47084a6 100644 --- a/src/app/components/pages/QuestionFinder.tsx +++ b/src/app/components/pages/QuestionFinder.tsx @@ -33,7 +33,8 @@ import { TAG_ID, itemiseTag, isLoggedIn, - SEARCH_RESULTS_PER_PAGE + SEARCH_RESULTS_PER_PAGE, + DIFFICULTY_ITEM_OPTIONS } from "../../services"; import {ContentSummaryDTO, Difficulty, ExamBoard} from "../../../IsaacApiTypes"; import {GroupBase} from "react-select/dist/declarations/src/types"; @@ -51,8 +52,9 @@ import classNames from "classnames"; import queryString from "query-string"; import { PageFragment } from "../elements/PageFragment"; import {RenderNothing} from "../elements/RenderNothing"; -import { Button, Card, CardBody, CardHeader, Col, Container, Dropdown, DropdownMenu, DropdownToggle, Form, Input, Label, Row, UncontrolledTooltip } from "reactstrap"; +import { Button, Card, CardBody, CardFooter, CardHeader, Col, Container, Dropdown, DropdownMenu, DropdownToggle, Form, Input, Label, Row, UncontrolledTooltip } from "reactstrap"; import { CollapsibleList } from "../elements/CollapsibleList"; +import { DifficultyIcons } from "../elements/svg/DifficultyIcons"; const selectStyle = { className: "basic-multi-select", classNamePrefix: "select", @@ -77,6 +79,13 @@ function processTagHierarchy(subjects: string[], fields: string[], topics: strin return selectionItems; } +function simplifyDifficultyLabel(difficultyLabel: string): string { + const labelLength = difficultyLabel.length; + const type = difficultyLabel.slice(0, labelLength - 4); + const level = difficultyLabel.slice(labelLength - 2, labelLength - 1); + return type + level; +} + export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { const dispatch = useAppDispatch(); const userContext = useUserViewingContext(); @@ -271,6 +280,8 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { }, [questionList]); const [revisionMode, setRevisionMode] = useState(!!userPreferences?.DISPLAY_SETTING?.HIDE_QUESTION_ATTEMPTS); + // TODO: Should be taken from the URI? Or from consent? Or default true? + const [llmMarked, setLlmMarked] = useState(false); useEffect(() => { if (revisionMode) { @@ -327,22 +338,36 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { placeholder={siteSpecific("e.g. Man vs. Horse", "e.g. Creating an AST")} onChange={(e) => handleSearch(e.target.value)} /> - {/* TODO: add magnifying glass symbol to end of input */} + {/* TODO: bring this into the search box + Search */} - + - - Filter by + + Filter + + + Filter by - + {getFilteredStageOptions().map((stage, index) => ( -
+
{
))} - {isAda && + {isAda && {getFilteredExamBoardOptions().map((board, index) => ( -
+
{
))} } - {isAda && - {/* TODO */} + {isAda && + {groupBaseTagOptions.map((tag, index) => ( + // TODO: adjust positioning + // TODO: make subList + + {tag.options.map((topic, index) => ( +
+ setSearchTopics( + s => s.includes(topic.value) + ? s.filter(v => v !== topic.value) + : [...s, topic.value] + )} + label={{topic.label}} + className="ps-3" + /> +
+ ))} +
+ ))}
} - - {/* TODO */} + + {DIFFICULTY_ITEM_OPTIONS.map((difficulty, index) => ( + // TODO: style to use coloured indicators +
+ setSearchDifficulties( + s => s.includes(difficulty.value) + ? s.filter(v => v !== difficulty.value) + : [...s, difficulty.value] + )} + label={
+ {simplifyDifficultyLabel(difficulty.label)} + +
} + /> +
+ ))}
- -
- {/* TODO: merge in LLM branch and add "Show LLM qs" here */} + + {/* TODO: actually make these buttons do something */} + {/* TODO: add images to attempt status */} +
+ + Not attempted + Not attempted +
} + /> +
+
+ + Completed + Completed +
} + /> +
+
+ + Try again + Try again +
} + /> +
+
+
+
+ {isAda &&
+ {setLlmMarked(p => !p);}} + label={ + {"Include "} + + {" questions"} + } + /> +
} +
{ setRevisionMode(r => !r); debouncedRevisionModeUpdate(); }} - label={} + } />
+ + + + +
{/* TODO: update styling of question block */} - + diff --git a/src/scss/common/checkbox.scss b/src/scss/common/checkbox.scss index dc5884abcc..921b64dd8b 100644 --- a/src/scss/common/checkbox.scss +++ b/src/scss/common/checkbox.scss @@ -1,13 +1,14 @@ .styled-checkbox-wrapper { display: flex; justify-content: center; - + align-items: center; + div { width: 1.6em; min-width: 1.6em; height: 1.6em; position: relative; - + input[type="checkbox"] { position: relative; appearance: none; @@ -19,11 +20,11 @@ outline: none; cursor: pointer; transition: all 250ms cubic-bezier(0.1, 0.75, 0.5, 1); - + &.checked { background: #000; border-color: #000; - + &:hover, &:disabled { background: #333; border-color: #333; @@ -39,7 +40,7 @@ } } } - + &:not(.checked):hover, &:not(.checked):disabled { background: #f8f8f8; border-color: #666; @@ -49,7 +50,7 @@ box-shadow: 0 0 0 0.1em #000; } } - + .tick { position: absolute; display: inline-block; @@ -61,7 +62,7 @@ -ms-transform: rotate(45deg); pointer-events: none; z-index: 1; - + &::before { content: ""; position: absolute; @@ -71,7 +72,7 @@ left: 11px; top: 2px; } - + &::after{ content: ""; position: absolute; @@ -83,7 +84,7 @@ } } } - + > label { width: fit-content; margin: 0; diff --git a/src/scss/common/elements.scss b/src/scss/common/elements.scss index 56fe4afd1e..2835078608 100644 --- a/src/scss/common/elements.scss +++ b/src/scss/common/elements.scss @@ -298,6 +298,10 @@ iframe.email-html { .collapsible-head { margin-left: 0; margin-right: 0; + border-top-style: solid; + border-bottom-style: solid; + border-width: 1px; + border-color: $gray-107; img { transition: transform 0.1s ease; From 3b1dcf4b76bf9259dab6f31cc96531e38ff1ce3a Mon Sep 17 00:00:00 2001 From: Skye Purchase Date: Wed, 24 Jul 2024 16:22:27 +0100 Subject: [PATCH 06/61] Implement filter counts --- .../components/elements/CollapsibleList.tsx | 13 +++- .../components/elements/svg/FilterCount.tsx | 22 ++++++ src/app/components/pages/QuestionFinder.tsx | 78 ++++++++++++++----- src/scss/common/filter.scss | 9 ++- src/scss/cs/filter.scss | 7 +- 5 files changed, 101 insertions(+), 28 deletions(-) create mode 100644 src/app/components/elements/svg/FilterCount.tsx diff --git a/src/app/components/elements/CollapsibleList.tsx b/src/app/components/elements/CollapsibleList.tsx index 0f8d8dd5ac..655f2e461c 100644 --- a/src/app/components/elements/CollapsibleList.tsx +++ b/src/app/components/elements/CollapsibleList.tsx @@ -1,12 +1,14 @@ import React, { useEffect, useRef, useState } from "react"; import { Col, Row } from "reactstrap"; import { Spacer } from "./Spacer"; +import { FilterCount } from "./svg/FilterCount"; import classNames from "classnames"; export interface CollapsibleListProps { title?: string; expanded?: boolean; // initial expanded state only subList?: boolean; + numberSelected?: number; children?: React.ReactNode; } @@ -24,19 +26,22 @@ export const CollapsibleList = (props: CollapsibleListProps) => { const title = props.title && props.subList ? props.title : {props.title}; + const children =
{props.children}
; + return {/* TODO:
*/} -
- {props.children} -
+ {/* TODO: this feels wrong find a better way */} + {props.subList ?
{children}
: children}
; -}; +}; \ No newline at end of file diff --git a/src/app/components/elements/svg/FilterCount.tsx b/src/app/components/elements/svg/FilterCount.tsx new file mode 100644 index 0000000000..663cb55d02 --- /dev/null +++ b/src/app/components/elements/svg/FilterCount.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import {Circle} from "./Circle"; + +const filterIconWidth = 25; + +export const FilterCount = ({count}: {count: number}) => { + return + {`${count} filters selected`} + + + { +
+ {count} +
+
} +
+
; +}; diff --git a/src/app/components/pages/QuestionFinder.tsx b/src/app/components/pages/QuestionFinder.tsx index f9f47084a6..f8909956c1 100644 --- a/src/app/components/pages/QuestionFinder.tsx +++ b/src/app/components/pages/QuestionFinder.tsx @@ -56,6 +56,14 @@ import { Button, Card, CardBody, CardFooter, CardHeader, Col, Container, Dropdow import { CollapsibleList } from "../elements/CollapsibleList"; import { DifficultyIcons } from "../elements/svg/DifficultyIcons"; +interface questionStatus { + notAttempted: boolean; + complete: boolean; + incorrect: boolean; + llmMarked: boolean; + revisionMode: boolean; +} + const selectStyle = { className: "basic-multi-select", classNamePrefix: "select", menuPortalTarget: document.body, styles: {menuPortal: (base: object) => ({...base, zIndex: 9999})} @@ -89,6 +97,7 @@ function simplifyDifficultyLabel(difficultyLabel: string): string { export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { const dispatch = useAppDispatch(); const userContext = useUserViewingContext(); + const userPreferences = useAppSelector((state: AppState) => state?.userPreferences); const params: {[key: string]: string | string[] | undefined} = useQueryParams(false); const history = useHistory(); const eventLog = useRef([]).current; // persist state but do not rerender on mutation @@ -108,6 +117,15 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { const [searchExamBoards, setSearchExamBoards] = useState( arrayFromPossibleCsv(params.examBoards) as ExamBoard[] ); + const [questionStatuses, setQuestionStatuses] = useState( + { + notAttempted: false, + complete: false, + incorrect: false, + llmMarked: false, + revisionMode: !!userPreferences?.DISPLAY_SETTING?.HIDE_QUESTION_ATTEMPTS + } + ); useEffect(function populateExamBoardFromUserContext() { if (!EXAM_BOARD_NULL_OPTIONS.includes(userContext.examBoard)) setSearchExamBoards([userContext.examBoard]); @@ -117,7 +135,6 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { if (!STAGE_NULL_OPTIONS.includes(userContext.stage)) setSearchStages([userContext.stage]); }, [userContext.stage]); - const userPreferences = useAppSelector((state: AppState) => state?.userPreferences); const [searchBook, setSearchBook] = useState(arrayFromPossibleCsv(params.book)); const [searchFastTrack, setSearchFastTrack] = useState(!!params.fasttrack); const [disableLoadMore, setDisableLoadMore] = useState(false); @@ -279,15 +296,11 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [questionList]); - const [revisionMode, setRevisionMode] = useState(!!userPreferences?.DISPLAY_SETTING?.HIDE_QUESTION_ATTEMPTS); - // TODO: Should be taken from the URI? Or from consent? Or default true? - const [llmMarked, setLlmMarked] = useState(false); - useEffect(() => { - if (revisionMode) { + if (questionStatuses.revisionMode) { setHideCompleted(false); } - }, [revisionMode]); + }, [questionStatuses.revisionMode]); const debouncedRevisionModeUpdate = useCallback(debounce(() => { if (user && isLoggedIn(user)) { @@ -365,7 +378,10 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => {
- + {getFilteredStageOptions().map((stage, index) => (
{
))}
- {isAda && + {isAda && {getFilteredExamBoardOptions().map((board, index) => (
{
))}
} - {isAda && + {isAda && {groupBaseTagOptions.map((tag, index) => ( - // TODO: adjust positioning // TODO: make subList {tag.options.map((topic, index) => ( -
+
{ ))} } - + {DIFFICULTY_ITEM_OPTIONS.map((difficulty, index) => ( // TODO: style to use coloured indicators
@@ -432,11 +456,16 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => {
))}
- + acc + item, 0)} + > {/* TODO: actually make these buttons do something */} - {/* TODO: add images to attempt status */}
setQuestionStatuses(s => {return {...s, notAttempted: !s.notAttempted};})} label={
Not attempted {
setQuestionStatuses(s => {return {...s, complete: !s.complete};})} label={
Completed {
setQuestionStatuses(s => {return {...s, incorrect: !s.incorrect};})} label={
Try again {
{isAda &&
{setLlmMarked(p => !p);}} + color="primary" + checked={questionStatuses.llmMarked} + onChange={() => setQuestionStatuses(s => {return {...s, llmMarked: !s.llmMarked};})} label={ {"Include "}
}
{ - setRevisionMode(r => !r); + setQuestionStatuses(s => {return {...s, revisionMode: !s.revisionMode};}); debouncedRevisionModeUpdate(); }} label={ +
} +
+ +
{isExpanded ? setExpanded(prevExpanded => prevExpanded + 1) : setExpanded(prevExpanded => prevExpanded - 1);}} > {getFilteredStageOptions().map((stage, index) => (
@@ -394,8 +449,9 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { ))} {isAda && {isExpanded ? setExpanded(prevExpanded => prevExpanded + 1) : setExpanded(prevExpanded => prevExpanded - 1);}} > {getFilteredExamBoardOptions().map((board, index) => (
@@ -409,12 +465,16 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { ))} } {isAda && {isExpanded ? setExpanded(prevExpanded => prevExpanded + 1) : setExpanded(prevExpanded => prevExpanded - 1);}} > {groupBaseTagOptions.map((tag, index) => ( // TODO: make subList - + {tag.options.map((topic, index) => (
{ ))} } {isExpanded ? setExpanded(prevExpanded => prevExpanded + 1) : setExpanded(prevExpanded => prevExpanded - 1);}} > {DIFFICULTY_ITEM_OPTIONS.map((difficulty, index) => ( - // TODO: style to use coloured indicators
{ ))} acc + item, 0)} + onExpand={(isExpanded) => {isExpanded ? setExpanded(prevExpanded => prevExpanded + 1) : setExpanded(prevExpanded => prevExpanded - 1);}} > {/* TODO: actually make these buttons do something */}
From b245f44f726c0ce01437066e6f9f9918ce059eb1 Mon Sep 17 00:00:00 2001 From: Skye Purchase Date: Thu, 25 Jul 2024 15:46:23 +0100 Subject: [PATCH 08/61] Search bar styling --- src/app/components/pages/QuestionFinder.tsx | 9 +----- src/scss/common/finder.scss | 33 +++++++++++++++++++-- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/app/components/pages/QuestionFinder.tsx b/src/app/components/pages/QuestionFinder.tsx index c8ad919aec..d358b92fa6 100644 --- a/src/app/components/pages/QuestionFinder.tsx +++ b/src/app/components/pages/QuestionFinder.tsx @@ -383,7 +383,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { - + { placeholder={siteSpecific("e.g. Man vs. Horse", "e.g. Creating an AST")} onChange={(e) => handleSearch(e.target.value)} /> - {/* TODO: bring this into the search box - Search */} diff --git a/src/scss/common/finder.scss b/src/scss/common/finder.scss index e0d22e51dc..fa7cf9360a 100644 --- a/src/scss/common/finder.scss +++ b/src/scss/common/finder.scss @@ -1,8 +1,35 @@ #finder-page { .finder-header { - display: flex; - flex-wrap: wrap; - align-items: center; + display: flex; + flex-wrap: wrap; + align-items: center; + } + + .finder-search { + &::after { + content: ""; + background-image: url(/assets/cs/icons/search-jet.svg); + background-repeat: no-repeat; + background-size: 18px 18px; + position: relative; + float: right; + width: 18px; + height: 18px; + top: -33px; + padding-right: 2rem; + } + + &::before { + content: ""; + display: block; + position: relative; + float: right; + width: 1px; + height: 34px; + top: 48px; + right: 46px; + background: #c2c2c2; + } } .search-item-icon { From 5f652a8adf34c97ed52583a92d27a6addfb91a66 Mon Sep 17 00:00:00 2001 From: Skye Purchase Date: Fri, 26 Jul 2024 14:00:16 +0100 Subject: [PATCH 09/61] Implement apply filters button --- src/app/components/handlers/ShowLoading.tsx | 2 +- src/app/components/pages/QuestionFinder.tsx | 39 ++++++++++++++------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/app/components/handlers/ShowLoading.tsx b/src/app/components/handlers/ShowLoading.tsx index 9222b6c8aa..3a7d617b37 100644 --- a/src/app/components/handlers/ShowLoading.tsx +++ b/src/app/components/handlers/ShowLoading.tsx @@ -17,7 +17,7 @@ const defaultPlaceholder =
; -export const ShowLoading = ({until, children, thenRender, placeholder=defaultPlaceholder, ifNotFound=}: ShowLoadingProps) => { +export const ShowLoading = >({until, children, thenRender, placeholder=defaultPlaceholder, ifNotFound=}: ShowLoadingProps) => { const [duringLoad, setDuringLoad] = useState(false); useEffect( () => { let timeout: number; diff --git a/src/app/components/pages/QuestionFinder.tsx b/src/app/components/pages/QuestionFinder.tsx index d358b92fa6..18f1ee2776 100644 --- a/src/app/components/pages/QuestionFinder.tsx +++ b/src/app/components/pages/QuestionFinder.tsx @@ -256,7 +256,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchStages]); - useEffect(() => { + const searchAndUpdateURL = () => { setPageCount(1); setDisableLoadMore(false); setDisplayQuestions(undefined); @@ -282,8 +282,17 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { } history.replace({search: queryString.stringify(params, {encode: false}), state: location.state}); + }; + + const [filtersApplied, setFiltersApplied] = useState(false); + const applyFilters = () => { + setFiltersApplied(true); + searchAndUpdateURL(); + }; + + // search for content whenever the searchQuery changes // eslint-disable-next-line react-hooks/exhaustive-deps - },[searchDebounce, searchQuery, searchTopics, searchExamBoards, searchBook, searchFastTrack, searchStages, searchDifficulties, selections, hideCompleted]); + useEffect(searchAndUpdateURL, [searchQuery]); const questionList = useMemo(() => { if (questions) { @@ -297,7 +306,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { } }, [questions]); - const [displayQuestions, setDisplayQuestions] = useState(undefined); + const [displayQuestions, setDisplayQuestions] = useState([]); const [pageCount, setPageCount] = useState(1); useEffect(() => { @@ -316,9 +325,8 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { || searchStages.length > 0 || Object.entries(questionStatuses) .filter(e => e[0] !== "revisionMode") // Ignore revision mode as it isn't really a filter - .some(e => e[1]) - || searchQuery.length > 0; - }, [questionStatuses, searchDifficulties, searchExamBoards, searchQuery, searchStages, searchTopics]); + .some(e => e[1]); + }, [questionStatuses, searchDifficulties, searchExamBoards, searchStages, searchTopics]); const clearFilters = () => { setSearchDifficulties([]); @@ -333,7 +341,6 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { llmMarked: false, revisionMode: !!userPreferences?.DISPLAY_SETTING?.HIDE_QUESTION_ATTEMPTS }); - setSearchQuery(""); }; useEffect(() => { @@ -388,6 +395,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { handleSearch(e.target.value)} /> @@ -608,8 +616,14 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { - @@ -628,9 +642,8 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { - {[searchQuery, searchTopics, searchBook, searchStages, searchDifficulties, searchExamBoards].every(v => v.length === 0) && - selections.every(v => v.length === 0) ? - Please select filters : + {!filtersApplied && searchQuery === "" ? + Please select and apply filters : (displayQuestions?.length ? <> @@ -655,7 +668,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { Not found what you're looking for? Try refining your search filters.
} : - No results found) + No results match your criteria) } From 9d02b77bedcf1ada20a1b30930edf1a4cd02ca84 Mon Sep 17 00:00:00 2001 From: Skye Purchase Date: Mon, 29 Jul 2024 11:19:23 +0100 Subject: [PATCH 10/61] Implement temporary question status filters --- src/app/components/pages/QuestionFinder.tsx | 38 ++++++++++++--------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/src/app/components/pages/QuestionFinder.tsx b/src/app/components/pages/QuestionFinder.tsx index 18f1ee2776..a65b14e044 100644 --- a/src/app/components/pages/QuestionFinder.tsx +++ b/src/app/components/pages/QuestionFinder.tsx @@ -62,6 +62,7 @@ interface questionStatus { incorrect: boolean; llmMarked: boolean; revisionMode: boolean; + hideCompleted: boolean; // TODO: remove when implementing desired filters } const selectStyle = { @@ -123,7 +124,8 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { complete: false, incorrect: false, llmMarked: false, - revisionMode: !!userPreferences?.DISPLAY_SETTING?.HIDE_QUESTION_ATTEMPTS + revisionMode: !!userPreferences?.DISPLAY_SETTING?.HIDE_QUESTION_ATTEMPTS, + hideCompleted: !!params.hideCompleted } ); const [numExpanded, setExpanded] = useState(0); @@ -159,8 +161,6 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { processTagHierarchy(subjects, fields, topics) ); - const [hideCompleted, setHideCompleted] = useState(!!params.hideCompleted); - const choices = [tags.allSubjectTags.map(itemiseTag)]; let index; for (index = 0; index < selections.length && index < 2; index++) { @@ -260,7 +260,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { setPageCount(1); setDisableLoadMore(false); setDisplayQuestions(undefined); - searchDebounce(searchQuery, searchTopics, searchExamBoards, searchBook, searchStages, searchDifficulties, selections, tiers, searchFastTrack, hideCompleted, 0); + searchDebounce(searchQuery, searchTopics, searchExamBoards, searchBook, searchStages, searchDifficulties, selections, tiers, searchFastTrack, questionStatuses.hideCompleted, 0); const params: {[key: string]: string} = {}; if (searchStages.length) params.stages = toSimpleCSV(searchStages); @@ -270,7 +270,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { if (isAda && searchExamBoards.length) params.examBoards = toSimpleCSV(searchExamBoards); if (isPhy && searchBook.length) params.book = toSimpleCSV(searchBook); if (isPhy && searchFastTrack) params.fasttrack = "set"; - if (hideCompleted) params.hideCompleted = "set"; + if (questionStatuses.hideCompleted) params.hideCompleted = "set"; if (isPhy) { tiers.forEach((tier, i) => { @@ -339,16 +339,11 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { complete: false, incorrect: false, llmMarked: false, - revisionMode: !!userPreferences?.DISPLAY_SETTING?.HIDE_QUESTION_ATTEMPTS + revisionMode: !!userPreferences?.DISPLAY_SETTING?.HIDE_QUESTION_ATTEMPTS, + hideCompleted: false }); }; - useEffect(() => { - if (questionStatuses.revisionMode) { - setHideCompleted(false); - } - }, [questionStatuses.revisionMode]); - const debouncedRevisionModeUpdate = useCallback(debounce(() => { if (user && isLoggedIn(user)) { const userToUpdate = {...user, password: null}; @@ -522,7 +517,17 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { numberSelected={Object.values(questionStatuses).reduce((acc, item) => acc + item, 0)} onExpand={(isExpanded) => {isExpanded ? setExpanded(prevExpanded => prevExpanded + 1) : setExpanded(prevExpanded => prevExpanded - 1);}} > - {/* TODO: actually make these buttons do something */} +
+ setQuestionStatuses(s => {return {...s, hideCompleted: !s.hideCompleted};})} + label={
+ Hide complete +
} + /> +
+ {/* TODO: implement new completeness filters
{ />
} /> -
+
*/}

+ {/* TODO: implement once necessary tags are available {isAda &&
{ {" questions"} } /> -
} +
}*/}
{
- - - - - + + + diff --git a/src/scss/common/finder.scss b/src/scss/common/finder.scss index fa7cf9360a..b2aaa3ad5b 100644 --- a/src/scss/common/finder.scss +++ b/src/scss/common/finder.scss @@ -51,5 +51,11 @@ margin-right: 0.5rem; } } + + .filter-btn { + position: -webkit-sticky; + position: sticky; + bottom: 1rem; + } } From 23a8348154123d275dd673bfc0e7c94194e8419a Mon Sep 17 00:00:00 2001 From: Skye Purchase Date: Mon, 29 Jul 2024 13:48:12 +0100 Subject: [PATCH 12/61] Add difficulty modal link --- src/app/components/pages/QuestionFinder.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/app/components/pages/QuestionFinder.tsx b/src/app/components/pages/QuestionFinder.tsx index c3318d4268..8ea83ca74c 100644 --- a/src/app/components/pages/QuestionFinder.tsx +++ b/src/app/components/pages/QuestionFinder.tsx @@ -494,6 +494,17 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { numberSelected={searchDifficulties.length} onExpand={(isExpanded) => {isExpanded ? setExpanded(prevExpanded => prevExpanded + 1) : setExpanded(prevExpanded => prevExpanded - 1);}} > +
+ +
{DIFFICULTY_ITEM_OPTIONS.map((difficulty, index) => (
{ onClick={(e) => { e.preventDefault(); // TODO: add modal - console.log("show LLM modal here"); + console.log("show revision mode modal here"); }}> Revision mode } From cf6e5632a5e9b92553b7ee8f25acb00c12c8d409 Mon Sep 17 00:00:00 2001 From: Skye Purchase Date: Mon, 29 Jul 2024 14:13:38 +0100 Subject: [PATCH 13/61] Fix style changes on Physics --- src/app/components/pages/QuestionFinder.tsx | 6 +++--- src/scss/common/finder.scss | 6 ++++++ src/scss/phy/filter.scss | 5 +++++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/app/components/pages/QuestionFinder.tsx b/src/app/components/pages/QuestionFinder.tsx index 8ea83ca74c..1c130ad01d 100644 --- a/src/app/components/pages/QuestionFinder.tsx +++ b/src/app/components/pages/QuestionFinder.tsx @@ -398,7 +398,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { - + @@ -588,7 +588,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { />
*/}
-
+
{/* TODO: implement once necessary tags are available {isAda &&
@@ -648,7 +648,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { {/* TODO: update styling of question block */} - + diff --git a/src/scss/common/finder.scss b/src/scss/common/finder.scss index b2aaa3ad5b..2ec88b0399 100644 --- a/src/scss/common/finder.scss +++ b/src/scss/common/finder.scss @@ -57,5 +57,11 @@ position: sticky; bottom: 1rem; } + + .filter-separator { + overflow: hidden; + border-top: solid #c2c2c2 1px; + border-bottom: none; + } } diff --git a/src/scss/phy/filter.scss b/src/scss/phy/filter.scss index 6da8ce338f..7ddfe2bdad 100644 --- a/src/scss/phy/filter.scss +++ b/src/scss/phy/filter.scss @@ -107,6 +107,11 @@ svg { stroke: $phy_extra_force_yellow; } + &.filter-count { + fill: $gray-120; + stroke: none; + } + &:focus { outline: none; stroke: black !important; From 90584380eead6f79ed18c25d4414c93f230c7f01 Mon Sep 17 00:00:00 2001 From: Skye Purchase Date: Mon, 29 Jul 2024 16:11:14 +0100 Subject: [PATCH 14/61] Add book filter options --- src/app/components/pages/QuestionFinder.tsx | 56 ++++++++++++++++++--- src/scss/phy/filter.scss | 2 +- 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/src/app/components/pages/QuestionFinder.tsx b/src/app/components/pages/QuestionFinder.tsx index 1c130ad01d..c96d4acf86 100644 --- a/src/app/components/pages/QuestionFinder.tsx +++ b/src/app/components/pages/QuestionFinder.tsx @@ -150,7 +150,8 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { if (!STAGE_NULL_OPTIONS.includes(userContext.stage)) setSearchStages([userContext.stage]); }, [userContext.stage]); - const [searchBook, setSearchBook] = useState(arrayFromPossibleCsv(params.book)); + const [searchBooks, setSearchBooks] = useState(arrayFromPossibleCsv(params.book)); + const [excludeBooks, setExcludeBooks] = useState(!!params.excludeBooks); const [searchFastTrack, setSearchFastTrack] = useState(!!params.fasttrack); const [disableLoadMore, setDisableLoadMore] = useState(false); @@ -260,7 +261,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { setPageCount(1); setDisableLoadMore(false); setDisplayQuestions(undefined); - searchDebounce(searchQuery, searchTopics, searchExamBoards, searchBook, searchStages, searchDifficulties, selections, tiers, searchFastTrack, questionStatuses.hideCompleted, 0); + searchDebounce(searchQuery, searchTopics, searchExamBoards, searchBooks, searchStages, searchDifficulties, selections, tiers, searchFastTrack, questionStatuses.hideCompleted, 0); const params: {[key: string]: string} = {}; if (searchStages.length) params.stages = toSimpleCSV(searchStages); @@ -268,7 +269,10 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { if (searchQuery.length) params.query = encodeURIComponent(searchQuery); if (isAda && searchTopics.length) params.topics = toSimpleCSV(searchTopics); if (isAda && searchExamBoards.length) params.examBoards = toSimpleCSV(searchExamBoards); - if (isPhy && searchBook.length) params.book = toSimpleCSV(searchBook); + if (isPhy && !excludeBooks && searchBooks.length) { + params.book = toSimpleCSV(searchBooks); + } + if (isPhy && excludeBooks) params.excludeBooks = "set"; if (isPhy && searchFastTrack) params.fasttrack = "set"; if (questionStatuses.hideCompleted) params.hideCompleted = "set"; @@ -291,6 +295,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { }; // search for content whenever the searchQuery changes + // but do not change whether filters have been applied or not // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(searchAndUpdateURL, [searchQuery]); @@ -323,16 +328,20 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { || searchTopics.length > 0 || searchExamBoards.length > 0 || searchStages.length > 0 + || searchBooks.length > 0 + || excludeBooks || Object.entries(questionStatuses) .filter(e => e[0] !== "revisionMode") // Ignore revision mode as it isn't really a filter .some(e => e[1]); - }, [questionStatuses, searchDifficulties, searchExamBoards, searchStages, searchTopics]); + }, [excludeBooks, questionStatuses, searchBooks.length, searchDifficulties.length, searchExamBoards.length, searchStages.length, searchTopics.length]); const clearFilters = () => { setSearchDifficulties([]); setSearchTopics([]); setSearchExamBoards([]); setSearchStages([]); + setSearchBooks([]); + setExcludeBooks(false); setQuestionStatuses( { notAttempted: false, @@ -523,6 +532,36 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => {
))} + {isExpanded ? setExpanded(prevExpanded => prevExpanded + 1) : setExpanded(prevExpanded => prevExpanded - 1);}} + > + <> +
+ setExcludeBooks(p => !p)} + label={Exclude skills book questions} + /> +
+ {bookOptions.map((book, index) => ( +
+ setSearchBooks( + s => s.includes(book.value) + ? s.filter(v => v !== book.value) + : [...s, book.value] + )} + label={{book.label}} + /> +
+ ))} + +
acc + item, 0)} @@ -634,11 +673,12 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { @@ -667,7 +707,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { From 8929e04b0225639a69c7af3674e2b6739a44e941 Mon Sep 17 00:00:00 2001 From: Skye Purchase Date: Tue, 30 Jul 2024 11:50:14 +0100 Subject: [PATCH 16/61] Increase width of question finder on Physics --- src/app/components/pages/QuestionFinder.tsx | 2 +- src/scss/phy/finder.scss | 5 +++++ src/scss/phy/isaac.scss | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 src/scss/phy/finder.scss diff --git a/src/app/components/pages/QuestionFinder.tsx b/src/app/components/pages/QuestionFinder.tsx index 49031ed1f5..e941a4f81c 100644 --- a/src/app/components/pages/QuestionFinder.tsx +++ b/src/app/components/pages/QuestionFinder.tsx @@ -381,7 +381,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => {
; - return + return diff --git a/src/scss/phy/finder.scss b/src/scss/phy/finder.scss new file mode 100644 index 0000000000..938f52966a --- /dev/null +++ b/src/scss/phy/finder.scss @@ -0,0 +1,5 @@ +@import "../common/finder"; + +.question-finder-container { + max-width: 1140px; +} diff --git a/src/scss/phy/isaac.scss b/src/scss/phy/isaac.scss index a2f7478b27..9b82dd4eb1 100644 --- a/src/scss/phy/isaac.scss +++ b/src/scss/phy/isaac.scss @@ -210,7 +210,7 @@ $theme-colors-border-subtle: map-merge($theme-colors-border-subtle, $custom-colo @import "my-account"; @import "../common/topic"; @import "../common/search"; -@import "../common/finder"; +@import "finder"; @import "../common/my-progress"; @import "gameboard"; @import "groups"; From 065bd2a5f41ef23df01cd450a93ba3dc0280b2e7 Mon Sep 17 00:00:00 2001 From: Skye Purchase Date: Tue, 30 Jul 2024 14:59:20 +0100 Subject: [PATCH 17/61] Implement visual filters --- .../elements/svg/HierarchyFilter.tsx | 43 +++++++++------ src/app/components/pages/QuestionFinder.tsx | 55 ++++++++++--------- 2 files changed, 55 insertions(+), 43 deletions(-) diff --git a/src/app/components/elements/svg/HierarchyFilter.tsx b/src/app/components/elements/svg/HierarchyFilter.tsx index 1260e00cb1..065d80f45f 100644 --- a/src/app/components/elements/svg/HierarchyFilter.tsx +++ b/src/app/components/elements/svg/HierarchyFilter.tsx @@ -31,7 +31,8 @@ interface HierarchySummaryProps { } interface HierarchyFilterProps extends HierarchySummaryProps { - setTierSelection: (tierIndex: number) => React.Dispatch[]>> + questionFinderFilter?: boolean; + setTierSelection: (tierIndex: number) => React.Dispatch[]>>; } function naturalLanguageList(list: string[]) { @@ -42,8 +43,8 @@ function naturalLanguageList(list: string[]) { return `${lowerCaseList.slice(0, lastIndex).join(", ")} and ${lowerCaseList[lastIndex]}`; } -function hexRowTranslation(deviceSize: DeviceSize, hexagon: HexagonProportions, i: number) { - if (i == 0 || deviceSize != "xs") { +function hexRowTranslation(deviceSize: DeviceSize, hexagon: HexagonProportions, i: number, questionFinderFilter: boolean) { + if (i == 0 || (deviceSize != "xs" && !questionFinderFilter)) { return `translate(0,${i * (6 * hexagon.quarterHeight + 2 * hexagon.padding)})`; } else { const x = (i * 2 - 1) * (hexagon.halfWidth + hexagon.padding); @@ -52,52 +53,58 @@ function hexRowTranslation(deviceSize: DeviceSize, hexagon: HexagonProportions, } } -function connectionRowTranslation(deviceSize: DeviceSize, hexagon: HexagonProportions, i: number) { - if (deviceSize != "xs") { +function connectionRowTranslation(deviceSize: DeviceSize, hexagon: HexagonProportions, i: number, questionFinderFilter: boolean) { + if (deviceSize != "xs" && !questionFinderFilter) { return `translate(${hexagon.halfWidth + hexagon.padding},${3 * hexagon.quarterHeight + hexagon.padding + i * (6 * hexagon.quarterHeight + 2 * hexagon.padding)})`; } else { return `translate(0,0)`; // positioning is managed absolutely not through transformation } } -function hexagonTranslation(deviceSize: DeviceSize, hexagon: HexagonProportions, i: number, j: number) { - if (i == 0 || deviceSize != "xs") { +function hexagonTranslation(deviceSize: DeviceSize, hexagon: HexagonProportions, i: number, j: number, questionFinderFilter: boolean) { + if (i == 0 || (deviceSize != "xs" && !questionFinderFilter)) { return `translate(${j * 2 * (hexagon.halfWidth + hexagon.padding)},0)`; } else { return `translate(0,${j * (4 * hexagon.quarterHeight + hexagon.padding)})`; } } -export function HierarchyFilterHexagonal({tiers, choices, selections, setTierSelection}: HierarchyFilterProps) { +export function HierarchyFilterHexagonal({tiers, choices, selections, questionFinderFilter, setTierSelection}: HierarchyFilterProps) { const deviceSize = useDeviceSize(); const leadingHexagon = calculateHexagonProportions(36, deviceSize === "xs" ? 2 : 8); - const hexagon = calculateHexagonProportions(36, deviceSize === "xs" ? 16 : 8); + const hexagon = calculateHexagonProportions(36, deviceSize === "xs" || !!questionFinderFilter ? 16 : 8); const focusPadding = 3; const maxOptions = choices.slice(1).map(c => c.length).reduce((a, b) => Math.max(a, b), 0); - const height = deviceSize != "xs" ? + const height = (deviceSize != "xs" && !questionFinderFilter) ? 2 * focusPadding + 4 * hexagon.quarterHeight + (tiers.length - 1) * (6 * hexagon.quarterHeight + 2 * hexagon.padding) : 2 * focusPadding + 4 * hexagon.quarterHeight + maxOptions * (4 * hexagon.quarterHeight + hexagon.padding) + (maxOptions ? hexagon.padding : 0); + const width = (8 * leadingHexagon.halfWidth) + (6 * leadingHexagon.padding) + (2 * focusPadding); - return + return Topic filter selector {/* Connections */} {tiers.slice(1).map((tier, i) => { const subject = selections?.[0]?.[0] ? selections[0][0].value : ""; - return + return c.value).indexOf(selections[i][0]?.value)} optionIndices={[...choices[i+1].keys()]} // range from 0 to choices[i+1].length targetIndices={selections[i+1]?.map(s => choices[i+1].map(c => c.value).indexOf(s.value)) || [-1]} leadingHexagonProportions={leadingHexagon} hexagonProportions={hexagon} connectionProperties={connectionProperties} - rowIndex={i} mobile={deviceSize === "xs"} className={`connection ${subject}`} + rowIndex={i} mobile={deviceSize === "xs" || !!questionFinderFilter} className={`connection ${subject}`} /> ; })} {/* Hexagons */} - {tiers.map((tier, i) => + {tiers.map((tier, i) => {choices[i].map((choice, j) => { const subject = i == 0 ? choice.value : selections[0][0].value; const isSelected = !!selections[i]?.map(s => s.value).includes(choice.value); @@ -111,7 +118,7 @@ export function HierarchyFilterHexagonal({tiers, choices, selections, setTierSel ); } - return + return
@@ -144,7 +151,7 @@ export function HierarchyFilterSummary({tiers, choices, selections}: HierarchySu const hexagon = calculateHexagonProportions(10, 2); const hexKeyPoints = addHexagonKeyPoints(hexagon); const connection = {length: 60}; - const height = `${hexagon.quarterHeight * 4 + hexagon.padding * 2 + 32}px` + const height = `${hexagon.quarterHeight * 4 + hexagon.padding * 2 + 32}px`; if (! selections[0]?.length) { return @@ -176,7 +183,7 @@ export function HierarchyFilterSummary({tiers, choices, selections}: HierarchySu className={`connection`} />} - + ; })} @@ -190,7 +197,7 @@ export function HierarchyFilterSummary({tiers, choices, selections}: HierarchySu
-
+
; })}
; diff --git a/src/app/components/pages/QuestionFinder.tsx b/src/app/components/pages/QuestionFinder.tsx index e941a4f81c..bdc0b6f370 100644 --- a/src/app/components/pages/QuestionFinder.tsx +++ b/src/app/components/pages/QuestionFinder.tsx @@ -463,35 +463,40 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => {
))} } - {isAda && {isExpanded ? setExpanded(prevExpanded => prevExpanded + 1) : setExpanded(prevExpanded => prevExpanded - 1);}} > - {groupBaseTagOptions.map((tag, index) => ( - // TODO: make subList - - {tag.options.map((topic, index) => ( -
- setSearchTopics( - s => s.includes(topic.value) - ? s.filter(v => v !== topic.value) - : [...s, topic.value] - )} - label={{topic.label}} - className="ps-3" - /> -
- ))} -
- ))} -
} + {siteSpecific( +
+ +
, + groupBaseTagOptions.map((tag, index) => ( + // TODO: make subList + + {tag.options.map((topic, index) => ( +
+ setSearchTopics( + s => s.includes(topic.value) + ? s.filter(v => v !== topic.value) + : [...s, topic.value] + )} + label={{topic.label}} + className="ps-3" + /> +
+ ))} +
+ )) + )} + Date: Tue, 30 Jul 2024 16:33:14 +0100 Subject: [PATCH 18/61] Fix difficulty icon alignment --- .../components/elements/svg/DifficultyIcons.tsx | 4 ++-- src/app/components/pages/QuestionFinder.tsx | 16 +++++++++------- src/scss/cs/isaac.scss | 2 ++ src/scss/phy/isaac.scss | 2 ++ 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/app/components/elements/svg/DifficultyIcons.tsx b/src/app/components/elements/svg/DifficultyIcons.tsx index 546eba2381..d8ff5724a2 100644 --- a/src/app/components/elements/svg/DifficultyIcons.tsx +++ b/src/app/components/elements/svg/DifficultyIcons.tsx @@ -38,12 +38,12 @@ function SingleDifficultyIconShape({difficultyCategory, difficultyCategoryLevel, ; } -export function DifficultyIcons({difficulty, blank} : {difficulty: Difficulty, blank?: boolean}) { +export function DifficultyIcons({difficulty, blank, classnames} : {difficulty: Difficulty, blank?: boolean, classnames?: string}) { const difficultyLabel = difficultyShortLabelMap[difficulty]; const difficultyCategory = difficultyLabel[0]; const difficultyLevel = parseInt(difficultyLabel[1]); - return
+ return
{ )} {isExpanded ? setExpanded(prevExpanded => prevExpanded + 1) : setExpanded(prevExpanded => prevExpanded - 1);}} > -
+
))} - + } acc + item, 0)} onExpand={(isExpanded) => {isExpanded ? setExpanded(prevExpanded => prevExpanded + 1) : setExpanded(prevExpanded => prevExpanded - 1);}} > diff --git a/src/scss/cs/isaac.scss b/src/scss/cs/isaac.scss index 6e04c0fb6f..169deb91fc 100644 --- a/src/scss/cs/isaac.scss +++ b/src/scss/cs/isaac.scss @@ -312,6 +312,8 @@ $spacers: map-merge($spacers, ( // Special Bootstrap variable that is used to ge 6: ($spacer * 6.25) // 100px-ish )); +$enable-negative-margins: true; + // all Bootstrap overrides must appear before this @import "~bootstrap/scss/bootstrap"; @import "~katex/dist/katex.min.css"; diff --git a/src/scss/phy/isaac.scss b/src/scss/phy/isaac.scss index 9b82dd4eb1..81a7d651aa 100644 --- a/src/scss/phy/isaac.scss +++ b/src/scss/phy/isaac.scss @@ -81,6 +81,8 @@ $question-font-weight: 600; // forms $border-color: black; +$enable-negative-margins: true; + // ISAAC // Node module imports @import "~bootstrap/scss/functions"; From cbffd3e770e7b89a8d62003c2a075da017eea313 Mon Sep 17 00:00:00 2001 From: Skye Purchase Date: Mon, 29 Jul 2024 16:11:14 +0100 Subject: [PATCH 19/61] Implement book question exclusion --- src/IsaacAppTypes.tsx | 1 + src/app/components/pages/QuestionFinder.tsx | 62 +++++++++++++++------ 2 files changed, 47 insertions(+), 16 deletions(-) diff --git a/src/IsaacAppTypes.tsx b/src/IsaacAppTypes.tsx index 61fb7a4158..b14565168e 100644 --- a/src/IsaacAppTypes.tsx +++ b/src/IsaacAppTypes.tsx @@ -561,6 +561,7 @@ export interface QuestionSearchQuery { stages?: string; difficulties?: string; examBoards?: string; + questionCategories?: string; fasttrack?: boolean; hideCompleted?: boolean; startIndex?: number; diff --git a/src/app/components/pages/QuestionFinder.tsx b/src/app/components/pages/QuestionFinder.tsx index d93e74d1b4..7281696e70 100644 --- a/src/app/components/pages/QuestionFinder.tsx +++ b/src/app/components/pages/QuestionFinder.tsx @@ -151,6 +151,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { }, [userContext.stage]); const [searchBooks, setSearchBooks] = useState(arrayFromPossibleCsv(params.book)); + const [excludeBooks, setExcludeBooks] = useState(!!params.excludeBooks); const [searchFastTrack, setSearchFastTrack] = useState(!!params.fasttrack); const [disableLoadMore, setDisableLoadMore] = useState(false); @@ -191,7 +192,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { const user = useAppSelector((state: AppState) => state && state.user); const searchDebounce = useCallback( - debounce((searchString: string, topics: string[], examBoards: string[], book: string[], stages: string[], difficulties: string[], hierarchySelections: Item[][], tiers: Tier[], fasttrack: boolean, hideCompleted: boolean, startIndex: number) => { + debounce((searchString: string, topics: string[], examBoards: string[], book: string[], stages: string[], difficulties: string[], hierarchySelections: Item[][], tiers: Tier[], excludeBooks: boolean, fasttrack: boolean, hideCompleted: boolean, startIndex: number) => { if ([searchString, topics, book, stages, difficulties, examBoards].every(v => v.length === 0) && hierarchySelections.every(v => v.length === 0) && !fasttrack) { // Nothing to search for dispatch(clearQuestionSearch); @@ -221,10 +222,11 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { fields: filterParams.fields?.join(",") || undefined, subjects: filterParams.subjects?.join(",") || undefined, topics: filterParams.topics?.join(",") || undefined, - books: book.join(",") || undefined, + books: (!excludeBooks && book.join(",")) || undefined, stages: stages.join(",") || undefined, difficulties: difficulties.join(",") || undefined, examBoards: examBoardString, + questionCategories: excludeBooks ? "problem_solving" : "problem_solving,book", fasttrack, hideCompleted, startIndex, @@ -260,7 +262,10 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { setPageCount(1); setDisableLoadMore(false); setDisplayQuestions(undefined); - searchDebounce(searchQuery, searchTopics, searchExamBoards, searchBooks, searchStages, searchDifficulties, selections, tiers, searchFastTrack, questionStatuses.hideCompleted, 0); + searchDebounce( + searchQuery, searchTopics, searchExamBoards, searchBooks, searchStages, + searchDifficulties, selections, tiers, excludeBooks, searchFastTrack, + questionStatuses.hideCompleted, 0); const params: {[key: string]: string} = {}; if (searchStages.length) params.stages = toSimpleCSV(searchStages); @@ -268,7 +273,10 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { if (searchQuery.length) params.query = encodeURIComponent(searchQuery); if (isAda && searchTopics.length) params.topics = toSimpleCSV(searchTopics); if (isAda && searchExamBoards.length) params.examBoards = toSimpleCSV(searchExamBoards); - if (isPhy && searchBooks.length) params.book = toSimpleCSV(searchBooks); + if (isPhy && !excludeBooks && searchBooks.length) { + params.book = toSimpleCSV(searchBooks); + } + if (isPhy && excludeBooks) params.excludeBooks = "set"; if (isPhy && searchFastTrack) params.fasttrack = "set"; if (questionStatuses.hideCompleted) params.hideCompleted = "set"; @@ -336,6 +344,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { setSearchExamBoards([]); setSearchStages([]); setSearchBooks([]); + setExcludeBooks(false); setQuestionStatuses( { notAttempted: false, @@ -534,23 +543,33 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { {isPhy && {isExpanded ? setExpanded(prevExpanded => prevExpanded + 1) : setExpanded(prevExpanded => prevExpanded - 1);}} > - {bookOptions.map((book, index) => ( + <>
setSearchBooks( - s => s.includes(book.value) - ? s.filter(v => v !== book.value) - : [...s, book.value] - )} - label={{book.label}} + checked={excludeBooks} + onChange={() => setExcludeBooks(p => !p)} + label={Exclude skills book questions} />
- ))} + {bookOptions.map((book, index) => ( +
+ setSearchBooks( + s => s.includes(book.value) + ? s.filter(v => v !== book.value) + : [...s, book.value] + )} + label={{book.label}} + /> +
+ ))} +
} { From 42f64b88401f7f8b41e7fd2d449db12c59b7f181 Mon Sep 17 00:00:00 2001 From: Meurig Thomas Date: Tue, 30 Jul 2024 09:20:54 +0100 Subject: [PATCH 22/61] Refactor out QuestionFinderFilterPanel --- .../panels/QuestionFinderFilterPanel.tsx | 371 +++++++++++++++++ src/app/components/pages/QuestionFinder.tsx | 389 ++---------------- 2 files changed, 401 insertions(+), 359 deletions(-) create mode 100644 src/app/components/elements/panels/QuestionFinderFilterPanel.tsx diff --git a/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx b/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx new file mode 100644 index 0000000000..f9a2acc4b8 --- /dev/null +++ b/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx @@ -0,0 +1,371 @@ +import React, { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react"; +import { Button, Card, CardBody, CardHeader, Col } from "reactstrap"; +import { CollapsibleList } from "../CollapsibleList"; +import { DIFFICULTY_ITEM_OPTIONS, getFilteredExamBoardOptions, getFilteredStageOptions, groupTagSelectionsByParent, isAda, isLoggedIn, isPhy, Item, siteSpecific, STAGE, TAG_ID, tags } from "../../../services"; +import { debounce } from "lodash"; +import { AppState, updateCurrentUser, useAppDispatch, useAppSelector } from "../../../state"; +import { Difficulty, ExamBoard } from "../../../../IsaacApiTypes"; +import { QuestionStatus } from "../../pages/QuestionFinder"; +import classNames from "classnames"; +import { StyledCheckbox } from "../inputs/StyledCheckbox"; +import { DifficultyIcons } from "../svg/DifficultyIcons"; +import { GroupBase } from "react-select"; +import { HierarchyFilterHexagonal, Tier } from "../svg/HierarchyFilter"; + + +const bookOptions: Item[] = [ + {value: "phys_book_step_up", label: "Step Up to GCSE Physics"}, + {value: "phys_book_gcse", label: "GCSE Physics"}, + {value: "physics_skills_19", label: "A Level Physics (3rd Edition)"}, + {value: "physics_linking_concepts", label: "Linking Concepts in Pre-Uni Physics"}, + {value: "maths_book_gcse", label: "GCSE Maths"}, + {value: "maths_book", label: "Pre-Uni Maths"}, + {value: "chemistry_16", label: "A-Level Physical Chemistry"} +]; +function simplifyDifficultyLabel(difficultyLabel: string): string { + const labelLength = difficultyLabel.length; + const type = difficultyLabel.slice(0, labelLength - 4); + const level = difficultyLabel.slice(labelLength - 2, labelLength - 1); + return type + level; +} + + +interface QuestionFinderFilterPanelProps { + searchDifficulties: Difficulty[]; setSearchDifficulties: Dispatch>; + searchTopics: string[], setSearchTopics: Dispatch>; + searchStages: STAGE[], setSearchStages: Dispatch>; + searchExamBoards: ExamBoard[], setSearchExamBoards: Dispatch>; + questionStatuses: QuestionStatus, setQuestionStatuses: Dispatch>; + searchBooks: string[], setSearchBooks: Dispatch>; + excludeBooks: boolean, setExcludeBooks: Dispatch>; + tiers: Tier[], choices: Item[][], selections: Item[][], setTierSelection: (tierIndex: number) => React.Dispatch[]>>, + applyFilters: () => void; clearFilters: () => void; + filtersSelected: boolean; searchDisabled: boolean; +} +export function QuestionFinderFilterPanel(props: QuestionFinderFilterPanelProps) { + const { + searchDifficulties, setSearchDifficulties, + searchTopics, setSearchTopics, + searchStages, setSearchStages, + searchExamBoards, setSearchExamBoards, + questionStatuses, setQuestionStatuses, + searchBooks, setSearchBooks, + excludeBooks, setExcludeBooks, + tiers, choices, selections, setTierSelection, + applyFilters, clearFilters, filtersSelected, searchDisabled + } = props; + const dispatch = useAppDispatch(); + const user = useAppSelector((state: AppState) => state?.user); + const userPreferences = useAppSelector((state: AppState) => state?.userPreferences); + + const [numExpanded, setExpanded] = useState(0); + const [allExpanded, setAllExpanded] = useState(undefined); + + useEffect(function syncAllExpandedAndNumExpanded() { + // If the user manually close all the list after expanding them + // OR if the user manually opens a list after closing them all + if ((numExpanded === 0 && allExpanded) + || (numExpanded > 0 && !allExpanded)) { + // Go into the undefined state until being clicked + setAllExpanded(undefined); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [numExpanded]); + + const debouncedRevisionModeUpdate = useCallback(debounce(() => { + if (user && isLoggedIn(user)) { + const userToUpdate = {...user, password: null}; + const userPreferencesToUpdate = { + DISPLAY_SETTING: {...userPreferences?.DISPLAY_SETTING, HIDE_QUESTION_ATTEMPTS: !userPreferences?.DISPLAY_SETTING?.HIDE_QUESTION_ATTEMPTS} + }; + dispatch(updateCurrentUser(userToUpdate, userPreferencesToUpdate, undefined, null, user, false)); + }}, 250, {trailing: true} + ), []); + + const tagOptions: { options: Item[]; label: string }[] = isPhy ? tags.allTags.map(groupTagSelectionsByParent) : tags.allSubcategoryTags.map(groupTagSelectionsByParent); + const groupBaseTagOptions: GroupBase>[] = tagOptions; + + return + + + Filter + + + Filter by + + {filtersSelected &&
+ +
} +
+ +
+
+ + {isExpanded ? setExpanded(prevExpanded => prevExpanded + 1) : setExpanded(prevExpanded => prevExpanded - 1);}} + > + {getFilteredStageOptions().map((stage, index) => ( +
+ setSearchStages(s => s.includes(stage.value) ? s.filter(v => v !== stage.value) : [...s, stage.value])} + label={{stage.label}} + /> +
+ ))} +
+ {isAda && {isExpanded ? setExpanded(prevExpanded => prevExpanded + 1) : setExpanded(prevExpanded => prevExpanded - 1);}} + > + {getFilteredExamBoardOptions().map((board, index) => ( +
+ setSearchExamBoards(s => s.includes(board.value) ? s.filter(v => v !== board.value) : [...s, board.value])} + label={{board.label}} + /> +
+ ))} +
} + tier.length) + .reverse() + .find(l => l > 0), + searchTopics.length + )} + onExpand={(isExpanded) => {isExpanded ? setExpanded(prevExpanded => prevExpanded + 1) : setExpanded(prevExpanded => prevExpanded - 1);}} + > + {siteSpecific( +
+ +
, + groupBaseTagOptions.map((tag, index) => ( + // TODO: make subList + + {tag.options.map((topic, index) => ( +
+ setSearchTopics( + s => s.includes(topic.value) + ? s.filter(v => v !== topic.value) + : [...s, topic.value] + )} + label={{topic.label}} + className="ps-3" + /> +
+ ))} +
+ )) + )} +
+ {isExpanded ? setExpanded(prevExpanded => prevExpanded + 1) : setExpanded(prevExpanded => prevExpanded - 1);}} + > +
+ +
+ {DIFFICULTY_ITEM_OPTIONS.map((difficulty, index) => ( +
+ setSearchDifficulties( + s => s.includes(difficulty.value) + ? s.filter(v => v !== difficulty.value) + : [...s, difficulty.value] + )} + label={
+ {simplifyDifficultyLabel(difficulty.label)} + +
} + /> +
+ ))} +
+ {isPhy && {isExpanded ? setExpanded(prevExpanded => prevExpanded + 1) : setExpanded(prevExpanded => prevExpanded - 1);}} + > + <> +
+ setExcludeBooks(p => !p)} + label={Exclude skills book questions} + /> +
+ {bookOptions.map((book, index) => ( +
+ setSearchBooks( + s => s.includes(book.value) + ? s.filter(v => v !== book.value) + : [...s, book.value] + )} + label={{book.label}} + /> +
+ ))} + +
} + acc + item, 0)} + onExpand={(isExpanded) => {isExpanded ? setExpanded(prevExpanded => prevExpanded + 1) : setExpanded(prevExpanded => prevExpanded - 1);}} + > +
+ setQuestionStatuses(s => {return {...s, hideCompleted: !s.hideCompleted};})} + label={
+ Hide complete +
} + /> +
+ {/* TODO: implement new completeness filters +
+ setQuestionStatuses(s => {return {...s, notAttempted: !s.notAttempted};})} + label={
+ Not attempted + Not attempted +
} + /> +
+
+ setQuestionStatuses(s => {return {...s, complete: !s.complete};})} + label={
+ Completed + Completed +
} + /> +
+
+ setQuestionStatuses(s => {return {...s, incorrect: !s.incorrect};})} + label={
+ Try again + Try again +
} + /> +
*/} +
+
+
+ {/* TODO: implement once necessary tags are available + {isAda &&
+ setQuestionStatuses(s => {return {...s, llmMarked: !s.llmMarked};})} + label={ + {"Include "} + + {" questions"} + } + /> +
}*/} +
+ { + setQuestionStatuses(s => {return {...s, revisionMode: !s.revisionMode};}); + debouncedRevisionModeUpdate(); + }} + label={} + /> +
+
+ + + +
+
; +} \ No newline at end of file diff --git a/src/app/components/pages/QuestionFinder.tsx b/src/app/components/pages/QuestionFinder.tsx index e736317b66..6881256aa0 100644 --- a/src/app/components/pages/QuestionFinder.tsx +++ b/src/app/components/pages/QuestionFinder.tsx @@ -3,25 +3,18 @@ import { AppState, clearQuestionSearch, searchQuestions, - updateCurrentUser, useAppDispatch, useAppSelector } from "../../state"; import debounce from "lodash/debounce"; -import {MultiValue} from "react-select"; import { tags, - DIFFICULTY_ICON_ITEM_OPTIONS, EXAM_BOARD_NULL_OPTIONS, getFilteredExamBoardOptions, - getFilteredStageOptions, - groupTagSelectionsByParent, isAda, isPhy, - isStaff, Item, logEvent, - selectOnChange, siteSpecific, STAGE, useUserViewingContext, @@ -29,34 +22,27 @@ import { useQueryParams, arrayFromPossibleCsv, toSimpleCSV, - itemiseByValue, TAG_ID, itemiseTag, - isLoggedIn, SEARCH_RESULTS_PER_PAGE, - DIFFICULTY_ITEM_OPTIONS } from "../../services"; import {ContentSummaryDTO, Difficulty, ExamBoard} from "../../../IsaacApiTypes"; -import {GroupBase} from "react-select/dist/declarations/src/types"; import {IsaacSpinner} from "../handlers/IsaacSpinner"; -import {StyledSelect} from "../elements/inputs/StyledSelect"; import { RouteComponentProps, useHistory, withRouter } from "react-router"; import { LinkToContentSummaryList } from "../elements/list-groups/ContentSummaryListGroupItem"; import { ShowLoading } from "../handlers/ShowLoading"; import { TitleAndBreadcrumb } from "../elements/TitleAndBreadcrumb"; import { MetaDescription } from "../elements/MetaDescription"; import { CanonicalHrefElement } from "../navigation/CanonicalHrefElement"; -import { HierarchyFilterHexagonal, Tier, TierID } from "../elements/svg/HierarchyFilter"; -import { StyledCheckbox } from "../elements/inputs/StyledCheckbox"; import classNames from "classnames"; import queryString from "query-string"; import { PageFragment } from "../elements/PageFragment"; import {RenderNothing} from "../elements/RenderNothing"; -import { Button, Card, CardBody, CardFooter, CardHeader, Col, Container, Dropdown, DropdownMenu, DropdownToggle, Form, Input, Label, Row, UncontrolledTooltip } from "reactstrap"; -import { CollapsibleList } from "../elements/CollapsibleList"; -import { DifficultyIcons } from "../elements/svg/DifficultyIcons"; +import { Button, Card, CardBody, CardHeader, Col, Container, Input, Label, Row } from "reactstrap"; +import { QuestionFinderFilterPanel } from "../elements/panels/QuestionFinderFilterPanel"; +import { Tier, TierID } from "../elements/svg/HierarchyFilter"; -interface questionStatus { +export interface QuestionStatus { notAttempted: boolean; complete: boolean; incorrect: boolean; @@ -88,13 +74,6 @@ function processTagHierarchy(subjects: string[], fields: string[], topics: strin return selectionItems; } -function simplifyDifficultyLabel(difficultyLabel: string): string { - const labelLength = difficultyLabel.length; - const type = difficultyLabel.slice(0, labelLength - 4); - const level = difficultyLabel.slice(labelLength - 2, labelLength - 1); - return type + level; -} - export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { const dispatch = useAppDispatch(); const userContext = useUserViewingContext(); @@ -118,7 +97,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { const [searchExamBoards, setSearchExamBoards] = useState( arrayFromPossibleCsv(params.examBoards) as ExamBoard[] ); - const [questionStatuses, setQuestionStatuses] = useState( + const [questionStatuses, setQuestionStatuses] = useState( { notAttempted: false, complete: false, @@ -128,19 +107,6 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { hideCompleted: !!params.hideCompleted } ); - const [numExpanded, setExpanded] = useState(0); - const [allExpanded, setAllExpanded] = useState(undefined); - - useEffect(function syncAllExpandedAndNumExpanded() { - // If the user manually close all the list after expanding them - // OR if the user manually opens a list after closing them all - if ((numExpanded === 0 && allExpanded) - || (numExpanded > 0 && !allExpanded)) { - // Go into the undefined state until being clicked - setAllExpanded(undefined); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [numExpanded]); useEffect(function populateExamBoardFromUserContext() { if (!EXAM_BOARD_NULL_OPTIONS.includes(userContext.examBoard)) setSearchExamBoards([userContext.examBoard]); @@ -176,25 +142,22 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { {id: "topics" as TierID, name: "Topic"} ].map(tier => ({...tier, for: "for_" + tier.id})).slice(0, index + 1); - const tagOptions: { options: Item[]; label: string }[] = isPhy ? tags.allTags.map(groupTagSelectionsByParent) : tags.allSubcategoryTags.map(groupTagSelectionsByParent); - const groupBaseTagOptions: GroupBase>[] = tagOptions; - const bookOptions: Item[] = [ - {value: "phys_book_step_up", label: "Step Up to GCSE Physics"}, - {value: "phys_book_gcse", label: "GCSE Physics"}, - {value: "physics_skills_19", label: "A Level Physics (3rd Edition)"}, - {value: "physics_linking_concepts", label: "Linking Concepts in Pre-Uni Physics"}, - {value: "maths_book_gcse", label: "GCSE Maths"}, - {value: "maths_book", label: "Pre-Uni Maths"}, - {value: "chemistry_16", label: "A-Level Physical Chemistry"} - ]; + const setTierSelection = (tierIndex: number) => { + return ((values: Item[]) => { + const newSelections = selections.slice(0, tierIndex); + newSelections.push(values); + setSelections(newSelections); + }) as React.Dispatch[]>>; + }; const {results: questions, totalResults: totalQuestions, nextSearchOffset} = useAppSelector((state: AppState) => state && state.questionSearchResult) || {}; - const user = useAppSelector((state: AppState) => state && state.user); + const nothingToSearchFor = + [searchQuery, searchTopics, searchBooks, searchStages, searchDifficulties, searchExamBoards].every(v => v.length === 0) && + selections.every(v => v.length === 0) && !searchFastTrack && !excludeBooks; const searchDebounce = useCallback( - debounce((searchString: string, topics: string[], examBoards: string[], book: string[], stages: string[], difficulties: string[], hierarchySelections: Item[][], tiers: Tier[], excludeBooks: boolean, fasttrack: boolean, hideCompleted: boolean, startIndex: number) => { - if ([searchString, topics, book, stages, difficulties, examBoards].every(v => v.length === 0) && hierarchySelections.every(v => v.length === 0) && !fasttrack) { - // Nothing to search for + debounce((searchString: string, topics: string[], examBoards: string[], book: string[], stages: string[], difficulties: string[], hierarchySelections: Item[][], tiers: Tier[], excludeBooks: boolean, fasttrack: boolean, hideCompleted: boolean, startIndex: number) => { + if (nothingToSearchFor) { dispatch(clearQuestionSearch); return; } @@ -238,14 +201,6 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { [] ); - const setTierSelection = (tierIndex: number) => { - return ((values: Item[]) => { - const newSelections = selections.slice(0, tierIndex); - newSelections.push(values); - setSelections(newSelections); - }) as React.Dispatch[]>>; - }; - useEffect(() => { // If a certain stage excludes a selected examboard remove it from query params if (isAda) { @@ -360,16 +315,6 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { }); }; - const debouncedRevisionModeUpdate = useCallback(debounce(() => { - if (user && isLoggedIn(user)) { - const userToUpdate = {...user, password: null}; - const userPreferencesToUpdate = { - DISPLAY_SETTING: {...userPreferences?.DISPLAY_SETTING, HIDE_QUESTION_ATTEMPTS: !userPreferences?.DISPLAY_SETTING?.HIDE_QUESTION_ATTEMPTS} - }; - dispatch(updateCurrentUser(userToUpdate, userPreferencesToUpdate, undefined, null, user, false)); - }}, 250, {trailing: true} - ), []); - // eslint-disable-next-line react-hooks/exhaustive-deps const handleSearch = useCallback( debounce((searchTerm: string) => { @@ -415,292 +360,18 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { - - - - Filter - - - Filter by - - {filtersSelected &&
- -
} -
- -
-
- - {isExpanded ? setExpanded(prevExpanded => prevExpanded + 1) : setExpanded(prevExpanded => prevExpanded - 1);}} - > - {getFilteredStageOptions().map((stage, index) => ( -
- setSearchStages(s => s.includes(stage.value) ? s.filter(v => v !== stage.value) : [...s, stage.value])} - label={{stage.label}} - /> -
- ))} -
- {isAda && {isExpanded ? setExpanded(prevExpanded => prevExpanded + 1) : setExpanded(prevExpanded => prevExpanded - 1);}} - > - {getFilteredExamBoardOptions().map((board, index) => ( -
- setSearchExamBoards(s => s.includes(board.value) ? s.filter(v => v !== board.value) : [...s, board.value])} - label={{board.label}} - /> -
- ))} -
} - tier.length) - .reverse() - .find(l => l > 0), - searchTopics.length - )} - onExpand={(isExpanded) => {isExpanded ? setExpanded(prevExpanded => prevExpanded + 1) : setExpanded(prevExpanded => prevExpanded - 1);}} - > - {siteSpecific( -
- -
, - groupBaseTagOptions.map((tag, index) => ( - // TODO: make subList - - {tag.options.map((topic, index) => ( -
- setSearchTopics( - s => s.includes(topic.value) - ? s.filter(v => v !== topic.value) - : [...s, topic.value] - )} - label={{topic.label}} - className="ps-3" - /> -
- ))} -
- )) - )} -
- {isExpanded ? setExpanded(prevExpanded => prevExpanded + 1) : setExpanded(prevExpanded => prevExpanded - 1);}} - > -
- -
- {DIFFICULTY_ITEM_OPTIONS.map((difficulty, index) => ( -
- setSearchDifficulties( - s => s.includes(difficulty.value) - ? s.filter(v => v !== difficulty.value) - : [...s, difficulty.value] - )} - label={
- {simplifyDifficultyLabel(difficulty.label)} - -
} - /> -
- ))} -
- {isPhy && {isExpanded ? setExpanded(prevExpanded => prevExpanded + 1) : setExpanded(prevExpanded => prevExpanded - 1);}} - > - <> -
- setExcludeBooks(p => !p)} - label={Exclude skills book questions} - /> -
- {bookOptions.map((book, index) => ( -
- setSearchBooks( - s => s.includes(book.value) - ? s.filter(v => v !== book.value) - : [...s, book.value] - )} - label={{book.label}} - /> -
- ))} - -
} - acc + item, 0)} - onExpand={(isExpanded) => {isExpanded ? setExpanded(prevExpanded => prevExpanded + 1) : setExpanded(prevExpanded => prevExpanded - 1);}} - > -
- setQuestionStatuses(s => {return {...s, hideCompleted: !s.hideCompleted};})} - label={
- Hide complete -
} - /> -
- {/* TODO: implement new completeness filters -
- setQuestionStatuses(s => {return {...s, notAttempted: !s.notAttempted};})} - label={
- Not attempted - Not attempted -
} - /> -
-
- setQuestionStatuses(s => {return {...s, complete: !s.complete};})} - label={
- Completed - Completed -
} - /> -
-
- setQuestionStatuses(s => {return {...s, incorrect: !s.incorrect};})} - label={
- Try again - Try again -
} - /> -
*/} -
-
-
- {/* TODO: implement once necessary tags are available - {isAda &&
- setQuestionStatuses(s => {return {...s, llmMarked: !s.llmMarked};})} - label={ - {"Include "} - - {" questions"} - } - /> -
}*/} -
- { - setQuestionStatuses(s => {return {...s, revisionMode: !s.revisionMode};}); - debouncedRevisionModeUpdate(); - }} - label={} - /> -
-
- - - -
-
+ {/* TODO: update styling of question block */} @@ -883,7 +554,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { {[searchQuery, searchTopics, searchBook, searchStages, searchDifficulties, searchExamBoards].every(v => v.length === 0) && - selections.every(v => v.length === 0) ? + selections.every(v => v.length === 0) ? // TODO: consider replacing with nothingToSearchFor Please select filters : (displayQuestions?.length ? <> From 504685877b67c3e4d2fb735fcb23b94d09a54ee0 Mon Sep 17 00:00:00 2001 From: Meurig Thomas Date: Tue, 30 Jul 2024 14:26:14 +0100 Subject: [PATCH 23/61] Refactor QF list state to use reducer --- .../components/elements/CollapsibleList.tsx | 44 ++------- .../panels/QuestionFinderFilterPanel.tsx | 96 ++++++++++++------- 2 files changed, 71 insertions(+), 69 deletions(-) diff --git a/src/app/components/elements/CollapsibleList.tsx b/src/app/components/elements/CollapsibleList.tsx index f92ea4fc1e..8aabf4dfec 100644 --- a/src/app/components/elements/CollapsibleList.tsx +++ b/src/app/components/elements/CollapsibleList.tsx @@ -1,24 +1,20 @@ -import React, { useEffect, useLayoutEffect, useRef, useState } from "react"; +import React, { useLayoutEffect, useRef, useState } from "react"; import { Col, Row } from "reactstrap"; import { Spacer } from "./Spacer"; import { FilterCount } from "./svg/FilterCount"; import classNames from "classnames"; -import { isDefined } from "../../services"; export interface CollapsibleListProps { title?: string; - expanded?: boolean; // initial expanded state only - subList?: boolean; + asSubList?: boolean; + expanded: boolean; + toggle: () => void; numberSelected?: number; - allExpanded?: boolean; - onExpand?: (expanded: boolean) => void; children?: React.ReactNode; } export const CollapsibleList = (props: CollapsibleListProps) => { - - const firstUpdate = useRef(true); - const [expanded, setExpanded] = useState(props.expanded || false); + const {expanded, toggle} = props; const [expandedHeight, setExpandedHeight] = useState(0); const listRef = useRef(null); @@ -28,36 +24,12 @@ export const CollapsibleList = (props: CollapsibleListProps) => { } }, [expanded]); - useEffect(function callExpansionEventHook() { - if (firstUpdate.current) { - // Notify that this component started expanded - if (props.onExpand && expanded) { - props.onExpand(expanded); - } - - firstUpdate.current = false; - return; - } - if (props.onExpand) { - props.onExpand(expanded); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [expanded]); - - useEffect(() => { - if (isDefined(props.allExpanded)) { - // sub-lists should not expand on opening all lists - setExpanded(!props.subList && props.allExpanded); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.allExpanded]); - - const title = props.title && props.subList ? props.title : {props.title}; + const title = props.title && props.asSubList ? props.title : {props.title}; const children =
{props.children}
; return -
listStateDispatch({type: "toggle", id: "stage"})} numberSelected={searchStages.length} - onExpand={(isExpanded) => {isExpanded ? setExpanded(prevExpanded => prevExpanded + 1) : setExpanded(prevExpanded => prevExpanded - 1);}} > {getFilteredStageOptions().map((stage, index) => (
@@ -131,9 +161,9 @@ export function QuestionFinderFilterPanel(props: QuestionFinderFilterPanelProps) ))} {isAda && listStateDispatch({type: "toggle", id: "examBoard"})} numberSelected={searchExamBoards.length} - onExpand={(isExpanded) => {isExpanded ? setExpanded(prevExpanded => prevExpanded + 1) : setExpanded(prevExpanded => prevExpanded - 1);}} > {getFilteredExamBoardOptions().map((board, index) => (
@@ -147,7 +177,8 @@ export function QuestionFinderFilterPanel(props: QuestionFinderFilterPanelProps) ))} } listStateDispatch({type: "toggle", id: "topics"})} numberSelected={siteSpecific( // Find the last non-zero tier in the tree // FIXME: Use `filter` and `at` when Safari supports it @@ -156,7 +187,6 @@ export function QuestionFinderFilterPanel(props: QuestionFinderFilterPanelProps) .find(l => l > 0), searchTopics.length )} - onExpand={(isExpanded) => {isExpanded ? setExpanded(prevExpanded => prevExpanded + 1) : setExpanded(prevExpanded => prevExpanded - 1);}} > {siteSpecific(
@@ -165,8 +195,9 @@ export function QuestionFinderFilterPanel(props: QuestionFinderFilterPanelProps) groupBaseTagOptions.map((tag, index) => ( // TODO: make subList listStateDispatch({type: "toggle", id: `topics ${sublistDelimiter} ${tag.label}`})} > {tag.options.map((topic, index) => (
@@ -184,14 +215,14 @@ export function QuestionFinderFilterPanel(props: QuestionFinderFilterPanelProps)
))}
- )) - )} + ))) + } + listStateDispatch({type: "toggle", id: "difficulty"})} numberSelected={searchDifficulties.length} - onExpand={(isExpanded) => {isExpanded ? setExpanded(prevExpanded => prevExpanded + 1) : setExpanded(prevExpanded => prevExpanded - 1);}} >
{/* TODO:
*/} - + {/* TODO: this feels wrong find a better way */} {props.asSubList ?
{children}
: children}
diff --git a/src/scss/common/elements.scss b/src/scss/common/elements.scss index 2835078608..756f2d64c9 100644 --- a/src/scss/common/elements.scss +++ b/src/scss/common/elements.scss @@ -309,5 +309,9 @@ iframe.email-html { } .collapsible-body { - transition: max-height 0.3s ease; + transition: max-height 0.3s ease-in-out; + max-height: 0; + &.open { + max-height: 500vh; // Sufficiently large max-height to allow for nested lists + } } From 072ef338f8b05f9d7210ed42161e37f3cfc66fef Mon Sep 17 00:00:00 2001 From: Meurig Thomas Date: Wed, 31 Jul 2024 11:50:44 +0100 Subject: [PATCH 25/61] Remove exclude books check --- src/app/components/pages/QuestionFinder.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/pages/QuestionFinder.tsx b/src/app/components/pages/QuestionFinder.tsx index 6881256aa0..0e283ca9f8 100644 --- a/src/app/components/pages/QuestionFinder.tsx +++ b/src/app/components/pages/QuestionFinder.tsx @@ -153,7 +153,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { const {results: questions, totalResults: totalQuestions, nextSearchOffset} = useAppSelector((state: AppState) => state && state.questionSearchResult) || {}; const nothingToSearchFor = [searchQuery, searchTopics, searchBooks, searchStages, searchDifficulties, searchExamBoards].every(v => v.length === 0) && - selections.every(v => v.length === 0) && !searchFastTrack && !excludeBooks; + selections.every(v => v.length === 0) && !searchFastTrack; const searchDebounce = useCallback( debounce((searchString: string, topics: string[], examBoards: string[], book: string[], stages: string[], difficulties: string[], hierarchySelections: Item[][], tiers: Tier[], excludeBooks: boolean, fasttrack: boolean, hideCompleted: boolean, startIndex: number) => { From 1a2691f9d0a77888887657c27a9c5181f9c8dcf4 Mon Sep 17 00:00:00 2001 From: Meurig Thomas Date: Wed, 31 Jul 2024 12:19:43 +0100 Subject: [PATCH 26/61] Remove unused code & comments from new QF --- src/app/components/pages/QuestionFinder.tsx | 177 +------------------- 1 file changed, 6 insertions(+), 171 deletions(-) diff --git a/src/app/components/pages/QuestionFinder.tsx b/src/app/components/pages/QuestionFinder.tsx index 0e283ca9f8..2103bfbd2d 100644 --- a/src/app/components/pages/QuestionFinder.tsx +++ b/src/app/components/pages/QuestionFinder.tsx @@ -51,11 +51,6 @@ export interface QuestionStatus { hideCompleted: boolean; // TODO: remove when implementing desired filters } -const selectStyle = { - className: "basic-multi-select", classNamePrefix: "select", - menuPortalTarget: document.body, styles: {menuPortal: (base: object) => ({...base, zIndex: 9999})} -}; - function processTagHierarchy(subjects: string[], fields: string[], topics: string[]): Item[][] { const tagHierarchy = tags.getTagHierarchy(); const selectionItems: Item[][] = []; @@ -118,7 +113,6 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { const [searchBooks, setSearchBooks] = useState(arrayFromPossibleCsv(params.book)); const [excludeBooks, setExcludeBooks] = useState(!!params.excludeBooks); - const [searchFastTrack, setSearchFastTrack] = useState(!!params.fasttrack); const [disableLoadMore, setDisableLoadMore] = useState(false); const subjects = arrayFromPossibleCsv(params.subjects); @@ -153,10 +147,10 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { const {results: questions, totalResults: totalQuestions, nextSearchOffset} = useAppSelector((state: AppState) => state && state.questionSearchResult) || {}; const nothingToSearchFor = [searchQuery, searchTopics, searchBooks, searchStages, searchDifficulties, searchExamBoards].every(v => v.length === 0) && - selections.every(v => v.length === 0) && !searchFastTrack; + selections.every(v => v.length === 0); const searchDebounce = useCallback( - debounce((searchString: string, topics: string[], examBoards: string[], book: string[], stages: string[], difficulties: string[], hierarchySelections: Item[][], tiers: Tier[], excludeBooks: boolean, fasttrack: boolean, hideCompleted: boolean, startIndex: number) => { + debounce((searchString: string, topics: string[], examBoards: string[], book: string[], stages: string[], difficulties: string[], hierarchySelections: Item[][], tiers: Tier[], excludeBooks: boolean, hideCompleted: boolean, startIndex: number) => { if (nothingToSearchFor) { dispatch(clearQuestionSearch); return; @@ -190,13 +184,13 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { difficulties: difficulties.join(",") || undefined, examBoards: examBoardString, questionCategories: excludeBooks ? "problem_solving" : "problem_solving,book", - fasttrack, + fasttrack: false, hideCompleted, startIndex, limit: SEARCH_RESULTS_PER_PAGE + 1 // request one more than we need, as to know if there are more results })); - logEvent(eventLog,"SEARCH_QUESTIONS", {searchString, topics, examBoards, book, stages, difficulties, fasttrack, startIndex}); + logEvent(eventLog,"SEARCH_QUESTIONS", {searchString, topics, examBoards, book, stages, difficulties, startIndex}); }, 250), [] ); @@ -219,8 +213,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { setDisplayQuestions(undefined); searchDebounce( searchQuery, searchTopics, searchExamBoards, searchBooks, searchStages, - searchDifficulties, selections, tiers, excludeBooks, searchFastTrack, - questionStatuses.hideCompleted, 0); + searchDifficulties, selections, tiers, excludeBooks, questionStatuses.hideCompleted, 0); const params: {[key: string]: string} = {}; if (searchStages.length) params.stages = toSimpleCSV(searchStages); @@ -232,7 +225,6 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { params.book = toSimpleCSV(searchBooks); } if (isPhy && excludeBooks) params.excludeBooks = "set"; - if (isPhy && searchFastTrack) params.fasttrack = "set"; if (questionStatuses.hideCompleted) params.hideCompleted = "set"; if (isPhy) { @@ -400,7 +392,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { searchBooks, searchStages, searchDifficulties, selections, tiers, - excludeBooks, searchFastTrack, + excludeBooks, questionStatuses.hideCompleted, nextSearchOffset ? nextSearchOffset - 1 @@ -427,162 +419,5 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => {
- - - {/* Previous finder. TODO: remove */} - - {/* - - - - Specify your search criteria and we will find questions related to your chosen filter(s). - - - - - - searchStages.includes(o.value))} - options={getFilteredStageOptions()} - onChange={selectOnChange(setSearchStages, true)} - /> - - - - - - {isAda && - - searchExamBoards.includes(o.value))} - options={getFilteredExamBoardOptions({byStages: searchStages})} - onChange={(s: MultiValue>) => selectOnChange(setSearchExamBoards, true)(s)} - /> - } - - - - - {siteSpecific( - , - tag.options))} - options={groupBaseTagOptions} onChange={(x : readonly Item[], {action: _action}) => { - selectOnChange(setSearchTopics, true)(x); - }} - /> - )} - - - - {isPhy && - - { - selectOnChange(setSearchBook, true)(e); - }} - options={bookOptions} - /> - } - - - {isPhy && isStaff(user) && -
- -
- } -
- - - - handleSearch(e.target.value)} - /> - - - - {user && isLoggedIn(user) && -
- -
- { - setRevisionMode(r => !r); - debouncedRevisionModeUpdate(); - }} - label={

Revision mode

} - /> - - - Revision mode hides your previous answers, so you can practice questions that you have answered before. - -
-
- setHideCompleted(h => !h)} - label={

Hide completed questions

} - disabled={revisionMode} - /> -
- -
-
} -
-
- - - -

- Results -

- -
- - - {[searchQuery, searchTopics, searchBook, searchStages, searchDifficulties, searchExamBoards].every(v => v.length === 0) && - selections.every(v => v.length === 0) ? // TODO: consider replacing with nothingToSearchFor - Please select filters : - (displayQuestions?.length ? - <> - ({...q, correct: revisionMode ? undefined : q.correct}) as ContentSummaryDTO)}/> - - - - - - {displayQuestions && (totalQuestions ?? 0) > displayQuestions.length && -
- {`${totalQuestions} questions match your criteria.`}
- Not found what you're looking for? Try refining your search filters. -
} - : - No results found) - } -
-
-
*/} ; }); From 77c28f9b7368cc4596e7409e4d1120df99f12b3e Mon Sep 17 00:00:00 2001 From: Skye Purchase Date: Wed, 31 Jul 2024 14:32:03 +0100 Subject: [PATCH 27/61] Remove revision mode option --- .../panels/QuestionFinderFilterPanel.tsx | 88 ++++++------------- src/app/components/pages/QuestionFinder.tsx | 7 +- 2 files changed, 27 insertions(+), 68 deletions(-) diff --git a/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx b/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx index 1a2a784281..acfb66d2a7 100644 --- a/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx +++ b/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx @@ -1,9 +1,7 @@ -import React, { Dispatch, SetStateAction, useCallback, useReducer } from "react"; +import React, { Dispatch, SetStateAction, useReducer } from "react"; import { Button, Card, CardBody, CardHeader, Col } from "reactstrap"; import { CollapsibleList } from "../CollapsibleList"; -import { DIFFICULTY_ITEM_OPTIONS, getFilteredExamBoardOptions, getFilteredStageOptions, groupTagSelectionsByParent, isAda, isLoggedIn, isPhy, Item, siteSpecific, STAGE, TAG_ID, tags } from "../../../services"; -import { debounce } from "lodash"; -import { AppState, updateCurrentUser, useAppDispatch, useAppSelector } from "../../../state"; +import { DIFFICULTY_ITEM_OPTIONS, getFilteredExamBoardOptions, getFilteredStageOptions, groupTagSelectionsByParent, isAda, isPhy, Item, siteSpecific, STAGE, TAG_ID, tags } from "../../../services"; import { Difficulty, ExamBoard } from "../../../../IsaacApiTypes"; import { QuestionStatus } from "../../pages/QuestionFinder"; import classNames from "classnames"; @@ -94,24 +92,9 @@ export function QuestionFinderFilterPanel(props: QuestionFinderFilterPanelProps) tiers, choices, selections, setTierSelection, applyFilters, clearFilters, filtersSelected, searchDisabled } = props; - const dispatch = useAppDispatch(); - const user = useAppSelector((state: AppState) => state?.user); - const userPreferences = useAppSelector((state: AppState) => state?.userPreferences); - const [listState, listStateDispatch] = useReducer(listStateReducer, initialListState); const anyExpandedLists = Object.values(listState).some(v => v); - - const debouncedRevisionModeUpdate = useCallback(debounce(() => { - if (user && isLoggedIn(user)) { - const userToUpdate = {...user, password: null}; - const userPreferencesToUpdate = { - DISPLAY_SETTING: {...userPreferences?.DISPLAY_SETTING, HIDE_QUESTION_ATTEMPTS: !userPreferences?.DISPLAY_SETTING?.HIDE_QUESTION_ATTEMPTS} - }; - dispatch(updateCurrentUser(userToUpdate, userPreferencesToUpdate, undefined, null, user, false)); - }}, 250, {trailing: true} - ), []); - const tagOptions: { options: Item[]; label: string }[] = isPhy ? tags.allTags.map(groupTagSelectionsByParent) : tags.allSubcategoryTags.map(groupTagSelectionsByParent); const groupBaseTagOptions: GroupBase>[] = tagOptions; @@ -347,50 +330,31 @@ export function QuestionFinderFilterPanel(props: QuestionFinderFilterPanelProps)
} />
*/} -
-
-
- {/* TODO: implement once necessary tags are available - {isAda &&
- setQuestionStatuses(s => {return {...s, llmMarked: !s.llmMarked};})} - label={ - {"Include "} - - {" questions"} - } - /> -
}*/} -
- { - setQuestionStatuses(s => {return {...s, revisionMode: !s.revisionMode};}); - debouncedRevisionModeUpdate(); - }} - label={} - /> -
+ {/* TODO: implement once necessary tags are available +
+
+
+ {isAda &&
+ setQuestionStatuses(s => {return {...s, llmMarked: !s.llmMarked};})} + label={ + {"Include "} + + {" questions"} + } + /> +
}*/}
- + listStateDispatch({type: "toggle", id: "stage"})} + title={listTitles.stage} expanded={listState.stage.state} + toggle={() => listStateDispatch({type: "toggle", id: "stage", focus: below["md"](deviceSize)})} numberSelected={searchStages.length} > {getFilteredStageOptions().map((stage, index) => ( @@ -144,8 +214,8 @@ export function QuestionFinderFilterPanel(props: QuestionFinderFilterPanelProps) ))} {isAda && listStateDispatch({type: "toggle", id: "examBoard"})} + title={listTitles.examBoard} expanded={listState.examBoard.state} + toggle={() => listStateDispatch({type: "toggle", id: "examBoard", focus: below["md"](deviceSize)})} numberSelected={searchExamBoards.length} > {getFilteredExamBoardOptions({byStages: searchStages}).map((board, index) => ( @@ -160,8 +230,8 @@ export function QuestionFinderFilterPanel(props: QuestionFinderFilterPanelProps) ))} } listStateDispatch({type: "toggle", id: "topics"})} + title={listTitles.topics} expanded={listState.topics.state} + toggle={() => listStateDispatch({type: "toggle", id: "topics", focus: below["md"](deviceSize)})} numberSelected={siteSpecific( // Find the last non-zero tier in the tree // FIXME: Use `filter` and `at` when Safari supports it @@ -179,8 +249,8 @@ export function QuestionFinderFilterPanel(props: QuestionFinderFilterPanelProps) // TODO: make subList listStateDispatch({type: "toggle", id: `topics ${sublistDelimiter} ${tag.label}`})} + expanded={listState[`topics ${sublistDelimiter} ${tag.label}`]?.state} + toggle={() => listStateDispatch({type: "toggle", id: `topics ${sublistDelimiter} ${tag.label}`, focus: true})} > {tag.options.map((topic, index) => (
@@ -203,8 +273,8 @@ export function QuestionFinderFilterPanel(props: QuestionFinderFilterPanelProps) listStateDispatch({type: "toggle", id: "difficulty"})} + title={listTitles.difficulty} expanded={listState.difficulty.state} + toggle={() => listStateDispatch({type: "toggle", id: "difficulty", focus: below["md"](deviceSize)})} numberSelected={searchDifficulties.length} >
@@ -237,8 +307,8 @@ export function QuestionFinderFilterPanel(props: QuestionFinderFilterPanelProps) ))} {isPhy && listStateDispatch({type: "toggle", id: "books"})} + title={listTitles.books} expanded={listState.books.state} + toggle={() => listStateDispatch({type: "toggle", id: "books", focus: below["md"](deviceSize)})} numberSelected={excludeBooks ? 1 : searchBooks.length} > <> @@ -267,8 +337,8 @@ export function QuestionFinderFilterPanel(props: QuestionFinderFilterPanelProps) } listStateDispatch({type: "toggle", id: "questionStatus"})} + title={listTitles.questionStatus} expanded={listState.questionStatus.state} + toggle={() => listStateDispatch({type: "toggle", id: "questionStatus", focus: below["md"](deviceSize)})} numberSelected={Object.values(questionStatuses).reduce((acc, item) => acc + item, 0)} >
@@ -362,4 +432,4 @@ export function QuestionFinderFilterPanel(props: QuestionFinderFilterPanelProps) ; -} \ No newline at end of file +} diff --git a/src/app/components/pages/QuestionFinder.tsx b/src/app/components/pages/QuestionFinder.tsx index 31a94bce96..c5d7ca231d 100644 --- a/src/app/components/pages/QuestionFinder.tsx +++ b/src/app/components/pages/QuestionFinder.tsx @@ -340,7 +340,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { - + { - + { {/* TODO: update styling of question block */} - + From e82997650bbb60bc96913b032ba124da9842e3e1 Mon Sep 17 00:00:00 2001 From: Skye Purchase Date: Thu, 1 Aug 2024 11:39:28 +0100 Subject: [PATCH 30/61] Fix closed filters on large screens --- .../components/elements/panels/QuestionFinderFilterPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx b/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx index 52723a07f9..d188f76094 100644 --- a/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx +++ b/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx @@ -196,7 +196,7 @@ export function QuestionFinderFilterPanel(props: QuestionFinderFilterPanelProps)
- + listStateDispatch({type: "toggle", id: "stage", focus: below["md"](deviceSize)})} From 43748afca9bb6314c30d6b712442a9f21de3d504 Mon Sep 17 00:00:00 2001 From: Skye Purchase Date: Thu, 1 Aug 2024 15:58:46 +0100 Subject: [PATCH 31/61] Implement new summary item difficulty styling --- .../StageAndDifficultySummaryIcons.tsx | 55 ++++++++++++++----- .../ContentSummaryListGroupItem.tsx | 7 ++- .../panels/QuestionFinderFilterPanel.tsx | 14 ++--- src/app/services/constants.ts | 11 ++++ 4 files changed, 61 insertions(+), 26 deletions(-) diff --git a/src/app/components/elements/StageAndDifficultySummaryIcons.tsx b/src/app/components/elements/StageAndDifficultySummaryIcons.tsx index 74e1fcac41..e92a5910b9 100644 --- a/src/app/components/elements/StageAndDifficultySummaryIcons.tsx +++ b/src/app/components/elements/StageAndDifficultySummaryIcons.tsx @@ -1,21 +1,50 @@ import React from "react"; import classNames from "classnames"; -import {isAda, isPhy, STAGE, stageLabelMap} from "../../services"; +import {simpleDifficultyLabelMap, siteSpecific, STAGE, stageLabelMap} from "../../services"; import {DifficultyIcons} from "./svg/DifficultyIcons"; import {ViewingContext} from "../../../IsaacAppTypes"; +import { Difficulty } from "../../../IsaacApiTypes"; export function StageAndDifficultySummaryIcons({audienceViews, className}: {audienceViews: ViewingContext[], className?: string}) { // FIXME find a better way than hiding the whole thing on mobile - return
- {audienceViews.map((view, i) => -
0), "d-flex d-md-block": isPhy, "d-block text-center mx-2 my-1": isAda})}> - {view.stage && view.stage !== STAGE.ALL &&
- {stageLabelMap[view.stage]} -
} - {view.difficulty &&
- -
} -
) - } -
+ // ^ fixed in new redesign + const difficulties: Difficulty[] = audienceViews.map(v => v.difficulty).filter(v => v !== undefined); + return siteSpecific( +
+ {audienceViews.map((view, i) => +
0})}> + {view.stage && view.stage !== STAGE.ALL &&
+ {stageLabelMap[view.stage]} +
} + {view.difficulty &&
+ +
} +
) + } +
, +
+ { + difficulties.every((v, _i, arr) => v === arr[0]) + ?
+ {difficulties.length > 0 && <> +
+ {simpleDifficultyLabelMap[difficulties[0]]} +
+
+ +
+ } +
+ : audienceViews.map(view => +
+ {view.stage && view.stage !== STAGE.ALL &&
+ {stageLabelMap[view.stage]} +
} + {view.difficulty &&
+ +
} +
) + } +
, + ); } diff --git a/src/app/components/elements/list-groups/ContentSummaryListGroupItem.tsx b/src/app/components/elements/list-groups/ContentSummaryListGroupItem.tsx index 5c351a9c87..b060be749d 100644 --- a/src/app/components/elements/list-groups/ContentSummaryListGroupItem.tsx +++ b/src/app/components/elements/list-groups/ContentSummaryListGroupItem.tsx @@ -27,6 +27,7 @@ import {ShortcutResponse} from "../../../../IsaacAppTypes"; import {Markup} from "../markup"; import classNames from "classnames"; import {ListGroup, ListGroupItem, UncontrolledTooltip} from "reactstrap"; +import { CSSModule } from "reactstrap/types/lib/utils"; export const ContentSummaryListGroupItem = ({item, search, displayTopicTitle}: {item: ShortcutResponse; search?: string; displayTopicTitle?: boolean}) => { const componentId = useRef(uuid_v4().slice(0, 4)).current; @@ -47,7 +48,7 @@ export const ContentSummaryListGroupItem = ({item, search, displayTopicTitle}: { } const iconClasses = `search-item-icon ${itemSubject?.id}-fill`; const hierarchyTags = tags.getByIdsAsHierarchy((item.tags || []) as TAG_ID[]) - .filter((t, i) => !isAda || i !== 0); // CS always has Computer Science at the top level + .filter((_t, i) => !isAda || i !== 0); // CS always has Computer Science at the top level // FIXME "correct" never actually exists on questions here... const questionIconLabel = item.correct ? "Completed question icon" : "Question icon"; @@ -100,7 +101,7 @@ export const ContentSummaryListGroupItem = ({item, search, displayTopicTitle}: { case (DOCUMENT_TYPE.TOPIC_SUMMARY): linkDestination = `/${documentTypePathPrefix[DOCUMENT_TYPE.TOPIC_SUMMARY]}/${item.id?.slice("topic_summary_".length)}`; icon = Topic summary page icon; - typeLabel = "Topic" + typeLabel = "Topic"; break; case (DOCUMENT_TYPE.GENERIC): linkDestination = `/${documentTypePathPrefix[DOCUMENT_TYPE.GENERIC]}/${item.id}`; @@ -167,7 +168,7 @@ export const LinkToContentSummaryList = ({items, search, displayTopicTitle, ...r tag?: React.ElementType; flush?: boolean; className?: string; - cssModule?: any; + cssModule?: CSSModule; }) => { return {items.map(item => )} diff --git a/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx b/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx index d188f76094..d83ba4f3e9 100644 --- a/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx +++ b/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx @@ -4,13 +4,13 @@ import { CollapsibleList } from "../CollapsibleList"; import { above, below, - DIFFICULTY_ITEM_OPTIONS, getFilteredExamBoardOptions, getFilteredStageOptions, groupTagSelectionsByParent, isAda, isPhy, Item, + SIMPLE_DIFFICULTY_ITEM_OPTIONS, siteSpecific, STAGE, TAG_ID, @@ -35,12 +35,6 @@ const bookOptions: Item[] = [ {value: "maths_book", label: "Pre-Uni Maths"}, {value: "chemistry_16", label: "A-Level Physical Chemistry"} ]; -function simplifyDifficultyLabel(difficultyLabel: string): string { - const labelLength = difficultyLabel.length; - const type = difficultyLabel.slice(0, labelLength - 4); - const level = difficultyLabel.slice(labelLength - 2, labelLength - 1); - return type + level; -} const sublistDelimiter = " >>> "; type TopLevelListsState = { @@ -288,7 +282,7 @@ export function QuestionFinderFilterPanel(props: QuestionFinderFilterPanelProps) What do the different difficulty levels mean?
- {DIFFICULTY_ITEM_OPTIONS.map((difficulty, index) => ( + {SIMPLE_DIFFICULTY_ITEM_OPTIONS.map((difficulty, index) => (
- {simplifyDifficultyLabel(difficulty.label)} - + {difficulty.label} +
} />
diff --git a/src/app/services/constants.ts b/src/app/services/constants.ts index f9a0536c1d..38e1a7ff68 100644 --- a/src/app/services/constants.ts +++ b/src/app/services/constants.ts @@ -390,6 +390,14 @@ export const difficultyLabelMap: {[difficulty in Difficulty]: string} = { challenge_2: "Challenge\u00A0(C2)", challenge_3: "Challenge\u00A0(C3)", }; +export const simpleDifficultyLabelMap: {[difficulty in Difficulty]: string} = { + practice_1: "Practice\u00A01", + practice_2: "Practice\u00A02", + practice_3: "Practice\u00A03", + challenge_1: "Challenge\u00A01", + challenge_2: "Challenge\u00A02", + challenge_3: "Challenge\u00A03", +}; export const difficultyIconLabelMap: {[difficulty in Difficulty]: string} = { practice_1: `Practice (P1) ${siteSpecific("\u2B22\u2B21\u2B21", "\u25CF\u25CB")}`, practice_2: `Practice (P2) ${siteSpecific("\u2B22\u2B22\u2B21", "\u25CF\u25CF")}`, @@ -405,6 +413,9 @@ export const difficultiesOrdered: Difficulty[] = siteSpecific( export const DIFFICULTY_ITEM_OPTIONS: {value: Difficulty, label: string}[] = difficultiesOrdered.map(d => ( {value: d, label: difficultyLabelMap[d]} )); +export const SIMPLE_DIFFICULTY_ITEM_OPTIONS: {value: Difficulty, label: string}[] = difficultiesOrdered.map(d => ( + {value: d, label: simpleDifficultyLabelMap[d]} +)); export const DIFFICULTY_ICON_ITEM_OPTIONS: {value: Difficulty, label: string}[] = difficultiesOrdered.map(d => ( {value: d, label: difficultyIconLabelMap[d]} )); From 5f1790b1ef3f6a2338225e912a0806d69fa2a803 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Mon, 5 Aug 2024 12:14:41 +0100 Subject: [PATCH 32/61] Add modal for difficulty explanation --- .../modals/QuestionFinderDifficultyModal.tsx | 42 +++++++++++++++++++ .../panels/QuestionFinderFilterPanel.tsx | 22 +++++----- 2 files changed, 53 insertions(+), 11 deletions(-) create mode 100644 src/app/components/elements/modals/QuestionFinderDifficultyModal.tsx diff --git a/src/app/components/elements/modals/QuestionFinderDifficultyModal.tsx b/src/app/components/elements/modals/QuestionFinderDifficultyModal.tsx new file mode 100644 index 0000000000..b2b7494744 --- /dev/null +++ b/src/app/components/elements/modals/QuestionFinderDifficultyModal.tsx @@ -0,0 +1,42 @@ +import React from "react"; +import { Col, Row } from "reactstrap"; +import { difficultyLabelMap, siteSpecific } from "../../../services"; +import { closeActiveModal, store } from "../../../state"; +import { DifficultyIcons } from "../svg/DifficultyIcons"; +import { Difficulty } from "../../../../IsaacApiTypes"; +import { ActiveModal } from "../../../../IsaacAppTypes"; + +const DifficultyPanel = ({difficulty, explanation} : {difficulty: Difficulty, explanation: string}) => { + return + +
+ {difficultyLabelMap[difficulty]} +
+ +
+
+
+ +
+ {explanation} +
+
+ ; +}; + +const QuestionFinderDifficultyModal = () => { + return + + + + + ; +}; + +export const questionFinderDifficultyModal = () : ActiveModal => { + return { + closeAction: () => store.dispatch(closeActiveModal()), + title: `Difficulty ${siteSpecific("Levels", "levels")}`, + body: , + }; +}; \ No newline at end of file diff --git a/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx b/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx index d83ba4f3e9..6d4bd782fd 100644 --- a/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx +++ b/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx @@ -24,6 +24,8 @@ import { StyledCheckbox } from "../inputs/StyledCheckbox"; import { DifficultyIcons } from "../svg/DifficultyIcons"; import { GroupBase } from "react-select"; import { HierarchyFilterHexagonal, Tier } from "../svg/HierarchyFilter"; +import { openActiveModal, useAppDispatch } from "../../../state"; +import { questionFinderDifficultyModal } from "../modals/QuestionFinderDifficultyModal"; const bookOptions: Item[] = [ @@ -141,6 +143,7 @@ export function QuestionFinderFilterPanel(props: QuestionFinderFilterPanelProps) const [listState, listStateDispatch] = useReducer(listStateReducer, groupBaseTagOptions, initialiseListState); const deviceSize = useDeviceSize(); + const dispatch = useAppDispatch(); const [filtersVisible, setFiltersVisible] = useState(above["lg"](deviceSize)); @@ -271,17 +274,14 @@ export function QuestionFinderFilterPanel(props: QuestionFinderFilterPanelProps) toggle={() => listStateDispatch({type: "toggle", id: "difficulty", focus: below["md"](deviceSize)})} numberSelected={searchDifficulties.length} > -
- -
+ {SIMPLE_DIFFICULTY_ITEM_OPTIONS.map((difficulty, index) => (
Date: Mon, 5 Aug 2024 13:46:44 +0100 Subject: [PATCH 33/61] Improve collapsible list animation --- .../components/elements/CollapsibleList.tsx | 19 +++++++++++++++---- src/scss/common/elements.scss | 6 ++---- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/app/components/elements/CollapsibleList.tsx b/src/app/components/elements/CollapsibleList.tsx index 826bf46885..7eba5c99df 100644 --- a/src/app/components/elements/CollapsibleList.tsx +++ b/src/app/components/elements/CollapsibleList.tsx @@ -16,9 +16,17 @@ export interface CollapsibleListProps { export const CollapsibleList = (props: CollapsibleListProps) => { const {expanded, toggle} = props; + const [expandedHeight, setExpandedHeight] = useState(0); + const listRef = useRef(null); + + useLayoutEffect(() => { + if (expanded) { + setExpandedHeight(listRef?.current ? [...listRef.current.children].map(c => c.clientHeight).reduce((a, b) => a + b, 0) : 0); + } + }, [expanded, props.children]); const title = props.title && props.asSubList ? props.title : {props.title}; - const children =
{props.children}
; + const children =
{props.children}
; return @@ -31,9 +39,12 @@ export const CollapsibleList = (props: CollapsibleListProps) => { {/* TODO:
*/} - - {/* TODO: this feels wrong find a better way */} - {props.asSubList ?
{children}
: children} + + +
+ {children} +
+
; }; diff --git a/src/scss/common/elements.scss b/src/scss/common/elements.scss index 756f2d64c9..29003f960f 100644 --- a/src/scss/common/elements.scss +++ b/src/scss/common/elements.scss @@ -309,9 +309,7 @@ iframe.email-html { } .collapsible-body { - transition: max-height 0.3s ease-in-out; + transition: max-height 0.3s ease-in-out, height 0.3s ease-in-out; + // height must be animated alongside max-height to prevent jumping if the inner content changes height during the animation max-height: 0; - &.open { - max-height: 500vh; // Sufficiently large max-height to allow for nested lists - } } From c4cfe9224696958fefede6ee7ff0b7682965a763 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Mon, 5 Aug 2024 13:54:38 +0100 Subject: [PATCH 34/61] Remove unnecessary div around collapsible lists --- src/app/components/elements/CollapsibleList.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/app/components/elements/CollapsibleList.tsx b/src/app/components/elements/CollapsibleList.tsx index 7eba5c99df..6734f84ec3 100644 --- a/src/app/components/elements/CollapsibleList.tsx +++ b/src/app/components/elements/CollapsibleList.tsx @@ -26,7 +26,6 @@ export const CollapsibleList = (props: CollapsibleListProps) => { }, [expanded, props.children]); const title = props.title && props.asSubList ? props.title : {props.title}; - const children =
{props.children}
; return @@ -41,8 +40,8 @@ export const CollapsibleList = (props: CollapsibleListProps) => { {/* TODO:
*/} -
- {children} +
+ {props.children}
From efc3806a23f522a38d4567bfca3b97b86e9c8e9d Mon Sep 17 00:00:00 2001 From: Skye Purchase Date: Mon, 5 Aug 2024 14:09:09 +0100 Subject: [PATCH 35/61] Restyle question list group items --- src/app/components/elements/CollapsibleList.tsx | 3 +-- .../list-groups/ContentSummaryListGroupItem.tsx | 14 +++++++------- .../elements/panels/QuestionFinderFilterPanel.tsx | 1 - src/scss/cs/boards.scss | 10 ++++++---- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/app/components/elements/CollapsibleList.tsx b/src/app/components/elements/CollapsibleList.tsx index 6734f84ec3..c5f94d6fbc 100644 --- a/src/app/components/elements/CollapsibleList.tsx +++ b/src/app/components/elements/CollapsibleList.tsx @@ -1,4 +1,4 @@ -import React, { useLayoutEffect, useRef, useState } from "react"; +import React from "react"; import { Col, Row } from "reactstrap"; import { Spacer } from "./Spacer"; import { FilterCount } from "./svg/FilterCount"; @@ -11,7 +11,6 @@ export interface CollapsibleListProps { toggle: () => void; numberSelected?: number; children?: React.ReactNode; - } export const CollapsibleList = (props: CollapsibleListProps) => { diff --git a/src/app/components/elements/list-groups/ContentSummaryListGroupItem.tsx b/src/app/components/elements/list-groups/ContentSummaryListGroupItem.tsx index b060be749d..8bf5d681a9 100644 --- a/src/app/components/elements/list-groups/ContentSummaryListGroupItem.tsx +++ b/src/app/components/elements/list-groups/ContentSummaryListGroupItem.tsx @@ -40,6 +40,8 @@ export const ContentSummaryListGroupItem = ({item, search, displayTopicTitle}: { let itemClasses = "p-0 content-summary-link "; itemClasses += isContentsIntendedAudience ? "bg-transparent " : "de-emphasised "; + let caret = true; + let title = item.title; let titleClasses = "content-summary-link-title flex-grow-1 "; const itemSubject = tags.getSpecifiedTag(TAG_LEVEL.subject, item.tags as TAG_ID[]); @@ -57,8 +59,8 @@ export const ContentSummaryListGroupItem = ({item, search, displayTopicTitle}: { : , item.correct ? - {questionIconLabel}/ : - {questionIconLabel}/ + {questionIconLabel}/ : + {questionIconLabel}/ ); const deviceSize = useDeviceSize(); @@ -82,9 +84,7 @@ export const ContentSummaryListGroupItem = ({item, search, displayTopicTitle}: { linkDestination = `/${documentTypePathPrefix[DOCUMENT_TYPE.QUESTION]}/${item.id}`; icon = questionIcon; audienceViews = filterAudienceViewsByProperties(determineAudienceViews(item.audience), AUDIENCE_DISPLAY_FIELDS); - if (isAda) { - typeLabel = "Question"; - } + caret = false; break; case (DOCUMENT_TYPE.CONCEPT): linkDestination = `/${documentTypePathPrefix[DOCUMENT_TYPE.CONCEPT]}/${item.id}`; @@ -121,7 +121,7 @@ export const ContentSummaryListGroupItem = ({item, search, displayTopicTitle}: { {siteSpecific( icon, -
+
{icon}
{typeLabel}
@@ -156,7 +156,7 @@ export const ContentSummaryListGroupItem = ({item, search, displayTopicTitle}: {
} {audienceViews && audienceViews.length > 0 && }
- {isAda &&
{"Go
} + {isAda && caret &&
{"Go
} ; }; diff --git a/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx b/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx index 6d4bd782fd..8bed39b7b2 100644 --- a/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx +++ b/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx @@ -243,7 +243,6 @@ export function QuestionFinderFilterPanel(props: QuestionFinderFilterPanelProps)
, groupBaseTagOptions.map((tag, index) => ( - // TODO: make subList Date: Mon, 5 Aug 2024 14:09:44 +0100 Subject: [PATCH 36/61] Restyle 'Load more' button --- src/app/components/pages/QuestionFinder.tsx | 51 +++++++++++---------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/src/app/components/pages/QuestionFinder.tsx b/src/app/components/pages/QuestionFinder.tsx index c5d7ca231d..d38154a56b 100644 --- a/src/app/components/pages/QuestionFinder.tsx +++ b/src/app/components/pages/QuestionFinder.tsx @@ -383,30 +383,6 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { (displayQuestions?.length ? <> - - - - - {displayQuestions && (totalQuestions ?? 0) > displayQuestions.length &&
{`${totalQuestions} questions match your criteria.`}
@@ -418,6 +394,33 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { + {(filtersApplied || searchQuery !== "") && + (displayQuestions?.length ?? 0) > 0 && + + + + + } ; From f7073b1ad764c561d3cb66022c0438c14efce9fd Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Mon, 5 Aug 2024 14:38:04 +0100 Subject: [PATCH 37/61] CSS improvements on lg screens --- .../components/elements/CollapsibleList.tsx | 4 ++-- .../panels/QuestionFinderFilterPanel.tsx | 22 ++++++++++--------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/app/components/elements/CollapsibleList.tsx b/src/app/components/elements/CollapsibleList.tsx index c5f94d6fbc..bc1fd69c5a 100644 --- a/src/app/components/elements/CollapsibleList.tsx +++ b/src/app/components/elements/CollapsibleList.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useLayoutEffect, useRef, useState } from "react"; import { Col, Row } from "reactstrap"; import { Spacer } from "./Spacer"; import { FilterCount } from "./svg/FilterCount"; @@ -39,7 +39,7 @@ export const CollapsibleList = (props: CollapsibleListProps) => { {/* TODO:
*/} -
+
{props.children}
diff --git a/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx b/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx index 8bed39b7b2..966e694dde 100644 --- a/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx +++ b/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx @@ -26,6 +26,7 @@ import { GroupBase } from "react-select"; import { HierarchyFilterHexagonal, Tier } from "../svg/HierarchyFilter"; import { openActiveModal, useAppDispatch } from "../../../state"; import { questionFinderDifficultyModal } from "../modals/QuestionFinderDifficultyModal"; +import { Spacer } from "../Spacer"; const bookOptions: Item[] = [ @@ -161,24 +162,25 @@ export function QuestionFinderFilterPanel(props: QuestionFinderFilterPanelProps) return - +
Filter - - Filter by - - {filtersSelected &&
+
+ + {filtersSelected &&
+ > + Clear all +
} -
+ {below["md"](deviceSize) &&
-
+
} Date: Mon, 5 Aug 2024 16:28:27 +0100 Subject: [PATCH 38/61] Correct sizing and y-alignment of progress icon --- .../list-groups/ContentSummaryListGroupItem.tsx | 2 +- src/scss/cs/boards.scss | 12 ++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/app/components/elements/list-groups/ContentSummaryListGroupItem.tsx b/src/app/components/elements/list-groups/ContentSummaryListGroupItem.tsx index 8bf5d681a9..4adca65a4a 100644 --- a/src/app/components/elements/list-groups/ContentSummaryListGroupItem.tsx +++ b/src/app/components/elements/list-groups/ContentSummaryListGroupItem.tsx @@ -121,7 +121,7 @@ export const ContentSummaryListGroupItem = ({item, search, displayTopicTitle}: { {siteSpecific( icon, -
+
{icon}
{typeLabel}
diff --git a/src/scss/cs/boards.scss b/src/scss/cs/boards.scss index 8e31571951..32e2e5676a 100644 --- a/src/scss/cs/boards.scss +++ b/src/scss/cs/boards.scss @@ -43,23 +43,19 @@ } .question-progress-icon { - // FIXME: commented code is based on trial and error so far - // width: 95px !important; - // min-width: 95px !important; height: 88px; display: block; padding: 14px 0 !important; position: relative; + align-content: center; .inner-progress-icon { @extend .text-center; @extend .px-2; position: relative; - top: calc(50% + 5px); - transform: translateY(-50%); - /* img { - width: 2rem; + img { + max-width: 2rem; max-height: 2rem; - } */ + } .icon-title { @extend .font-size-0-75; @extend .fw-bold; From f35af7132cb2b685678ec30b4aa173bc357215d8 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Mon, 5 Aug 2024 17:25:09 +0100 Subject: [PATCH 39/61] Make search input button clickable --- src/app/components/pages/QuestionFinder.tsx | 18 ++++++------ src/scss/common/finder.scss | 31 +++++++++++++-------- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/src/app/components/pages/QuestionFinder.tsx b/src/app/components/pages/QuestionFinder.tsx index d38154a56b..b7a2b63ae8 100644 --- a/src/app/components/pages/QuestionFinder.tsx +++ b/src/app/components/pages/QuestionFinder.tsx @@ -38,7 +38,7 @@ import classNames from "classnames"; import queryString from "query-string"; import { PageFragment } from "../elements/PageFragment"; import {RenderNothing} from "../elements/RenderNothing"; -import { Button, Card, CardBody, CardHeader, Col, Container, Input, Label, Row } from "reactstrap"; +import { Button, Card, CardBody, CardHeader, Col, Container, Input, InputGroup, Label, Row } from "reactstrap"; import { QuestionFinderFilterPanel } from "../elements/panels/QuestionFinderFilterPanel"; import { Tier, TierID } from "../elements/svg/HierarchyFilter"; @@ -342,13 +342,15 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { - handleSearch(e.target.value)} - /> + + handleSearch(e.target.value)} + /> + From ece4a902a6a3aa090cbddefa30c77da1b1157329 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Tue, 6 Aug 2024 11:50:10 +0100 Subject: [PATCH 41/61] Tidy "no results" box, allow customising Collapsibles --- src/app/components/elements/CollapsibleList.tsx | 3 ++- src/app/components/pages/QuestionFinder.tsx | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/app/components/elements/CollapsibleList.tsx b/src/app/components/elements/CollapsibleList.tsx index bc1fd69c5a..b9d0031874 100644 --- a/src/app/components/elements/CollapsibleList.tsx +++ b/src/app/components/elements/CollapsibleList.tsx @@ -11,6 +11,7 @@ export interface CollapsibleListProps { toggle: () => void; numberSelected?: number; children?: React.ReactNode; + className?: string; } export const CollapsibleList = (props: CollapsibleListProps) => { @@ -26,7 +27,7 @@ export const CollapsibleList = (props: CollapsibleListProps) => { const title = props.title && props.asSubList ? props.title : {props.title}; - return + return
, -
+
{ difficulties.every((v, _i, arr) => v === arr[0]) ?
{difficulties.length > 0 && <> -
+
{simpleDifficultyLabelMap[difficulties[0]]}
-
+
} diff --git a/src/app/components/elements/list-groups/ContentSummaryListGroupItem.tsx b/src/app/components/elements/list-groups/ContentSummaryListGroupItem.tsx index 4adca65a4a..28d25d0037 100644 --- a/src/app/components/elements/list-groups/ContentSummaryListGroupItem.tsx +++ b/src/app/components/elements/list-groups/ContentSummaryListGroupItem.tsx @@ -128,7 +128,7 @@ export const ContentSummaryListGroupItem = ({item, search, displayTopicTitle}: { )}
-
+
{title ?? ""} From 57edbd523c7d2ab8965d0495f90bb88fcef3d2dd Mon Sep 17 00:00:00 2001 From: Skye Purchase Date: Mon, 5 Aug 2024 14:33:43 +0100 Subject: [PATCH 43/61] Change Ada page title --- src/app/components/pages/QuestionFinder.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/pages/QuestionFinder.tsx b/src/app/components/pages/QuestionFinder.tsx index d80754ebe8..0d5637e6c6 100644 --- a/src/app/components/pages/QuestionFinder.tsx +++ b/src/app/components/pages/QuestionFinder.tsx @@ -334,7 +334,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => {
; return - + From 142844c5a83b0aa32150d80196b9fde6da766e59 Mon Sep 17 00:00:00 2001 From: Skye Purchase Date: Mon, 5 Aug 2024 14:48:34 +0100 Subject: [PATCH 44/61] Fix 'apply filters' padding --- src/app/components/pages/QuestionFinder.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/pages/QuestionFinder.tsx b/src/app/components/pages/QuestionFinder.tsx index 0d5637e6c6..c1d188fbf1 100644 --- a/src/app/components/pages/QuestionFinder.tsx +++ b/src/app/components/pages/QuestionFinder.tsx @@ -378,7 +378,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { Showing 30 of 1235. - + {!filtersApplied && searchQuery === "" ? Please select and apply filters : From a5d24dd57ed97c490b15089cbde0eb624a966c7b Mon Sep 17 00:00:00 2001 From: Skye Purchase Date: Mon, 5 Aug 2024 16:29:15 +0100 Subject: [PATCH 45/61] Add Ada background image --- src/app/components/pages/QuestionFinder.tsx | 2 +- src/scss/cs/finder.scss | 14 ++++++++++++++ src/scss/cs/isaac.scss | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 src/scss/cs/finder.scss diff --git a/src/app/components/pages/QuestionFinder.tsx b/src/app/components/pages/QuestionFinder.tsx index c1d188fbf1..6f0b8f0bd9 100644 --- a/src/app/components/pages/QuestionFinder.tsx +++ b/src/app/components/pages/QuestionFinder.tsx @@ -354,7 +354,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { - + Date: Tue, 6 Aug 2024 10:25:40 +0100 Subject: [PATCH 46/61] Improve question block logic Fixed search button on Physics during rebasing. --- src/app/components/pages/QuestionFinder.tsx | 24 ++++++--------------- src/scss/common/finder.scss | 1 + 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/src/app/components/pages/QuestionFinder.tsx b/src/app/components/pages/QuestionFinder.tsx index 6f0b8f0bd9..29704d9b11 100644 --- a/src/app/components/pages/QuestionFinder.tsx +++ b/src/app/components/pages/QuestionFinder.tsx @@ -369,30 +369,20 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { filtersSelected, searchDisabled: filtersApplied || !filtersSelected }} /> - - {/* TODO: update styling of question block */} - Showing 30 of 1235. + Showing {displayQuestions?.length ?? 0} of {totalQuestions}. - + - {!filtersApplied && searchQuery === "" ? - Please select and apply filters : - (displayQuestions?.length ? - <> - - {displayQuestions && (totalQuestions ?? 0) > displayQuestions.length && -
- {`${totalQuestions} questions match your criteria.`}
- Not found what you're looking for? Try refining your search filters. -
} - : - No results match your criteria. - ) + {displayQuestions?.length + ? + : (!filtersApplied && searchQuery === "" + ? Please select and apply filters + : No results match your criteria) }
diff --git a/src/scss/common/finder.scss b/src/scss/common/finder.scss index 7d322ebabe..1a1c988926 100644 --- a/src/scss/common/finder.scss +++ b/src/scss/common/finder.scss @@ -15,6 +15,7 @@ border-radius: 0 var(--bs-border-radius) var(--bs-border-radius) 0; padding: 0.45rem 0; width: $search-button-offset; + min-width: 0; &:active { background-color: lightgray !important; } From 2e320f5c22e5cb773abe8932460ed843f1ff24d1 Mon Sep 17 00:00:00 2001 From: Skye Purchase Date: Tue, 6 Aug 2024 15:05:02 +0100 Subject: [PATCH 47/61] Move difficulty icons below title on small screens --- .../StageAndDifficultySummaryIcons.tsx | 11 +++++--- .../ContentSummaryListGroupItem.tsx | 28 ++++++++++++------- src/app/components/pages/QuestionFinder.tsx | 2 +- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/app/components/elements/StageAndDifficultySummaryIcons.tsx b/src/app/components/elements/StageAndDifficultySummaryIcons.tsx index 01f9c84613..a06d3cff3d 100644 --- a/src/app/components/elements/StageAndDifficultySummaryIcons.tsx +++ b/src/app/components/elements/StageAndDifficultySummaryIcons.tsx @@ -5,9 +5,12 @@ import {DifficultyIcons} from "./svg/DifficultyIcons"; import {ViewingContext} from "../../../IsaacAppTypes"; import { Difficulty } from "../../../IsaacApiTypes"; -export function StageAndDifficultySummaryIcons({audienceViews, className}: {audienceViews: ViewingContext[], className?: string}) { - // FIXME find a better way than hiding the whole thing on mobile - // ^ fixed in new redesign +export function StageAndDifficultySummaryIcons({audienceViews, className, stack}: { + audienceViews: ViewingContext[], + className?: string, + stack?: boolean, +}) { + // FIXME find a better way than hiding the whole thing on mobile for Physics const difficulties: Difficulty[] = audienceViews.map(v => v.difficulty).filter(v => v !== undefined); return siteSpecific(
@@ -22,7 +25,7 @@ export function StageAndDifficultySummaryIcons({audienceViews, className}: {audi
) }
, -
+
{ difficulties.every((v, _i, arr) => v === arr[0]) ?
diff --git a/src/app/components/elements/list-groups/ContentSummaryListGroupItem.tsx b/src/app/components/elements/list-groups/ContentSummaryListGroupItem.tsx index 28d25d0037..ec32124809 100644 --- a/src/app/components/elements/list-groups/ContentSummaryListGroupItem.tsx +++ b/src/app/components/elements/list-groups/ContentSummaryListGroupItem.tsx @@ -1,6 +1,7 @@ import {ContentSummaryDTO} from "../../../../IsaacApiTypes"; import { AUDIENCE_DISPLAY_FIELDS, + below, determineAudienceViews, DOCUMENT_TYPE, documentTypePathPrefix, @@ -29,7 +30,12 @@ import classNames from "classnames"; import {ListGroup, ListGroupItem, UncontrolledTooltip} from "reactstrap"; import { CSSModule } from "reactstrap/types/lib/utils"; -export const ContentSummaryListGroupItem = ({item, search, displayTopicTitle}: {item: ShortcutResponse; search?: string; displayTopicTitle?: boolean}) => { +export const ContentSummaryListGroupItem = ({item, search, displayTopicTitle, noCaret}: { + item: ShortcutResponse; + search?: string; + displayTopicTitle?: boolean; + noCaret?: boolean; +}) => { const componentId = useRef(uuid_v4().slice(0, 4)).current; const userContext = useUserViewingContext(); const user = useAppSelector(selectors.user.orNull); @@ -40,8 +46,7 @@ export const ContentSummaryListGroupItem = ({item, search, displayTopicTitle}: { let itemClasses = "p-0 content-summary-link "; itemClasses += isContentsIntendedAudience ? "bg-transparent " : "de-emphasised "; - let caret = true; - + let stack = false; let title = item.title; let titleClasses = "content-summary-link-title flex-grow-1 "; const itemSubject = tags.getSpecifiedTag(TAG_LEVEL.subject, item.tags as TAG_ID[]); @@ -52,7 +57,6 @@ export const ContentSummaryListGroupItem = ({item, search, displayTopicTitle}: { const hierarchyTags = tags.getByIdsAsHierarchy((item.tags || []) as TAG_ID[]) .filter((_t, i) => !isAda || i !== 0); // CS always has Computer Science at the top level - // FIXME "correct" never actually exists on questions here... const questionIconLabel = item.correct ? "Completed question icon" : "Question icon"; const questionIcon = siteSpecific( item.correct ? @@ -84,7 +88,7 @@ export const ContentSummaryListGroupItem = ({item, search, displayTopicTitle}: { linkDestination = `/${documentTypePathPrefix[DOCUMENT_TYPE.QUESTION]}/${item.id}`; icon = questionIcon; audienceViews = filterAudienceViewsByProperties(determineAudienceViews(item.audience), AUDIENCE_DISPLAY_FIELDS); - caret = false; + stack = below["md"](deviceSize); break; case (DOCUMENT_TYPE.CONCEPT): linkDestination = `/${documentTypePathPrefix[DOCUMENT_TYPE.CONCEPT]}/${item.id}`; @@ -127,7 +131,7 @@ export const ContentSummaryListGroupItem = ({item, search, displayTopicTitle}: {
)} -
+
@@ -154,23 +158,27 @@ export const ContentSummaryListGroupItem = ({item, search, displayTopicTitle}: { {`This content has ${notRelevantMessage(userContext)}.`}
} - {audienceViews && audienceViews.length > 0 && } + {audienceViews && audienceViews.length > 0 && }
- {isAda && caret &&
{"Go
} + {isAda && !noCaret &&
{"Go
} ; }; -export const LinkToContentSummaryList = ({items, search, displayTopicTitle, ...rest}: { +export const LinkToContentSummaryList = ({items, search, displayTopicTitle, noCaret, ...rest}: { items: ContentSummaryDTO[]; search?: string; displayTopicTitle?: boolean; + noCaret?: boolean; tag?: React.ElementType; flush?: boolean; className?: string; cssModule?: CSSModule; }) => { return - {items.map(item => )} + {items.map(item => )} ; }; diff --git a/src/app/components/pages/QuestionFinder.tsx b/src/app/components/pages/QuestionFinder.tsx index 29704d9b11..69c4290ef9 100644 --- a/src/app/components/pages/QuestionFinder.tsx +++ b/src/app/components/pages/QuestionFinder.tsx @@ -379,7 +379,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { {displayQuestions?.length - ? + ? : (!filtersApplied && searchQuery === "" ? Please select and apply filters : No results match your criteria) From 88a6e0ab31952e53461b22947de5c564b432e409 Mon Sep 17 00:00:00 2001 From: Skye Purchase Date: Tue, 6 Aug 2024 15:13:29 +0100 Subject: [PATCH 48/61] Improve styling on Physics --- src/app/components/elements/StageAndDifficultySummaryIcons.tsx | 1 - src/app/components/pages/QuestionFinder.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/components/elements/StageAndDifficultySummaryIcons.tsx b/src/app/components/elements/StageAndDifficultySummaryIcons.tsx index a06d3cff3d..169d722f39 100644 --- a/src/app/components/elements/StageAndDifficultySummaryIcons.tsx +++ b/src/app/components/elements/StageAndDifficultySummaryIcons.tsx @@ -10,7 +10,6 @@ export function StageAndDifficultySummaryIcons({audienceViews, className, stack} className?: string, stack?: boolean, }) { - // FIXME find a better way than hiding the whole thing on mobile for Physics const difficulties: Difficulty[] = audienceViews.map(v => v.difficulty).filter(v => v !== undefined); return siteSpecific(
diff --git a/src/app/components/pages/QuestionFinder.tsx b/src/app/components/pages/QuestionFinder.tsx index 69c4290ef9..c41d21d4f1 100644 --- a/src/app/components/pages/QuestionFinder.tsx +++ b/src/app/components/pages/QuestionFinder.tsx @@ -408,7 +408,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { setDisableLoadMore(true); }} disabled={disableLoadMore} - outline + outline={isAda} > Load more From 71f604f40f8bc4c1ad40a015facacce2f30b95f7 Mon Sep 17 00:00:00 2001 From: Skye Purchase Date: Tue, 6 Aug 2024 16:36:25 +0100 Subject: [PATCH 49/61] Revert commit 66fe6ec --- .../components/elements/StageAndDifficultySummaryIcons.tsx | 6 +++--- .../elements/list-groups/ContentSummaryListGroupItem.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/components/elements/StageAndDifficultySummaryIcons.tsx b/src/app/components/elements/StageAndDifficultySummaryIcons.tsx index 169d722f39..7a632fba9e 100644 --- a/src/app/components/elements/StageAndDifficultySummaryIcons.tsx +++ b/src/app/components/elements/StageAndDifficultySummaryIcons.tsx @@ -24,15 +24,15 @@ export function StageAndDifficultySummaryIcons({audienceViews, className, stack}
) }
, -
+
{ difficulties.every((v, _i, arr) => v === arr[0]) ?
{difficulties.length > 0 && <> -
+
{simpleDifficultyLabelMap[difficulties[0]]}
-
+
} diff --git a/src/app/components/elements/list-groups/ContentSummaryListGroupItem.tsx b/src/app/components/elements/list-groups/ContentSummaryListGroupItem.tsx index ec32124809..ae148f2050 100644 --- a/src/app/components/elements/list-groups/ContentSummaryListGroupItem.tsx +++ b/src/app/components/elements/list-groups/ContentSummaryListGroupItem.tsx @@ -132,7 +132,7 @@ export const ContentSummaryListGroupItem = ({item, search, displayTopicTitle, no )}
-
+
{title ?? ""} From a15eb43f805133fbca28913360dd06e3225f007e Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Wed, 7 Aug 2024 12:14:37 +0100 Subject: [PATCH 50/61] Fill results from user context if no URL params provided --- src/app/components/pages/QuestionFinder.tsx | 22 +++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/app/components/pages/QuestionFinder.tsx b/src/app/components/pages/QuestionFinder.tsx index c41d21d4f1..487774bb29 100644 --- a/src/app/components/pages/QuestionFinder.tsx +++ b/src/app/components/pages/QuestionFinder.tsx @@ -100,17 +100,23 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { } ); - useEffect(function populateExamBoardFromUserContext() { - if (!EXAM_BOARD_NULL_OPTIONS.includes(userContext.examBoard)) { - setSearchExamBoards(arr => arr.length > 0 ? arr : [userContext.examBoard]); - } - }, [userContext.examBoard]); + const [populatedUserContext, setPopulatedUserContext] = useState(false); - useEffect(function populateStageFromUserContext() { + useEffect(function populateFromUserContext() { if (!STAGE_NULL_OPTIONS.includes(userContext.stage)) { setSearchStages(arr => arr.length > 0 ? arr : [userContext.stage]); } - }, [userContext.stage]); + if (!EXAM_BOARD_NULL_OPTIONS.includes(userContext.examBoard)) { + setSearchExamBoards(arr => arr.length > 0 ? arr : [userContext.examBoard]); + } + setPopulatedUserContext(!!userContext.stage && !!userContext.examBoard); + }, [userContext.stage, userContext.examBoard]); + + // this acts as an "on complete load", needed as we can only correctly update the URL once we have the user context + useEffect(() => { + searchAndUpdateURL(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [populatedUserContext]); const [searchBooks, setSearchBooks] = useState(arrayFromPossibleCsv(params.book)); const [excludeBooks, setExcludeBooks] = useState(!!params.excludeBooks); @@ -195,7 +201,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { logEvent(eventLog,"SEARCH_QUESTIONS", {searchString, topics, examBoards, book, stages, difficulties, startIndex}); }, 250), - [] + [nothingToSearchFor] ); useEffect(() => { From 338d662b9503a0018739386db35f5925769c5e2f Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Wed, 7 Aug 2024 12:25:56 +0100 Subject: [PATCH 51/61] Tidy up hook calls, move related code together --- src/app/components/pages/QuestionFinder.tsx | 34 ++++++++++----------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/src/app/components/pages/QuestionFinder.tsx b/src/app/components/pages/QuestionFinder.tsx index 487774bb29..d6f95a4972 100644 --- a/src/app/components/pages/QuestionFinder.tsx +++ b/src/app/components/pages/QuestionFinder.tsx @@ -204,19 +204,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { [nothingToSearchFor] ); - useEffect(() => { - // If a certain stage excludes a selected examboard remove it from query params - if (isAda) { - setSearchExamBoards( - getFilteredExamBoardOptions({byStages: searchStages}) - .filter(o => searchExamBoards.includes(o.value)) - .map(o => o.value) - ); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [searchStages]); - - const searchAndUpdateURL = () => { + const searchAndUpdateURL = useCallback(() => { setPageCount(1); setDisableLoadMore(false); setDisplayQuestions(undefined); @@ -246,7 +234,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { } history.replace({search: queryString.stringify(params, {encode: false}), state: location.state}); - }; + }, [excludeBooks, history, location.state, questionStatuses.hideCompleted, searchBooks, searchDebounce, searchDifficulties, searchExamBoards, searchQuery, searchStages, searchTopics, selections, tiers]); const [filtersApplied, setFiltersApplied] = useState(false); const applyFilters = () => { @@ -254,11 +242,21 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { searchAndUpdateURL(); }; - // search for content whenever the searchQuery changes - // but do not change whether filters have been applied or not + // Automatically search for content whenever the searchQuery changes, without changing whether filters have been applied or not // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(searchAndUpdateURL, [searchQuery]); + // If the stages filter changes, update the exam board filter selections to remove now-incompatible ones + useEffect(() => { + if (isAda) { + setSearchExamBoards(examBoards => + getFilteredExamBoardOptions({byStages: searchStages}) + .filter(o => examBoards.includes(o.value)) + .map(o => o.value) + ); + } + }, [searchStages]); + const questionList = useMemo(() => { if (questions) { if (questions.length < SEARCH_RESULTS_PER_PAGE + 1) { @@ -297,7 +295,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { .some(e => e[1]); }, [questionStatuses, searchBooks, searchDifficulties, searchExamBoards, searchStages, searchTopics, selections]); - const clearFilters = () => { + const clearFilters = useCallback(() => { setSearchDifficulties([]); setSearchTopics([]); setSearchExamBoards([]); @@ -313,7 +311,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { llmMarked: false, hideCompleted: false }); - }; + }, []); // eslint-disable-next-line react-hooks/exhaustive-deps const handleSearch = useCallback( From 65aba478f1ff982861e38c3d775d983ff5d7bfc8 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Wed, 7 Aug 2024 14:15:29 +0100 Subject: [PATCH 52/61] Continue refactoring for code clarity --- .../panels/QuestionFinderFilterPanel.tsx | 10 +-- src/app/components/pages/QuestionFinder.tsx | 62 ++++++++----------- 2 files changed, 32 insertions(+), 40 deletions(-) diff --git a/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx b/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx index 60bdb9591c..63cca87797 100644 --- a/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx +++ b/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx @@ -121,7 +121,7 @@ interface QuestionFinderFilterPanelProps { searchTopics: string[], setSearchTopics: Dispatch>; searchStages: STAGE[], setSearchStages: Dispatch>; searchExamBoards: ExamBoard[], setSearchExamBoards: Dispatch>; - questionStatuses: QuestionStatus, setQuestionStatuses: Dispatch>; + searchStatuses: QuestionStatus, setSearchStatuses: Dispatch>; searchBooks: string[], setSearchBooks: Dispatch>; excludeBooks: boolean, setExcludeBooks: Dispatch>; tiers: Tier[], choices: Item[][], selections: Item[][], setTierSelection: (tierIndex: number) => React.Dispatch[]>>, @@ -134,7 +134,7 @@ export function QuestionFinderFilterPanel(props: QuestionFinderFilterPanelProps) searchTopics, setSearchTopics, searchStages, setSearchStages, searchExamBoards, setSearchExamBoards, - questionStatuses, setQuestionStatuses, + searchStatuses, setSearchStatuses, searchBooks, setSearchBooks, excludeBooks, setExcludeBooks, tiers, choices, selections, setTierSelection, @@ -342,13 +342,13 @@ export function QuestionFinderFilterPanel(props: QuestionFinderFilterPanelProps) listStateDispatch({type: "toggle", id: "questionStatus", focus: below["md"](deviceSize)})} - numberSelected={Object.values(questionStatuses).reduce((acc, item) => acc + item, 0)} + numberSelected={Object.values(searchStatuses).reduce((acc, item) => acc + item, 0)} >
setQuestionStatuses(s => {return {...s, hideCompleted: !s.hideCompleted};})} + checked={searchStatuses.hideCompleted} + onChange={() => setSearchStatuses(s => {return {...s, hideCompleted: !s.hideCompleted};})} label={
Hide complete
} diff --git a/src/app/components/pages/QuestionFinder.tsx b/src/app/components/pages/QuestionFinder.tsx index d6f95a4972..d1ade9a31b 100644 --- a/src/app/components/pages/QuestionFinder.tsx +++ b/src/app/components/pages/QuestionFinder.tsx @@ -75,22 +75,12 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { const history = useHistory(); const eventLog = useRef([]).current; // persist state but do not rerender on mutation - const [searchTopics, setSearchTopics] = useState( - arrayFromPossibleCsv(params.topics) - ); - const [searchQuery, setSearchQuery] = useState( - params.query ? (params.query instanceof Array ? params.query[0] : params.query) : "" - ); - const [searchStages, setSearchStages] = useState( - arrayFromPossibleCsv(params.stages) as STAGE[] - ); - const [searchDifficulties, setSearchDifficulties] = useState( - arrayFromPossibleCsv(params.difficulties) as Difficulty[] - ); - const [searchExamBoards, setSearchExamBoards] = useState( - arrayFromPossibleCsv(params.examBoards) as ExamBoard[] - ); - const [questionStatuses, setQuestionStatuses] = useState( + const [searchTopics, setSearchTopics] = useState(arrayFromPossibleCsv(params.topics)); + const [searchQuery, setSearchQuery] = useState(params.query ? (params.query instanceof Array ? params.query[0] : params.query) : ""); + const [searchStages, setSearchStages] = useState(arrayFromPossibleCsv(params.stages) as STAGE[]); + const [searchDifficulties, setSearchDifficulties] = useState(arrayFromPossibleCsv(params.difficulties) as Difficulty[]); + const [searchExamBoards, setSearchExamBoards] = useState(arrayFromPossibleCsv(params.examBoards) as ExamBoard[]); + const [searchStatuses, setSearchStatuses] = useState( { notAttempted: false, complete: false, @@ -99,6 +89,8 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { hideCompleted: !!params.hideCompleted } ); + const [searchBooks, setSearchBooks] = useState(arrayFromPossibleCsv(params.book)); + const [excludeBooks, setExcludeBooks] = useState(!!params.excludeBooks); const [populatedUserContext, setPopulatedUserContext] = useState(false); @@ -112,27 +104,26 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { setPopulatedUserContext(!!userContext.stage && !!userContext.examBoard); }, [userContext.stage, userContext.examBoard]); - // this acts as an "on complete load", needed as we can only correctly update the URL once we have the user context + // this acts as an "on complete load", needed as we can only correctly update the URL once we have the user context *and* React has processed the above setStates useEffect(() => { searchAndUpdateURL(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [populatedUserContext]); - const [searchBooks, setSearchBooks] = useState(arrayFromPossibleCsv(params.book)); - const [excludeBooks, setExcludeBooks] = useState(!!params.excludeBooks); const [disableLoadMore, setDisableLoadMore] = useState(false); - const subjects = arrayFromPossibleCsv(params.subjects); - const fields = arrayFromPossibleCsv(params.fields); - const topics = arrayFromPossibleCsv(params.topics); const [selections, setSelections] = useState[][]>( - processTagHierarchy(subjects, fields, topics) + processTagHierarchy( + arrayFromPossibleCsv(params.subjects), + arrayFromPossibleCsv(params.fields), + arrayFromPossibleCsv(params.topics) + ) ); const choices = [tags.allSubjectTags.map(itemiseTag)]; - let index; - for (index = 0; index < selections.length && index < 2; index++) { - const selection = selections[index]; + let tierIndex; + for (tierIndex = 0; tierIndex < selections.length && tierIndex < 2; tierIndex++) { + const selection = selections[tierIndex]; if (selection.length !== 1) break; choices.push(tags.getChildren(selection[0].value).map(itemiseTag)); } @@ -141,7 +132,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { {id: "subjects" as TierID, name: "Subject"}, {id: "fields" as TierID, name: "Field"}, {id: "topics" as TierID, name: "Topic"} - ].map(tier => ({...tier, for: "for_" + tier.id})).slice(0, index + 1); + ].map(tier => ({...tier, for: "for_" + tier.id})).slice(0, tierIndex + 1); const setTierSelection = (tierIndex: number) => { return ((values: Item[]) => { @@ -210,7 +201,8 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { setDisplayQuestions(undefined); searchDebounce( searchQuery, searchTopics, searchExamBoards, searchBooks, searchStages, - searchDifficulties, selections, tiers, excludeBooks, questionStatuses.hideCompleted, 0); + searchDifficulties, selections, tiers, excludeBooks, searchStatuses.hideCompleted, 0 + ); const params: {[key: string]: string} = {}; if (searchStages.length) params.stages = toSimpleCSV(searchStages); @@ -222,7 +214,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { params.book = toSimpleCSV(searchBooks); } if (isPhy && excludeBooks) params.excludeBooks = "set"; - if (questionStatuses.hideCompleted) params.hideCompleted = "set"; + if (searchStatuses.hideCompleted) params.hideCompleted = "set"; if (isPhy) { tiers.forEach((tier, i) => { @@ -234,7 +226,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { } history.replace({search: queryString.stringify(params, {encode: false}), state: location.state}); - }, [excludeBooks, history, location.state, questionStatuses.hideCompleted, searchBooks, searchDebounce, searchDifficulties, searchExamBoards, searchQuery, searchStages, searchTopics, selections, tiers]); + }, [excludeBooks, history, location.state, searchStatuses.hideCompleted, searchBooks, searchDebounce, searchDifficulties, searchExamBoards, searchQuery, searchStages, searchTopics, selections, tiers]); const [filtersApplied, setFiltersApplied] = useState(false); const applyFilters = () => { @@ -289,11 +281,11 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { || searchStages.length > 0 || searchBooks.length > 0 || selections.some(tier => tier.length > 0) - || Object.entries(questionStatuses) + || Object.entries(searchStatuses) .filter(e => e[0] !== "revisionMode" && e[0] !== "hideCompleted") // Ignore revision mode as it isn't really a filter .some(e => e[1]); - }, [questionStatuses, searchBooks, searchDifficulties, searchExamBoards, searchStages, searchTopics, selections]); + }, [searchStatuses, searchBooks, searchDifficulties, searchExamBoards, searchStages, searchTopics, selections]); const clearFilters = useCallback(() => { setSearchDifficulties([]); @@ -303,7 +295,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { setSearchBooks([]); setExcludeBooks(false); setSelections([[], [], []]); - setQuestionStatuses( + setSearchStatuses( { notAttempted: false, complete: false, @@ -365,7 +357,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { searchTopics, setSearchTopics, searchStages, setSearchStages, searchExamBoards, setSearchExamBoards, - questionStatuses, setQuestionStatuses, + searchStatuses, setSearchStatuses, searchBooks, setSearchBooks, excludeBooks, setExcludeBooks, tiers, choices, selections, setTierSelection, @@ -404,7 +396,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { searchDifficulties, selections, tiers, excludeBooks, - questionStatuses.hideCompleted, + searchStatuses.hideCompleted, nextSearchOffset ? nextSearchOffset - 1 : 0); From 4fc63c079483eac4a566b591d32460ee638f5f90 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Mon, 12 Aug 2024 10:43:25 +0100 Subject: [PATCH 53/61] Update difficulty modal texts --- .../modals/QuestionFinderDifficultyModal.tsx | 62 ++++++++++--------- .../panels/QuestionFinderFilterPanel.tsx | 2 +- 2 files changed, 35 insertions(+), 29 deletions(-) diff --git a/src/app/components/elements/modals/QuestionFinderDifficultyModal.tsx b/src/app/components/elements/modals/QuestionFinderDifficultyModal.tsx index b2b7494744..47045b354a 100644 --- a/src/app/components/elements/modals/QuestionFinderDifficultyModal.tsx +++ b/src/app/components/elements/modals/QuestionFinderDifficultyModal.tsx @@ -1,42 +1,48 @@ import React from "react"; -import { Col, Row } from "reactstrap"; -import { difficultyLabelMap, siteSpecific } from "../../../services"; +import { Col } from "reactstrap"; +import { siteSpecific } from "../../../services"; import { closeActiveModal, store } from "../../../state"; -import { DifficultyIcons } from "../svg/DifficultyIcons"; -import { Difficulty } from "../../../../IsaacApiTypes"; import { ActiveModal } from "../../../../IsaacAppTypes"; -const DifficultyPanel = ({difficulty, explanation} : {difficulty: Difficulty, explanation: string}) => { - return - -
- {difficultyLabelMap[difficulty]} -
- -
-
-
- -
- {explanation} -
-
- ; -}; - const QuestionFinderDifficultyModal = () => { return - - - - + {siteSpecific(

+ Practice questions let you directly apply one idea. +

    +
  • + P1 covers revision of a previous stage or topics near the beginning of a course +
  • +
  • + P3 covers later topics. +
  • +
+ Challenge questions are solved by combining multiple concepts and creativity. +
    +
  • + C1 can be attempted near the beginning of your course +
  • +
  • + C3 require more creativity and could be attempted later in a course. +
  • +
+

,

+ We split our questions into two categories: +

    +
  • + Practice questions focus on one concept +
  • +
  • + Challenge questions combine multiple concepts +
  • +
+

)} ; }; export const questionFinderDifficultyModal = () : ActiveModal => { return { closeAction: () => store.dispatch(closeActiveModal()), - title: `Difficulty ${siteSpecific("Levels", "levels")}`, + title: siteSpecific("Difficulty Levels", "What do the difficulty levels mean?"), body: , }; -}; \ No newline at end of file +}; diff --git a/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx b/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx index 63cca87797..6a48d804fc 100644 --- a/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx +++ b/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx @@ -289,7 +289,7 @@ export function QuestionFinderFilterPanel(props: QuestionFinderFilterPanelProps) e.preventDefault(); dispatch(openActiveModal(questionFinderDifficultyModal())); }}> - Learn more about difficulty levels + {siteSpecific("Learn more about difficulty levels", "What do the difficulty levels mean?")} {SIMPLE_DIFFICULTY_ITEM_OPTIONS.map((difficulty, index) => (
From 266009b2e577cefb8e91fcff0ac12af2f728a134 Mon Sep 17 00:00:00 2001 From: Skye Purchase Date: Mon, 12 Aug 2024 12:29:08 +0100 Subject: [PATCH 54/61] Change hide complete filter wording on Physics --- .../components/elements/panels/QuestionFinderFilterPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx b/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx index 6a48d804fc..490d1ef458 100644 --- a/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx +++ b/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx @@ -350,7 +350,7 @@ export function QuestionFinderFilterPanel(props: QuestionFinderFilterPanelProps) checked={searchStatuses.hideCompleted} onChange={() => setSearchStatuses(s => {return {...s, hideCompleted: !s.hideCompleted};})} label={
- Hide complete + {siteSpecific("Hide fully correct", "Hide complete")}
} />
From cf2df2ddf95b150ce4326e2ead055dbf66243733 Mon Sep 17 00:00:00 2001 From: Skye Purchase Date: Mon, 12 Aug 2024 12:46:00 +0100 Subject: [PATCH 55/61] Improve apply filters disable condition --- src/app/components/pages/QuestionFinder.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/app/components/pages/QuestionFinder.tsx b/src/app/components/pages/QuestionFinder.tsx index d1ade9a31b..181181c6b1 100644 --- a/src/app/components/pages/QuestionFinder.tsx +++ b/src/app/components/pages/QuestionFinder.tsx @@ -280,12 +280,10 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { || searchExamBoards.length > 0 || searchStages.length > 0 || searchBooks.length > 0 + || excludeBooks || selections.some(tier => tier.length > 0) - || Object.entries(searchStatuses) - .filter(e => e[0] !== "revisionMode" - && e[0] !== "hideCompleted") // Ignore revision mode as it isn't really a filter - .some(e => e[1]); - }, [searchStatuses, searchBooks, searchDifficulties, searchExamBoards, searchStages, searchTopics, selections]); + || Object.entries(searchStatuses).some(e => e[1]); + }, [searchDifficulties, searchTopics, searchExamBoards, searchStages, excludeBooks, selections, searchStatuses]); const clearFilters = useCallback(() => { setSearchDifficulties([]); From 115ab84722fa717dc385a999ff5c01130f7c775c Mon Sep 17 00:00:00 2001 From: Skye Purchase Date: Mon, 12 Aug 2024 14:16:56 +0100 Subject: [PATCH 56/61] Use hexagons for Physics filter count --- src/app/components/elements/svg/FilterCount.tsx | 11 ++++++++--- src/scss/phy/filter.scss | 3 ++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/app/components/elements/svg/FilterCount.tsx b/src/app/components/elements/svg/FilterCount.tsx index 663cb55d02..4a02e59da8 100644 --- a/src/app/components/elements/svg/FilterCount.tsx +++ b/src/app/components/elements/svg/FilterCount.tsx @@ -1,5 +1,7 @@ import React from "react"; import {Circle} from "./Circle"; +import { isPhy, siteSpecific } from "../../../services"; +import { Hexagon } from "./Hexagon"; const filterIconWidth = 25; @@ -11,12 +13,15 @@ export const FilterCount = ({count}: {count: number}) => { > {`${count} filters selected`} - - { + {siteSpecific( + , + + )} +
{count}
-
} +
; }; diff --git a/src/scss/phy/filter.scss b/src/scss/phy/filter.scss index efa27131aa..dd77309a57 100644 --- a/src/scss/phy/filter.scss +++ b/src/scss/phy/filter.scss @@ -108,7 +108,8 @@ svg { } &.filter-count { - fill: $gray-120; + fill: $phy_green; + opacity: .1; stroke: none; } From 5e3b691ea1a6c392b5e82db65c0c1d423fd7593d Mon Sep 17 00:00:00 2001 From: Skye Purchase Date: Mon, 12 Aug 2024 14:29:37 +0100 Subject: [PATCH 57/61] Fix ESLint warning --- src/app/components/pages/QuestionFinder.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/pages/QuestionFinder.tsx b/src/app/components/pages/QuestionFinder.tsx index 181181c6b1..2ff99e0d9a 100644 --- a/src/app/components/pages/QuestionFinder.tsx +++ b/src/app/components/pages/QuestionFinder.tsx @@ -283,7 +283,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => { || excludeBooks || selections.some(tier => tier.length > 0) || Object.entries(searchStatuses).some(e => e[1]); - }, [searchDifficulties, searchTopics, searchExamBoards, searchStages, excludeBooks, selections, searchStatuses]); + }, [searchDifficulties, searchTopics, searchExamBoards, searchStages, searchBooks, excludeBooks, selections, searchStatuses]); const clearFilters = useCallback(() => { setSearchDifficulties([]); From 8c291c2c497adb9349dd2e1d929bf68bf3df89ef Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Mon, 12 Aug 2024 15:43:57 +0100 Subject: [PATCH 58/61] Fix buggy nested collapsible list behaviour --- .../components/elements/CollapsibleList.tsx | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/app/components/elements/CollapsibleList.tsx b/src/app/components/elements/CollapsibleList.tsx index b9d0031874..bf785981f2 100644 --- a/src/app/components/elements/CollapsibleList.tsx +++ b/src/app/components/elements/CollapsibleList.tsx @@ -18,17 +18,26 @@ export const CollapsibleList = (props: CollapsibleListProps) => { const {expanded, toggle} = props; const [expandedHeight, setExpandedHeight] = useState(0); const listRef = useRef(null); + const headRef = useRef(null); + + useLayoutEffect(() => { + if (!listRef.current) return; + console.log(listRef.current.clientHeight); + setExpandedHeight(listRef.current.clientHeight); + }, [listRef.current]); useLayoutEffect(() => { if (expanded) { - setExpandedHeight(listRef?.current ? [...listRef.current.children].map(c => c.clientHeight).reduce((a, b) => a + b, 0) : 0); + setExpandedHeight(listRef?.current ? [...listRef.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]); const title = props.title && props.asSubList ? props.title : {props.title}; - return - + return +
- - {/* TODO:
*/} - +
+
{props.children} From f61d385d8256d11385b6f9f25ee65102fae63b99 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Mon, 12 Aug 2024 15:45:25 +0100 Subject: [PATCH 59/61] Remove `data-targetHeight` on collapsibles' rows --- src/app/components/elements/CollapsibleList.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/components/elements/CollapsibleList.tsx b/src/app/components/elements/CollapsibleList.tsx index bf785981f2..8ea403dd25 100644 --- a/src/app/components/elements/CollapsibleList.tsx +++ b/src/app/components/elements/CollapsibleList.tsx @@ -49,7 +49,6 @@ export const CollapsibleList = (props: CollapsibleListProps) => {
From 2e595b2074062b60e1880c9cf60e9f77c50b7a03 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Mon, 12 Aug 2024 15:51:37 +0100 Subject: [PATCH 60/61] Add white background to "Apply filters" button --- .../components/elements/panels/QuestionFinderFilterPanel.tsx | 2 +- src/scss/common/finder.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx b/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx index 490d1ef458..973b5a5876 100644 --- a/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx +++ b/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx @@ -428,7 +428,7 @@ export function QuestionFinderFilterPanel(props: QuestionFinderFilterPanelProps) } />
}*/} - + diff --git a/src/scss/common/finder.scss b/src/scss/common/finder.scss index 1a1c988926..e84727df60 100644 --- a/src/scss/common/finder.scss +++ b/src/scss/common/finder.scss @@ -63,7 +63,7 @@ .filter-btn { position: -webkit-sticky; position: sticky; - bottom: 1rem; + bottom: 0; } .filter-separator { From 5db0c53d7790dcda336df208f541f6c3d34dbfae Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Mon, 12 Aug 2024 16:11:03 +0100 Subject: [PATCH 61/61] Remove console log --- src/app/components/elements/CollapsibleList.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/components/elements/CollapsibleList.tsx b/src/app/components/elements/CollapsibleList.tsx index 8ea403dd25..fbed44923d 100644 --- a/src/app/components/elements/CollapsibleList.tsx +++ b/src/app/components/elements/CollapsibleList.tsx @@ -22,7 +22,6 @@ export const CollapsibleList = (props: CollapsibleListProps) => { useLayoutEffect(() => { if (!listRef.current) return; - console.log(listRef.current.clientHeight); setExpandedHeight(listRef.current.clientHeight); }, [listRef.current]);