Add a static promo to the home page (#8400)
This commit is contained in:
Родитель
a181b75d34
Коммит
a5dd629f91
|
@ -119,6 +119,7 @@ module.exports = {
|
|||
'enableDevTools',
|
||||
'enableFeatureDiscoTaar',
|
||||
'enableFeatureExperienceSurvey',
|
||||
'enableFeatureHeroRecommendation',
|
||||
'enableFeatureRecommendedBadges',
|
||||
'enableRequestID',
|
||||
'enableStrictMode',
|
||||
|
@ -378,4 +379,6 @@ module.exports = {
|
|||
experiments: {},
|
||||
|
||||
extensionWorkshopUrl: 'https://extensionworkshop.com',
|
||||
|
||||
enableFeatureHeroRecommendation: false,
|
||||
};
|
||||
|
|
|
@ -32,4 +32,5 @@ module.exports = {
|
|||
},
|
||||
|
||||
extensionWorkshopUrl: 'https://extensionworkshop-dev.allizom.org',
|
||||
enableFeatureHeroRecommendation: true,
|
||||
};
|
||||
|
|
|
@ -31,4 +31,5 @@ module.exports = {
|
|||
},
|
||||
|
||||
extensionWorkshopUrl: 'https://extensionworkshop-dev.allizom.org',
|
||||
enableFeatureHeroRecommendation: true,
|
||||
};
|
||||
|
|
Двоичные данные
src/amo/components/HeroRecommendation/img/christin-hume-mfB1B1s4sMc-unsplash.png
Normal file
Двоичные данные
src/amo/components/HeroRecommendation/img/christin-hume-mfB1B1s4sMc-unsplash.png
Normal file
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 252 KiB |
|
@ -0,0 +1,124 @@
|
|||
/* @flow */
|
||||
import invariant from 'invariant';
|
||||
import * as React from 'react';
|
||||
import { compose } from 'redux';
|
||||
|
||||
import translate from 'core/i18n/translate';
|
||||
import type { I18nType } from 'core/types/i18n';
|
||||
|
||||
import promoImage from './img/christin-hume-mfB1B1s4sMc-unsplash.png';
|
||||
import './styles.scss';
|
||||
|
||||
type Props = {|
|
||||
body: string,
|
||||
heading: string,
|
||||
linkText: string,
|
||||
linkHref: string,
|
||||
|};
|
||||
|
||||
type InternalProps = {|
|
||||
...Props,
|
||||
i18n: I18nType,
|
||||
|};
|
||||
|
||||
export class HeroRecommendationBase extends React.Component<InternalProps> {
|
||||
renderOverlayShape() {
|
||||
const gradientA = 'HeroRecommendation-gradient-a';
|
||||
const gradientB = 'HeroRecommendation-gradient-b';
|
||||
|
||||
return (
|
||||
<svg
|
||||
className="HeroRecommendation-overlayShape"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 334 307"
|
||||
>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id={gradientA}
|
||||
x1="37.554%"
|
||||
x2="20.41%"
|
||||
y1="51.425%"
|
||||
y2="49.159%"
|
||||
>
|
||||
<stop
|
||||
className="HeroRecommendation-gradientA-startColor"
|
||||
offset="0%"
|
||||
/>
|
||||
<stop
|
||||
className="HeroRecommendation-gradientA-endColor"
|
||||
offset="100%"
|
||||
/>
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id={gradientB}
|
||||
x1="0%"
|
||||
x2="100%"
|
||||
y1="36.092%"
|
||||
y2="36.092%"
|
||||
>
|
||||
<stop
|
||||
className="HeroRecommendation-gradientB-startColor"
|
||||
offset="0%"
|
||||
/>
|
||||
<stop
|
||||
className="HeroRecommendation-gradientB-endColor"
|
||||
offset="100%"
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g fill="none">
|
||||
<path
|
||||
className="HeroRecommendation-solidSwoosh"
|
||||
d="M0 307h267c-52.113-48.37-94.22-112.103-120.226-188.126C122.438 47.736 63.457 3.044 0 0v307z"
|
||||
/>
|
||||
<path
|
||||
fill={`url(#${gradientA})`}
|
||||
d="M0 307c17.502-9.934 34.574-21.458 51.072-34.602C121.246 216.49 216.932 232.615 271 307H0z"
|
||||
transform="matrix(-1 0 0 1 334 0)"
|
||||
/>
|
||||
<path
|
||||
fill={`url(#${gradientB})`}
|
||||
d="M334 307H114c31.79-24.866 61.545-54.258 88.533-88.135C237.964 174.386 285.12 148.906 334 143v164z"
|
||||
transform="matrix(-1 0 0 1 334 0)"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { body, heading, i18n, linkText, linkHref } = this.props;
|
||||
|
||||
invariant(body, 'The body property is required');
|
||||
invariant(heading, 'The heading property is required');
|
||||
invariant(linkText, 'The linkText property is required');
|
||||
invariant(linkHref, 'The linkHref property is required');
|
||||
|
||||
// translators: If uppercase does not work in your locale, change it to lowercase.
|
||||
// This is used as a secondary heading.
|
||||
const recommended = i18n.gettext('RECOMMENDED');
|
||||
|
||||
return (
|
||||
<section className="HeroRecommendation HeroRecommendation-purple">
|
||||
<div>
|
||||
<img className="HeroRecommendation-image" alt="" src={promoImage} />
|
||||
</div>
|
||||
<div className="HeroRecommendation-info">
|
||||
<div className="HeroRecommendation-recommended">{recommended}</div>
|
||||
<h2 className="HeroRecommendation-heading">{heading}</h2>
|
||||
<p className="HeroRecommendation-body">{body}</p>
|
||||
<a className="HeroRecommendation-link" href={linkHref}>
|
||||
<span className="HeroRecommendation-linkText">{linkText}</span>
|
||||
</a>
|
||||
</div>
|
||||
{this.renderOverlayShape()}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const HeroRecommendation: React.ComponentType<Props> = compose(translate())(
|
||||
HeroRecommendationBase,
|
||||
);
|
||||
|
||||
export default HeroRecommendation;
|
|
@ -0,0 +1,174 @@
|
|||
@import '~amo/css/styles';
|
||||
|
||||
.HeroRecommendation {
|
||||
color: $white;
|
||||
display: flex;
|
||||
padding: 24px;
|
||||
position: relative;
|
||||
|
||||
@include respond-to(extraExtraLarge) {
|
||||
padding: 36px;
|
||||
|
||||
@include padding-start(96px);
|
||||
}
|
||||
}
|
||||
|
||||
.HeroRecommendation-info {
|
||||
@include margin-start(24px);
|
||||
|
||||
@include respond-to(extraLarge) {
|
||||
@include margin-start(96px);
|
||||
}
|
||||
|
||||
@include respond-to(extraExtraLarge) {
|
||||
@include margin-start(136px);
|
||||
}
|
||||
}
|
||||
|
||||
$overlayShapeZIndex: 1;
|
||||
|
||||
.HeroRecommendation-overlayShape {
|
||||
bottom: 0;
|
||||
height: 70%;
|
||||
left: 0;
|
||||
max-height: 307px;
|
||||
position: absolute;
|
||||
z-index: $overlayShapeZIndex;
|
||||
|
||||
[dir='rtl'] & {
|
||||
left: auto;
|
||||
right: 0;
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
}
|
||||
|
||||
.HeroRecommendation-image {
|
||||
height: 0;
|
||||
visibility: hidden;
|
||||
width: 0;
|
||||
|
||||
@include respond-to(medium) {
|
||||
height: auto;
|
||||
visibility: visible;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@include respond-to(extraExtraLarge) {
|
||||
width: 480px;
|
||||
}
|
||||
}
|
||||
|
||||
.HeroRecommendation-recommended {
|
||||
font-size: $font-size-s;
|
||||
letter-spacing: 0.1em;
|
||||
line-height: 1.143;
|
||||
margin: 0;
|
||||
opacity: 0.5;
|
||||
|
||||
@include respond-to(medium) {
|
||||
font-size: $font-size-default;
|
||||
}
|
||||
|
||||
@include respond-to(extraExtraLarge) {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.HeroRecommendation-heading {
|
||||
color: $white;
|
||||
font-size: $font-size-l;
|
||||
line-height: 1.188;
|
||||
margin: 14px 0 0 0;
|
||||
|
||||
@include respond-to(extraExtraLarge) {
|
||||
font-size: $font-size-xl;
|
||||
}
|
||||
}
|
||||
|
||||
.HeroRecommendation-body {
|
||||
font-size: $font-size-s;
|
||||
font-weight: normal;
|
||||
line-height: 1.666;
|
||||
margin: 24px 0;
|
||||
|
||||
@include respond-to(medium) {
|
||||
font-size: $font-size-m;
|
||||
}
|
||||
|
||||
@include respond-to(extraExtraLarge) {
|
||||
margin-bottom: 84px;
|
||||
}
|
||||
}
|
||||
|
||||
.HeroRecommendation-link {
|
||||
border: 4px solid $white;
|
||||
display: inline-block;
|
||||
font-size: $font-size-m;
|
||||
line-height: 1.25;
|
||||
padding: 12px 48px;
|
||||
// The theme styles will add a hover effect.
|
||||
transition: background-color $transition-medium ease-in-out;
|
||||
white-space: nowrap;
|
||||
|
||||
&,
|
||||
&:active,
|
||||
&:link,
|
||||
&:hover,
|
||||
&:visited {
|
||||
color: $white;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.HeroRecommendation-heading,
|
||||
.HeroRecommendation-body,
|
||||
.HeroRecommendation-link {
|
||||
position: relative;
|
||||
z-index: $overlayShapeZIndex + 1;
|
||||
}
|
||||
|
||||
.HeroRecommendation-purple {
|
||||
$heroPurple1: #20123a;
|
||||
$heroPurple2: #712290;
|
||||
$heroPurple3: #592acb;
|
||||
$heroPurple4: #312a65;
|
||||
$heroPurple5: #9059ff;
|
||||
$heroPurple6: #e31587;
|
||||
$heroPurple7: #312a65;
|
||||
|
||||
background-image: linear-gradient($heroPurple1, $heroPurple2);
|
||||
|
||||
.HeroRecommendation-gradientA-startColor {
|
||||
stop-color: $heroPurple3;
|
||||
}
|
||||
|
||||
.HeroRecommendation-gradientA-endColor {
|
||||
stop-color: $heroPurple4;
|
||||
}
|
||||
|
||||
.HeroRecommendation-gradientB-startColor {
|
||||
stop-color: $heroPurple5;
|
||||
}
|
||||
|
||||
.HeroRecommendation-gradientB-endColor {
|
||||
stop-color: $heroPurple6;
|
||||
}
|
||||
|
||||
.HeroRecommendation-solidSwoosh {
|
||||
fill: $heroPurple7;
|
||||
}
|
||||
|
||||
.HeroRecommendation-link {
|
||||
&,
|
||||
&:hover,
|
||||
&:active {
|
||||
// Fill the background on small screens in case the overlayShape
|
||||
// overlaps the button. Also, fill the background when hovering.
|
||||
background-color: $heroPurple2;
|
||||
}
|
||||
|
||||
@include respond-to(large) {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,12 +11,15 @@ import FeaturedCollectionCard from 'amo/components/FeaturedCollectionCard';
|
|||
import HomeHeroGuides from 'amo/components/HomeHeroGuides';
|
||||
import HeadLinks from 'amo/components/HeadLinks';
|
||||
import HeadMetaTags from 'amo/components/HeadMetaTags';
|
||||
import HeroRecommendation from 'amo/components/HeroRecommendation';
|
||||
import LandingAddonsCard from 'amo/components/LandingAddonsCard';
|
||||
import Link from 'amo/components/Link';
|
||||
import { fetchHomeAddons } from 'amo/reducers/home';
|
||||
import { makeQueryStringWithUTM } from 'amo/utils';
|
||||
import {
|
||||
ADDON_TYPE_EXTENSION,
|
||||
ADDON_TYPE_THEME,
|
||||
CLIENT_APP_ANDROID,
|
||||
INSTALL_SOURCE_FEATURED,
|
||||
SEARCH_SORT_POPULAR,
|
||||
SEARCH_SORT_RANDOM,
|
||||
|
@ -70,6 +73,7 @@ export class HomeBase extends React.Component {
|
|||
static propTypes = {
|
||||
_config: PropTypes.object,
|
||||
_getFeaturedCollectionsMetadata: PropTypes.func,
|
||||
clientApp: PropTypes.string.isRequired,
|
||||
collections: PropTypes.array.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
errorHandler: PropTypes.object.isRequired,
|
||||
|
@ -184,6 +188,7 @@ export class HomeBase extends React.Component {
|
|||
const {
|
||||
_config,
|
||||
_getFeaturedCollectionsMetadata,
|
||||
clientApp,
|
||||
collections,
|
||||
errorHandler,
|
||||
i18n,
|
||||
|
@ -242,6 +247,30 @@ export class HomeBase extends React.Component {
|
|||
|
||||
{errorHandler.renderErrorIfPresent()}
|
||||
|
||||
{_config.get('enableFeatureHeroRecommendation') &&
|
||||
clientApp !== CLIENT_APP_ANDROID ? (
|
||||
<HeroRecommendation
|
||||
// TODO: replace with a real value.
|
||||
// See https://github.com/mozilla/addons-frontend/issues/8406
|
||||
// This is a brand name so it should not be localized.
|
||||
heading="Forest Preserve Nougat (beta)"
|
||||
// TODO: replace with a real value.
|
||||
// See https://github.com/mozilla/addons-frontend/issues/8406
|
||||
body={`Lorem ipsum dolor sit amet, consectetur adipiscing elit,
|
||||
sed do eiusmod tempor incididunt ut labore et dolore magna
|
||||
aliqua. Sed augue lacus viverra vitae.`}
|
||||
linkText={i18n.gettext('Get Started')}
|
||||
// TODO: replace with a real value.
|
||||
// See https://github.com/mozilla/addons-frontend/issues/8406
|
||||
linkHref={`https://forest-preserve-nougat.com/${makeQueryStringWithUTM(
|
||||
{
|
||||
utm_content: 'homepage-primary-hero',
|
||||
utm_campaign: '',
|
||||
},
|
||||
)}`}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<HomeHeroGuides />
|
||||
|
||||
<LandingAddonsCard
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
/* @flow */
|
||||
import React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { fakeI18n } from 'tests/unit/helpers';
|
||||
|
||||
import { HeroRecommendationBase } from 'amo/components/HeroRecommendation';
|
||||
|
||||
const render = (moreProps = {}) => {
|
||||
const props = {
|
||||
body: `Lorem ipsum dolor sit amet, consectetur adipiscing elit,
|
||||
sed do eiusmod tempor incididunt ut labore et dolore magna
|
||||
aliqua. Sed augue lacus viverra vitae.`,
|
||||
heading: 'Forest Preserve Nougat (beta)',
|
||||
i18n: fakeI18n({ includeJedSpy: false }),
|
||||
linkHref: 'https://forest-preserve-nougat.com/',
|
||||
linkText: 'Get Started',
|
||||
...moreProps,
|
||||
};
|
||||
return <HeroRecommendationBase {...props} />;
|
||||
};
|
||||
|
||||
storiesOf('HeroRecommendation', module).add('default', () => {
|
||||
return render();
|
||||
});
|
|
@ -5,6 +5,7 @@ import 'core/css/inc/lib.scss';
|
|||
import './setup/styles.scss';
|
||||
|
||||
// Components
|
||||
import './amo/HeroRecommendation';
|
||||
import './ui/Badge';
|
||||
import './ui/Button';
|
||||
import './ui/IconRecommendedBadge';
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import HeroRecommendation, {
|
||||
HeroRecommendationBase,
|
||||
} from 'amo/components/HeroRecommendation';
|
||||
import { fakeI18n, shallowUntilTarget } from 'tests/unit/helpers';
|
||||
|
||||
describe(__filename, () => {
|
||||
const render = (moreProps = {}) => {
|
||||
const props = {
|
||||
body: 'Example of a promo description',
|
||||
heading: 'Promo Title Example',
|
||||
i18n: fakeI18n(),
|
||||
linkText: 'Get It Now',
|
||||
linkHref: 'https://promo-site.com/',
|
||||
...moreProps,
|
||||
};
|
||||
return shallowUntilTarget(
|
||||
<HeroRecommendation {...props} />,
|
||||
HeroRecommendationBase,
|
||||
);
|
||||
};
|
||||
|
||||
it('renders a heading', () => {
|
||||
const heading = 'Forest Preserve Nougat (beta)';
|
||||
const root = render({ heading });
|
||||
|
||||
expect(root.find('.HeroRecommendation-heading')).toHaveText(heading);
|
||||
});
|
||||
|
||||
it('renders a body', () => {
|
||||
const body = 'Change the way you shop with Forest Preserve Nougat.';
|
||||
const root = render({ body });
|
||||
|
||||
expect(root.find('.HeroRecommendation-body')).toHaveText(body);
|
||||
});
|
||||
|
||||
it('renders a link', () => {
|
||||
const linkText = 'Shop For Mall Music Now';
|
||||
const linkHref = 'https://internet-mall-music.com/';
|
||||
const root = render({ linkText, linkHref });
|
||||
|
||||
const link = root.find('.HeroRecommendation-link');
|
||||
expect(link).toHaveProp('href', linkHref);
|
||||
expect(link).toHaveText(linkText);
|
||||
});
|
||||
});
|
|
@ -13,6 +13,7 @@ import FeaturedCollectionCard from 'amo/components/FeaturedCollectionCard';
|
|||
import HomeHeroGuides from 'amo/components/HomeHeroGuides';
|
||||
import HeadLinks from 'amo/components/HeadLinks';
|
||||
import HeadMetaTags from 'amo/components/HeadMetaTags';
|
||||
import HeroRecommendation from 'amo/components/HeroRecommendation';
|
||||
import LandingAddonsCard from 'amo/components/LandingAddonsCard';
|
||||
import { fetchHomeAddons, loadHomeAddons } from 'amo/reducers/home';
|
||||
import { createInternalCollection } from 'amo/reducers/collections';
|
||||
|
@ -21,6 +22,8 @@ import {
|
|||
ADDON_TYPE_EXTENSION,
|
||||
ADDON_TYPE_THEME,
|
||||
ADDON_TYPE_THEMES_FILTER,
|
||||
CLIENT_APP_ANDROID,
|
||||
CLIENT_APP_FIREFOX,
|
||||
SEARCH_SORT_RANDOM,
|
||||
SEARCH_SORT_TRENDING,
|
||||
VIEW_CONTEXT_HOME,
|
||||
|
@ -501,6 +504,44 @@ describe(__filename, () => {
|
|||
expect(root.find(HeadLinks)).toHaveLength(1);
|
||||
});
|
||||
|
||||
describe('HeroRecommendation', () => {
|
||||
it('renders when enabled', () => {
|
||||
const { store } = dispatchClientMetadata({
|
||||
clientApp: CLIENT_APP_FIREFOX,
|
||||
});
|
||||
const root = render({
|
||||
_config: getFakeConfig({ enableFeatureHeroRecommendation: true }),
|
||||
store,
|
||||
});
|
||||
|
||||
expect(root.find(HeroRecommendation)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('does not render when enabled on Android', () => {
|
||||
const { store } = dispatchClientMetadata({
|
||||
clientApp: CLIENT_APP_ANDROID,
|
||||
});
|
||||
const root = render({
|
||||
_config: getFakeConfig({ enableFeatureHeroRecommendation: true }),
|
||||
store,
|
||||
});
|
||||
|
||||
expect(root.find(HeroRecommendation)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('does not render when disabled', () => {
|
||||
const { store } = dispatchClientMetadata({
|
||||
clientApp: CLIENT_APP_FIREFOX,
|
||||
});
|
||||
const root = render({
|
||||
_config: getFakeConfig({ enableFeatureHeroRecommendation: false }),
|
||||
store,
|
||||
});
|
||||
|
||||
expect(root.find(HeroRecommendation)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFeaturedCollectionsMetadata', () => {
|
||||
it('exposes a `footerText` prop', () => {
|
||||
const metadata = getFeaturedCollectionsMetadata(fakeI18n());
|
||||
|
|
Загрузка…
Ссылка в новой задаче