From 4927b24a509e3a6773bbec1b68856614828b1f32 Mon Sep 17 00:00:00 2001 From: Matthew Trew Date: Fri, 26 Jul 2024 14:53:27 +0200 Subject: [PATCH 1/2] Add filtering options to ExamSpecifications This allows the component to be re-used when we create separate pages for different countries. --- .../components/pages/ExamSpecifications.tsx | 59 ++++++++++++------- src/test/ExamSpecifications.test.ts | 38 ++++++++++++ 2 files changed, 77 insertions(+), 20 deletions(-) create mode 100644 src/test/ExamSpecifications.test.ts diff --git a/src/app/components/pages/ExamSpecifications.tsx b/src/app/components/pages/ExamSpecifications.tsx index ed7a187c7a..fb7a7f3e00 100644 --- a/src/app/components/pages/ExamSpecifications.tsx +++ b/src/app/components/pages/ExamSpecifications.tsx @@ -5,30 +5,49 @@ import {PageFragment} from "../elements/PageFragment"; import {CS_EXAM_BOARDS_BY_STAGE, EXAM_BOARD, STAGE, STAGES_CS, stageLabelMap} from "../../services"; import {TitleAndBreadcrumb} from "../elements/TitleAndBreadcrumb"; import {MetaDescription} from "../elements/MetaDescription"; +import {ExamBoard} from "../../../IsaacApiTypes"; -const getStageFromURL = () => { +interface ExamSpecificationsProps { + // Show only tabs for the following stages + stageFilter?: STAGE[] + // Show only tabs for the following exam boards + examBoardFilter?: ExamBoard[] +} + +const getStageFromURL = (stageFilter: STAGE[]) => { const urlStage = window.location.hash.split("/")[0].slice(1); - return STAGES_CS.includes(urlStage as typeof STAGES_CS[number]) - ? urlStage as typeof STAGES_CS[number] - : STAGE.A_LEVEL; + return stageFilter.includes(urlStage as typeof STAGES_CS[number]) + ? urlStage as typeof STAGES_CS[number] + : stageFilter[0]; }; -const getExamBoardFromURL = () => { +const getExamBoardFromURL = (examBoardFilter: ExamBoard[]) => { const urlExamBoard = window.location.hash.split("/")[1]; - return Object.values(EXAM_BOARD).includes(urlExamBoard as EXAM_BOARD) + return examBoardFilter.includes(urlExamBoard as EXAM_BOARD) ? urlExamBoard as EXAM_BOARD - : EXAM_BOARD.AQA; + : examBoardFilter[0]; +}; + +export const getFilteredExamBoardsByStage = (stages: STAGE[], examBoards: ExamBoard[]) => { + return Object.fromEntries( + Object.entries(CS_EXAM_BOARDS_BY_STAGE).filter(([stage, _board]) => { + return stages.includes(stage as STAGE); + }).map(([stage, boards]) => [stage, boards.filter(board => examBoards.includes(board))]) + ); }; -export const ExamSpecifications = () => { - const STAGES_WITH_EXAM_SPECIFICATIONS = [STAGE.A_LEVEL, STAGE.GCSE]; - const [stageTab, setStageTab] = useState(getStageFromURL()); - const [examBoardTab, setExamBoardTab] = useState(getExamBoardFromURL()); - const [stageTabOverride, _setStageTabOverride] = useState(STAGES_WITH_EXAM_SPECIFICATIONS.indexOf(stageTab) + 1 || undefined); - const [examBoardTabOverride, setExamBoardTabOverride] = useState(CS_EXAM_BOARDS_BY_STAGE[stageTab].indexOf(examBoardTab) + 1 || undefined); +export const ExamSpecifications = ({stageFilter, examBoardFilter}: ExamSpecificationsProps) => { + const STAGES: STAGE[] = stageFilter ?? [STAGE.A_LEVEL, STAGE.GCSE]; + const EXAM_BOARDS: ExamBoard[] = examBoardFilter ?? [EXAM_BOARD.AQA, EXAM_BOARD.CIE, EXAM_BOARD.OCR, EXAM_BOARD.EDUQAS]; + const FILTERED_EXAM_BOARDS_BY_STAGE = getFilteredExamBoardsByStage(STAGES, EXAM_BOARDS); + + const [stageTab, setStageTab] = useState(getStageFromURL(STAGES) as typeof STAGES_CS[number]); + const [examBoardTab, setExamBoardTab] = useState(getExamBoardFromURL(EXAM_BOARDS)); + const [stageTabOverride, _setStageTabOverride] = useState(Object.keys(FILTERED_EXAM_BOARDS_BY_STAGE).indexOf(stageTab) + 1 || undefined); + const [examBoardTabOverride, setExamBoardTabOverride] = useState(FILTERED_EXAM_BOARDS_BY_STAGE[stageTab].indexOf(examBoardTab) + 1 || undefined); const [fragmentId, setFragmentId] = useState(""); - const stageExamBoards = CS_EXAM_BOARDS_BY_STAGE[stageTab]; + const stageExamBoards = FILTERED_EXAM_BOARDS_BY_STAGE[stageTab]; const metaDescription = ({ [STAGE.A_LEVEL]: "Discover our free A level computer science topics and questions. We cover AQA, CIE, OCR, Eduqas, and WJEC. Learn or revise for your exams with us today.", @@ -40,13 +59,13 @@ export const ExamSpecifications = () => { [STAGE.ADVANCED]: "Discover our free Advanced computer science topics and questions. Learn or revise for your exams with us today.", })[stageTab]; - const examBoardTabs = STAGES_WITH_EXAM_SPECIFICATIONS.reduce((acc: {[stage: string]: React.JSX.Element}, stage) => ({ + const examBoardTabs = Object.keys(FILTERED_EXAM_BOARDS_BY_STAGE).reduce((acc: {[stage: string]: React.JSX.Element}, stage) => ({ ...acc, - [stageLabelMap[stage]]: { - setExamBoardTab(CS_EXAM_BOARDS_BY_STAGE[stageTab][n - 1] as EXAM_BOARD); + setExamBoardTab(FILTERED_EXAM_BOARDS_BY_STAGE[stageTab][n - 1] as EXAM_BOARD); setExamBoardTabOverride(undefined); }} > @@ -58,11 +77,11 @@ export const ExamSpecifications = () => { const stageTabs = { - const newStage = STAGES_WITH_EXAM_SPECIFICATIONS[n - 1] as typeof STAGES_CS[number]; - const newExamBoard = CS_EXAM_BOARDS_BY_STAGE[newStage].includes(examBoardTab) ? examBoardTab : CS_EXAM_BOARDS_BY_STAGE[newStage][0]; + const newStage = Object.keys(FILTERED_EXAM_BOARDS_BY_STAGE)[n - 1] as typeof STAGES_CS[number]; + const newExamBoard = FILTERED_EXAM_BOARDS_BY_STAGE[newStage].includes(examBoardTab) ? examBoardTab : FILTERED_EXAM_BOARDS_BY_STAGE[newStage][0]; setStageTab(newStage); setExamBoardTab(newExamBoard as EXAM_BOARD); - setExamBoardTabOverride(CS_EXAM_BOARDS_BY_STAGE[newStage].indexOf(newExamBoard) + 1); + setExamBoardTabOverride(FILTERED_EXAM_BOARDS_BY_STAGE[newStage].indexOf(newExamBoard) + 1); }} > {examBoardTabs} diff --git a/src/test/ExamSpecifications.test.ts b/src/test/ExamSpecifications.test.ts new file mode 100644 index 0000000000..766902b180 --- /dev/null +++ b/src/test/ExamSpecifications.test.ts @@ -0,0 +1,38 @@ +import {getFilteredExamBoardsByStage} from "../app/components/pages/ExamSpecifications"; +import {EXAM_BOARD, STAGE} from "../app/services"; + + +describe("ExamSpecifications", () => { + it('should filter the available stage and exam board when a single stage/board pair is provided', async () => { + // Arrange + const stageFilter = [STAGE.GCSE]; + const examBoardFilter = [EXAM_BOARD.WJEC]; + + const expected = { + "gcse": [EXAM_BOARD.WJEC] + }; + + // Act + const actual = getFilteredExamBoardsByStage(stageFilter, examBoardFilter); + + // Assert + expect(actual).toEqual(expected); + }); + + it('should filter the available stage and exam board when multiple stage/board pairs are provided', async () => { + // Arrange + const stageFilter = [STAGE.GCSE, STAGE.A_LEVEL]; + const examBoardFilter = [EXAM_BOARD.WJEC, EXAM_BOARD.AQA]; + + const expected = { + "gcse": [EXAM_BOARD.AQA, EXAM_BOARD.WJEC], + "a_level": [EXAM_BOARD.AQA, EXAM_BOARD.WJEC] + }; + + // Act + const actual = getFilteredExamBoardsByStage(stageFilter, examBoardFilter); + + // Assert + expect(actual).toEqual(expected); + }); +}); \ No newline at end of file From 9f3560fba7323e39e3736027ecb8ecf658e39141 Mon Sep 17 00:00:00 2001 From: Matthew Trew Date: Fri, 26 Jul 2024 16:08:53 +0200 Subject: [PATCH 2/2] Add new exam spec directory and Wales pages, plus routes --- .../pages/ExamSpecificationsDirectory.tsx | 93 +++++++++++++++++++ src/app/components/site/cs/RoutesCS.tsx | 7 +- 2 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 src/app/components/pages/ExamSpecificationsDirectory.tsx diff --git a/src/app/components/pages/ExamSpecificationsDirectory.tsx b/src/app/components/pages/ExamSpecificationsDirectory.tsx new file mode 100644 index 0000000000..9bf82f197a --- /dev/null +++ b/src/app/components/pages/ExamSpecificationsDirectory.tsx @@ -0,0 +1,93 @@ +import React from "react"; +import {Button, Card, CardBody, CardFooter, CardText, CardTitle, Col, Container, Row} from "reactstrap"; +import {TitleAndBreadcrumb} from "../elements/TitleAndBreadcrumb"; +import {MetaDescription} from "../elements/MetaDescription"; +import {Link} from "react-router-dom"; + + +export const ExamSpecificationsDirectory = () => { + return + + + + + + + +

Ada CS

+
+ +
    +
  • Core
  • +
  • Advanced
  • +
+
+
+ + + +
+ + + + + +

England

+
+ +
    +
  • GCSE
  • +
  • A Level
  • +
+
+
+ + + +
+ + + + + +

Scotland

+
+ +
    +
  • National 5
  • +
  • Higher
  • +
  • Advanced Higher
  • +
+
+
+ + + +
+ + + + + +

Wales

+
+ +
    +
  • GCSE
  • +
  • A Level
  • +
+
+
+ + + +
+ +
+
; +}; \ No newline at end of file diff --git a/src/app/components/site/cs/RoutesCS.tsx b/src/app/components/site/cs/RoutesCS.tsx index 6171cef2a6..c052292834 100644 --- a/src/app/components/site/cs/RoutesCS.tsx +++ b/src/app/components/site/cs/RoutesCS.tsx @@ -4,7 +4,7 @@ import {AllTopics} from "../../pages/AllTopics"; import StaticPageRoute from "../../navigation/StaticPageRoute"; import {Topic} from "../../pages/Topic"; import {Redirect} from "react-router"; -import {isLoggedIn, isStaff, isTeacherOrAbove, isTutorOrAbove} from "../../../services"; +import {EXAM_BOARD, isLoggedIn, isStaff, isTeacherOrAbove, isTutorOrAbove} from "../../../services"; import {SingleAssignmentProgress} from "../../pages/SingleAssignmentProgress"; import {ExamSpecifications} from "../../pages/ExamSpecifications"; import {News} from "../../pages/News"; @@ -30,6 +30,7 @@ import EventDetails from "../../pages/EventDetails"; import {RedirectToEvent} from "../../navigation/RedirectToEvent"; import { QuestionFinder } from "../../pages/QuestionFinder"; import { OnlineCourses } from "../../pages/OnlineCourses"; +import {ExamSpecificationsDirectory} from "../../pages/ExamSpecificationsDirectory"; const Equality = lazy(() => import('../../pages/Equality')); @@ -85,7 +86,9 @@ export const RoutesCS = [ // Topics and content , , - , + , + , + , // News ,