Add a static promo to the home page (#8400)

This commit is contained in:
Kumar McMillan 2019-07-30 10:43:02 -05:00 коммит произвёл GitHub
Родитель a181b75d34
Коммит a5dd629f91
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
11 изменённых файлов: 445 добавлений и 0 удалений

Просмотреть файл

@ -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,
};

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 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());