From 12c7e1dc6b095de88344132f0aa81b4f910feb91 Mon Sep 17 00:00:00 2001 From: Marius Date: Tue, 2 Jun 2026 21:50:21 +0300 Subject: [PATCH 1/2] 857 - Enhance IsaacCard in Editor --- src/app/components/content/IsaacCard.tsx | 33 ++++++++-- src/scss/common/cards.scss | 22 +++++++ .../components/content/IsaacCard.test.tsx | 65 +++++++++++++++++++ 3 files changed, 114 insertions(+), 6 deletions(-) create mode 100644 src/test/components/content/IsaacCard.test.tsx diff --git a/src/app/components/content/IsaacCard.tsx b/src/app/components/content/IsaacCard.tsx index 0372ec600e..4a64a78dfd 100644 --- a/src/app/components/content/IsaacCard.tsx +++ b/src/app/components/content/IsaacCard.tsx @@ -5,19 +5,18 @@ import { apiHelper, isAppLink } from "../../services"; import { Link } from "react-router-dom"; import { IsaacCardDTO } from "../../../IsaacApiTypes"; -// NOTE: Currently not in use by CS. See ticket #212 for more details. - interface IsaacCardProps { doc: IsaacCardDTO; imageClassName?: string; } export const IsaacCard = ({ doc, imageClassName }: IsaacCardProps) => { - const { title, subtitle, image, clickUrl, disabled, verticalContent } = doc; + const { title, subtitle, image, clickUrl, disabled, verticalContent, buttonText } = doc; const classes = classNames({ "menu-card": true, disabled: disabled, "isaac-card-vertical": verticalContent }); const imgSrc = image?.src && apiHelper.determineImageUrl(image.src); + const showButton = Boolean(buttonText && clickUrl); - const link = + const stretchedLink = clickUrl && isAppLink(clickUrl) ? ( ) : ( @@ -25,6 +24,28 @@ export const IsaacCard = ({ doc, imageClassName }: IsaacCardProps) => { ); + const button = ( +
+ {clickUrl && isAppLink(clickUrl) ? ( + + {buttonText} + + ) : ( + + {buttonText} + + )} +
+ ); + + const footer = showButton ? button : clickUrl && stretchedLink; + return verticalContent ? ( {image && ( @@ -44,7 +65,7 @@ export const IsaacCard = ({ doc, imageClassName }: IsaacCardProps) => { {subtitle} - {clickUrl && link} + {footer} ) : ( @@ -65,7 +86,7 @@ export const IsaacCard = ({ doc, imageClassName }: IsaacCardProps) => { - {clickUrl && link} + {footer} ); }; diff --git a/src/scss/common/cards.scss b/src/scss/common/cards.scss index ef9610d0de..e363a43617 100644 --- a/src/scss/common/cards.scss +++ b/src/scss/common/cards.scss @@ -272,3 +272,25 @@ .card-deck + .card-deck { margin-top: 1.5rem; } + +// Custom call-to-action button for the IsaacCard (set via "card button text"/"card button link" +// in the Editor). Mirrors the look and behaviour of the CareerCard/NewsCard "Read more" links. +.isaac-card-link-container { + display: flex; + justify-content: center; + // Sufficient spacing between the card text and the button. + margin-top: 1.5rem; + padding: 0 0 1rem; +} + +.isaac-card-link { + color: #6b009a; + font-size: 1.1875rem; + text-decoration: none; + + &:hover, + &:focus { + color: #6b009a; + text-decoration: underline; + } +} diff --git a/src/test/components/content/IsaacCard.test.tsx b/src/test/components/content/IsaacCard.test.tsx new file mode 100644 index 0000000000..1869dba5fd --- /dev/null +++ b/src/test/components/content/IsaacCard.test.tsx @@ -0,0 +1,65 @@ +import { screen } from "@testing-library/react"; +import { renderTestEnvironment } from "../../utils"; +import { IsaacCard } from "../../../app/components/content/IsaacCard"; +import { IsaacCardDTO } from "../../../IsaacApiTypes"; + +const renderIsaacCard = (doc: IsaacCardDTO) => { + renderTestEnvironment({ + PageComponent: IsaacCard, + componentProps: { doc }, + initialRouteEntries: ["/"], + role: "ANONYMOUS", + }); +}; + +describe("IsaacCard", () => { + it("renders the title and subtitle", () => { + renderIsaacCard({ title: "Card title", subtitle: "Card subtitle", verticalContent: true }); + expect(screen.getByText("Card title")).toBeInTheDocument(); + expect(screen.getByText("Card subtitle")).toBeInTheDocument(); + }); + + it("renders a custom, centred button when buttonText and clickUrl are provided", () => { + renderIsaacCard({ + title: "Careers", + subtitle: "Explore computing careers", + clickUrl: "/careers_in_computer_science", + buttonText: "Read more", + verticalContent: true, + }); + const button = screen.getByRole("link", { name: "Read more" }); + expect(button).toBeInTheDocument(); + expect(button).toHaveAttribute("href", "/careers_in_computer_science"); + expect(button).toHaveClass("isaac-card-link"); + expect(button.parentElement).toHaveClass("isaac-card-link-container"); + }); + + it("opens external button links in a new tab", () => { + renderIsaacCard({ + title: "External", + subtitle: "An external resource", + clickUrl: "https://example.com/resource", + buttonText: "Visit resource", + verticalContent: true, + }); + const button = screen.getByRole("link", { name: "Visit resource" }); + expect(button).toHaveAttribute("href", "https://example.com/resource"); + expect(button).toHaveAttribute("target", "_blank"); + expect(button).toHaveAttribute("rel", "noopener noreferrer"); + }); + + it("falls back to a stretched whole-card link when no buttonText is provided", () => { + renderIsaacCard({ + title: "Create a Group", + subtitle: "Create and manage student groups.", + clickUrl: "/groups", + verticalContent: true, + }); + // No visible call-to-action button is rendered... + expect(screen.queryByRole("link", { name: /read more/i })).not.toBeInTheDocument(); + // ...but the whole card is clickable via a stretched link. + const link = document.querySelector("a.stretched-link"); + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute("href", "/groups"); + }); +}); From 82c08ca1f84c76c8b06477edeb25674e6f56e8d3 Mon Sep 17 00:00:00 2001 From: Marius Date: Fri, 12 Jun 2026 12:51:22 +0300 Subject: [PATCH 2/2] 857 - Enhance IsaacCard in Editor --- src/scss/common/cards.scss | 3 --- src/test/components/content/IsaacCard.test.tsx | 2 -- 2 files changed, 5 deletions(-) diff --git a/src/scss/common/cards.scss b/src/scss/common/cards.scss index e363a43617..b3c232ac45 100644 --- a/src/scss/common/cards.scss +++ b/src/scss/common/cards.scss @@ -273,12 +273,9 @@ margin-top: 1.5rem; } -// Custom call-to-action button for the IsaacCard (set via "card button text"/"card button link" -// in the Editor). Mirrors the look and behaviour of the CareerCard/NewsCard "Read more" links. .isaac-card-link-container { display: flex; justify-content: center; - // Sufficient spacing between the card text and the button. margin-top: 1.5rem; padding: 0 0 1rem; } diff --git a/src/test/components/content/IsaacCard.test.tsx b/src/test/components/content/IsaacCard.test.tsx index 1869dba5fd..cdf8570e40 100644 --- a/src/test/components/content/IsaacCard.test.tsx +++ b/src/test/components/content/IsaacCard.test.tsx @@ -55,9 +55,7 @@ describe("IsaacCard", () => { clickUrl: "/groups", verticalContent: true, }); - // No visible call-to-action button is rendered... expect(screen.queryByRole("link", { name: /read more/i })).not.toBeInTheDocument(); - // ...but the whole card is clickable via a stretched link. const link = document.querySelector("a.stretched-link"); expect(link).toBeInTheDocument(); expect(link).toHaveAttribute("href", "/groups");