Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/shared/assets/img/named/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ export namespace imageName {
let poly1: string;
let requestFormInformation: string;
let ws2DefaultImagev01Final: string;
let moreGradsMoreInnovation: string;
}
2 changes: 2 additions & 0 deletions packages/shared/assets/img/named/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import * as intro01 from "./intro01.jpg";
import * as poly1 from "./poly01.jpg";
import * as requestFormInformation from "./request-form-information.jpeg";
import * as ws2DefaultImagev01Final from "./WS2-DefaultImagev01-Final.png";
import * as moreGradsMoreInnovation from "./more-grads-more-innovation.png"

export const imageName = {
anon: anon.default,
Expand All @@ -44,4 +45,5 @@ export const imageName = {
poly1: poly1.default,
requestFormInformation: requestFormInformation.default,
ws2DefaultImagev01Final: ws2DefaultImagev01Final.default,
moreGradsMoreInnovation: moreGradsMoreInnovation.default,
};
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
112 changes: 112 additions & 0 deletions packages/unity-bootstrap-theme/src/scss/extends/_cards.scss
Original file line number Diff line number Diff line change
Expand Up @@ -775,3 +775,115 @@ Cards - Table of Contents
flex-direction: column;
justify-content: center;
}

/*------------------------------------------------------------------
12. Content Spotlight
--------------------------------------------------------------------*/

.content-spotlight {
position: relative;
background-color: #000;
overflow: hidden;
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
padding: 20rem $uds-size-spacing-4 $uds-size-spacing-6 $uds-size-spacing-4;
min-height: 600px;

@include media-breakpoint-up(lg) {
padding: $uds-size-spacing-12 $uds-size-spacing-6;
min-height: auto;
}
}

.content-spotlight-image-container {
position: absolute;
inset: 0;
pointer-events: none;
}

.content-spotlight-image-wrapper {
position: relative;
width: 100%;
height: 100%;
left: 0;
}

.content-spotlight-image {
width: 85%;
height: 100%;
object-fit: cover;
position: absolute;
top: 0;
left: 15%;

@include media-breakpoint-down(lg) {
height: 55%;
width: 100%;
left: 0;
top: 0;
object-position: center;
}
}

.content-spotlight-overlay {
position: absolute;
inset: 0;
background-image: linear-gradient(180deg, rgba(25,25,25,0) 0%, rgba(25,25,25,0) 25%, rgba(25,25,25,1) 45%);
z-index: 1;

@include media-breakpoint-up(lg) {
background-image: linear-gradient(86.74deg, #000000 25%, rgba(0, 0, 0, 0) 55.34%);
}
}

.content-spotlight-content {
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 1rem;
width: 100%;
margin-top: auto;

@include media-breakpoint-up(lg) {
gap: .75rem;
}

& > *:last-child {
margin-top: 0.75rem;
}

@include media-breakpoint-up(lg) {
max-width: 50%;
margin-top: 0;
}
}

.content-spotlight-icon {
color: $uds-color-base-gold;
font-size: 25px;
}

.content-spotlight-title {
color: $uds-color-base-white;
margin: 0;

.highlight {
color: $uds-color-base-gold;
}
}

.content-spotlight-description {
font-family: $uds-font-family-base;
font-size: $uds-size-font-large;
color: $uds-color-base-white;
margin: 0;
max-width: 100%;

@include media-breakpoint-up(lg) {
max-width: 420px;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// @ts-check
import PropTypes from "prop-types";
import React from "react";
import { sanitizeDangerousMarkup } from "@asu/shared";

/**
* @typedef {Object} ContentSpotlightProps
* @property {string} backgroundImage - URL of the background image
* @property {string|string[]} [icon] - FontAwesome icon name (e.g. 'graduation-cap') or array ['fab', 'icon-name']
* @property {string|React.ReactNode} title - Main title
* @property {string} [highlightText] - Text to highlight in gold within the title.
* @property {string} [description] - Body text
* @property {Object} [button] - Button config
* @property {string} button.label - Button text
* @property {string} button.href - Button URL
* @property {string} button.color - Button color
*/

/**
* @param {ContentSpotlightProps} props
* @returns {JSX.Element}
*/
export const ContentSpotlight = ({
backgroundImage,
icon,
title,
highlightText,
description,
button,
}) => {
// Icon rendering helper
const renderIcon = () => {
if (!icon) return null;
let iconClasses = "";
if (Array.isArray(icon)) {
iconClasses = icon.join(" ");
} else {
iconClasses = icon.includes(" ") ? icon : `fas fa-${icon}`;
}

return <i className={iconClasses} aria-hidden="true" />;
};

return (
<div className="content-spotlight">
<div className="content-spotlight-image-container">
<div className="content-spotlight-image-wrapper">
<div className="content-spotlight-overlay" />
<img
src={backgroundImage}
alt=""
aria-hidden="true"
className="content-spotlight-image"
/>
</div>
</div>

<div className="content-spotlight-content">
{icon && <div className="content-spotlight-icon">{renderIcon()}</div>}

<h2 className="content-spotlight-title">
{title}
{highlightText && (
<>
{title && highlightText && <br />}
<span className="highlight">{highlightText}</span>
</>
)}
</h2>

{description && (
<p
className="content-spotlight-description"
dangerouslySetInnerHTML={sanitizeDangerousMarkup(description)}
/>
)}

{button && (
<a
href={button.href}
className={`btn ${button.color ? `btn-${button.color}` : "btn-gold"}`}
>
{button.label}
</a>
)}
</div>
</div>
);
};

ContentSpotlight.propTypes = {
backgroundImage: PropTypes.string.isRequired,
icon: PropTypes.oneOfType([
PropTypes.string,
PropTypes.arrayOf(PropTypes.string),
]),
title: PropTypes.node.isRequired,
highlightText: PropTypes.string,
description: PropTypes.string,
button: PropTypes.shape({
label: PropTypes.string.isRequired,
href: PropTypes.string.isRequired,
color: PropTypes.string,
}),
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// @ts-check
import React from "react";
import { ContentSpotlight } from "./ContentSpotlight";
import {imageName} from "@asu/shared";

export default {
title: "Components/ContentSpotlight",
component: ContentSpotlight,
parameters: {
layout: "fullscreen", // Hero components usually full width
docs: {
description: {
component:
"A spotlight component with background image, gradient overlay, and highlighted text.",
},
},
},
argTypes: {
backgroundImage: { control: "text" },
icon: { control: "text" },
title: { control: "text" },
highlightText: { control: "text" },
description: { control: "text" },
},
};

const Template = args => <ContentSpotlight {...args} />;

export const Default = Template.bind({});
Default.args = {
backgroundImage: imageName.moreGradsMoreInnovation,
icon: "graduation-cap",
title: "More grads,",
highlightText: "more innovation",
description:
"ASU graduates more than 33,700 thinkers, innovators and master learners every year – more than any other public university in the U.S.",
button: {
label: "See more ASU facts and figures",
href: "#",
color: "gold",
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// @ts-check
import { render, screen } from "@testing-library/react";
import React from "react";
import { expect, describe, test, afterEach } from "vitest";

import { ContentSpotlight } from "./ContentSpotlight";

const defaultArgs = {
backgroundImage: "https://example.com/image.jpg",
icon: "graduation-cap",
title: "Test Title",
highlightText: "Highlight",
description: "Test description",
button: {
label: "Test Button",
href: "#",
color: "gold",
},
};

describe("ContentSpotlight", () => {
test("renders with title and highlight", () => {
render(<ContentSpotlight {...defaultArgs} />);
expect(screen.getByText("Test Title")).toBeInTheDocument();
expect(screen.getByText("Highlight")).toBeInTheDocument();
});

test("renders button", () => {
render(<ContentSpotlight {...defaultArgs} />);
const button = screen.getByRole("link", { name: "Test Button" });
expect(button).toBeInTheDocument();
expect(button).toHaveAttribute("href", "#");
});

test("renders icon", () => {
const { container } = render(<ContentSpotlight {...defaultArgs} />);
expect(container.querySelector(".fa-graduation-cap")).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { initContentSpotlight as default } from "../../core/utils";
1 change: 1 addition & 0 deletions packages/unity-react-core/src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * from "./ButtonIconOnly/ButtonIconOnly";
export * from "./ButtonTag/ButtonTag";
export * from "./Card/Card";
export * from "./CardArrangement/CardArrangement";
export * from "./ContentSpotlight/ContentSpotlight";
export * from "./Divider/Divider";
export * from "./FeedAnatomy";
export * from "./Hero/Hero";
Expand Down
7 changes: 7 additions & 0 deletions packages/unity-react-core/src/core/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
initImageCarousel,
initImageGalleryCarousel,
} from "../../components/ComponentCarousel/ComponentCarousel";
import { ContentSpotlight } from "../../components/ContentSpotlight/ContentSpotlight";
import { Divider } from "../../components/Divider/Divider";
import { GridLinks } from "../../components/GridLinks/GridLinks";
import { Hero } from "../../components/Hero/Hero";
Expand Down Expand Up @@ -94,6 +95,12 @@ export const initCard = ({ targetSelector, props }) =>
export const initCardArrangement = ({ targetSelector, props }) =>
RenderReact(CardArrangement, props, document.querySelector(targetSelector));

/**
* @param {ComponentProps} props
*/
export const initContentSpotlight = ({ targetSelector, props }) =>
RenderReact(ContentSpotlight, props, document.querySelector(targetSelector));

/**
* @param {ComponentProps} props
*/
Expand Down