Skip to content

Commit 9e39c0e

Browse files
authored
Merge pull request #1273 from isaacphysics/redesign/subject-specific-pages
Subject-specific pages
2 parents 4896cfe + 709513b commit 9e39c0e

15 files changed

+218
-81
lines changed
Lines changed: 4 additions & 0 deletions
Loading
Lines changed: 5 additions & 0 deletions
Loading

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { selectors, useAppSelector } from "../../../state";
99
import { Link } from "react-router-dom";
1010
import { Tag } from "../../../../IsaacAppTypes";
1111
import { AffixButton } from "../AffixButton";
12-
import { getHumanContext } from "../../../services/context";
12+
import { getHumanContext } from "../../../services/pageContext";
1313

1414
export const SidebarLayout = (props: RowProps) => {
1515
const { className, ...rest } = props;

src/app/components/pages/Concepts.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@ import {matchesAllWordsInAnyOrder, pushConceptsToHistory, searchResultIsPublic,
99
import {TitleAndBreadcrumb} from "../elements/TitleAndBreadcrumb";
1010
import {ShortcutResponse, Tag} from "../../../IsaacAppTypes";
1111
import {IsaacSpinner} from "../handlers/IsaacSpinner";
12-
import { SubjectSpecificConceptListSidebar, MainContent, SidebarLayout } from "../elements/layout/SidebarLayout";
12+
import { SubjectSpecificConceptListSidebar, MainContent, SidebarLayout, GenericConceptsSidebar } from "../elements/layout/SidebarLayout";
13+
import { useUrlPageTheme } from "../../services/pageContext";
1314

1415
// This component is Isaac Physics only (currently)
1516
export const Concepts = withRouter((props: RouteComponentProps) => {
1617
const {location, history} = props;
1718
const dispatch = useAppDispatch();
1819
const user = useAppSelector(selectors.user.orNull);
1920
const concepts = useAppSelector((state: AppState) => state?.concepts?.results || null);
21+
const pageContext = useUrlPageTheme();
2022

2123
const subject = useAppSelector(selectors.pageContext.subject);
2224

@@ -83,17 +85,17 @@ export const Concepts = withRouter((props: RouteComponentProps) => {
8385
const shortcutAndFilteredSearchResults = (shortcutResponse || []).concat(filteredSearchResults || []);
8486

8587
return (
86-
<Container id="search-page">
88+
<Container id="search-page" { ...(pageContext?.subject && { "data-bs-theme" : pageContext.subject })}>
8789
<TitleAndBreadcrumb
8890
currentPageTitle="Concepts"
8991
icon={{type: "hex", icon: "page-icon-concept"}}
9092
/>
9193
<SidebarLayout>
92-
<SubjectSpecificConceptListSidebar
94+
{pageContext?.subject ? <SubjectSpecificConceptListSidebar
9395
searchText={searchText} setSearchText={setSearchText}
9496
conceptFilters={conceptFilters} setConceptFilters={setConceptFilters}
9597
applicableTags={applicableTags} tagCounts={tagCounts}
96-
/>
98+
/> : <GenericConceptsSidebar />}
9799
<MainContent>
98100
<Card>
99101
<CardHeader className="search-header">
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import React from "react";
2+
import { RouteComponentProps, withRouter } from "react-router";
3+
import { Container } from "reactstrap";
4+
import { TitleAndBreadcrumb } from "../elements/TitleAndBreadcrumb";
5+
import { getHumanContext, useUrlPageTheme } from "../../services/pageContext";
6+
7+
export const LessonsAndRevision = withRouter((props: RouteComponentProps) => {
8+
const pageContext = useUrlPageTheme();
9+
10+
return <Container data-bs-theme={pageContext?.subject}>
11+
<TitleAndBreadcrumb
12+
currentPageTitle="Lessons and revision"
13+
icon={pageContext?.subject ? {
14+
type: "hex",
15+
subject: pageContext.subject,
16+
icon: "page-icon-lessons"
17+
} : undefined}
18+
/>
19+
<div className="mt-5">This is a lessons and revision page for {getHumanContext(pageContext)}!</div>
20+
</Container>;
21+
});

src/app/components/pages/QuestionFinder.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, {ReactNode, useCallback, useEffect, useMemo, useState} from "react";
2-
import {AppState, clearQuestionSearch, searchQuestions, selectors, useAppDispatch, useAppSelector} from "../../state";
2+
import {AppState, clearQuestionSearch, searchQuestions, useAppDispatch, useAppSelector} from "../../state";
33
import debounce from "lodash/debounce";
44
import {
55
arrayFromPossibleCsv,
@@ -21,6 +21,7 @@ import {
2121
tags,
2222
toSimpleCSV,
2323
useQueryParams,
24+
useUrlPageTheme,
2425
} from "../../services";
2526
import {ContentSummaryDTO, Difficulty, ExamBoard} from "../../../IsaacApiTypes";
2627
import {IsaacSpinner} from "../handlers/IsaacSpinner";
@@ -105,7 +106,7 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => {
105106
const user = useAppSelector((state: AppState) => state && state.user);
106107
const params = useQueryParams<FilterParams, false>(false);
107108
const history = useHistory();
108-
const pageContext = useAppSelector(selectors.pageContext.context);
109+
const pageContext = useUrlPageTheme();
109110

110111
const [searchTopics, setSearchTopics] = useState<string[]>(arrayFromPossibleCsv(params.topics));
111112
const [searchQuery, setSearchQuery] = useState<string>(params.query ? (params.query instanceof Array ? params.query[0] : params.query) : "");
@@ -391,12 +392,11 @@ export const QuestionFinder = withRouter(({location}: RouteComponentProps) => {
391392
<IsaacSpinner />
392393
</div>;
393394

394-
return <Container id="finder-page" className={classNames("mb-5")}>
395+
return <Container id="finder-page" className={classNames("mb-5")} { ...(pageContext?.subject && { "data-bs-theme" : pageContext.subject })}>
395396
<TitleAndBreadcrumb
396397
currentPageTitle={siteSpecific("Question Finder", "Questions")}
397398
help={pageHelp}
398399
icon={{type: "hex", icon: "page-icon-finder"}}
399-
// TODO: add a subject field to icon if this is a context-specific QF
400400
/>
401401
<SidebarLayout>
402402
<QuestionFinderSidebar />
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import React from "react";
2+
import { RouteComponentProps, withRouter } from "react-router";
3+
import { Container } from "reactstrap";
4+
import { TitleAndBreadcrumb } from "../elements/TitleAndBreadcrumb";
5+
import { getHumanContext, useUrlPageTheme } from "../../services/pageContext";
6+
7+
export const QuestionPacks = withRouter((props: RouteComponentProps) => {
8+
const pageContext = useUrlPageTheme();
9+
10+
return <Container data-bs-theme={pageContext?.subject}>
11+
<TitleAndBreadcrumb
12+
currentPageTitle="Question packs"
13+
icon={pageContext?.subject ? {
14+
type: "hex",
15+
subject: pageContext.subject,
16+
icon: "page-icon-finder"
17+
} : undefined}
18+
/>
19+
<div className="mt-5">This is a question packs listing page for {getHumanContext(pageContext)}!</div>
20+
</Container>;
21+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import React from "react";
2+
import { RouteComponentProps, withRouter } from "react-router";
3+
import { Container } from "reactstrap";
4+
import { TitleAndBreadcrumb } from "../elements/TitleAndBreadcrumb";
5+
import { getHumanContext, useUrlPageTheme } from "../../services/pageContext";
6+
7+
export const QuickQuizzes = withRouter((props: RouteComponentProps) => {
8+
const pageContext = useUrlPageTheme();
9+
10+
return <Container data-bs-theme={pageContext?.subject}>
11+
<TitleAndBreadcrumb
12+
currentPageTitle="Quick quizzes"
13+
icon={pageContext?.subject ? {
14+
type: "hex",
15+
subject: pageContext.subject,
16+
icon: "page-icon-finder"
17+
} : undefined}
18+
/>
19+
<div className="mt-5">This is a quick quizzes listing page for {getHumanContext(pageContext)}!</div>
20+
</Container>;
21+
});

src/app/components/pages/SubjectLandingPage.tsx

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,11 @@
1-
import React, { useEffect } from "react";
2-
import { pageContextSlice, selectors, useAppSelector } from "../../state";
3-
import { useDispatch } from "react-redux";
1+
import React from "react";
42
import { RouteComponentProps, withRouter } from "react-router";
5-
import { Subject } from "../../../IsaacAppTypes";
6-
import { Stage } from "../../../IsaacApiTypes";
73
import { Container } from "reactstrap";
84
import { TitleAndBreadcrumb } from "../elements/TitleAndBreadcrumb";
9-
import { getHumanContext } from "../../services/context";
5+
import { getHumanContext, useUrlPageTheme } from "../../services/pageContext";
106

117
export const SubjectLandingPage = withRouter((props: RouteComponentProps) => {
12-
const {location} = props;
13-
const pageContext = useAppSelector(selectors.pageContext.context);
14-
const dispatch = useDispatch();
15-
16-
useEffect(() => {
17-
const [subject, stage] = location.pathname.split("/").filter(Boolean);
18-
dispatch(pageContextSlice.actions.updatePageContext({subject: subject as Subject, stage: stage as Stage}));
19-
}, [dispatch, location.pathname]);
8+
const pageContext = useUrlPageTheme();
209

2110
return <Container data-bs-theme={pageContext?.subject}>
2211
<TitleAndBreadcrumb

src/app/components/pages/quizzes/PracticeQuizzes.tsx

Lines changed: 46 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { withRouter } from "react-router-dom";
22
import React, { useEffect, useState } from "react";
33
import { Button, ListGroupItem, Input, ListGroup, Col, Container } from "reactstrap";
44
import { TitleAndBreadcrumb } from "../../elements/TitleAndBreadcrumb";
5-
import { isEventLeaderOrStaff, isTutorOrAbove, siteSpecific } from "../../../services";
5+
import { isEventLeaderOrStaff, isLoggedIn, isTutorOrAbove, siteSpecific } from "../../../services";
66
import { QuizSummaryDTO } from "../../../../IsaacApiTypes";
77
import { ShowLoading } from "../../handlers/ShowLoading";
88
import { useGetAvailableQuizzesQuery } from "../../../state/slices/api/quizApi";
@@ -11,23 +11,26 @@ import { Spacer } from "../../elements/Spacer";
1111
import { Link } from "react-router-dom";
1212
import { PageFragment } from "../../elements/PageFragment";
1313
import { MainContent, PracticeQuizzesSidebar, SidebarLayout } from "../../elements/layout/SidebarLayout";
14+
import { useUrlPageTheme } from "../../../services/pageContext";
15+
import { selectors, useAppSelector } from "../../../state";
1416

15-
const PracticeQuizzesComponent = ({user}: QuizzesPageProps) => {
17+
const PracticeQuizzesComponent = (props: QuizzesPageProps) => {
1618
const {data: quizzes} = useGetAvailableQuizzesQuery(0);
1719
const [filterText, setFilterText] = useState<string>("");
1820
const [copied, setCopied] = useState(false);
1921

22+
const user = useAppSelector(selectors.user.orNull);
23+
24+
const pageContext = useUrlPageTheme();
25+
2026
useEffect(() => {
2127
if (location.search.includes("filter")) {
2228
setFilterText(new URLSearchParams(location.search).get("filter") || "");
2329
}
2430
}, []);
2531

26-
if (!user) {
27-
return <p>You must be logged in to view practice tests.</p>;
28-
}
29-
3032
const showQuiz = (quiz: QuizSummaryDTO) => {
33+
if (!user || !isLoggedIn(user)) return false;
3134
switch (user.role) {
3235
case "STUDENT":
3336
case "TUTOR":
@@ -47,46 +50,49 @@ const PracticeQuizzesComponent = ({user}: QuizzesPageProps) => {
4750
{isTutorOrAbove(user) && ((quiz.hiddenFromRoles && !quiz.hiddenFromRoles?.includes("STUDENT")) || quiz.visibleToStudents) && <div className="small text-muted d-block ms-2">visible to students</div>}
4851
</>;
4952

50-
return <Container>
53+
return <Container { ...(pageContext?.subject && { "data-bs-theme" : pageContext.subject })}>
5154
<TitleAndBreadcrumb currentPageTitle={siteSpecific("Practice Tests", "Practice tests")} />
5255
<SidebarLayout>
5356
<PracticeQuizzesSidebar />
5457
<MainContent>
5558
<PageFragment fragmentId="help_toptext_practice_tests" />
56-
<ShowLoading until={quizzes}>
57-
{quizzes && <>
58-
{quizzes.length === 0 && <p><em>There are no practice tests currently available.</em></p>}
59-
<Col xs={12} className="mb-4">
60-
<Input type="text" placeholder="Filter tests by name..." value={filterText} onChange={(e) => setFilterText(e.target.value)} />
61-
<button className={`copy-test-filter-link m-0 ${copied ? "clicked" : ""}`} tabIndex={-1} onClick={() => {
62-
if (filterText.trim()) {
63-
navigator.clipboard.writeText(`${window.location.host}${window.location.pathname}?filter=${filterText.trim()}#practice`);
64-
}
65-
setCopied(true);
66-
}} onMouseLeave={() => setCopied(false)} />
67-
</Col>
68-
<ListGroup className="mb-3 quiz-list">
69-
{quizzes.filter((quiz) => showQuiz(quiz) && quiz.title?.toLowerCase().includes(filterText.toLowerCase())).map(quiz => <ListGroupItem className="p-0 bg-transparent" key={quiz.id}>
70-
<div className="d-flex flex-grow-1 flex-column flex-sm-row align-items-center p-3">
71-
<div>
72-
<span className="mb-2 mb-sm-0 pe-2">{quiz.title}</span>
73-
{roleVisibilitySummary(quiz)}
74-
{quiz.summary && <div className="small text-muted d-none d-md-block">{quiz.summary}</div>}
59+
{!user
60+
? <b>You must be logged in to view practice tests.</b>
61+
: <ShowLoading until={quizzes}>
62+
{quizzes && <>
63+
{quizzes.length === 0 && <p><em>There are no practice tests currently available.</em></p>}
64+
<Col xs={12} className="mb-4">
65+
<Input type="text" placeholder="Filter tests by name..." value={filterText} onChange={(e) => setFilterText(e.target.value)} />
66+
<button className={`copy-test-filter-link m-0 ${copied ? "clicked" : ""}`} tabIndex={-1} onClick={() => {
67+
if (filterText.trim()) {
68+
navigator.clipboard.writeText(`${window.location.host}${window.location.pathname}?filter=${filterText.trim()}#practice`);
69+
}
70+
setCopied(true);
71+
}} onMouseLeave={() => setCopied(false)} />
72+
</Col>
73+
<ListGroup className="mb-3 quiz-list">
74+
{quizzes.filter((quiz) => showQuiz(quiz) && quiz.title?.toLowerCase().includes(filterText.toLowerCase())).map(quiz => <ListGroupItem className="p-0 bg-transparent" key={quiz.id}>
75+
<div className="d-flex flex-grow-1 flex-column flex-sm-row align-items-center p-3">
76+
<div>
77+
<span className="mb-2 mb-sm-0 pe-2">{quiz.title}</span>
78+
{roleVisibilitySummary(quiz)}
79+
{quiz.summary && <div className="small text-muted d-none d-md-block">{quiz.summary}</div>}
80+
</div>
81+
<Spacer />
82+
{isTutorOrAbove(user) && <div className="d-none d-md-flex align-items-center me-4">
83+
<Link to={{pathname: `/test/preview/${quiz.id}`}}>
84+
<span>Preview</span>
85+
</Link>
86+
</div>}
87+
<Button tag={Link} to={{pathname: `/test/attempt/${quiz.id}`}}>
88+
{siteSpecific("Take Test", "Take test")}
89+
</Button>
7590
</div>
76-
<Spacer />
77-
{isTutorOrAbove(user) && <div className="d-none d-md-flex align-items-center me-4">
78-
<Link to={{pathname: `/test/preview/${quiz.id}`}}>
79-
<span>Preview</span>
80-
</Link>
81-
</div>}
82-
<Button tag={Link} to={{pathname: `/test/attempt/${quiz.id}`}}>
83-
{siteSpecific("Take Test", "Take test")}
84-
</Button>
85-
</div>
86-
</ListGroupItem>)}
87-
</ListGroup>
88-
</>}
89-
</ShowLoading>
91+
</ListGroupItem>)}
92+
</ListGroup>
93+
</>}
94+
</ShowLoading>
95+
}
9096
</MainContent>
9197
</SidebarLayout>
9298
</Container>;

0 commit comments

Comments
 (0)