Skip to content

Commit c1811a5

Browse files
Core 898 landing flex footer (#2726)
* Pull copyright and cookie yes toggle out of default footer * Add footer for landing layout flex pages * Add hover for cookie yes button in footer Seemed like this should underline on hover like the links, but maybe not. * Small fixes to flex landing page footer style * Add contact dialog * Update tests * Export isFlexPage from fallback-to Use export in landing layout instead of duplicating condition * Slight improvements to tests Check for more differences between layouts Better handling of props in flex footer test * Fix layouts tests * Reuse user-event user in flex footer test * Set source_url in contact form params
1 parent b5f92ff commit c1811a5

File tree

10 files changed

+378
-21
lines changed

10 files changed

+378
-21
lines changed

src/app/components/shell/router-helpers/fallback-to.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,16 @@ export default function FallbackTo({name}) {
2121
return <LoadedPage name={name} data={data} />;
2222
}
2323

24+
export const isFlexPage = (data) => (
25+
typeof data.meta?.type === 'string' &&
26+
['pages.FlexPage', 'pages.RootPage'].includes(data.meta.type)
27+
);
28+
2429
// eslint-disable-next-line complexity
2530
function LoadedPage({data, name}) {
2631
const {setLayoutParameters, layoutParameters} = useLayoutContext();
2732
const hasError = 'error' in data;
28-
const isFlex =
29-
!hasError &&
30-
['pages.FlexPage', 'pages.RootPage'].includes(data.meta.type);
33+
const isFlex = !hasError && isFlexPage(data);
3134

3235
React.useEffect(() => {
3336
if (isFlex) {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import React from 'react';
2+
3+
export default function CookieYesToggle() {
4+
return (
5+
<button
6+
type="button"
7+
className="cky-banner-element small"
8+
>
9+
Manage cookies
10+
</button>
11+
);
12+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import React from 'react';
2+
import RawHTML from '~/components/jsx-helpers/raw-html';
3+
4+
type Props = {
5+
copyright: string | undefined;
6+
apStatement: string | undefined;
7+
}
8+
9+
export default function Copyright({copyright, apStatement}: Props) {
10+
const updatedCopyright = copyright
11+
? copyright.replace(/-\d+/, `-${new Date().getFullYear()}`)
12+
: copyright;
13+
14+
return (
15+
<React.Fragment>
16+
<RawHTML html={updatedCopyright} />
17+
<RawHTML Tag="ap-html" html={apStatement} />
18+
</React.Fragment>
19+
);
20+
}

src/app/layouts/default/footer/footer.js

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import React from 'react';
22
import RawHTML from '~/components/jsx-helpers/raw-html';
33
import LoaderPage from '~/components/jsx-helpers/loader-page';
4+
import Copyright from './copyright';
5+
import CookieYesToggle from './cookie-yes-toggle';
46
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
57
import {faFacebookF} from '@fortawesome/free-brands-svg-icons/faFacebookF';
68
import {faXTwitter} from '@fortawesome/free-brands-svg-icons/faXTwitter';
@@ -23,8 +25,6 @@ function Footer({
2325
facebookLink, twitterLink, linkedinLink
2426
}
2527
}) {
26-
const updatedCopyright = copyright ? copyright.replace(/-\d+/, `-${new Date().getFullYear()}`) : copyright;
27-
2828
return (
2929
<React.Fragment>
3030
<div className="top">
@@ -61,8 +61,7 @@ function Footer({
6161
<div className="bottom">
6262
<div className="boxed">
6363
<div className="copyrights">
64-
<RawHTML html={updatedCopyright} />
65-
<RawHTML Tag="ap-html" html={apStatement} />
64+
<Copyright copyright={copyright} apStatement={apStatement} />
6665
</div>
6766
<ul className="social">
6867
<li>
@@ -127,17 +126,6 @@ function Footer({
127126
);
128127
}
129128

130-
function CookieYesToggle() {
131-
return (
132-
<button
133-
type="button"
134-
className="cky-banner-element small"
135-
>
136-
Manage cookies
137-
</button>
138-
);
139-
}
140-
141129
export default function FooterLoader() {
142130
return (
143131
<footer className="page-footer" data-analytics-nav="Footer">

src/app/layouts/default/footer/footer.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,11 @@ $lower-footer: #3b3b3b;
119119
line-height: inherit;
120120
padding: inherit;
121121
text-align: inherit;
122+
text-decoration: none;
123+
124+
&:hover {
125+
text-decoration: underline;
126+
}
122127
}
123128

124129
&.col1 {
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
@import 'pattern-library/core/pattern-library/headers';
2+
3+
.contact-dialog {
4+
width: 75vw;
5+
height: 75vh;
6+
7+
.main-region, iframe {
8+
width: 100%;
9+
height: 100%;
10+
}
11+
}
12+
13+
.page-footer {
14+
color: text-color(footer);
15+
display: grid;
16+
17+
.flex-page {
18+
background-color: ui-color(footer-bg);
19+
20+
a {
21+
color: inherit;
22+
}
23+
24+
@include width-up-to($phone-max) {
25+
padding: $normal-margin;
26+
}
27+
28+
@include wider-than($phone-max) {
29+
padding: 2.5rem 0;
30+
}
31+
32+
.boxed {
33+
display: grid;
34+
grid-row-gap: 2rem;
35+
36+
@include width-up-to($phone-max) {
37+
grid-template: 'copyrights'
38+
'col1'
39+
'col2';
40+
}
41+
42+
@include wider-than($phone-max) {
43+
align-items: start;
44+
grid-column-gap: 4rem;
45+
grid-template: 'copyrights col1 col2' / minmax(auto, 70rem) auto auto;
46+
}
47+
48+
@include wider-than($tablet-max) {
49+
grid-column-gap: 8rem;
50+
}
51+
}
52+
53+
.list-of-links {
54+
list-style-type: none;
55+
padding: 0;
56+
margin: 0;
57+
display: flex;
58+
flex-direction: column;
59+
gap: 0.5rem;
60+
}
61+
62+
.copyrights {
63+
@include body-font(1.2rem);
64+
display: grid;
65+
grid-area: copyrights;
66+
grid-gap: 1rem;
67+
}
68+
69+
.column {
70+
display: grid;
71+
grid-gap: 0.5rem;
72+
73+
a {
74+
@include width-up-to($phone-max) {
75+
padding: 1.15rem 0;
76+
}
77+
text-decoration: none;
78+
79+
&:hover {
80+
text-decoration: underline;
81+
}
82+
}
83+
84+
button {
85+
background-color: transparent;
86+
color: inherit;
87+
display: inline;
88+
font-size: inherit;
89+
font-weight: inherit;
90+
line-height: inherit;
91+
padding: inherit;
92+
text-align: inherit;
93+
text-decoration: none;
94+
95+
&:hover {
96+
text-decoration: underline;
97+
}
98+
}
99+
100+
&.col1 {
101+
grid-area: col1;
102+
}
103+
104+
&.col2 {
105+
grid-area: col2;
106+
}
107+
}
108+
}
109+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import React from 'react';
2+
import Copyright from '~/layouts/default/footer/copyright';
3+
import CookieYesToggle from '~/layouts/default/footer/cookie-yes-toggle';
4+
import LoaderPage from '~/components/jsx-helpers/loader-page';
5+
import {useDialog} from '~/components/dialog/dialog';
6+
import './flex.scss';
7+
8+
type Props = {
9+
data: {
10+
copyright: string | undefined;
11+
apStatement: string | undefined;
12+
}
13+
}
14+
15+
function ListOfLinks({children}: React.PropsWithChildren<object>) {
16+
return (
17+
<ul className="list-of-links">
18+
{React.Children.toArray(children).map((c, i) => (<li key={i}>{c}</li>))}
19+
</ul>
20+
);
21+
}
22+
23+
export function useContactDialog() {
24+
const [Dialog, open, close] = useDialog();
25+
26+
function ContactDialog(
27+
{contactFormParams, className}: {
28+
contactFormParams?: {key: string; value: string}[];
29+
className?: string;
30+
}
31+
) {
32+
const contactFormUrl = React.useMemo(() => {
33+
const formUrl = 'https://openstax.org/embedded/contact';
34+
35+
if (contactFormParams !== undefined) {
36+
const params = contactFormParams
37+
.map(({key, value}) => encodeURIComponent(`${key}=${value}`))
38+
.map((p) => `body=${p}`)
39+
.join('&');
40+
41+
return `${formUrl}?${params}`;
42+
}
43+
44+
return formUrl;
45+
}, [contactFormParams]);
46+
47+
React.useEffect(
48+
() => {
49+
const closeOnSubmit = ({data}: MessageEvent) => {
50+
if (data === 'CONTACT_FORM_SUBMITTED') {
51+
close();
52+
}
53+
};
54+
55+
window.addEventListener('message', closeOnSubmit, false);
56+
return () => {
57+
window.removeEventListener('message', closeOnSubmit, false);
58+
};
59+
},
60+
[]
61+
);
62+
63+
return (
64+
<Dialog className={className}>
65+
<iframe id="contact-us" src={contactFormUrl} />
66+
</Dialog>
67+
);
68+
};
69+
70+
return {ContactDialog, open};
71+
}
72+
73+
function FlexFooter({data}: Props) {
74+
const {ContactDialog, open: openContactDialog} = useContactDialog();
75+
const contactFormParams = [{key: 'source_url', value: window.location.href}];
76+
77+
return (
78+
<div className="flex-page">
79+
<div className="boxed">
80+
<div className="copyrights">
81+
<Copyright {...data} />
82+
</div>
83+
<div className="column col1">
84+
<ListOfLinks>
85+
<button onClick={openContactDialog}>
86+
Contact Us
87+
<ContactDialog
88+
className="contact-dialog"
89+
contactFormParams={contactFormParams}
90+
/>
91+
</button>
92+
<a href="/tos">Terms of Use</a>
93+
<a href="/privacy">Privacy Notice</a>
94+
</ListOfLinks>
95+
</div>
96+
<div className="column col2">
97+
<ListOfLinks>
98+
<a href="/accessibility-statement">Accessibility Statement</a>
99+
<a href="/privacy">Privacy Notice</a>
100+
<CookieYesToggle />
101+
</ListOfLinks>
102+
</div>
103+
</div>
104+
</div>
105+
);
106+
}
107+
108+
export default function FooterLoader() {
109+
return (
110+
<footer className="page-footer" data-analytics-nav="Footer">
111+
<LoaderPage slug="footer" Child={FlexFooter} />
112+
</footer>
113+
);
114+
}

src/app/layouts/landing/landing.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
import React from 'react';
22
import Header from './header/header';
3-
import Footer from '../default/footer/footer';
43
import {SalesforceContextProvider} from '~/contexts/salesforce';
54
import useMainClassContext, {
65
MainClassContextProvider
76
} from '~/contexts/main-class';
87
import useLanguageContext from '~/contexts/language';
98
import ReactModal from 'react-modal';
109
import cn from 'classnames';
11-
import {LinkFields} from '../../pages/flex-page/components/Link';
10+
import { LinkFields } from '../../pages/flex-page/components/Link';
11+
import JITLoad from '~/helpers/jit-load';
12+
import { isFlexPage } from '~/components/shell/router-helpers/fallback-to';
1213
import './landing.scss';
1314

1415
type Props = {
1516
data: {
1617
title: string;
18+
meta?: {
19+
type: string;
20+
},
1721
layout: Array<{
1822
value: {
1923
navLinks: LinkFields[];
@@ -23,6 +27,14 @@ type Props = {
2327
};
2428
};
2529

30+
function Footer({data}: Props) {
31+
const importFn = isFlexPage(data)
32+
? () => import('~/layouts/landing/footer/flex')
33+
: () => import('~/layouts/default/footer/footer');
34+
35+
return <JITLoad importFn={importFn} />;
36+
}
37+
2638
export default function LandingLayout({
2739
children,
2840
data
@@ -44,7 +56,7 @@ export default function LandingLayout({
4456
</MainClassContextProvider>
4557
</SalesforceContextProvider>
4658
<footer id="footer">
47-
<Footer />
59+
<Footer data={data} />
4860
</footer>
4961
</React.Fragment>
5062
);

0 commit comments

Comments
 (0)