Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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