From 23b50aaa837e9471c7e12c2ce4ec9b1579ce3f99 Mon Sep 17 00:00:00 2001 From: OpenStaxClaude Date: Fri, 10 Apr 2026 12:41:05 +0000 Subject: [PATCH 01/14] WIP: Add NavBar plain CSS file for CORE-1701 migration - Create NavBar.css with all styled-components converted to plain CSS - Includes fadeIn animation, responsive breakpoints, z-index management - Uses CSS variables for theme values (colors, spacing, z-indices) - Follows PLAIN_CSS_MIGRATION_GUIDE patterns Related to CORE-1701 Co-Authored-By: Claude Sonnet 4.5 Co-Authored-By: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/app/components/NavBar/NavBar.css | 232 +++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 src/app/components/NavBar/NavBar.css diff --git a/src/app/components/NavBar/NavBar.css b/src/app/components/NavBar/NavBar.css new file mode 100644 index 0000000000..fb763cd88a --- /dev/null +++ b/src/app/components/NavBar/NavBar.css @@ -0,0 +1,232 @@ +/* NavBar Component Styles - Migrated from styled-components to plain CSS */ + +/* Keyframe animations */ +@keyframes fadeIn { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +/* BarWrapper - Main navbar container */ +.navbar-bar-wrapper { + overflow: visible; + z-index: var(--navbar-z-index, 30); + background: var(--navbar-bg, #fff); + position: relative; + padding: 0 var(--navbar-padding-desktop, 3.2rem); + box-shadow: 0 0.2rem 0.2rem 0 rgba(0, 0, 0, 0.1); +} + +@media print { + .navbar-bar-wrapper { + display: none; + } +} + +@media screen and (max-width: 75em) { + .navbar-bar-wrapper { + padding: 0 var(--navbar-padding-mobile, 1.6rem); + } +} + +/* TopBar - Inner navbar container with flex layout */ +.navbar-topbar { + overflow: visible; + display: flex; + justify-content: space-between; + align-items: center; + height: var(--navbar-height-desktop, 6rem); + max-width: var(--navbar-max-width, 128rem); + margin: 0 auto; +} + +@media screen and (max-width: 75em) { + .navbar-topbar { + height: var(--navbar-height-mobile, 5.2rem); + } +} + +@media print { + .navbar-topbar { + display: none; + } +} + +/* HeaderImage - Logo image */ +.navbar-header-image { + display: block; + width: auto; + height: var(--navbar-logo-height-desktop, 3.5rem); +} + +@media screen and (max-width: 75em) { + .navbar-header-image { + height: var(--navbar-logo-height-mobile, 2.8rem); + } +} + +/* Link - Login/nav links */ +.navbar-link { + display: block; + letter-spacing: -0.072rem; + font-size: 1.8rem; + text-decoration: none; + font-weight: bold; + padding: 1rem 0; + color: var(--nav-text-color, #5f6163); + animation: 100ms fadeIn ease-out; +} + +.navbar-link:hover, +.navbar-link:active, +.navbar-link:focus { + color: var(--nav-text-hover, #424242); + padding-bottom: 0.6rem; + border-bottom: 0.4rem solid var(--nav-border-color, #63a524); +} + +@media screen and (max-width: 75em) { + .navbar-link { + font-size: 1.4rem; + letter-spacing: 0.02rem; + padding: 0.7rem 0; + } + + .navbar-link:hover, + .navbar-link:active, + .navbar-link:focus { + padding: 0.7rem 0; + border-bottom: none; + } +} + +/* DropdownContainer - User profile dropdown container */ +.navbar-dropdown-container { + animation: 100ms fadeIn ease-out; + overflow: visible; + position: relative; + height: 100%; + display: flex; + align-items: center; +} + +@media screen and (max-width: 75em) { + .navbar-dropdown-container { + height: auto; + } +} + +/* Mobile Menu Overlay */ +.navbar-mobile-menu-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: var(--mobile-overlay-bg, rgba(255, 255, 255, 0.98)); + z-index: var(--mobile-overlay-z-index, 31); +} + +/* Mobile Menu Modal */ +.navbar-mobile-menu-modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + flex-direction: column; + align-items: center; + outline: none; +} + +.navbar-mobile-menu-modal [role="dialog"] { + outline: none; +} + +/* Dropdown Overlay - Mobile menu content */ +.navbar-dropdown-overlay { + padding-top: calc(20vh + 5vw); + display: flex; + flex-direction: column; + align-items: center; + overflow: visible; +} + +.navbar-dropdown-overlay > div { + width: min-content; + overflow: visible; +} + +/* Overlay Logo */ +.navbar-overlay-logo { + width: auto; + height: var(--navbar-logo-height-mobile, 2.8rem); + position: absolute; + left: 1.6rem; + top: var(--navbar-overlay-logo-top, 1.2rem); /* (navMobileHeight - headerImageMobileHeight) / 2 = (5.2 - 2.8) / 2 = 1.2rem */ +} + +/* Overlay Heading */ +.navbar-overlay-heading { + font-size: 1.8rem; + font-weight: bold; + letter-spacing: -0.072rem; + line-height: 3rem; + color: var(--nav-text-color, #5f6163); + padding-bottom: 0; + margin: 0; +} + +/* Dropdown List - Menu items */ +.navbar-dropdown-list { + margin: 0; + padding: 0.6rem 0; + background: none; +} + +.navbar-dropdown-list > li { + overflow: visible; + display: block; +} + +.navbar-dropdown-list > li a { + overflow: visible; + white-space: nowrap; + display: block; + padding: 0 1rem; + font-size: 1.4rem; + font-weight: normal; + letter-spacing: -0.01rem; + line-height: 2.5rem; + color: var(--nav-text-color, #5f6163); + cursor: pointer; + text-decoration: none; +} + +.navbar-dropdown-list > li a:hover { + color: var(--link-hover, #0064a0); +} + +.navbar-dropdown-list > li a:focus-visible { + outline: 0.2rem solid var(--focus-outline-color, #007297); + outline-offset: 0.2rem; +} + +/* TimesIcon - Close button for mobile menu */ +.navbar-times-icon { + cursor: pointer; + border: none; + padding: 0; + background: none; + position: absolute; + height: var(--navbar-height-mobile, 5.2rem); + width: var(--navbar-height-mobile, 5.2rem); + top: 0; + right: 0; + color: var(--nav-text-color, #5f6163); +} From a87b8b87b48577000db6da54e8bfddd50c66f7f4 Mon Sep 17 00:00:00 2001 From: OpenStaxClaude Date: Fri, 10 Apr 2026 15:34:59 +0000 Subject: [PATCH 02/14] Migrate NavBar component from styled-components to plain CSS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complete the NavBar migration by updating index.tsx to use plain CSS classes and CSS variables instead of styled-components. Changes: - Create constants.ts to export navDesktopHeight, navMobileHeight, and maxNavWidth - Replace all Styled.* components with plain HTML elements + className - Import and use react-aria-components ModalOverlay and Modal directly - Add CSS variable bindings from theme for colors, z-indices, and dimensions - Migrate Redux connect() HOCs to useSelector hooks for: - ConnectedNavigationBar (default export) - ConnectedLoginButton - Import theme and Color for CSS variable values - Import Times component directly instead of through styled wrapper - Import NavBar.css for styles The component now follows the plain CSS migration pattern: - CSS classes defined in NavBar.css - Theme values bound as CSS variables via style prop - Redux hooks instead of connect() HOC - All functionality preserved with improved performance 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/app/components/NavBar/constants.ts | 5 + src/app/components/NavBar/index.tsx | 163 ++++++++++++++++++------- 2 files changed, 127 insertions(+), 41 deletions(-) create mode 100644 src/app/components/NavBar/constants.ts diff --git a/src/app/components/NavBar/constants.ts b/src/app/components/NavBar/constants.ts new file mode 100644 index 0000000000..07e84f696f --- /dev/null +++ b/src/app/components/NavBar/constants.ts @@ -0,0 +1,5 @@ +import { contentWrapperMaxWidth } from '../../content/components/constants'; + +export const maxNavWidth = contentWrapperMaxWidth; +export const navDesktopHeight = 6.0; +export const navMobileHeight = 5.2; diff --git a/src/app/components/NavBar/index.tsx b/src/app/components/NavBar/index.tsx index 9d7892935e..1e19f0c2d9 100644 --- a/src/app/components/NavBar/index.tsx +++ b/src/app/components/NavBar/index.tsx @@ -1,22 +1,26 @@ import React, { FunctionComponent, useEffect, useState } from 'react'; import { FormattedMessage, useIntl } from 'react-intl'; -import { connect } from 'react-redux'; -import { Dialog } from 'react-aria-components'; +import { useSelector } from 'react-redux'; +import { Dialog, ModalOverlay, Modal } from 'react-aria-components'; import { ProfileMenu, ProfileMenuButton, ProfileMenuItem, UserIcon } from '@openstax/ui-components'; +import classNames from 'classnames'; +import Color from 'color'; import openstaxLogo from '../../../assets/logo.svg'; import * as authSelect from '../../auth/selectors'; import * as selectHighlights from '../../content/highlights/selectors'; import { User } from '../../auth/types'; import * as selectNavigation from '../../navigation/selectors'; import { AppState } from '../../types'; -import * as Styled from './styled'; import * as guards from '../../guards'; import showConfirmation from '../../content/highlights/components/utils/showConfirmation'; import { useServices } from '../../context/Services'; import { assertWindow } from '../../utils'; import { useMatchMobileQuery } from '../../reactUtils'; +import theme from '../../theme'; +import Times from '../Times'; +import './NavBar.css'; -export { maxNavWidth, navDesktopHeight, navMobileHeight } from './styled'; +export { maxNavWidth, navDesktopHeight, navMobileHeight } from './constants'; if (typeof(window) !== 'undefined') { import(/* webpackChunkName: "focus-within-polyfill" */ 'focus-within-polyfill'); @@ -46,25 +50,60 @@ export const MobileDropdown: FunctionComponent<{ const intl = useIntl(); return ( - - + + - +
- onOpenChange(false)} /> +
- {msg => {msg}} + {msg =>

{msg}

}
- +
  • {msg => ( @@ -99,12 +138,12 @@ export const MobileDropdown: FunctionComponent<{ )}
  • - +
    - +
    -
    -
    + + ); }; @@ -155,7 +194,7 @@ const LoggedInState: FunctionComponent<{ // On desktop, render ProfileMenu with its popover if (isMobile) { return ( - +
    setOverlayOpen(true)} data-testid='user-nav-toggle' @@ -174,12 +213,12 @@ const LoggedInState: FunctionComponent<{ onOpenChange={setOverlayOpen} onAction={handleAction} /> - +
    ); } return ( - +
    - +
    ); }; const LoggedOutState: FunctionComponent<{currentPath: string}> = ({currentPath}) => - {(msg) => {msg} - } + {(msg) => + {msg} + } ; -export const ConnectedLoginButton = connect( - (state: AppState) => ({ - currentPath: selectNavigation.pathname(state), - }) -)(LoggedOutState); +export const ConnectedLoginButton = () => { + const currentPath = useSelector((state: AppState) => selectNavigation.pathname(state)); + return ; +}; interface NavigationBarProps { user?: User; @@ -215,6 +263,7 @@ interface NavigationBarProps { params: unknown; hasUnsavedHighlight: boolean; } + const NavigationBar = ({user, loggedOut, currentPath, hasUnsavedHighlight, params}: NavigationBarProps) => { const logoUrl = guards.isPortaled(params) ? `/${params.portalName}/` : '/'; const unsavedHighlightsHandler = useUnsavedHighlightsValidator(hasUnsavedHighlight); @@ -222,31 +271,63 @@ const NavigationBar = ({user, loggedOut, currentPath, hasUnsavedHighlight, param return ( - - +
    +
    unsavedHighlightsHandler(e, logoUrl)} > - {loggedOut && } {user && } - - +
    +
    + ); +}; + +const ConnectedNavigationBar = () => { + const currentPath = useSelector((state: AppState) => selectNavigation.pathname(state)); + const params = useSelector((state: AppState) => selectNavigation.params(state)); + const loggedOut = useSelector((state: AppState) => authSelect.loggedOut(state)); + const user = useSelector((state: AppState) => authSelect.user(state)); + const hasUnsavedHighlight = useSelector((state: AppState) => selectHighlights.hasUnsavedHighlight(state)); + + return ( + ); }; -export default connect( - (state: AppState) => ({ - currentPath: selectNavigation.pathname(state), - params: selectNavigation.params(state), - loggedOut: authSelect.loggedOut(state), - user: authSelect.user(state), - hasUnsavedHighlight: selectHighlights.hasUnsavedHighlight(state), - }) -)(NavigationBar); +export default ConnectedNavigationBar; From 92a80c9c1b90a37456073bd732dca56c79d81e1a Mon Sep 17 00:00:00 2001 From: OpenStaxClaude Date: Fri, 10 Apr 2026 15:40:57 +0000 Subject: [PATCH 03/14] Migrate Footer component from styled-components to plain CSS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes: - Created Footer.css with all styled-components converted to plain CSS classes - Updated Footer/index.tsx to use plain CSS with className instead of Styled.* components - Migrated social media icons (Facebook, Twitter, Instagram, LinkedIn) to inline SVG - Wrapped icon components with styled() for component selector compatibility - Bound CSS variables from theme for dynamic values (colors, backgrounds) - Preserved complex CSS grid layouts for responsive behavior - Maintained vertical nav toolbar styling with clamp() calculation - Used classNames package for className composition - Preserved all functionality including: - Normal footer with mission, columns, social links - Portal footer variant - Contact dialog integration - Manage cookies links - Responsive breakpoints (37.5em, 37.6em, 60.1em) Migration follows the patterns established in PLAIN_CSS_MIGRATION_GUIDE.md and PLAIN_CSS_MIGRATION_LEARNINGS.md. All CSS variables are set before spreading style props to allow overrides. Note: Tests will be updated in a separate commit per task requirements. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/app/components/Footer/Footer.css | 475 +++++++++++++++++++++++++++ src/app/components/Footer/index.tsx | 348 +++++++++++++++----- 2 files changed, 737 insertions(+), 86 deletions(-) create mode 100644 src/app/components/Footer/Footer.css diff --git a/src/app/components/Footer/Footer.css b/src/app/components/Footer/Footer.css new file mode 100644 index 0000000000..b8b45cd4e2 --- /dev/null +++ b/src/app/components/Footer/Footer.css @@ -0,0 +1,475 @@ +/* Footer Component Styles */ + +/* Base footer styles */ +.footer-wrapper { + z-index: 0; + opacity: 1; + transition: opacity 0.2s; + font-size: 1.6rem; + font-weight: normal; + letter-spacing: normal; + line-height: 2.5rem; + color: var(--footer-text-color, #d5d5d5); +} + +/* Hide footer when printing */ +@media print { + .footer-wrapper { + display: none; + } +} + +/* Vertical nav toolbar styling when nav is closed + * IMPORTANT: These breakpoint values are derived from constants.ts: + * + * 90em = remsToEms(contentWrapperMaxWidth + verticalNavbarMaxWidth * 2) + * = remsToEms(128 + 8 * 2) = remsToEms(144) = 144 * 10/16 = 90em + * (exported as contentWrapperAndNavWidthBreakpoint) + * + * If constants.ts values change, this breakpoint MUST be updated accordingly. + * The conversion formula is: rems * 10 / 16 = ems (because 1rem = 10px, 1em = 16px in media queries) + */ +@media (min-width: 60.1em) and (max-width: 90em) { + .footer-wrapper--vertical-nav-toolbar { + padding-left: clamp( + 0rem, + calc(8rem - (100vw - 128rem) / 2), + 8rem + ); + } +} + +.footer-inner { + color: var(--footer-text-color, #d5d5d5); + display: grid; +} + +/* Top section */ +.footer-top { + background-color: var(--footer-top-bg, #424242); +} + +/* Top section responsive padding */ +@media (min-width: 60.1em) { + .footer-top { + padding: 7rem 0; + } +} + +@media (max-width: 37.5em) { + .footer-top { + padding: 2rem 0; + } +} + +@media (max-width: 60.1em) and (min-width: 37.6em) { + .footer-top { + padding: 4rem 0; + } +} + +/* Top boxed layout - complex grid */ +.footer-top-boxed { + max-width: calc(128rem + 1.5rem * 2); + align-items: center; + display: grid; + flex-flow: column nowrap; + margin: 0 auto; + padding-left: 1.5rem; + padding-right: 1.5rem; + width: 100%; + grid-row-gap: 2rem; + overflow: visible; +} + +@media (min-width: 37.6em) { + .footer-top-boxed { + align-items: start; + grid-column-gap: 4rem; + grid-template: + "headline col1 col2 col3" "mission col1 col2 col3" / minmax(auto, 50rem) + auto auto auto; + } +} + +@media (max-width: 37.5em) { + .footer-top-boxed { + grid-template: "headline" "mission" "col1" "col2" "col3"; + } +} + +@media (min-width: 60.1em) { + .footer-top-boxed { + grid-column-gap: 8rem; + } +} + +/* Heading */ +.footer-heading { + grid-area: headline; + margin: 0; +} + +@media (min-width: 37.6em) { + .footer-heading { + font-size: 2.4rem; + font-weight: bold; + letter-spacing: -0.096rem; + line-height: normal; + } +} + +@media (max-width: 37.5em) { + .footer-heading { + font-size: 2rem; + font-weight: bold; + letter-spacing: -0.08rem; + line-height: normal; + } +} + +/* Mission section */ +.footer-mission { + grid-area: mission; +} + +@media (min-width: 37.6em) { + .footer-mission { + font-size: 1.8rem; + font-weight: normal; + letter-spacing: normal; + line-height: 3rem; + } +} + +.footer-mission a { + color: var(--footer-text-color, #d5d5d5); + font-weight: bold; + text-underline-position: under; +} + +.footer-mission a:hover, +.footer-mission a:active, +.footer-mission a:focus { + color: inherit; +} + +/* Column base styles */ +.footer-column { + display: grid; + grid-gap: 0.5rem; + overflow: visible; +} + +.footer-column-1 { + grid-area: col1; +} + +.footer-column-2 { + grid-area: col2; +} + +.footer-column-3 { + grid-area: col3; +} + +.footer-column-heading { + font-size: 1.8rem; + font-weight: bold; + letter-spacing: -0.072rem; + line-height: normal; + margin: 0; +} + +@media (max-width: 37.5em) { + .footer-column-heading { + line-height: 4.5rem; + } +} + +/* Footer links */ +.footer-link { + color: var(--footer-text-color, #d5d5d5); + text-decoration: none; +} + +.footer-link:hover { + text-decoration: underline; +} + +.footer-link:hover, +.footer-link:active, +.footer-link:focus { + color: inherit; +} + +@media (max-width: 37.5em) { + .footer-link { + line-height: 4.5rem; + } +} + +/* Footer button (styled as link) */ +.footer-button { + font-size: 1.6rem; + font-weight: normal; + letter-spacing: normal; + line-height: 2.5rem; + color: var(--footer-text-color, #d5d5d5); + text-decoration: none; + cursor: pointer; + padding: 0; + border: none; + background-color: transparent; +} + +.footer-button:hover { + text-decoration: underline; +} + +.footer-button:hover, +.footer-button:active, +.footer-button:focus { + color: inherit; +} + +@media (max-width: 37.5em) { + .footer-button { + line-height: 4.5rem; + } +} + +/* Manage cookies link - special override for ui-components */ +.footer-manage-cookies-link.footer-manage-cookies-link { + font-size: 1.6rem; + font-weight: normal; + letter-spacing: normal; + line-height: 2.5rem; + color: var(--footer-text-color, #d5d5d5); + text-decoration: none; + text-align: left; +} + +.footer-manage-cookies-link.footer-manage-cookies-link:hover { + text-decoration: underline; +} + +.footer-manage-cookies-link.footer-manage-cookies-link:hover, +.footer-manage-cookies-link.footer-manage-cookies-link:active, +.footer-manage-cookies-link.footer-manage-cookies-link:focus { + color: inherit; +} + +@media (max-width: 37.5em) { + .footer-manage-cookies-link.footer-manage-cookies-link { + line-height: 4.5rem; + } +} + +/* Flex manage cookies link */ +.footer-manage-cookies-flex-link { + font-size: 1.6rem; + font-weight: normal; + letter-spacing: normal; + line-height: 2.5rem; + color: var(--footer-text-color, #d5d5d5); + text-decoration: none; + cursor: pointer; + padding: 0; + border: none; + background-color: transparent; +} + +.footer-manage-cookies-flex-link:hover { + text-decoration: underline; +} + +.footer-manage-cookies-flex-link:hover, +.footer-manage-cookies-flex-link:active, +.footer-manage-cookies-flex-link:focus { + color: inherit; +} + +/* Link list wrapper */ +.footer-link-list-wrapper { + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 0.5rem; + list-style: none; +} + +/* Bottom link */ +.footer-bottom-link { + color: var(--footer-text-color, #d5d5d5); + display: inline-grid; + grid-auto-flow: column; + grid-column-gap: 0.7rem; + overflow: hidden; +} + +.footer-bottom-link:hover, +.footer-bottom-link:active, +.footer-bottom-link:focus { + color: inherit; +} + +/* Bottom section */ +.footer-bottom { + font-size: 1.2rem; + font-weight: normal; + letter-spacing: normal; + line-height: normal; + background-color: var(--footer-bottom-bg, #3b3b3b); +} + +@media (min-width: 37.6em) { + .footer-bottom { + padding: 2.5rem 0; + } +} + +@media (max-width: 37.5em) { + .footer-bottom { + padding: 1.5rem; + } +} + +/* Bottom boxed layout */ +.footer-bottom-boxed { + max-width: calc(128rem + 1.5rem * 2); + align-items: center; + display: grid; + flex-flow: column nowrap; + margin: 0 auto; + padding-left: 1.5rem; + padding-right: 1.5rem; + width: 100%; + grid-gap: 1.5rem 4rem; + overflow: visible; +} + +@media (min-width: 37.6em) { + .footer-bottom-boxed { + grid-auto-flow: column; + } +} + +@media (max-width: 37.5em) { + .footer-bottom-boxed { + padding: 0; + } +} + +/* Portal bottom boxed layout */ +.footer-portal-bottom-boxed { + max-width: calc(128rem + 1.5rem * 2); + align-items: start; + display: grid; + flex-flow: column nowrap; + margin: 0 auto; + padding-left: 1.5rem; + padding-right: 1.5rem; + width: 100%; + grid-gap: 1.5rem 4rem; + overflow: visible; +} + +@media (min-width: 37.6em) { + .footer-portal-bottom-boxed { + grid-template: "col1 col2 col3" / minmax(auto, 70rem) auto auto; + } +} + +@media (max-width: 37.5em) { + .footer-portal-bottom-boxed { + padding: 0; + grid-template: + "col1" + "col2" + "col3"; + } +} + +/* Copyrights */ +.footer-copyrights { + display: grid; + grid-gap: 1rem; + overflow: visible; +} + +.footer-copyrights [data-html="copyright"] { + overflow: visible; +} + +.footer-copyrights a { + color: var(--footer-text-color, #d5d5d5); + overflow: visible; +} + +.footer-copyrights a:hover, +.footer-copyrights a:active, +.footer-copyrights a:focus { + color: inherit; +} + +.footer-copyrights sup { + font-size: 66%; + margin-left: 0.1rem; + position: relative; + top: -0.25em; + vertical-align: top; +} + +/* Social menu */ +.footer-social { + align-items: center; + display: grid; + grid-auto-flow: column; + grid-gap: 1rem; + justify-content: end; + list-style: none; + overflow: visible; +} + +/* Social icon */ +.footer-social-icon { + font-size: 1.6rem; + background-color: var(--footer-social-icon-bg, #767676); + color: var(--footer-social-icon-color, #fff); + align-content: center; + border-radius: 50%; + display: grid; + height: 3rem; + justify-content: center; + overflow: hidden; + width: 3rem; +} + +/* Icon styles */ +.footer-icon { + height: 1em; +} + +/* Footer logo */ +.footer-logo { + height: 4.7rem; + transform: translateY(0.2rem); +} + +/* Contact dialog */ +.footer-contact-dialog > div > div { + width: 75vw; + height: 75vh; +} + +.footer-contact-dialog > div > div > header { + margin-bottom: 0; +} + +.footer-contact-dialog > div > div > iframe { + border: none; + width: 100%; + height: 100%; +} diff --git a/src/app/components/Footer/index.tsx b/src/app/components/Footer/index.tsx index a0744fa90a..03b1a821db 100644 --- a/src/app/components/Footer/index.tsx +++ b/src/app/components/Footer/index.tsx @@ -1,6 +1,8 @@ import React from 'react'; import { FormattedMessage, useIntl } from 'react-intl'; import { useDispatch } from 'react-redux'; +import classNames from 'classnames'; +import styled from 'styled-components/macro'; import { useAnalyticsEvent } from '../../../helpers/analytics'; import { openKeyboardShortcutsMenu as openKeyboardShortcutsMenuAction } from '../../content/keyboardShortcuts/actions'; import RiceWhiteLogo from '../../../assets/rice-logo-white.png'; @@ -9,12 +11,16 @@ import { isVerticalNavOpenConnector } from '../../content/components/utils/sideb import { State } from '../../content/types'; import * as selectNavigation from '../../navigation/selectors'; import * as guards from '../../guards'; -import * as Styled from './styled'; import { MessageEvent } from '@openstax/types/lib.dom'; import { useSelector } from 'react-redux'; import { captureOpeningElement } from '../../content/utils/focusManager'; import { useModalFocusManagement } from '../../content/hooks/useModalFocusManagement'; +import { ManageCookiesLink as RawCookiesLink } from '@openstax/ui-components'; +import theme from '../../theme'; +import Modal from '../Modal'; +import './Footer.css'; +// Constants const fbUrl = 'https://www.facebook.com/openstax'; const twitterUrl = 'https://twitter.com/openstax'; const instagramUrl = 'https://www.instagram.com/openstax/'; @@ -24,16 +30,136 @@ const copyrightLink = 'https://creativecommons.org/licenses/by/4.0/'; export const supportCenterLink = 'https://help.openstax.org/s/'; const systemStatusLink = 'https://status.openstax.org/'; const newsletterLink = 'http://www2.openstax.org/l/218812/2016-10-04/lvk'; +const textColor = '#d5d5d5'; -const Mission = htmlMessage( - 'i18n:footer:copyright:mission-text', - Styled.Mission +// Icon components +interface IconProps extends React.SVGAttributes { + className?: string; +} + +/** + * Facebook icon for social media links. + * SVG path from Font Awesome Free (https://fontawesome.com - MIT License) + * + * Note: Wrapped with styled() to enable styled-components styling + */ +function FacebookIconBase({ className, ...props }: IconProps) { + return ( + + ); +} + +const FBIcon = styled(FacebookIconBase)` + height: 1em; +`; + +/** + * X (Twitter) icon for social media links. + * SVG path from Font Awesome Free (https://fontawesome.com - MIT License) + */ +function XTwitterBase({ className, ...props }: IconProps) { + return ( + + ); +} + +const TwitterIcon = styled(XTwitterBase)` + height: 1em; +`; + +/** + * Instagram icon for social media links. + * SVG path from Font Awesome Free (https://fontawesome.com - MIT License) + * + * Note: Wrapped with styled() to enable styled-components styling + */ +function InstagramIconBase({ className, ...props }: IconProps) { + return ( + + ); +} + +const IGIcon = styled(InstagramIconBase)` + height: 1em; +`; + +/** + * LinkedIn icon for social media links. + * SVG path from Font Awesome Free (https://fontawesome.com - MIT License) + * + * Note: Wrapped with styled() to enable styled-components styling + */ +function LinkedInIconBase({ className, ...props }: IconProps) { + return ( + + ); +} + +const LinkedInIcon = styled(LinkedInIconBase)` + height: 1em; +`; + +// Plain div wrappers for htmlMessage +const MissionDiv = ({ children, ...props }: React.HTMLAttributes) => ( +
    {children}
    ); -const Copyrights = htmlMessage( - 'i18n:footer:copyright:bottom-text', - Styled.Copyrights + +const CopyrightsDiv = ({ children, ...props }: React.HTMLAttributes) => ( +
    {children}
    ); +const Mission = htmlMessage('i18n:footer:copyright:mission-text', MissionDiv); +const Copyrights = htmlMessage('i18n:footer:copyright:bottom-text', CopyrightsDiv); + const BareMessage: React.FunctionComponent<{ id: string }> = ({ id }) => ( {msg => msg} ); @@ -41,9 +167,9 @@ const BareMessage: React.FunctionComponent<{ id: string }> = ({ id }) => ( const ColumnHeadingMessage: React.FunctionComponent<{ id: string }> = ({ id, }) => ( - +

    - +

    ); const FooterLinkMessage: React.FunctionComponent<{ @@ -52,13 +178,14 @@ const FooterLinkMessage: React.FunctionComponent<{ target?: string; rel?: string; }> = ({ id, href, target, rel }) => ( - - + ); const SocialIconMessage: React.FunctionComponent<{ @@ -66,21 +193,28 @@ const SocialIconMessage: React.FunctionComponent<{ href: string; Icon: React.ComponentType; }> = ({ id, href, Icon }) => ( - - - +
  • + + + +
  • ); function LinkList({ children }: React.PropsWithChildren<{}>) { return ( - + {React.Children.toArray(children).map((c, i) =>
  • {c}
  • )} - +
    ); } @@ -88,7 +222,7 @@ const OpenKeyboardShortcutsLink = () => { const dispatch = useDispatch(); const trackOpenCloseKS = useAnalyticsEvent('openCloseKeyboardShortcuts'); - const openKeyboardShortcutsMenu = (event: React.MouseEvent) => { + const openKeyboardShortcutsMenu = (event: React.MouseEvent) => { event.preventDefault(); captureOpeningElement('keyboardshortcuts'); dispatch(openKeyboardShortcutsMenuAction()); @@ -98,20 +232,24 @@ const OpenKeyboardShortcutsLink = () => { return ( {(txt) => ( - {txt} - + )} ); }; const Column1 = () => ( - +
    @@ -130,11 +268,11 @@ const Column1 = () => ( /> - +
    ); const Column2 = () => ( - +
    @@ -146,11 +284,11 @@ const Column2 = () => ( /> - +
    ); const Column3 = () => ( - +
    ( href='/privacy-policy' id='i18n:footer:column3:privacy-policy' /> - + - + - +
    ); const SocialDirectory = () => ( - + - - - - +
  • + + {useIntl().formatMessage({ + +
  • +
    ); function getValues() { @@ -215,36 +358,51 @@ const NormalFooter = ({ }: { isVerticalNavOpen: State['tocOpen']; }) => ( - - - - - +
    +
    +
    +

    - +

    - - - - +
    +
    +
    +
    - - - - +
    +
    +
    + ); const PortalColumn1 = () => ( - +
    - +
    ); export function ContactDialog({ @@ -275,14 +433,14 @@ export function ContactDialog({ const { closeButtonRef } = useModalFocusManagement({ modalId: 'contactdialog', isOpen }); return !isOpen ? null : ( -