Skip to content

Exam specification directory page #1024

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 39 additions & 20 deletions src/app/components/pages/ExamSpecifications.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof STAGES_CS[number]>(getStageFromURL());
const [examBoardTab, setExamBoardTab] = useState<EXAM_BOARD>(getExamBoardFromURL());
const [stageTabOverride, _setStageTabOverride] = useState<number | undefined>(STAGES_WITH_EXAM_SPECIFICATIONS.indexOf(stageTab) + 1 || undefined);
const [examBoardTabOverride, setExamBoardTabOverride] = useState<number | undefined>(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<typeof STAGES_CS[number]>(getStageFromURL(STAGES) as typeof STAGES_CS[number]);
const [examBoardTab, setExamBoardTab] = useState<ExamBoard>(getExamBoardFromURL(EXAM_BOARDS));
const [stageTabOverride, _setStageTabOverride] = useState<number | undefined>(Object.keys(FILTERED_EXAM_BOARDS_BY_STAGE).indexOf(stageTab) + 1 || undefined);
const [examBoardTabOverride, setExamBoardTabOverride] = useState<number | undefined>(FILTERED_EXAM_BOARDS_BY_STAGE[stageTab].indexOf(examBoardTab) + 1 || undefined);
const [fragmentId, setFragmentId] = useState<string>("");

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.",
Expand All @@ -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]]: <Tabs
[stageLabelMap[stage as STAGE]]: <Tabs
style="tabs" className="pt-3" tabContentClass="pt-3"
activeTabOverride={examBoardTabOverride}
onActiveTabChange={(n) => {
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);
}}
>
Expand All @@ -58,11 +77,11 @@ export const ExamSpecifications = () => {
const stageTabs = <Tabs style={"buttons"} className={"mt-3"} tabContentClass={"mt-3"}
activeTabOverride={stageTabOverride}
onActiveTabChange={(n) => {
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}
Expand Down
93 changes: 93 additions & 0 deletions src/app/components/pages/ExamSpecificationsDirectory.tsx
Original file line number Diff line number Diff line change
@@ -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 <Container className="mb-5">
<TitleAndBreadcrumb currentPageTitle={"Exam specifications"}/>
<MetaDescription description="Discover our free A level computer science topics and questions."/>
<Row className={"justify-content-center d-flex flex-row card-deck row-cols-1 row-cols-md-2 row-cols-xl-4 my-3"}>
<Col className={"my-3"}>
<Card className={"cs-card w-100"}>
<CardBody>
<CardTitle>
<h3>Ada CS</h3>
</CardTitle>
<CardText>
<ul>
<li>Core</li>
<li>Advanced</li>
</ul>
</CardText>
</CardBody>
<CardFooter className={"border-top-0 pb-3"}>
<Button className="justify-content-end" color='secondary' disabled outline tag={Link} to="/">
Coming soon
</Button>
</CardFooter>
</Card>
</Col>
<Col className={"my-3"}>
<Card className={"cs-card w-100"}>
<CardBody>
<CardTitle>
<h3>England</h3>
</CardTitle>
<CardText>
<ul>
<li>GCSE</li>
<li>A Level</li>
</ul>
</CardText>
</CardBody>
<CardFooter className={"border-top-0 pb-3"}>
<Button className="justify-content-end" color='secondary' outline tag={Link}
to="/exam_specifications_england">Show me</Button>
</CardFooter>
</Card>
</Col>
<Col className={"my-3"}>
<Card className={"cs-card w-100"}>
<CardBody>
<CardTitle>
<h3>Scotland</h3>
</CardTitle>
<CardText>
<ul>
<li>National 5</li>
<li>Higher</li>
<li>Advanced Higher</li>
</ul>
</CardText>
</CardBody>
<CardFooter className={"border-top-0 pb-3"}>
<Button color='secondary' outline tag={Link} to="/concepts/sqa_computing_science">
Show me
</Button>
</CardFooter>
</Card>
</Col>
<Col className={"my-3"}>
<Card className={"cs-card w-100"}>
<CardBody>
<CardTitle>
<h3>Wales</h3>
</CardTitle>
<CardText>
<ul>
<li>GCSE</li>
<li>A Level</li>
</ul>
</CardText>
</CardBody>
<CardFooter className={"border-top-0 pb-3"}>
<Button color='secondary' outline tag={Link} to="/exam_specifications_wales">Show me</Button>
</CardFooter>
</Card>
</Col>
</Row>
</Container>;
};
7 changes: 5 additions & 2 deletions src/app/components/site/cs/RoutesCS.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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'));

Expand Down Expand Up @@ -85,7 +86,9 @@ export const RoutesCS = [
// Topics and content
<TrackedRoute key={key++} exact path="/topics" component={AllTopics} />,
<TrackedRoute key={key++} exact path="/topics/:topicName" component={Topic} />,
<TrackedRoute key={key++} exact path="/exam_specifications" component={ExamSpecifications} />,
<TrackedRoute key={key++} exact path="/exam_specifications_england" component={ExamSpecifications} />,
<TrackedRoute key={key++} exact path="/exam_specifications_wales" component={ExamSpecifications} componentProps={{'examBoardFilter': [EXAM_BOARD.WJEC]}} />,
Comment on lines +89 to +90
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is possible to add a <Redirect key={key++} from="/exam_specifications_scotland" to="/concepts/sqa_computing_science" /> if you don't want to feel left out!

<TrackedRoute key={key++} exact path="/exam_specifications" component={ExamSpecificationsDirectory} />,

// News
<TrackedRoute key={key++} exact path="/news" component={News} />,
Expand Down
38 changes: 38 additions & 0 deletions src/test/ExamSpecifications.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});