From 39b208c878cb0cf5566d8624f4c041655a622957 Mon Sep 17 00:00:00 2001 From: Marius Date: Tue, 13 Jan 2026 10:30:24 +0200 Subject: [PATCH 1/5] Revert "768 Booster video CTA (#440)" This reverts commit c88e06384fe22c4118df876ef6af01541740b07d. --- src/app/components/elements/PageTitle.tsx | 64 +++++------------------ src/app/components/pages/Events.tsx | 10 ++-- 2 files changed, 20 insertions(+), 54 deletions(-) diff --git a/src/app/components/elements/PageTitle.tsx b/src/app/components/elements/PageTitle.tsx index 476e2ce4ba..ee6cbefdf5 100644 --- a/src/app/components/elements/PageTitle.tsx +++ b/src/app/components/elements/PageTitle.tsx @@ -1,6 +1,5 @@ import React, { ReactElement, useEffect, useRef } from "react"; -import { UncontrolledTooltip, Button } from "reactstrap"; -import { Link } from "react-router-dom"; +import { UncontrolledTooltip } from "reactstrap"; import { AUDIENCE_DISPLAY_FIELDS, filterAudienceViewsByProperties, @@ -17,15 +16,6 @@ import { Helmet } from "react-helmet"; import { Markup } from "./markup"; import { EditablePageTitle } from "./inputs/EditablePageTitle"; -declare global { - interface Window { - followedAtLeastOneSoftLink?: boolean; - } - - // eslint-disable-next-line no-var - var followedAtLeastOneSoftLink: boolean | undefined; -} - function AudienceViewer({ audienceViews }: { audienceViews: ViewingContext[] }) { const userContext = useUserContext(); const viewsWithMyStage = audienceViews.filter((vc) => vc.stage === userContext.stage); @@ -56,68 +46,33 @@ export interface PageTitleProps { subTitle?: string; disallowLaTeX?: boolean; help?: ReactElement; - boosterVideoButton?: boolean; className?: string; audienceViews?: ViewingContext[]; onTitleEdit?: (newTitle: string) => void; } - export const PageTitle = ({ currentPageTitle, subTitle, disallowLaTeX, help, - boosterVideoButton, className, audienceViews, onTitleEdit, }: PageTitleProps) => { const dispatch = useAppDispatch(); const openModal = useAppSelector((state: AppState) => Boolean(state?.activeModals?.length)); - const user = useAppSelector((state: AppState) => state?.user); const headerRef = useRef(null); useEffect(() => { dispatch(mainContentIdSlice.actions.set("main-heading")); - }, [dispatch]); - + }, []); useEffect(() => { document.title = currentPageTitle + " — Isaac " + SITE_SUBJECT_TITLE; const element = headerRef.current; - if (element && globalThis.followedAtLeastOneSoftLink && !openModal) { + if (element && (window as any).followedAtLeastOneSoftLink && !openModal) { element.focus(); } - }, [currentPageTitle, openModal]); - - // Extract nested ternary logic - const renderHelpOrBoosterButton = () => { - if (boosterVideoButton) { - const targetPath = user?.loggedIn - ? "/pages/test_page_booster_recording" - : "/login?target=/pages/test_page_booster_recording"; - - return ( - - ); - } - - if (help) { - return ( - -
- Help -
- - {help} - -
- ); - } - - return null; - }; + }, [currentPageTitle]); return (

@@ -133,7 +88,16 @@ export const PageTitle = ({ {audienceViews && } - {renderHelpOrBoosterButton()} + {help && ( + +
+ Help +
+ + {help} + +
+ )}

); }; diff --git a/src/app/components/pages/Events.tsx b/src/app/components/pages/Events.tsx index fc970f2729..086d728f16 100644 --- a/src/app/components/pages/Events.tsx +++ b/src/app/components/pages/Events.tsx @@ -95,6 +95,8 @@ export const Events = withRouter(({ history, location }: RouteComponentProps) => dispatch(getEventMapData(startIndex, -1, typeFilter, statusFilter, stageFilter)); }, [dispatch, typeFilter, statusFilter, stageFilter]); + const pageHelp = Follow the links below to find out more about our FREE events.; + const metaDescriptionCS = "A level and GCSE Computer Science live online training. Revision and extension workshops for students."; @@ -102,7 +104,7 @@ export const Events = withRouter(({ history, location }: RouteComponentProps) => <>
- +
{/* Filters */} @@ -121,7 +123,7 @@ export const Events = withRouter(({ history, location }: RouteComponentProps) => query.show_reservations_only = selectedFilter === EventStatusFilter["My event reservations"] ? true : undefined; query.event_status = selectedFilter == EventStatusFilter["All events"] ? "all" : undefined; - history.push({ pathname: location.pathname, search: queryString.stringify(query as never) }); + history.push({ pathname: location.pathname, search: queryString.stringify(query as any) }); }} > {/* Tutors are considered students w.r.t. events currently, so cannot see teacher-only events */} @@ -148,7 +150,7 @@ export const Events = withRouter(({ history, location }: RouteComponentProps) => onChange={(e) => { const selectedType = e.target.value as EventTypeFilter; query.types = selectedType !== EventTypeFilter["All events"] ? selectedType : undefined; - history.push({ pathname: location.pathname, search: queryString.stringify(query as never) }); + history.push({ pathname: location.pathname, search: queryString.stringify(query as any) }); }} > {Object.entries(EventTypeFilter).map(([typeLabel, typeValue]) => ( @@ -166,7 +168,7 @@ export const Events = withRouter(({ history, location }: RouteComponentProps) => const selectedStage = e.target.value as EventStageFilter; query.show_stage_only = selectedStage !== EventStageFilter["All stages"] ? selectedStage : undefined; - history.push({ pathname: location.pathname, search: queryString.stringify(query as never) }); + history.push({ pathname: location.pathname, search: queryString.stringify(query as any) }); }} > {Object.entries(EventStageFilter) From 51c319c7dfcaf29df8f08beb9fef8e1e930cf450 Mon Sep 17 00:00:00 2001 From: Marius Date: Tue, 13 Jan 2026 11:08:33 +0200 Subject: [PATCH 2/5] Remove password strength --- .../inputs/RegistrationPasswordInputs.tsx | 87 ++++++++++++++----- 1 file changed, 66 insertions(+), 21 deletions(-) diff --git a/src/app/components/elements/inputs/RegistrationPasswordInputs.tsx b/src/app/components/elements/inputs/RegistrationPasswordInputs.tsx index d2908a10f6..efcd993ddc 100644 --- a/src/app/components/elements/inputs/RegistrationPasswordInputs.tsx +++ b/src/app/components/elements/inputs/RegistrationPasswordInputs.tsx @@ -1,8 +1,13 @@ import React, { useState } from "react"; -import { Col, FormFeedback, FormGroup, Label, Row, UncontrolledTooltip } from "reactstrap"; -import { PasswordFeedback, ValidationUser } from "../../../../IsaacAppTypes"; +import { Col, FormGroup, Label, Row, UncontrolledTooltip } from "reactstrap"; +import { ValidationUser } from "../../../../IsaacAppTypes"; import { Immutable } from "immer"; -import { PASSWORD_REQUIREMENTS, passwordDebounce, validatePassword } from "../../../services"; +import { + PASSWORD_REQUIREMENTS, + validatePassword, + validatePasswordMatch, + getPasswordValidationErrors, +} from "../../../services"; import Password from "./Password"; interface PasswordInputProps { userToUpdate: Immutable; @@ -22,9 +27,19 @@ export const RegistrationPasswordInputs = ({ defaultPassword, }: PasswordInputProps) => { const [isPasswordVisible, setIsPasswordVisible] = useState(false); - const [passwordFeedback, setPasswordFeedback] = useState(null); + const [passwordTouched, setPasswordTouched] = useState(false); + const [confirmTouched, setConfirmTouched] = useState(false); - const passwordIsValid = userToUpdate.password == unverifiedPassword && validatePassword(userToUpdate.password || ""); + const passwordIsValid = validatePassword(unverifiedPassword || ""); + const passwordsMatch = validatePasswordMatch(unverifiedPassword || "", userToUpdate.password || ""); + const bothPasswordsValid = passwordIsValid && passwordsMatch; + + // Get specific validation errors for display + const passwordErrors = unverifiedPassword ? getPasswordValidationErrors(unverifiedPassword) : []; + + // Show errors if submission attempted OR if user has touched the field and there are errors + const showPasswordErrors = (submissionAttempted || passwordTouched) && !!unverifiedPassword && !passwordIsValid; + const showMatchError = (submissionAttempted || confirmTouched) && !!userToUpdate.password && !passwordsMatch; return ( @@ -42,21 +57,39 @@ export const RegistrationPasswordInputs = ({ isPasswordVisible={isPasswordVisible} setIsPasswordVisible={setIsPasswordVisible} defaultValue={defaultPassword} + invalid={showPasswordErrors ? true : undefined} onChange={(e) => { - passwordDebounce(e.target.value, setPasswordFeedback); + const newValue = e.target.value; + setUnverifiedPassword(newValue); + // Mark as touched when user starts typing + if (newValue.length > 0) { + setPasswordTouched(true); + } }} onBlur={(e) => { - setUnverifiedPassword(e.target.value); - passwordDebounce(e.target.value, setPasswordFeedback); + const newValue = e.target.value; + setUnverifiedPassword(newValue); + setPasswordTouched(true); }} showToggleIcon={true} required={true} /> - {passwordFeedback && ( - - Password strength: - {passwordFeedback.feedbackText} - + + {/* Show validation errors */} + {showPasswordErrors && passwordErrors.length > 0 && ( +
+ Password requirements: +
    + {passwordErrors.map((error) => ( +
  • {error}
  • + ))} +
+
+ )} + + {/* If no password entered on submit */} + {submissionAttempted && (!unverifiedPassword || unverifiedPassword.length === 0) && ( +
Password is required
)} @@ -70,20 +103,32 @@ export const RegistrationPasswordInputs = ({ isPasswordVisible={isPasswordVisible} setIsPasswordVisible={setIsPasswordVisible} disabled={!unverifiedPassword} - invalid={submissionAttempted && !passwordIsValid} + invalid={showMatchError || (submissionAttempted && !bothPasswordsValid) ? true : undefined} onChange={(e: React.ChangeEvent) => { setUserToUpdate({ ...userToUpdate, password: e.target.value }); + if (e.target.value.length > 0) { + setConfirmTouched(true); + } + }} + onBlur={() => { + setConfirmTouched(true); }} ariaDescribedBy="invalidPassword" required={true} /> - {/* Feedback that appears for password match before submission */} - - {userToUpdate.password && - (!(userToUpdate.password == unverifiedPassword) - ? "Passwords don't match." - : !validatePassword(userToUpdate.password || "") && PASSWORD_REQUIREMENTS)} - + + {/* Feedback for password match */} + {showMatchError &&
Passwords do not match
} + + {(submissionAttempted || confirmTouched) && userToUpdate.password && passwordsMatch && !passwordIsValid && ( +
+ Please ensure your password meets all requirements above +
+ )} + + {submissionAttempted && (!userToUpdate.password || userToUpdate.password.length === 0) && ( +
Please confirm your password
+ )}
From a37a56aa5f4605bd80d31293debb8d669e5da216 Mon Sep 17 00:00:00 2001 From: Marius Date: Tue, 13 Jan 2026 11:20:01 +0200 Subject: [PATCH 3/5] Fix --- .../elements/inputs/RegistrationPasswordInputs.tsx | 9 ++++++++- src/app/services/validation.ts | 4 +--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/app/components/elements/inputs/RegistrationPasswordInputs.tsx b/src/app/components/elements/inputs/RegistrationPasswordInputs.tsx index efcd993ddc..7f1171d1f7 100644 --- a/src/app/components/elements/inputs/RegistrationPasswordInputs.tsx +++ b/src/app/components/elements/inputs/RegistrationPasswordInputs.tsx @@ -81,7 +81,14 @@ export const RegistrationPasswordInputs = ({ Password requirements:
    {passwordErrors.map((error) => ( -
  • {error}
  • +
  • + {error.split("\n").map((line, i) => ( + + {i > 0 &&
    } + {line} +
    + ))} +
  • ))}
diff --git a/src/app/services/validation.ts b/src/app/services/validation.ts index eb5ea8920c..6b25865171 100644 --- a/src/app/services/validation.ts +++ b/src/app/services/validation.ts @@ -134,9 +134,7 @@ export const getPasswordValidationErrors = (password: string): string[] => { errors.push("Password must contain at least one uppercase letter"); } if (!/[!-/:-@[-`{-~]/.test(password)) { - errors.push( - "Password must contain at least one punctuation character (e.g., !@#$%^&*()-_=+[]{};:'\",.<>/?\\|`~)", - ); + errors.push("Password must contain at least one special character\n(e.g., !@#$%^&*()-_=+[]{};:'\",.<>/?\\|`~)"); } return errors; } else { From 67536c69d44cdff27727087ee7d19870d357b912 Mon Sep 17 00:00:00 2001 From: Marius Date: Tue, 13 Jan 2026 12:52:37 +0200 Subject: [PATCH 4/5] Revert "Fix" This reverts commit a37a56aa5f4605bd80d31293debb8d669e5da216. --- .../elements/inputs/RegistrationPasswordInputs.tsx | 9 +-------- src/app/services/validation.ts | 4 +++- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/app/components/elements/inputs/RegistrationPasswordInputs.tsx b/src/app/components/elements/inputs/RegistrationPasswordInputs.tsx index 7f1171d1f7..efcd993ddc 100644 --- a/src/app/components/elements/inputs/RegistrationPasswordInputs.tsx +++ b/src/app/components/elements/inputs/RegistrationPasswordInputs.tsx @@ -81,14 +81,7 @@ export const RegistrationPasswordInputs = ({ Password requirements:
    {passwordErrors.map((error) => ( -
  • - {error.split("\n").map((line, i) => ( - - {i > 0 &&
    } - {line} -
    - ))} -
  • +
  • {error}
  • ))}
diff --git a/src/app/services/validation.ts b/src/app/services/validation.ts index 6b25865171..eb5ea8920c 100644 --- a/src/app/services/validation.ts +++ b/src/app/services/validation.ts @@ -134,7 +134,9 @@ export const getPasswordValidationErrors = (password: string): string[] => { errors.push("Password must contain at least one uppercase letter"); } if (!/[!-/:-@[-`{-~]/.test(password)) { - errors.push("Password must contain at least one special character\n(e.g., !@#$%^&*()-_=+[]{};:'\",.<>/?\\|`~)"); + errors.push( + "Password must contain at least one punctuation character (e.g., !@#$%^&*()-_=+[]{};:'\",.<>/?\\|`~)", + ); } return errors; } else { From 64501d760e2f3ba31196d738463d85ec8fd51709 Mon Sep 17 00:00:00 2001 From: Marius Date: Tue, 13 Jan 2026 12:52:38 +0200 Subject: [PATCH 5/5] Revert "Remove password strength" This reverts commit 51c319c7dfcaf29df8f08beb9fef8e1e930cf450. --- .../inputs/RegistrationPasswordInputs.tsx | 87 +++++-------------- 1 file changed, 21 insertions(+), 66 deletions(-) diff --git a/src/app/components/elements/inputs/RegistrationPasswordInputs.tsx b/src/app/components/elements/inputs/RegistrationPasswordInputs.tsx index efcd993ddc..d2908a10f6 100644 --- a/src/app/components/elements/inputs/RegistrationPasswordInputs.tsx +++ b/src/app/components/elements/inputs/RegistrationPasswordInputs.tsx @@ -1,13 +1,8 @@ import React, { useState } from "react"; -import { Col, FormGroup, Label, Row, UncontrolledTooltip } from "reactstrap"; -import { ValidationUser } from "../../../../IsaacAppTypes"; +import { Col, FormFeedback, FormGroup, Label, Row, UncontrolledTooltip } from "reactstrap"; +import { PasswordFeedback, ValidationUser } from "../../../../IsaacAppTypes"; import { Immutable } from "immer"; -import { - PASSWORD_REQUIREMENTS, - validatePassword, - validatePasswordMatch, - getPasswordValidationErrors, -} from "../../../services"; +import { PASSWORD_REQUIREMENTS, passwordDebounce, validatePassword } from "../../../services"; import Password from "./Password"; interface PasswordInputProps { userToUpdate: Immutable; @@ -27,19 +22,9 @@ export const RegistrationPasswordInputs = ({ defaultPassword, }: PasswordInputProps) => { const [isPasswordVisible, setIsPasswordVisible] = useState(false); - const [passwordTouched, setPasswordTouched] = useState(false); - const [confirmTouched, setConfirmTouched] = useState(false); + const [passwordFeedback, setPasswordFeedback] = useState(null); - const passwordIsValid = validatePassword(unverifiedPassword || ""); - const passwordsMatch = validatePasswordMatch(unverifiedPassword || "", userToUpdate.password || ""); - const bothPasswordsValid = passwordIsValid && passwordsMatch; - - // Get specific validation errors for display - const passwordErrors = unverifiedPassword ? getPasswordValidationErrors(unverifiedPassword) : []; - - // Show errors if submission attempted OR if user has touched the field and there are errors - const showPasswordErrors = (submissionAttempted || passwordTouched) && !!unverifiedPassword && !passwordIsValid; - const showMatchError = (submissionAttempted || confirmTouched) && !!userToUpdate.password && !passwordsMatch; + const passwordIsValid = userToUpdate.password == unverifiedPassword && validatePassword(userToUpdate.password || ""); return ( @@ -57,39 +42,21 @@ export const RegistrationPasswordInputs = ({ isPasswordVisible={isPasswordVisible} setIsPasswordVisible={setIsPasswordVisible} defaultValue={defaultPassword} - invalid={showPasswordErrors ? true : undefined} onChange={(e) => { - const newValue = e.target.value; - setUnverifiedPassword(newValue); - // Mark as touched when user starts typing - if (newValue.length > 0) { - setPasswordTouched(true); - } + passwordDebounce(e.target.value, setPasswordFeedback); }} onBlur={(e) => { - const newValue = e.target.value; - setUnverifiedPassword(newValue); - setPasswordTouched(true); + setUnverifiedPassword(e.target.value); + passwordDebounce(e.target.value, setPasswordFeedback); }} showToggleIcon={true} required={true} /> - - {/* Show validation errors */} - {showPasswordErrors && passwordErrors.length > 0 && ( -
- Password requirements: -
    - {passwordErrors.map((error) => ( -
  • {error}
  • - ))} -
-
- )} - - {/* If no password entered on submit */} - {submissionAttempted && (!unverifiedPassword || unverifiedPassword.length === 0) && ( -
Password is required
+ {passwordFeedback && ( + + Password strength: + {passwordFeedback.feedbackText} + )} @@ -103,32 +70,20 @@ export const RegistrationPasswordInputs = ({ isPasswordVisible={isPasswordVisible} setIsPasswordVisible={setIsPasswordVisible} disabled={!unverifiedPassword} - invalid={showMatchError || (submissionAttempted && !bothPasswordsValid) ? true : undefined} + invalid={submissionAttempted && !passwordIsValid} onChange={(e: React.ChangeEvent) => { setUserToUpdate({ ...userToUpdate, password: e.target.value }); - if (e.target.value.length > 0) { - setConfirmTouched(true); - } - }} - onBlur={() => { - setConfirmTouched(true); }} ariaDescribedBy="invalidPassword" required={true} /> - - {/* Feedback for password match */} - {showMatchError &&
Passwords do not match
} - - {(submissionAttempted || confirmTouched) && userToUpdate.password && passwordsMatch && !passwordIsValid && ( -
- Please ensure your password meets all requirements above -
- )} - - {submissionAttempted && (!userToUpdate.password || userToUpdate.password.length === 0) && ( -
Please confirm your password
- )} + {/* Feedback that appears for password match before submission */} + + {userToUpdate.password && + (!(userToUpdate.password == unverifiedPassword) + ? "Passwords don't match." + : !validatePassword(userToUpdate.password || "") && PASSWORD_REQUIREMENTS)} +