Skip to content

Commit 1acdfcc

Browse files
committed
Allow multiple topics to be open at once
1 parent c836ec8 commit 1acdfcc

File tree

4 files changed

+63
-166
lines changed

4 files changed

+63
-166
lines changed

src/app/components/elements/layout/SidebarLayout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ const ContentSidebar = (props: ContentSidebarProps) => {
109109
</>;
110110
};
111111

112-
const KeyItem = (props: React.HTMLAttributes<HTMLSpanElement> & {icon: string, text: string}) => {
112+
export const KeyItem = (props: React.HTMLAttributes<HTMLSpanElement> & {icon: string, text: string}) => {
113113
const { icon, text, ...rest } = props;
114114
return <span {...rest} className={classNames(rest.className, "d-flex align-items-center pt-2")}><img className="pe-2" src={`/assets/phy/icons/redesign/${icon}.svg`} alt=""/> {text}</span>;
115115
};

src/app/components/elements/panels/QuestionFinderFilterPanel.tsx

Lines changed: 33 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import { openActiveModal, useAppDispatch } from "../../../state";
2828
import { questionFinderDifficultyModal } from "../modals/QuestionFinderDifficultyModal";
2929
import { Spacer } from "../Spacer";
3030

31-
3231
const bookOptions: Item<string>[] = [
3332
{value: "phys_book_step_up", label: "Step Up to GCSE Physics"},
3433
{value: "phys_book_gcse", label: "GCSE Physics"},
@@ -144,7 +143,7 @@ export function QuestionFinderFilterPanel(props: QuestionFinderFilterPanelProps)
144143
applyFilters, clearFilters, validFiltersSelected,
145144
searchDisabled, setSearchDisabled
146145
} = props;
147-
const groupBaseTagOptions: GroupBase<Item<string>>[] = tags.allSubcategoryTags.map(groupTagSelectionsByParent);
146+
const groupBaseTagOptions: GroupBase<Item<string>>[] = siteSpecific(tags.allSubjectTags.map(groupTagSelectionsByParent), tags.allSubcategoryTags.map(groupTagSelectionsByParent));
148147

149148
const [listState, listStateDispatch] = useReducer(listStateReducer, groupBaseTagOptions, initialiseListState);
150149
const deviceSize = useDeviceSize();
@@ -175,22 +174,14 @@ export function QuestionFinderFilterPanel(props: QuestionFinderFilterPanelProps)
175174
<h6 className="filter-question-text">Filter questions by</h6>,
176175
<>
177176
<div>
178-
<img
179-
src="/assets/common/icons/filter-icon.svg"
180-
alt="Filter"
181-
style={{width: 18}}
182-
className="ms-1 me-2"
183-
/>
177+
<img src="/assets/common/icons/filter-icon.svg" alt="Filter" style={{width: 18}} className="ms-1 me-2"/>
184178
<b>Filter by</b>
185179
</div>
186180
<Spacer/>
187181
{validFiltersSelected && <div className="pe-1 pe-lg-0">
188182
<button
189183
className={classNames("text-black pe-lg-0 py-0 me-2 me-lg-0 bg-opacity-10 btn-link", {"bg-white": isAda})}
190-
onClick={(e) => {
191-
e.stopPropagation();
192-
clearFilters();
193-
}}
184+
onClick={(e) => { e.stopPropagation(); clearFilters(); }}
194185
>
195186
Clear all
196187
</button>
@@ -282,8 +273,7 @@ export function QuestionFinderFilterPanel(props: QuestionFinderFilterPanelProps)
282273
</div>
283274
))}
284275
</CollapsibleList>
285-
)))
286-
}
276+
)))}
287277
</CollapsibleList>
288278

289279
<CollapsibleList
@@ -357,68 +347,49 @@ export function QuestionFinderFilterPanel(props: QuestionFinderFilterPanelProps)
357347
color="primary"
358348
checked={searchStatuses.notAttempted}
359349
onChange={() => setSearchStatuses(s => {return {...s, notAttempted: !s.notAttempted};})}
360-
label={<div>
361-
<span>{siteSpecific("Not started", "Not attempted")}</span>
362-
{siteSpecific(
363-
<svg
364-
className={"search-item-icon ps-2 icon-status"}
365-
aria-label={"Not started"}>
366-
<use href={`/assets/phy/icons/question-hex.svg#icon`}
367-
xlinkHref={`/assets/phy/icons/question-hex.svg#icon`}/>
368-
</svg>,
369-
<img
370-
src="/assets/common/icons/not-started.svg"
371-
alt="Not attempted"
372-
className="ps-2 icon-status"
373-
/>
374-
)}
375-
</div>}
350+
label={siteSpecific(
351+
<div className="d-flex">
352+
Not started
353+
</div>,
354+
<div>
355+
Not attempted
356+
<img className="ps-2 icon-status" src="/assets/common/icons/not-started.svg" alt="Not attempted" />
357+
</div>
358+
)}
376359
/>
377360
</div>
378361
<div className={classNames("w-100 ps-3 py-1 d-flex align-items-center", {"bg-white": isAda, "ms-2": isPhy})}>
379362
<StyledCheckbox
380363
color="primary"
381364
checked={searchStatuses.complete}
382365
onChange={() => setSearchStatuses(s => {return {...s, complete: !s.complete};})}
383-
label={<div>
384-
<span>{siteSpecific("Fully correct", "Completed")}</span>
385-
{siteSpecific(
386-
<svg
387-
className={"search-item-icon ps-2 icon-status correct-fill"}
388-
aria-label={"Fully correct"}>
389-
<use href={`/assets/phy/icons/tick-rp-hex.svg#icon`}
390-
xlinkHref={`/assets/phy/icons/tick-rp-hex.svg#icon`}/>
391-
</svg>,
392-
<img
393-
src="/assets/common/icons/completed.svg"
394-
alt="Completed"
395-
className="ps-2 icon-status"
396-
/>
397-
)}
398-
</div>}
366+
label={siteSpecific(
367+
<div className="d-flex">
368+
Fully correct
369+
<img className="ps-2" src={`/assets/phy/icons/redesign/status-correct.svg`} alt="Fully correct"/>
370+
</div>,
371+
<div>
372+
Completed
373+
<img className="ps-2 icon-status" src="/assets/common/icons/completed.svg" alt="Completed" />
374+
</div>
375+
)}
399376
/>
400377
</div>
401378
<div className={classNames("w-100 ps-3 py-1 d-flex align-items-center", {"bg-white": isAda, "ms-2": isPhy})}>
402379
<StyledCheckbox
403380
color="primary"
404381
checked={searchStatuses.tryAgain}
405382
onChange={() => setSearchStatuses(s => {return {...s, tryAgain: !s.tryAgain};})}
406-
label={<div>
407-
<span>{siteSpecific("In progress", "Try again")}</span>
408-
{siteSpecific(
409-
<svg
410-
className={"search-item-icon ps-2 icon-status almost-fill"}
411-
aria-label={"In progress"}>
412-
<use href={`/assets/phy/icons/incomplete-hex.svg#icon`}
413-
xlinkHref={`/assets/phy/icons/incomplete-hex.svg#icon`}/>
414-
</svg>,
415-
<img
416-
src="/assets/common/icons/incorrect.svg"
417-
alt="Try again"
418-
className="ps-2 icon-status"
419-
/>
420-
)}
421-
</div>}
383+
label={siteSpecific(
384+
<div className="d-flex">
385+
In progress
386+
<img className="ps-2" src={`/assets/phy/icons/redesign/status-in-progress.svg`} alt="In Progress"/>
387+
</div>,
388+
<div>
389+
Try again
390+
<img className="ps-2 icon-status" src="/assets/common/icons/incorrect.svg" alt="Try again" />
391+
</div>
392+
)}
422393
/>
423394
</div>
424395
</CollapsibleList>

src/app/components/elements/svg/HierarchyFilter.tsx

Lines changed: 23 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {HexagonConnection} from "./HexagonConnection";
1818
import classNames from "classnames";
1919
import {StyledSelect} from "../inputs/StyledSelect";
2020
import { Label } from "reactstrap";
21+
import { StyledCheckbox } from "../inputs/StyledCheckbox";
2122

2223
export type TierID = "subjects" | "fields" | "topics";
2324
export interface Tier {id: TierID; name: string; for: string}
@@ -43,108 +44,30 @@ function naturalLanguageList(list: string[]) {
4344
return `${lowerCaseList.slice(0, lastIndex).join(", ")} and ${lowerCaseList[lastIndex]}`;
4445
}
4546

46-
function hexRowTranslation(deviceSize: DeviceSize, hexagon: HexagonProportions, i: number, questionFinderFilter: boolean) {
47-
if (i == 0 || (deviceSize != "xs" && !questionFinderFilter)) {
48-
return `translate(0,${i * (6 * hexagon.quarterHeight + 2 * hexagon.padding)})`;
49-
} else {
50-
const x = (i * 2 - 1) * (hexagon.halfWidth + hexagon.padding);
51-
const y = 3 * hexagon.quarterHeight + hexagon.padding + (hexagon.quarterHeight + hexagon.padding /* xs y diff */);
52-
return `translate(${x},${y})`;
53-
}
54-
}
55-
56-
function connectionRowTranslation(deviceSize: DeviceSize, hexagon: HexagonProportions, i: number, questionFinderFilter: boolean) {
57-
if (deviceSize != "xs" && !questionFinderFilter) {
58-
return `translate(${hexagon.halfWidth + hexagon.padding},${3 * hexagon.quarterHeight + hexagon.padding + i * (6 * hexagon.quarterHeight + 2 * hexagon.padding)})`;
59-
} else {
60-
return `translate(0,0)`; // positioning is managed absolutely not through transformation
61-
}
62-
}
63-
64-
function hexagonTranslation(deviceSize: DeviceSize, hexagon: HexagonProportions, i: number, j: number, questionFinderFilter: boolean) {
65-
if (i == 0 || (deviceSize != "xs" && !questionFinderFilter)) {
66-
return `translate(${j * 2 * (hexagon.halfWidth + hexagon.padding)},0)`;
67-
} else {
68-
return `translate(0,${j * (4 * hexagon.quarterHeight + hexagon.padding)})`;
69-
}
70-
}
71-
7247
export function HierarchyFilterHexagonal({tiers, choices, selections, questionFinderFilter, setTierSelection}: HierarchyFilterProps) {
73-
const deviceSize = useDeviceSize();
74-
const leadingHexagon = calculateHexagonProportions(36, deviceSize === "xs" ? 2 : 8);
75-
const hexagon = calculateHexagonProportions(36, deviceSize === "xs" || !!questionFinderFilter ? 16 : 8);
76-
const focusPadding = 3;
77-
78-
const maxOptions = choices.slice(1).map(c => c.length).reduce((a, b) => Math.max(a, b), 0);
79-
const height = (deviceSize != "xs" && !questionFinderFilter) ?
80-
2 * focusPadding + 4 * hexagon.quarterHeight + (tiers.length - 1) * (6 * hexagon.quarterHeight + 2 * hexagon.padding) :
81-
2 * focusPadding + 4 * hexagon.quarterHeight + maxOptions * (4 * hexagon.quarterHeight + hexagon.padding) + (maxOptions ? hexagon.padding : 0);
82-
const width = (8 * leadingHexagon.halfWidth) + (6 * leadingHexagon.padding) + (2 * focusPadding);
83-
84-
return <svg
85-
viewBox={questionFinderFilter ? `0 0 ${width} ${height}` : ""}
86-
width={questionFinderFilter ? "auto" : "100%"}
87-
className={classNames({"mx-auto d-block": questionFinderFilter})}
88-
height={`${height}px`}
89-
>
90-
<title>Topic filter selector</title>
91-
<g id="hexagonal-filter" transform={`translate(${focusPadding},${focusPadding})`}>
92-
{/* Connections */}
93-
{tiers.slice(1).map((tier, i) => {
94-
const subject = selections?.[0]?.[0] ? selections[0][0].value : "";
95-
return <g key={tier.for} transform={connectionRowTranslation(deviceSize, hexagon, i, !!questionFinderFilter)}>
96-
<HexagonConnection
97-
sourceIndex={choices[i].map(c => c.value).indexOf(selections[i][0]?.value)}
98-
optionIndices={[...choices[i+1].keys()]} // range from 0 to choices[i+1].length
99-
targetIndices={selections[i+1]?.map(s => choices[i+1].map(c => c.value).indexOf(s.value)) || [-1]}
100-
leadingHexagonProportions={leadingHexagon} hexagonProportions={hexagon} connectionProperties={connectionProperties}
101-
rowIndex={i} mobile={deviceSize === "xs" || !!questionFinderFilter} className={`connection ${subject}`}
48+
return <div>
49+
{tiers.map((tier, i) => ( // Subject / Field / Topic
50+
choices[i].map((choice, j) => {
51+
const isSelected = !!selections[i]?.map(s => s.value).includes(choice.value);
52+
function selectValue() {
53+
setTierSelection(i)(isSelected ?
54+
selections[i].filter(s => s.value !== choice.value) : // remove
55+
[...(selections[i] || []), choice] // add
56+
);
57+
}
58+
59+
return <div key={choice.value} className="ps-3 ms-2">
60+
<StyledCheckbox
61+
color="primary"
62+
checked={isSelected}
63+
onChange={selectValue}
64+
label={<span>{choice.label}</span>}
65+
className="ps-3"
10266
/>
103-
</g>;
104-
})}
105-
106-
{/* Hexagons */}
107-
{tiers.map((tier, i) => <g key={tier.for} transform={hexRowTranslation(deviceSize, hexagon, i, !!questionFinderFilter)}>
108-
{choices[i].map((choice, j) => {
109-
const subject = i == 0 ? choice.value : selections[0][0].value;
110-
const isSelected = !!selections[i]?.map(s => s.value).includes(choice.value);
111-
const longWordInLabel = choice.label.split(/\s/).some(word => word.length > 10);
112-
const tag = tags.getById(choice.value);
113-
const isComingSoon = isDefined(tag.comingSoonDate);
114-
function selectValue() {
115-
setTierSelection(i)(isSelected ?
116-
selections[i].filter(s => s.value !== choice.value) : // remove
117-
[...(selections[i] || []), choice] // add
118-
);
119-
}
120-
121-
return <g key={choice.value} transform={hexagonTranslation(deviceSize, i === 0 ? leadingHexagon : hexagon, i, j, !!questionFinderFilter)}>
122-
<Hexagon {...hexagon} className={classNames("hex", subject, {"active": isSelected && !isComingSoon, "de-emph": isComingSoon})} />
123-
<foreignObject width={hexagon.halfWidth * 2} height={hexagon.quarterHeight * 4}>
124-
<div className={classNames("hexagon-tier-title", {"active": isSelected && !isComingSoon, "de-emph": isComingSoon, "small": longWordInLabel})}>
125-
{choice.label}
126-
</div>
127-
{tag.comingSoonDate && <div className={classNames(subject, "hexagon-coming-soon")}>
128-
Coming {tag.comingSoonDate}
129-
</div>}
130-
</foreignObject>
131-
132-
<Hexagon
133-
{...hexagon} className={classNames("hex none", {"clickable": !isComingSoon})} properties={{clickable: !isComingSoon}} role="button"
134-
tabIndex={isComingSoon ? -1 : 0} onClick={isComingSoon ? noop : selectValue} onKeyPress={isComingSoon ? noop : ifKeyIsEnter(selectValue)}
135-
>
136-
{!isComingSoon && <title>
137-
{`${isSelected ? "Remove" : "Add"} the ${tier.name.toLowerCase()} "${choice.label}" ${isSelected ? "from" : "to"} your ${siteSpecific("gameboard", "quiz")} filter`}
138-
</title>}
139-
</Hexagon>
140-
{isComingSoon && <title>
141-
This topic is coming soon
142-
</title>}
143-
</g>;
144-
})}
145-
</g>)}
146-
</g>
147-
</svg>;
67+
</div>;
68+
})
69+
))}
70+
</div>;
14871
}
14972

15073
export function HierarchyFilterSummary({tiers, choices, selections}: HierarchySummaryProps) {

src/app/components/pages/QuestionFinder.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,8 +167,10 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => {
167167
let tierIndex;
168168
for (tierIndex = 0; tierIndex < selections.length && tierIndex < 2; tierIndex++) {
169169
const selection = selections[tierIndex];
170-
if (selection.length !== 1) break;
171-
choices.push(tags.getChildren(selection[0].value).map(itemiseTag));
170+
if (selection.length === 0) break;
171+
choices[tierIndex+1] = [];
172+
for (let i = 0; i < selection.length; i++)
173+
choices[tierIndex+1].push(...tags.getChildren(selection[i].value).map(itemiseTag));
172174
}
173175

174176
const tiers: Tier[] = [
@@ -281,7 +283,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => {
281283
if (searchStages.length) params.stages = toSimpleCSV(searchStages);
282284
if (searchDifficulties.length) params.difficulties = toSimpleCSV(searchDifficulties);
283285
if (searchQuery.length) params.query = encodeURIComponent(searchQuery);
284-
if (isAda && searchTopics.length) params.topics = toSimpleCSV(searchTopics);
286+
if (searchTopics.length) params.topics = toSimpleCSV(searchTopics);
285287
if (isAda && searchExamBoards.length) params.examBoards = toSimpleCSV(searchExamBoards);
286288
if (isPhy && !excludeBooks && searchBooks.length) {
287289
params.book = toSimpleCSV(searchBooks);
@@ -356,6 +358,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => {
356358
|| excludeBooks
357359
|| selections.some(tier => tier.length > 0)
358360
|| Object.entries(searchStatuses).some(e => e[1]));
361+
if (isPhy) applyFilters();
359362
}, [searchDifficulties, searchTopics, searchExamBoards, searchStages, searchBooks, excludeBooks, selections, searchStatuses]);
360363

361364
const clearFilters = useCallback(() => {

0 commit comments

Comments
 (0)