From 79cb9b1e8411139d8c09842d59fa95ac9b4c88c7 Mon Sep 17 00:00:00 2001 From: jomcarvajal Date: Tue, 7 Oct 2025 11:24:47 -0600 Subject: [PATCH 01/11] Adding Banner folder --- .lastsync | 1 + package.json | 6 +- src/components/Banner/Banner.spec.tsx | 25 + src/components/Banner/Banner.stories.tsx | 15 + src/components/Banner/Banner.styles.tsx | 54 + src/components/Banner/Banner.tsx | 23 + .../Banner/GracePeriodNotification.spec.tsx | 16 + .../GracePeriodNotification.stories.tsx | 9 + .../Banner/GracePeriodNotification.tsx | 18 + src/components/Button.tsx | 4 +- src/components/DismissIcon.spec.tsx | 18 + src/components/DismissIcon.stories.tsx | 5 + src/components/DismissIcon.tsx | 12 + src/components/Error.tsx | 2 +- src/components/Html.spec.tsx | 40 + src/components/Html.stories.tsx | 11 + src/components/Html.tsx | 14 + .../{ => MessageBox}/MessageBox.spec.tsx | 0 .../{ => MessageBox}/MessageBox.stories.tsx | 2 +- .../{ => MessageBox}/MessageBox.styles.tsx | 2 +- .../{ => MessageBox}/MessageBox.tsx | 0 src/index.ts | 8 +- yarn.lock | 1572 +++++++++-------- 23 files changed, 1087 insertions(+), 770 deletions(-) create mode 100644 .lastsync create mode 100644 src/components/Banner/Banner.spec.tsx create mode 100644 src/components/Banner/Banner.stories.tsx create mode 100644 src/components/Banner/Banner.styles.tsx create mode 100644 src/components/Banner/Banner.tsx create mode 100644 src/components/Banner/GracePeriodNotification.spec.tsx create mode 100644 src/components/Banner/GracePeriodNotification.stories.tsx create mode 100644 src/components/Banner/GracePeriodNotification.tsx create mode 100644 src/components/DismissIcon.spec.tsx create mode 100644 src/components/DismissIcon.stories.tsx create mode 100644 src/components/DismissIcon.tsx create mode 100644 src/components/Html.spec.tsx create mode 100644 src/components/Html.stories.tsx create mode 100644 src/components/Html.tsx rename src/components/{ => MessageBox}/MessageBox.spec.tsx (100%) rename src/components/{ => MessageBox}/MessageBox.stories.tsx (86%) rename src/components/{ => MessageBox}/MessageBox.styles.tsx (93%) rename src/components/{ => MessageBox}/MessageBox.tsx (100%) diff --git a/.lastsync b/.lastsync new file mode 100644 index 000000000..94f729137 --- /dev/null +++ b/.lastsync @@ -0,0 +1 @@ +9014da9bbe05bd45f38841c02e34dcb5a8e3e1d8 diff --git a/package.json b/package.json index d04714737..5938c1470 100644 --- a/package.json +++ b/package.json @@ -33,11 +33,11 @@ "styled-components": "*" }, "devDependencies": { - "npm-run-all": "^4.1.5", "@ladle/react": "^2.1.2", - "@openstax/ts-utils": "^1.27.6", + "@openstax/ts-utils": "^1.32.5", "@playwright/test": "^1.25.0", "@testing-library/dom": "^10.4.0", + "@types/dompurify": "^3.0.0", "@testing-library/jest-dom": "^6.4.8", "@testing-library/react": "^12.0.0", "@testing-library/user-event": "^14.5.2", @@ -59,6 +59,7 @@ "jest-environment-node": "^29.6.2", "microbundle": "^0.15.1", "node-fetch": "<3.0.0", + "npm-run-all": "^4.1.5", "react": "^17.0.2", "react-dom": "^17.0.2", "react-is": "^16.8.0", @@ -72,6 +73,7 @@ "@sentry/react": "^7.120.3", "classnames": "^2.3.1", "crypto": "npm:crypto-browserify@^3.12.0", + "dompurify": "^3.0.1", "react-aria": "^3.37.0", "react-aria-components": "1.10.1", "stream": "npm:stream-browserify@^3.0.0" diff --git a/src/components/Banner/Banner.spec.tsx b/src/components/Banner/Banner.spec.tsx new file mode 100644 index 000000000..814de8101 --- /dev/null +++ b/src/components/Banner/Banner.spec.tsx @@ -0,0 +1,25 @@ +import { Banner } from "./Banner"; +import renderer from 'react-test-renderer'; + +describe('Banner', () => { + it('matches snapshot (single message, no dismiss)', () => { + const tree = renderer.create( + + ).toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('matches snapshot (multiple messages, with dismiss)', () => { + const tree = renderer.create( + {}} /> + ).toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('matches snapshot (error, with dismiss)', () => { + const tree = renderer.create( + {}} /> + ).toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); \ No newline at end of file diff --git a/src/components/Banner/Banner.stories.tsx b/src/components/Banner/Banner.stories.tsx new file mode 100644 index 000000000..44ca7295a --- /dev/null +++ b/src/components/Banner/Banner.stories.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { Banner } from './Banner'; + +export const Error = () => ; + +export const Warning = () => ; + +export const Note = () => ; + +export const MultipleMessages = () => ; + +export const Dismissible = () => { + const [visible, setVisible] = React.useState(true); + return visible ? setVisible(false)} /> : null; +}; \ No newline at end of file diff --git a/src/components/Banner/Banner.styles.tsx b/src/components/Banner/Banner.styles.tsx new file mode 100644 index 000000000..82b5c8031 --- /dev/null +++ b/src/components/Banner/Banner.styles.tsx @@ -0,0 +1,54 @@ +import styled from 'styled-components'; +import { Button, ButtonLink } from '../Button'; +import { colors } from '../../theme'; + +export type BannerSeverity = 'note' | 'warning' | 'error'; + +export const Severity = styled.span` + font-weight: bold; + text-transform: uppercase; +`; + +export const StyledBanner = styled.div<{severity: BannerSeverity}>` + position: relative; + background: ${({severity}) => severity === 'error' ? '#F8E8EA' : '#fff5e0'}; + color: ${({severity}) => severity === 'error' ? colors.palette.darkRed : '#976502'}; + border: ${({severity}) => severity === 'error' ? `1px solid ${colors.palette.lightRed}` : '1px solid #fdbd3e'}; + padding: .6rem 1.6rem; + margin: 0 0 1.6rem 0; + line-height: 2rem; + display: flex; + align-items: center; + + a { + text-decoration: none; + color: ${colors.palette.mediumBlue}; + + &:hover { + text-decoration: underline; + color: ${colors.link.hover} + } + } + + ${ButtonLink} { + font-size: 1.6rem; + } +`; + +export const CloseButton = styled(Button)<{severity: BannerSeverity}>` + color: ${({severity}) => severity === 'error' ? colors.palette.darkRed : '#976502'}; + overflow: visible; + background: none; + border: none; + padding: 0; + font: inherit; + cursor: pointer; + outline: inherit; + box-shadow: none; + margin-left: 2.4rem; + + &:not([disabled]):hover, + &:not([disabled]):active { + background: none; + } +`; \ No newline at end of file diff --git a/src/components/Banner/Banner.tsx b/src/components/Banner/Banner.tsx new file mode 100644 index 000000000..5946d6fc7 --- /dev/null +++ b/src/components/Banner/Banner.tsx @@ -0,0 +1,23 @@ +import { DismissIcon } from "../DismissIcon"; +import { Html } from "../Html"; +import { CloseButton, Severity, StyledBanner, BannerSeverity } from "./Banner.styles"; + +export const Banner = (props: {messages: string[]; severity: BannerSeverity; onDismiss?: () => void}) => { + const numWarnings = props.messages.length; + + return +
+ {props.severity !== 'error' ? {props.severity === 'note' ? 'Note: ' : 'Warning: '} : null} + {props.messages.map((message, i) => + 1} key={i}> + {numWarnings > 1 ? `[${i + 1} of ${numWarnings}]: ${message}`: message} + + )} +
+ {props.onDismiss + ? + + : null} +
; +}; \ No newline at end of file diff --git a/src/components/Banner/GracePeriodNotification.spec.tsx b/src/components/Banner/GracePeriodNotification.spec.tsx new file mode 100644 index 000000000..f1080394b --- /dev/null +++ b/src/components/Banner/GracePeriodNotification.spec.tsx @@ -0,0 +1,16 @@ +import { GracePeriodNotification } from "./GracePeriodNotification"; +import renderer from 'react-test-renderer'; + +describe('GracePeriodNotification', () => { + it('matches snapshot', () => { + const tree = renderer.create( + {}} + onDismiss={() => {}} + /> + ).toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); \ No newline at end of file diff --git a/src/components/Banner/GracePeriodNotification.stories.tsx b/src/components/Banner/GracePeriodNotification.stories.tsx new file mode 100644 index 000000000..a0f0a4d81 --- /dev/null +++ b/src/components/Banner/GracePeriodNotification.stories.tsx @@ -0,0 +1,9 @@ +import { GracePeriodNotification } from "./GracePeriodNotification"; + +export const Default = () => + alert('handle checkout')} + onDismiss={() => alert('dismiss')} + />; \ No newline at end of file diff --git a/src/components/Banner/GracePeriodNotification.tsx b/src/components/Banner/GracePeriodNotification.tsx new file mode 100644 index 000000000..7582e8e1f --- /dev/null +++ b/src/components/Banner/GracePeriodNotification.tsx @@ -0,0 +1,18 @@ +import { ButtonLink } from "../Button"; +import { CloseButton, StyledBanner } from "./Banner.styles"; +import { DismissIcon } from "../DismissIcon"; + + +export const GracePeriodNotification = +(props: {date: string; paymentsFaqUrl: string; handleCheckout: () => void; onDismiss: () => void}) => + +
+ Note: + Your free access to this course expires on {props.date}. Purchase extended course access here or read more about payments on our FAQ page. +
+ + +
; \ No newline at end of file diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 569bcc0ab..fbf8968d7 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -43,12 +43,12 @@ interface ButtonOptions { type ButtonBase = React.ComponentPropsWithoutRef<'button'> & ButtonOptions; type LinkButtonBase = React.ComponentPropsWithoutRef<'a'> & ButtonOptions; -interface ButtonProps extends ButtonBase { +export interface ButtonProps extends ButtonBase { isWaiting?: never; waitingText?: never; } -interface WaitingButtonProps extends ButtonBase { +export interface WaitingButtonProps extends ButtonBase { isWaiting: boolean; waitingText: string; } diff --git a/src/components/DismissIcon.spec.tsx b/src/components/DismissIcon.spec.tsx new file mode 100644 index 000000000..7cd1d7864 --- /dev/null +++ b/src/components/DismissIcon.spec.tsx @@ -0,0 +1,18 @@ +import { DismissIcon } from './DismissIcon'; +import renderer from 'react-test-renderer'; + +describe('DismissIcon', () => { + it('matches body snapshot', () => { + const tree = renderer.create( + + ).toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('matches body snapshot (aria-hidden)', () => { + const tree = renderer.create( +