diff --git a/cypress/support/component-index-ada.html b/cypress/support/component-index-ada.html index 9fec638481..f26c2cdff2 100644 --- a/cypress/support/component-index-ada.html +++ b/cypress/support/component-index-ada.html @@ -29,6 +29,6 @@ Ada Computer Science -
+
diff --git a/cypress/support/component-index-phy.html b/cypress/support/component-index-phy.html index 418b516597..07053155b2 100644 --- a/cypress/support/component-index-phy.html +++ b/cypress/support/component-index-phy.html @@ -29,6 +29,6 @@ Isaac Science -
+
diff --git a/src/app/components/content/IsaacQuestion.tsx b/src/app/components/content/IsaacQuestion.tsx index 7f57f887aa..62e30ba953 100644 --- a/src/app/components/content/IsaacQuestion.tsx +++ b/src/app/components/content/IsaacQuestion.tsx @@ -284,9 +284,7 @@ export const IsaacQuestion = withRouter(({doc, location}: {doc: ApiTypes.Questio } {/* Physics Hints */} - {isPhy &&
- -
} + {isPhy && } {/* LLM free-text question validation response */} diff --git a/src/app/components/elements/Book.tsx b/src/app/components/elements/Book.tsx index 344be1ce46..83c40ef7b1 100644 --- a/src/app/components/elements/Book.tsx +++ b/src/app/components/elements/Book.tsx @@ -10,6 +10,7 @@ import { BookPage } from "./BookPage"; import { skipToken } from "@reduxjs/toolkit/query"; import { ShowLoadingQuery } from "../handlers/ShowLoadingQuery"; import { TeacherNotes } from "./TeacherNotes"; +import { EditContentButton } from "./EditContentButton"; interface BookProps { match: { params: { bookId: string } }; @@ -62,6 +63,7 @@ export const Book = ({match: {params: {bookId}}}: BookProps) => { thenRender={(bookDetailPage) => } /> : <> +
diff --git a/src/app/components/elements/BookPage.tsx b/src/app/components/elements/BookPage.tsx index 50261ca92a..a2cc7b51d9 100644 --- a/src/app/components/elements/BookPage.tsx +++ b/src/app/components/elements/BookPage.tsx @@ -3,14 +3,18 @@ import { IsaacContentValueOrChildren } from "../content/IsaacContentValueOrChild import { ListView } from "./list-groups/ListView"; import { IsaacBookDetailPageDTO } from "../../../IsaacApiTypes"; import { TeacherNotes } from "./TeacherNotes"; +import { Markup } from "./markup"; +import { EditContentButton } from "./EditContentButton"; export const BookPage = ({ page }: { page: IsaacBookDetailPageDTO }) => { return
<> + + -

{page.title}

+

{page.title}

{!!page.gameboards?.length && <>

Questions

diff --git a/src/app/components/elements/CollapsibleList.tsx b/src/app/components/elements/CollapsibleList.tsx index fda6cb24ec..4dbc1c9c8b 100644 --- a/src/app/components/elements/CollapsibleList.tsx +++ b/src/app/components/elements/CollapsibleList.tsx @@ -45,8 +45,8 @@ export const CollapsibleList = (props: CollapsibleListProps) => { ? {props.title && props.asSubList ? props.title : {props.title}} : props.title; - return -
+ return +
diff --git a/src/app/components/elements/inputs/StyledCheckbox.tsx b/src/app/components/elements/inputs/StyledCheckbox.tsx index d0bddb91b5..3afe55fe7d 100644 --- a/src/app/components/elements/inputs/StyledCheckbox.tsx +++ b/src/app/components/elements/inputs/StyledCheckbox.tsx @@ -2,7 +2,7 @@ import React, {useEffect, useMemo, useState} from "react"; import {InputProps} from "reactstrap"; import {v4} from "uuid"; import {Spacer} from "../Spacer"; -import {ifKeyIsEnter, isAda} from "../../../services"; +import {ifKeyIsEnter, isAda, isPhy} from "../../../services"; import classNames from "classnames"; // A custom checkbox, dealing with mouse and keyboard input. Pass `onChange((e : ChangeEvent) => void)`, `checked: bool`, and `label: Element` as required as props to use. @@ -15,7 +15,7 @@ export const StyledCheckbox = (props: InputProps & {partial?: boolean}) => { const [checked, setChecked] = useState(props.checked ?? false); const id = useMemo(() => {return (props.id ?? "") + "-" + v4();}, [props.id]); const onCheckChange = (e: React.ChangeEvent) => { - props.onChange && props.onChange(e); + if (props.onChange) props.onChange(e); setChecked(e.target.checked); }; @@ -38,3 +38,13 @@ export const StyledCheckbox = (props: InputProps & {partial?: boolean}) => {
; }; + +interface CheckboxWrapperProps extends React.HTMLAttributes { + active?: boolean; +} + +// in many places, we want a stylised wrapper around the checkbox indicating selection. +export const CheckboxWrapper = (props: CheckboxWrapperProps) => { + const {active, className, ...rest} = props; + return
; +}; diff --git a/src/app/components/elements/layout/SidebarLayout.tsx b/src/app/components/elements/layout/SidebarLayout.tsx index 89c61930a4..a8e3fabeb0 100644 --- a/src/app/components/elements/layout/SidebarLayout.tsx +++ b/src/app/components/elements/layout/SidebarLayout.tsx @@ -30,6 +30,7 @@ import { CollapsibleList } from "../CollapsibleList"; import { extendUrl } from "../../pages/subjectLandingPageComponents"; import { getProgressIcon } from "../../pages/Gameboard"; import { tags as tagsService } from "../../../services"; +import { Markup } from "../markup"; export const SidebarLayout = (props: RowProps) => { const { className, ...rest } = props; @@ -468,7 +469,7 @@ export const GenericConceptsSidebar = (props: ConceptListSidebarProps) => { const descendentTags = tags.getDirectDescendents(subjectTag.id); const isSelected = conceptFilters.includes(subjectTag) || descendentTags.some(tag => conceptFilters.includes(tag)); const isPartial = descendentTags.some(tag => conceptFilters.includes(tag)) && descendentTags.some(tag => !conceptFilters.includes(tag)); - return
+ return
{ const descendentTags = tags.getDirectDescendents(subjectTag.id); const isSelected = filterTags?.includes(subjectTag) || descendentTags.some(tag => filterTags?.includes(tag)); const isPartial = descendentTags.some(tag => filterTags?.includes(tag)) && descendentTags.some(tag => !filterTags?.includes(tag)); - return
  • + return
  • { {section.label} - {section.title} + {section.title}
  • } checked={pageId === section.bookPageId} onClick={() => history.push(`/books/${urlBookId}/${section.bookPageId?.slice((book.id?.length ?? 0) + 1)}`)} diff --git a/src/app/components/elements/list-groups/AbstractListViewItem.tsx b/src/app/components/elements/list-groups/AbstractListViewItem.tsx index 4636f0ae6d..6b71d20392 100644 --- a/src/app/components/elements/list-groups/AbstractListViewItem.tsx +++ b/src/app/components/elements/list-groups/AbstractListViewItem.tsx @@ -131,7 +131,7 @@ export const AbstractListViewItem = ({icon, title, subject, subtitle, breadcrumb
    }
    {subtitle &&
    - {subtitle} + {subtitle}
    } {breadcrumb && diff --git a/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx b/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx index 028a85638d..d19c40a2ee 100644 --- a/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx +++ b/src/app/components/elements/panels/QuestionFinderFilterPanel.tsx @@ -22,7 +22,7 @@ import { import { Difficulty, ExamBoard } from "../../../../IsaacApiTypes"; import { pageStageToSearchStage, QuestionStatus } from "../../pages/QuestionFinder"; import classNames from "classnames"; -import { StyledCheckbox } from "../inputs/StyledCheckbox"; +import { CheckboxWrapper, StyledCheckbox } from "../inputs/StyledCheckbox"; import { DifficultyIcons } from "../svg/DifficultyIcons"; import { GroupBase } from "react-select"; import { HierarchyFilterTreeList } from "../svg/HierarchyFilter"; @@ -159,14 +159,14 @@ export function QuestionFinderFilterPanel(props: QuestionFinderFilterPanelProps) numberSelected={(isAda && searchStages.includes(STAGE.ALL)) ? searchStages.length - 1 : searchStages.length} > {getFilteredStageOptions().filter(stage => pageStageToSearchStage(pageContext?.stage).includes(stage.value) || !pageContext?.stage?.length).map((stage, index) => ( -
    + setSearchStages(s => s.includes(stage.value) ? s.filter(v => v !== stage.value) : [...s, stage.value])} label={{stage.label}} /> -
    + ))} } {isAda && {getFilteredExamBoardOptions({byStages: searchStages}).map((board, index) => ( -
    + setSearchExamBoards(s => s.includes(board.value) ? s.filter(v => v !== board.value) : [...s, board.value])} label={{board.label}} /> -
    + ))}
    } l.value !== pageContext?.subject).length, searchTopics.length)} > {siteSpecific( -
    - -
    , + , groupBaseTagOptions.map((tag, index) => ( listStateDispatch({type: "toggle", id: `topics ${sublistDelimiter} ${tag.label}`, focus: true})} > {tag.options.map((topic, index) => ( -
    +
    {siteSpecific("Learn more about difficulty levels", "What do the difficulty levels mean?")} {SIMPLE_DIFFICULTY_ITEM_OPTIONS.map((difficulty, index) => ( -
    +
    } /> -
    + ))} {isPhy && bookOptions.length > 0 && <> -
    + setExcludeBooks(p => !p)} label={Exclude skills book questions} /> -
    + +
    {bookOptions.map((book, index) => ( -
    + setSearchBooks( - s => s.includes(book.tag) - ? s.filter(v => v !== book.tag) - : [...s, book.tag] - )} + onChange={() => { + if (excludeBooks) { + setExcludeBooks(false); + setSearchBooks([book.tag]); + } else { + setSearchBooks( + s => s.includes(book.tag) + ? s.filter(v => v !== book.tag) + : [...s, book.tag] + ); + } + }} label={{book.shortTitle ?? book.title}} /> -
    + ))} } @@ -290,7 +296,7 @@ export function QuestionFinderFilterPanel(props: QuestionFinderFilterPanelProps) toggle={() => listStateDispatch({type: "toggle", id: "questionStatus", focus: below["md"](deviceSize)})} numberSelected={Object.values(searchStatuses).reduce((acc, item) => acc + item, 0)} > -
    + )} /> -
    -
    + + )} /> -
    -
    + + )} /> -
    + {/* TODO: implement once necessary tags are available
    diff --git a/src/app/components/elements/svg/HierarchyFilter.tsx b/src/app/components/elements/svg/HierarchyFilter.tsx index 0fd8eb4ad4..f214b6fb40 100644 --- a/src/app/components/elements/svg/HierarchyFilter.tsx +++ b/src/app/components/elements/svg/HierarchyFilter.tsx @@ -4,7 +4,7 @@ import { TAG_LEVEL } from "../../../services"; import classNames from "classnames"; -import { StyledCheckbox } from "../inputs/StyledCheckbox"; +import { CheckboxWrapper, StyledCheckbox } from "../inputs/StyledCheckbox"; import { ChoiceTree, getChoiceTreeLeaves } from "../panels/QuestionFinderFilterPanel"; import { pruneTreeNode } from "../../../services/questionHierarchy"; @@ -23,7 +23,7 @@ interface HierarchyFilterProps { } export function HierarchyFilterTreeList({tier, index, choices, selections, questionFinderFilter, className, root, setSelections}: HierarchyFilterProps) { - return
    + return
    {choices[tier] && choices[tier][index] && choices[tier][index].map((choice) => { const isSelected = selections[tier] && selections[tier][index]?.map(s => s.value).includes(choice.value); const isLeaf = getChoiceTreeLeaves(selections).map(l => l.value).includes(choice.value); @@ -42,22 +42,20 @@ export function HierarchyFilterTreeList({tier, index, choices, selections, quest setSelections(newSelections); }; - return
    -
    - {choice.label}} - className={classNames({"icon-checkbox-off": !isSelected, "icon icon-checkbox-partial-alt": isSelected && !isLeaf, "icon-checkbox-selected": isLeaf})} - /> -
    + return + {choice.label}} + className={classNames({"icon-checkbox-off": !isSelected, "icon icon-checkbox-partial-alt": isSelected && !isLeaf, "icon-checkbox-selected": isLeaf})} + /> {tier < 2 && choices[tier+1] && choice.value in choices[tier+1] && } -
    ; + ; } )}
    ; diff --git a/src/app/components/navigation/TrackedRoute.tsx b/src/app/components/navigation/TrackedRoute.tsx index 35ef16ac1b..ee40921bb2 100644 --- a/src/app/components/navigation/TrackedRoute.tsx +++ b/src/app/components/navigation/TrackedRoute.tsx @@ -2,7 +2,7 @@ import React, {useEffect} from "react"; import {Redirect, Route, RouteComponentProps, RouteProps, useLocation} from "react-router"; import {FigureNumberingContext, PotentialUser} from "../../../IsaacAppTypes"; import {ShowLoading} from "../handlers/ShowLoading"; -import {selectors, useAppSelector} from "../../state"; +import {docSlice, selectors, useAppDispatch, useAppSelector} from "../../state"; import { isNotPartiallyLoggedIn, isTeacherOrAbove, @@ -34,9 +34,11 @@ export const TrackedRoute = function({component, componentProps, ...rest}: Track // Store react-router's location, rather than window's location, during the react render to track changes in history so that we // can ensure it handles the location correctly even if there is a react-router before the useEffect is called. const location = useLocation(); + const dispatch = useAppDispatch(); useEffect(() => { // Use window's location for the origin to match trackPageview's normal URL format - react-router does not record origin. trackPageview({ url: window.location.origin + location.pathname + location.search + location.hash }); + dispatch(docSlice.actions.resetPage()); // reset redux's doc after any page change }, [location.pathname]); const user = useAppSelector(selectors.user.orNull); diff --git a/src/app/components/pages/SubjectLandingPage.tsx b/src/app/components/pages/SubjectLandingPage.tsx index 3bd130c4cb..af311cb7bf 100644 --- a/src/app/components/pages/SubjectLandingPage.tsx +++ b/src/app/components/pages/SubjectLandingPage.tsx @@ -139,7 +139,7 @@ export const LandingPageFooter = ({context}: {context: PageContextState}) => {

    There was an error loading the events list. Please try again later!

    )} thenRender={({events}) => { const eventStages = (event: AugmentedEvent) => event.audience?.map(a => a.stage?.map(s => STAGE_TO_LEARNING_STAGE[s])).flat() ?? []; const relevantEvents = events.filter(event => context?.subject && event.tags?.includes(context.subject) diff --git a/src/app/components/site/phy/HeaderPhy.tsx b/src/app/components/site/phy/HeaderPhy.tsx index 61a8946f18..a9543002af 100644 --- a/src/app/components/site/phy/HeaderPhy.tsx +++ b/src/app/components/site/phy/HeaderPhy.tsx @@ -9,11 +9,11 @@ import { MenuOpenContext } from "../../navigation/NavigationBar"; import classNames from "classnames"; import { NavigationMenuPhy } from "./NavigationMenuPhy"; -export const LoginLogoutButton = (props : React.HTMLAttributes) => { +export const LoginLogoutButton = (props : React.HTMLAttributes) => { const user = useAppSelector(selectors.user.orNull); return user && (user.loggedIn - ? Log out + ? : ); }; @@ -60,7 +60,7 @@ export const HeaderPhy = () => { - + { useEffect( () => {document.title = SITE_TITLE;}, []); const user = useAppSelector(selectors.user.orNull); - - const {data: news} = useGetNewsPodListQuery({subject: "physics"}); + + const newsQuery = useGetNewsPodListQuery({subject: "physics"}); const [dashboardView, setDashboardView] = useState<"student" | "teacher" | undefined>(undefined); @@ -195,12 +195,15 @@ export const HomepagePhy = () => {

    There was an error loading the events list. Please try again later!

    )} thenRender={({events}) => { return - {events.map(event => - - )} + {events.length + ? events.map(event => + + ) + :

    No events available. Check back soon!

    } +
    ; }}/>
    @@ -210,11 +213,20 @@ export const HomepagePhy = () => { More news
    - {news && - {news.slice(0, 2).map(newsItem => - - )} - } +

    There was an error loading the news list. Please try again later!

    )} + thenRender={(news) => { + return + {news.length + ? news.slice(0, 2).map(newsItem => + + ) + :

    No news available. Check back soon!

    + } +
    ; + }} + />
    diff --git a/src/scss/common/elements.scss b/src/scss/common/elements.scss index b3595dfddc..2921a023ac 100644 --- a/src/scss/common/elements.scss +++ b/src/scss/common/elements.scss @@ -281,13 +281,12 @@ 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; +.collapsible-list-container { + border-top: 2px solid $gray-107; + + &:last-of-type { + border-bottom: 2px solid $gray-107; + } img { transition: transform 0.1s ease; @@ -340,7 +339,7 @@ iframe.email-html { overflow: hidden; border-top: 1px solid rgba(0, 0, 0, 0.15); opacity: 1; - width: 100%; + flex-grow: 1; } .section-divider-y { diff --git a/src/scss/common/questions.scss b/src/scss/common/questions.scss index b52e6fe528..b32b7c8dfc 100644 --- a/src/scss/common/questions.scss +++ b/src/scss/common/questions.scss @@ -24,7 +24,7 @@ $cloze-scrollbar-height: 4px; min-width: auto; } -.question-component { +form:has(> .question-component) { margin-bottom: 2rem; clear: both; @@ -70,6 +70,12 @@ $cloze-scrollbar-height: 4px; } } +form:has(> .question-component) { + background-color: white; + box-shadow: 0 2px 30px 0 rgba(0, 0, 0, 0.08); + padding: 1rem; +} + $inline-feedback-size: 20px; $inline-feedback-padding: 2px; @@ -395,13 +401,6 @@ $inline-feedback-padding: 2px; } } -// NOMENSA question.scss -.question-component { - background-color: white; - box-shadow: 0 2px 30px 0 rgba(0, 0, 0, 0.08); - padding: 1rem; -} - .cloze-question { // This is a work around, allowing KaTeX to be used for cloze questions. // There is no guarantee that it works all of the time, so needs to be diff --git a/src/scss/phy/checkbox.scss b/src/scss/phy/checkbox.scss index 02c9c4b054..f5d22ffcab 100644 --- a/src/scss/phy/checkbox.scss +++ b/src/scss/phy/checkbox.scss @@ -7,7 +7,20 @@ } } -.checkbox-region { +.checkbox-wrapper { + // prefer border over outline to prevent cutoff from overflows + border: 1px solid transparent; + + &:not(:has(> .checkbox-small)) { + padding: 2px 0; + } +} + +.checkbox-wrapper + .checkbox-wrapper { + margin-top: -1px; +} + +.checkbox-active { border-radius: 4px; .search-field { @@ -17,9 +30,9 @@ } } -.checkbox-region:not(.hierarchy-true-root) { +.checkbox-active:not(.hierarchy-true-root) { background-color: $color-neutral-100; - outline: 1px solid $color-neutral-200; + border: 1px solid $color-neutral-200; } .styled-checkbox-wrapper div input[type="checkbox"] { diff --git a/src/scss/phy/questions.scss b/src/scss/phy/questions.scss index e97f523213..f34810e169 100644 --- a/src/scss/phy/questions.scss +++ b/src/scss/phy/questions.scss @@ -125,26 +125,36 @@ } // styling for question components outside an accordion -.question-component { +form:has(> .question-component) { border: 2px solid var(--nav-primary); padding: 0; border-radius: 8px; margin-bottom: 1rem; - > div:first-child { + > .question-component > div:first-child { padding: 2rem 2rem 0.5rem; &:last-child { padding-bottom: 3rem; } } + + > .tabbed-hints { + padding: 0 2rem; + } } // any such styling that should not be present in an accordion -.isaac-accordion .question-component { +.isaac-accordion form:has(> .question-component) { border: unset; // padding is overridden above border-radius: unset; + box-shadow: none; + + > .tabbed-hints { + padding: 0; + } + } .content-metadata-container { diff --git a/src/test/pages/__image_snapshots__/ada/Question finder page should have no visual regressions #0.png b/src/test/pages/__image_snapshots__/ada/Question finder page should have no visual regressions #0.png index e510e587f3..861a2239db 100644 Binary files a/src/test/pages/__image_snapshots__/ada/Question finder page should have no visual regressions #0.png and b/src/test/pages/__image_snapshots__/ada/Question finder page should have no visual regressions #0.png differ diff --git a/src/test/pages/__image_snapshots__/phy/Homepage should have no visual regressions #0.png b/src/test/pages/__image_snapshots__/phy/Homepage should have no visual regressions #0.png index f4f9601eb3..4b69b6ff05 100644 Binary files a/src/test/pages/__image_snapshots__/phy/Homepage should have no visual regressions #0.png and b/src/test/pages/__image_snapshots__/phy/Homepage should have no visual regressions #0.png differ diff --git a/src/test/pages/__image_snapshots__/phy/Question types' regression test page should have no visual regressions #0.png b/src/test/pages/__image_snapshots__/phy/Question types' regression test page should have no visual regressions #0.png index 58a57f17a0..ec707db21f 100644 Binary files a/src/test/pages/__image_snapshots__/phy/Question types' regression test page should have no visual regressions #0.png and b/src/test/pages/__image_snapshots__/phy/Question types' regression test page should have no visual regressions #0.png differ