diff --git a/packages/unity-bootstrap-theme/src/scss/_unity-bootstrap-theme-extends.scss b/packages/unity-bootstrap-theme/src/scss/_unity-bootstrap-theme-extends.scss index 958037087..55ad80a7b 100644 --- a/packages/unity-bootstrap-theme/src/scss/_unity-bootstrap-theme-extends.scss +++ b/packages/unity-bootstrap-theme/src/scss/_unity-bootstrap-theme-extends.scss @@ -45,3 +45,4 @@ @import 'extends/image-based-cards'; @import 'extends/image-based-card-and-hover'; @import 'extends/ranking-cards'; +@import 'extends/highly-ranked'; diff --git a/packages/unity-bootstrap-theme/src/scss/extends/_highly-ranked.scss b/packages/unity-bootstrap-theme/src/scss/extends/_highly-ranked.scss new file mode 100644 index 000000000..2eb63481d --- /dev/null +++ b/packages/unity-bootstrap-theme/src/scss/extends/_highly-ranked.scss @@ -0,0 +1,94 @@ +@import "../_unity-bootstrap-theme-variables.scss"; + +.highly-ranked-section { + padding-top: $uds-size-spacing-12; + padding-bottom: $uds-size-spacing-12; + background-color: $faint; + + @media (max-width: map-get($grid-breakpoints, "lg")) { + padding-top: $uds-size-spacing-6; + padding-bottom: $uds-size-spacing-6; + } + + .highly-ranked-title { + margin-bottom: $uds-size-spacing-4; + + .highlight-gold { + display: inline-block; + padding: $uds-size-spacing-half; + background-color: $gold; + color: $dark; + line-height: 1.1; + } + } + + .highly-ranked-description { + font-size: 1.25rem; + line-height: $uds-size-spacing-3 + 0.25rem; + color: $dark; + margin-bottom: $uds-size-spacing-4; + + @media (max-width: map-get($grid-breakpoints, "md")) { + font-size: 1rem; + } + } + + .highly-ranked-cta { + margin-bottom: $uds-size-spacing-4; + } + + .highly-ranked-grid { + margin-top: $uds-size-spacing-4; + } + + .highly-ranked-card { + height: 100%; + display: flex; + flex-direction: column; + justify-content: flex-start; + gap: $uds-size-spacing-1; + + .ranking-value-container { + border-left: 8px solid $gold; + padding-left: $uds-size-spacing-2; + margin-bottom: 0; + + @media (max-width: map-get($grid-breakpoints, "md")) { + padding-left: $uds-size-spacing-3; + } + } + + .ranking-content { + padding-left: calc(8px + #{$uds-size-spacing-2}); + + @media (max-width: map-get($grid-breakpoints, "md")) { + padding-left: calc(8px + #{$uds-size-spacing-3}); + } + } + + .ranking-value { + font-size: $uds-size-font-xxxl; + font-weight: $font-weight-bold; + margin: 0; + line-height: 1.1; + color: $dark; + + @media (max-width: map-get($grid-breakpoints, "md")) { + font-size: 3rem; // 48px + } + } + + .ranking-title { + font-size: 1.5rem; + font-weight: $font-weight-bold; + margin: 0 0 $uds-size-spacing-1 0; + color: $dark; + } + + .ranking-description { + font-size: 1rem; + color: $dark; + margin-bottom: 0; + } + } +} diff --git a/packages/unity-react-core/src/components/HighlyRanked/HighlyRanked.jsx b/packages/unity-react-core/src/components/HighlyRanked/HighlyRanked.jsx new file mode 100644 index 000000000..39844dfd5 --- /dev/null +++ b/packages/unity-react-core/src/components/HighlyRanked/HighlyRanked.jsx @@ -0,0 +1,110 @@ +// @ts-check +import { sanitizeDangerousMarkup } from "@asu/shared"; +import classNames from "classnames"; +import PropTypes from "prop-types"; +import React from "react"; + +import { Button } from "../Button/Button"; + +/** + * @typedef {Object} RankingItem + * @property {string} value - The main ranking value (e.g., "400+", "#2") + * @property {string} title - The title of the ranking (e.g., "‘prestigious faculty’") + * @property {string} description - The description of the ranking + */ + +/** + * @typedef {Object} HighlyRankedProps + * @property {string} [title] - The section title + * @property {string} [description] - The section description + * @property {string} [ctaText] - The text for the CTA button + * @property {string} [ctaUrl] - The URL for the CTA button + * @property {"dark" | "gold" | "maroon" | "gray"} [ctaButtonColor] - The color of the CTA button + * @property {RankingItem[]} [rankings] - The list of rankings to display + */ + +/** + * @param {HighlyRankedProps} props + * @returns {JSX.Element} + */ +export const HighlyRanked = ({ + title = "Highly ranked", + description = "", + ctaText = "", + ctaUrl = "", + ctaButtonColor = "dark", + rankings = [], +}) => { + return ( +
+
+
+
+ {title && ( +

+ {title} +

+ )} + {description && ( +

+ )} + {ctaText && ctaUrl && ( +

+
+ )} +
+
+ +
+ {rankings.map((ranking, index) => ( +
+
+
+

{ranking.value}

+
+
+ {ranking.title && ( +

{ranking.title}

+ )} + {ranking.description && ( +

+ )} +

+
+
+ ))} +
+
+
+ ); +}; + +HighlyRanked.propTypes = { + title: PropTypes.string, + description: PropTypes.string, + ctaText: PropTypes.string, + ctaUrl: PropTypes.string, + rankings: PropTypes.arrayOf( + PropTypes.shape({ + value: PropTypes.string.isRequired, + title: PropTypes.string, + description: PropTypes.string, + }) + ), + ctaButtonColor: PropTypes.oneOf(["dark", "gold", "maroon", "gray"]), +}; diff --git a/packages/unity-react-core/src/components/HighlyRanked/HighlyRanked.stories.jsx b/packages/unity-react-core/src/components/HighlyRanked/HighlyRanked.stories.jsx new file mode 100644 index 000000000..1a605496f --- /dev/null +++ b/packages/unity-react-core/src/components/HighlyRanked/HighlyRanked.stories.jsx @@ -0,0 +1,65 @@ +import React from "react"; +import { HighlyRanked } from "./HighlyRanked"; + +export default { + title: "Components/HighlyRanked", + component: HighlyRanked, +}; + +const Template = args => ; + +export const Default = Template.bind({}); +Default.args = { + title: "ASU is highly ranked", + description: + "ASU consistently ranks among the best in the nation, recognized for our commitment to excellence and showcasing our dedication to providing quality education and impactful research.", + ctaText: "See more ASU rankings", + ctaUrl: "https://www.asu.edu/rankings", + rankings: [ + { + value: "400+", + title: "‘prestigious faculty’", + description: "National Academies-honored faculty", + }, + { + value: "#2", + title: "in the U.S. for employability", + description: "among public universities", + }, + { + value: "83", + title: "top-ranked programs", + description: "Ranked in the top 25 in the U.S., including 38 in the top 10", + }, + { + value: "$6.1", + title: "billion", + description: "FY24 economic impact on the state’s gross product", + }, + { + value: "Top 10", + title: "worldwide among universities granted U.S. patents", + description: "For two years", + }, + { + value: "270+", + title: "athletic championships", + description: "National and conference titles", + }, + { + value: "620,000+", + title: "alumni", + description: "Leading, shaping, changing our world", + }, + { + value: "Association of American Universities (AAU) member", + title: "", + description: "Along with Harvard, Stanford and MIT", + }, + { + value: "Top producer of elite scholars", + title: "", + description: "For 10 consecutive years", + }, + ], +}; diff --git a/packages/unity-react-core/src/components/HighlyRanked/HighlyRanked.test.jsx b/packages/unity-react-core/src/components/HighlyRanked/HighlyRanked.test.jsx new file mode 100644 index 000000000..b234d817f --- /dev/null +++ b/packages/unity-react-core/src/components/HighlyRanked/HighlyRanked.test.jsx @@ -0,0 +1,40 @@ +import { render, screen } from "@testing-library/react"; +import React from "react"; +import { expect, describe, it } from "vitest"; +import { HighlyRanked } from "./HighlyRanked"; + +describe("HighlyRanked component", () => { + const mockProps = { + title: "ASU is highly ranked", + description: "Test description", + ctaText: "CTA Rank", + ctaUrl: "#", + rankings: [ + { + value: "400+", + title: "faculty", + description: "description 1", + }, + ], + }; + + it("should render the title and description", () => { + render(); + expect(screen.getByText("ASU is highly ranked")).toBeInTheDocument(); + expect(screen.getByText("Test description")).toBeInTheDocument(); + }); + + it("should render the CTA button", () => { + render(); + const cta = screen.getByText("CTA Rank"); + expect(cta).toBeInTheDocument(); + expect(cta.closest("a")).toHaveAttribute("href", "#"); + }); + + it("should render the ranking cards", () => { + render(); + expect(screen.getByText("400+")).toBeInTheDocument(); + expect(screen.getByText("faculty")).toBeInTheDocument(); + expect(screen.getByText("description 1")).toBeInTheDocument(); + }); +}); diff --git a/packages/unity-react-core/src/components/HighlyRanked/init.js b/packages/unity-react-core/src/components/HighlyRanked/init.js new file mode 100644 index 000000000..984a01a02 --- /dev/null +++ b/packages/unity-react-core/src/components/HighlyRanked/init.js @@ -0,0 +1 @@ +export { initHighlyRanked as default } from "../../core/utils"; diff --git a/packages/unity-react-core/src/components/index.js b/packages/unity-react-core/src/components/index.js index 18baecc30..5fcfb35c7 100644 --- a/packages/unity-react-core/src/components/index.js +++ b/packages/unity-react-core/src/components/index.js @@ -10,6 +10,7 @@ export * from "./CardArrangement/CardArrangement"; export * from "./Divider/Divider"; export * from "./FeedAnatomy"; export * from "./Hero/Hero"; +export * from "./HighlyRanked/HighlyRanked"; export * from "./Image/Image"; export * from "./Pagination/Pagination"; export * from "./RankingCard/RankingCard"; diff --git a/packages/unity-react-core/src/core/utils/index.js b/packages/unity-react-core/src/core/utils/index.js index c49883b17..d9bb50281 100644 --- a/packages/unity-react-core/src/core/utils/index.js +++ b/packages/unity-react-core/src/core/utils/index.js @@ -20,6 +20,7 @@ import { import { Divider } from "../../components/Divider/Divider"; import { GridLinks } from "../../components/GridLinks/GridLinks"; import { Hero } from "../../components/Hero/Hero"; +import { HighlyRanked } from "../../components/HighlyRanked/HighlyRanked"; import { Image } from "../../components/Image/Image"; import { List } from "../../components/List/List"; import { Pagination } from "../../components/Pagination/Pagination"; @@ -169,3 +170,9 @@ export const initTooltip = ({ targetSelector, props }) => */ export const initList = ({ targetSelector, props }) => RenderReact(List, props, document.querySelector(targetSelector)); + +/** + * @param {ComponentProps} props + */ +export const initHighlyRanked = ({ targetSelector, props }) => + RenderReact(HighlyRanked, props, document.querySelector(targetSelector));