+ return
;
})}
diff --git a/src/app/components/elements/svg/FilterCount.tsx b/src/app/components/elements/svg/FilterCount.tsx
new file mode 100644
index 0000000000..4a02e59da8
--- /dev/null
+++ b/src/app/components/elements/svg/FilterCount.tsx
@@ -0,0 +1,27 @@
+import React from "react";
+import {Circle} from "./Circle";
+import { isPhy, siteSpecific } from "../../../services";
+import { Hexagon } from "./Hexagon";
+
+const filterIconWidth = 25;
+
+export const FilterCount = ({count}: {count: number}) => {
+ return
+ {`${count} filters selected`}
+
+ {siteSpecific(
+ ,
+
+ )}
+
+
+ {count}
+
+
+
+ ;
+};
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/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 d396d38fc9..2ff99e0d9a 100644
--- a/src/app/components/pages/QuestionFinder.tsx
+++ b/src/app/components/pages/QuestionFinder.tsx
@@ -3,26 +3,18 @@ import {
AppState,
clearQuestionSearch,
searchQuestions,
- updateCurrentUser,
useAppDispatch,
useAppSelector
} from "../../state";
-import * as RS from "reactstrap";
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,
@@ -30,33 +22,33 @@ import {
useQueryParams,
arrayFromPossibleCsv,
toSimpleCSV,
- itemiseByValue,
TAG_ID,
itemiseTag,
- isLoggedIn,
- SEARCH_RESULTS_PER_PAGE
+ SEARCH_RESULTS_PER_PAGE,
} 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";
-
-const selectStyle = {
- className: "basic-multi-select", classNamePrefix: "select",
- menuPortalTarget: document.body, styles: {menuPortal: (base: object) => ({...base, zIndex: 9999})}
-};
+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";
+
+export interface QuestionStatus {
+ notAttempted: boolean;
+ complete: boolean;
+ incorrect: boolean;
+ llmMarked: boolean;
+ hideCompleted: boolean; // TODO: remove when implementing desired filters
+}
function processTagHierarchy(subjects: string[], fields: string[], topics: string[]): Item[][] {
const tagHierarchy = tags.getTagHierarchy();
@@ -83,48 +75,55 @@ 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 [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,
+ incorrect: false,
+ llmMarked: false,
+ hideCompleted: !!params.hideCompleted
+ }
);
+ const [searchBooks, setSearchBooks] = useState(arrayFromPossibleCsv(params.book));
+ const [excludeBooks, setExcludeBooks] = useState(!!params.excludeBooks);
- useEffect(function populateExamBoardFromUserContext() {
- if (!EXAM_BOARD_NULL_OPTIONS.includes(userContext.examBoard)) setSearchExamBoards([userContext.examBoard]);
- }, [userContext.examBoard]);
+ const [populatedUserContext, setPopulatedUserContext] = useState(false);
- useEffect(function populateStageFromUserContext() {
- if (!STAGE_NULL_OPTIONS.includes(userContext.stage)) setSearchStages([userContext.stage]);
- }, [userContext.stage]);
+ useEffect(function populateFromUserContext() {
+ if (!STAGE_NULL_OPTIONS.includes(userContext.stage)) {
+ setSearchStages(arr => arr.length > 0 ? arr : [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 *and* React has processed the above setStates
+ useEffect(() => {
+ searchAndUpdateURL();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [populatedUserContext]);
- 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);
- 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 [hideCompleted, setHideCompleted] = useState(!!params.hideCompleted);
-
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));
}
@@ -133,27 +132,24 @@ 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);
-
- 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"}
- ];
+ ].map(tier => ({...tier, for: "for_" + tier.id})).slice(0, tierIndex + 1);
+
+ 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);
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) => {
- 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, hideCompleted: boolean, startIndex: number) => {
+ if (nothingToSearchFor) {
dispatch(clearQuestionSearch);
return;
}
@@ -181,46 +177,32 @@ 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,
- fasttrack,
+ questionCategories: isPhy
+ ? (excludeBooks ? "problem_solving" : "problem_solving,book")
+ : undefined,
+ 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),
- []
+ [nothingToSearchFor]
);
- 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) {
- setSearchExamBoards(
- getFilteredExamBoardOptions({byStages: searchStages})
- .filter(o => searchExamBoards.includes(o.value))
- .map(o => o.value)
- );
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [searchStages]);
-
- useEffect(() => {
+ const searchAndUpdateURL = useCallback(() => {
setPageCount(1);
setDisableLoadMore(false);
setDisplayQuestions(undefined);
- searchDebounce(searchQuery, searchTopics, searchExamBoards, searchBook, searchStages, searchDifficulties, selections, tiers, searchFastTrack, hideCompleted, 0);
+ searchDebounce(
+ searchQuery, searchTopics, searchExamBoards, searchBooks, searchStages,
+ searchDifficulties, selections, tiers, excludeBooks, searchStatuses.hideCompleted, 0
+ );
const params: {[key: string]: string} = {};
if (searchStages.length) params.stages = toSimpleCSV(searchStages);
@@ -228,9 +210,11 @@ 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 && searchFastTrack) params.fasttrack = "set";
- if (hideCompleted) params.hideCompleted = "set";
+ if (isPhy && !excludeBooks && searchBooks.length) {
+ params.book = toSimpleCSV(searchBooks);
+ }
+ if (isPhy && excludeBooks) params.excludeBooks = "set";
+ if (searchStatuses.hideCompleted) params.hideCompleted = "set";
if (isPhy) {
tiers.forEach((tier, i) => {
@@ -242,8 +226,28 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => {
}
history.replace({search: queryString.stringify(params, {encode: false}), state: location.state});
+ }, [excludeBooks, history, location.state, searchStatuses.hideCompleted, searchBooks, searchDebounce, searchDifficulties, searchExamBoards, searchQuery, searchStages, searchTopics, selections, tiers]);
+
+ const [filtersApplied, setFiltersApplied] = useState(false);
+ const applyFilters = () => {
+ setFiltersApplied(true);
+ searchAndUpdateURL();
+ };
+
+ // 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
- },[searchDebounce, searchQuery, searchTopics, searchExamBoards, searchBook, searchFastTrack, searchStages, searchDifficulties, selections, hideCompleted]);
+ 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) {
@@ -257,7 +261,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => {
}
}, [questions]);
- const [displayQuestions, setDisplayQuestions] = useState(undefined);
+ const [displayQuestions, setDisplayQuestions] = useState([]);
const [pageCount, setPageCount] = useState(1);
useEffect(() => {
@@ -269,23 +273,35 @@ 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);
-
- useEffect(() => {
- if (revisionMode) {
- setHideCompleted(false);
- }
- }, [revisionMode]);
-
- 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 filtersSelected = useMemo(() => {
+ setFiltersApplied(false);
+ return searchDifficulties.length > 0
+ || searchTopics.length > 0
+ || searchExamBoards.length > 0
+ || searchStages.length > 0
+ || searchBooks.length > 0
+ || excludeBooks
+ || selections.some(tier => tier.length > 0)
+ || Object.entries(searchStatuses).some(e => e[1]);
+ }, [searchDifficulties, searchTopics, searchExamBoards, searchStages, searchBooks, excludeBooks, selections, searchStatuses]);
+
+ const clearFilters = useCallback(() => {
+ setSearchDifficulties([]);
+ setSearchTopics([]);
+ setSearchExamBoards([]);
+ setSearchStages([]);
+ setSearchBooks([]);
+ setExcludeBooks(false);
+ setSelections([[], [], []]);
+ setSearchStatuses(
+ {
+ notAttempted: false,
+ complete: false,
+ incorrect: false,
+ llmMarked: false,
+ hideCompleted: false
+ });
+ }, []);
// eslint-disable-next-line react-hooks/exhaustive-deps
const handleSearch = useCallback(
@@ -311,163 +327,88 @@ 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
- searchExamBoards.includes(o.value))}
- options={getFilteredExamBoardOptions({byStages: searchStages})}
- onChange={(s: MultiValue- >) => selectOnChange(setSearchExamBoards, true)(s)}
+
+
+
+ Search for a question
+
+ handleSearch(e.target.value)}
/>
-
}
-
-
-
- Topic
- {siteSpecific(
- ,
- tag.options))}
- options={groupBaseTagOptions} onChange={(x : readonly Item[], {action: _action}) => {
- selectOnChange(setSearchTopics, true)(x);
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Showing {displayQuestions?.length ?? 0} of {totalQuestions} .
+
+
+
+
+ {displayQuestions?.length
+ ?
+ : (!filtersApplied && searchQuery === ""
+ ? Please select and apply filters
+ : No results match your criteria )
+ }
+
+
+
+ {(filtersApplied || searchQuery !== "") &&
+ (displayQuestions?.length ?? 0) > 0 &&
+
+
+ {
+ searchDebounce(
+ searchQuery, searchTopics,
+ searchExamBoards,
+ searchBooks, searchStages,
+ searchDifficulties,
+ selections, tiers,
+ excludeBooks,
+ searchStatuses.hideCompleted,
+ nextSearchOffset
+ ? nextSearchOffset - 1
+ : 0);
+ setPageCount(c => c + 1);
+ setDisableLoadMore(true);
}}
- />
- )}
-
-
-
- {isPhy &&
- Book
- {
- selectOnChange(setSearchBook, true)(e);
- }}
- options={bookOptions}
- />
- }
-
-
- {isPhy && isStaff(user) &&
-
- setSearchFastTrack(e.target.checked)} />{' '}Show FastTrack questions
-
- }
-
-
-
- Search
- 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) ?
- Please select filters :
- (displayQuestions?.length ?
- <>
- ({...q, correct: revisionMode ? undefined : q.correct}) as ContentSummaryDTO)}/>
-
-
- {
- searchDebounce(searchQuery, searchTopics, searchExamBoards, searchBook, searchStages, searchDifficulties, selections, tiers, searchFastTrack, hideCompleted, nextSearchOffset ? nextSearchOffset - 1 : 0);
- setPageCount(c => c + 1);
- setDisableLoadMore(true);
- }}
- disabled={disableLoadMore}
- >
- Load more
-
-
-
- {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 )
- }
-
-
-
- ;
+ disabled={disableLoadMore}
+ outline={isAda}
+ >
+ Load more
+
+
+ }
+
+
+ ;
});
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]}
));
diff --git a/src/scss/common/_utils.scss b/src/scss/common/_utils.scss
index 0b192ca899..171590fb96 100644
--- a/src/scss/common/_utils.scss
+++ b/src/scss/common/_utils.scss
@@ -29,6 +29,14 @@
overflow-x: auto !important;
}
+.h-min-content {
+ height: min-content !important;
+}
+
+.h-max-content {
+ height: max-content !important;
+}
+
.w-min-content {
width: min-content !important;
}
@@ -39,4 +47,4 @@
.w-max-content {
width: max-content !important;
-}
\ No newline at end of file
+}
diff --git a/src/scss/common/checkbox.scss b/src/scss/common/checkbox.scss
index c6b8f4aad5..921b64dd8b 100644
--- a/src/scss/common/checkbox.scss
+++ b/src/scss/common/checkbox.scss
@@ -1,35 +1,46 @@
.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;
width: 100%;
height: 100%;
- border: 0.15em solid #000;
+ border: 0.1em solid #000;
border-radius: 0.25em;
margin: 0;
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;
}
+
+ &[color="primary"] {
+ background: $primary;
+ border-color: $primary;
+
+ &:hover, &:disabled {
+ background: darken($primary, 10%);
+ border-color: darken($primary, 10%);
+ }
+ }
}
-
+
&:not(.checked):hover, &:not(.checked):disabled {
background: #f8f8f8;
border-color: #666;
@@ -39,7 +50,7 @@
box-shadow: 0 0 0 0.1em #000;
}
}
-
+
.tick {
position: absolute;
display: inline-block;
@@ -51,7 +62,7 @@
-ms-transform: rotate(45deg);
pointer-events: none;
z-index: 1;
-
+
&::before {
content: "";
position: absolute;
@@ -61,7 +72,7 @@
left: 11px;
top: 2px;
}
-
+
&::after{
content: "";
position: absolute;
@@ -73,10 +84,17 @@
}
}
}
-
+
> label {
width: fit-content;
margin: 0;
line-height: normal;
+
+ &.hover-override + div input[type="checkbox"]:not(:checked) {
+ &:hover {
+ background: #f8f8f8;
+ border-color: #666;
+ }
+ }
}
}
diff --git a/src/scss/common/elements.scss b/src/scss/common/elements.scss
index ecbf135ba5..29003f960f 100644
--- a/src/scss/common/elements.scss
+++ b/src/scss/common/elements.scss
@@ -294,3 +294,22 @@ 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;
+ }
+}
+
+.collapsible-body {
+ 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;
+}
diff --git a/src/scss/common/filter.scss b/src/scss/common/filter.scss
index 08b4941dac..da31eb5dee 100644
--- a/src/scss/common/filter.scss
+++ b/src/scss/common/filter.scss
@@ -1,4 +1,8 @@
-.hexagon-tier-title, .hexagon-level-title, .hexagon-tier-summary, .difficulty-title {
+.hexagon-tier-title,
+.hexagon-level-title,
+.hexagon-tier-summary,
+.difficulty-title,
+.filter-count-title {
pointer-events: none;
height: 100%;
display: flex;
@@ -35,7 +39,7 @@
font-weight: 100;
}
-.difficulty-title {
+.difficulty-title, .filter-count-title {
font-weight: 400;
font-size: 0.92rem;
&.active {color: white;}
@@ -44,4 +48,3 @@
font-size: 0.8rem;
}
}
-
diff --git a/src/scss/common/finder.scss b/src/scss/common/finder.scss
index e0d22e51dc..e84727df60 100644
--- a/src/scss/common/finder.scss
+++ b/src/scss/common/finder.scss
@@ -1,8 +1,43 @@
#finder-page {
.finder-header {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ }
+
+ .finder-search {
+ $search-button-offset: 50px;
+ .question-search-button {
+ background: white url(/assets/cs/icons/search-jet.svg) no-repeat center;
+ background-size: 20px 20px;
+ border: 1px solid black;
+ border-left: none;
+ 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;
+ }
+ }
+
+ &::before {
+ content: "";
+ display: block;
+ position: relative;
+ float: right;
+ width: 1px;
+ height: 34px;
+ top: 48px;
+ right: $search-button-offset;
+ background: #c2c2c2;
+ z-index: 4;
+ }
+
+ input#question-search-title {
+ padding-right: calc($search-button-offset + 1.2em) !important; // offset plus the default padding inside an input
+ height: 50px;
+ }
}
.search-item-icon {
@@ -24,5 +59,17 @@
margin-right: 0.5rem;
}
}
+
+ .filter-btn {
+ position: -webkit-sticky;
+ position: sticky;
+ bottom: 0;
+ }
+
+ .filter-separator {
+ overflow: hidden;
+ border-top: solid #c2c2c2 1px;
+ border-bottom: none;
+ }
}
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);
}
}
diff --git a/src/scss/cs/boards.scss b/src/scss/cs/boards.scss
index c06d0f603f..32e2e5676a 100644
--- a/src/scss/cs/boards.scss
+++ b/src/scss/cs/boards.scss
@@ -43,19 +43,17 @@
}
.question-progress-icon {
- 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;
+ max-width: 2rem;
max-height: 2rem;
}
.icon-title {
diff --git a/src/scss/cs/filter.scss b/src/scss/cs/filter.scss
index 308013772e..c12e617029 100644
--- a/src/scss/cs/filter.scss
+++ b/src/scss/cs/filter.scss
@@ -35,7 +35,7 @@
}
}
-$shapes: hex, square, octagon, diamond;
+$shapes: hex, square, octagon, diamond, circle;
svg {
@each $shape in $shapes {
@@ -71,6 +71,11 @@ svg {
stroke-width: 1px;
}
+ &.filter-count {
+ fill: $pink-300;
+ opacity: .05;
+ }
+
&:focus {
outline: none;
stroke: black !important;
diff --git a/src/scss/cs/finder.scss b/src/scss/cs/finder.scss
new file mode 100644
index 0000000000..2134d4936b
--- /dev/null
+++ b/src/scss/cs/finder.scss
@@ -0,0 +1,14 @@
+@import "../common/finder";
+
+@media (min-width: 992px) {
+ .finder-panel::before {
+ position: absolute;
+ content: "";
+ width: 500px;
+ height: 500px;
+ top: 430px;
+ left: 61%;
+ background-size: cover;
+ background-image: url("/assets/cs/decor/dots-circle-bg.svg");
+ }
+}
diff --git a/src/scss/cs/isaac.scss b/src/scss/cs/isaac.scss
index 6e04c0fb6f..84d3152e31 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";
@@ -383,7 +385,7 @@ $spacers: map-merge($spacers, ( // Special Bootstrap variable that is used to ge
@import "../common/glossary";
@import "quiz";
@import "../common/callout";
-@import "../common/finder";
+@import "finder";
@import "topics";
@import "expansion-layout";
diff --git a/src/scss/phy/filter.scss b/src/scss/phy/filter.scss
index 6da8ce338f..dd77309a57 100644
--- a/src/scss/phy/filter.scss
+++ b/src/scss/phy/filter.scss
@@ -44,7 +44,7 @@
}
}
-$shapes: hex, square, octagon, diamond;
+$shapes: hex, square, octagon, diamond, circle;
svg {
@each $shape in $shapes {
@@ -107,6 +107,12 @@ svg {
stroke: $phy_extra_force_yellow;
}
+ &.filter-count {
+ fill: $phy_green;
+ opacity: .1;
+ stroke: none;
+ }
+
&:focus {
outline: none;
stroke: black !important;
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..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";
@@ -210,7 +212,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";