Add skeleton and cherry-picked commits

- Reset layout to allow full width
- Added Footer & IconWithText components

Add temporary placeholder components

All components here are temporary and can be removed/replaced when their relevant PR is ready.

Add marketplace footer component

The component is added using a SlotFill to use the existing WC Admin footer and utilise full width correctly.

Setup initial content area layout

Update tab styles

Setup styling to match latest design

Finalise footer, wide layout, and tidy styles

- Applies the wide layout from latest MVP design
- Applies the correct footer links to titles
- Some general style tidying

Organise styles and setup variables

Swap to using CSS Grid for layouts

Update breakpoints

Restructure style naming and update to core styles

Add translation in some places

In-app marketplace search component. Cleaned up the commit history of this branch. Copied changes from these commits:

e9828422706176817e511778980005222aa36cc5
9ca2ae351c97fcd27ecd77a1464c2a9ca16de040
e47815705f3854bf50ff48d7975b7cf2f541614b
976811c458e67ae7fa107c8bf8554fdc3e809d85
46eafdf49fe39c12dee77d6ce0885bdeda527dea

Deleted unused import.
This commit is contained in:
Kyle Nel 2023-07-04 14:08:50 +02:00
parent 30e23d22d5
commit 28cb1008eb
No known key found for this signature in database
22 changed files with 579 additions and 18 deletions

View File

@ -0,0 +1,14 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="W" clip-path="url(#clip0_1256_184463)">
<rect width="16" height="16" fill="#7F54B3"/>
<rect id="Rectangle 1" width="16" height="16" rx="2" fill="#7F54B3"/>
<g id="Speech bubble">
<path id="Vector" d="M3.04855 3.86047C3.2036 3.67312 3.42971 3.55683 3.6752 3.55037C4.17911 3.51807 4.46983 3.75711 4.54735 4.26747C4.85745 6.34771 5.19339 8.11786 5.54871 9.57144L7.73878 5.41096C7.93905 5.03626 8.18454 4.83599 8.48818 4.81661C8.92749 4.7843 9.19882 5.0621 9.30865 5.65645C9.51538 6.81932 9.83194 7.96281 10.2519 9.06753C10.5167 6.53506 10.956 4.70032 11.5698 3.56975C11.6925 3.31134 11.9445 3.14337 12.2287 3.13045C12.4549 3.11107 12.681 3.18213 12.8554 3.33072C13.0363 3.46639 13.1461 3.67958 13.159 3.90569C13.1719 4.07366 13.1396 4.24163 13.0621 4.38376C12.6745 5.10732 12.3515 6.30895 12.0995 7.98865C11.854 9.6102 11.7571 10.8829 11.8217 11.7938C11.8476 12.0199 11.8024 12.246 11.7054 12.4463C11.6085 12.653 11.4018 12.7952 11.1757 12.8081C10.9108 12.8275 10.6524 12.7047 10.3875 12.4398C9.45724 11.4902 8.72076 10.0753 8.17808 8.19538C7.53851 9.47453 7.05398 10.4371 6.73742 11.0702C6.14953 12.2008 5.64562 12.7758 5.23215 12.8081C4.96082 12.8275 4.72825 12.6014 4.5409 12.1233C4.03053 10.8183 3.4814 8.29229 2.8935 4.54527C2.84182 4.29978 2.89996 4.05428 3.04855 3.86047Z" fill="white"/>
</g>
</g>
<defs>
<clipPath id="clip0_1256_184463">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,3 +1,14 @@
@import '../../stylesheets/_variables.scss';
.woocommerce-marketplace__content {
padding: 16px;
box-sizing: content-box;
margin: auto;
max-width: $content-max-width;
padding: 0 $content-spacing-small;
}
@media screen and (min-width: $breakpoint-medium) {
.woocommerce-marketplace__content {
padding: $content-spacing-large;
}
}

View File

@ -8,6 +8,7 @@
import './content.scss';
import Discover from '../discover/discover';
import Extensions from '../extensions/extensions';
import Footer from '../footer/footer';
export interface ContentProps {
selectedTab?: string | undefined;
@ -24,5 +25,12 @@ const renderContent = ( selectedTab?: string ): JSX.Element => {
export default function Content( props: ContentProps ): JSX.Element {
const { selectedTab } = props;
return <>{ renderContent( selectedTab ) }</>;
return (
<>
<div className="woocommerce-marketplace__content">
{ renderContent( selectedTab ) }
</div>
<Footer />
</>
);
}

View File

@ -0,0 +1 @@
// To keep StyleLint happy. Remove when file contains actual code.

View File

@ -5,11 +5,12 @@
/**
* Internal dependencies
*/
import './discover.scss';
export default function Discover() {
export default function Discover(): JSX.Element {
return (
<div className="woocommerce-marketplace__content">
<h1>Discover</h1>
<div className="woocommerce-marketplace__discover">
<h1>Discover Our Favorites</h1>
</div>
);
}

View File

@ -0,0 +1 @@
// To keep StyleLint happy. Remove when file contains actual code.

View File

@ -5,11 +5,13 @@
/**
* Internal dependencies
*/
import ProductList from '../product-list/product-list';
import './extensions.scss';
export default function Extensions() {
export default function Extensions(): JSX.Element {
return (
<div className="woocommerce-marketplace__content">
<h1>Extensions</h1>
<div className="woocommerce-marketplace__extensions">
<ProductList title="Extensions" />
</div>
);
}

View File

@ -0,0 +1,51 @@
@import '../../stylesheets/_variables.scss';
.woocommerce-admin-page__extensions .woocommerce-layout__footer {
background: #f6f7f7;
// Undo default fixed footer style used in WC Admin
position: unset;
width: 100%;
}
.woocommerce-marketplace__footer {
box-sizing: content-box;
max-width: $content-max-width;
margin: auto;
padding: 48px $content-spacing-large;
&-title {
color: $gutenberg-gray-900;
font-size: 20px;
font-style: normal;
font-weight: 500;
line-height: 28px;
max-width: 389px;
margin: 0 0 $content-spacing-large;
}
a {
text-decoration: none;
}
&-columns {
display: flex;
flex-direction: column;
gap: $large-gap;
}
&-logo {
color: $woo-purple-50;
display: flex;
font-size: 14px;
font-weight: 600;
line-height: 20px;
gap: $small-gap;
margin: 48px 0 0;
}
}
@media screen and (min-width: $breakpoint-medium) {
.woocommerce-marketplace__footer-columns {
flex-direction: row;
}
}

View File

@ -0,0 +1,89 @@
/**
* External dependencies
*/
import { WooFooterItem } from '@woocommerce/admin-layout';
import { __ } from '@wordpress/i18n';
import { check, commentContent, lock } from '@wordpress/icons';
import { createInterpolateElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import './footer.scss';
import IconWithText from '../icon-with-text/icon-with-text';
import WooIcon from '../../assets/images/woo-icon.svg';
const refundPolicyTitle = createInterpolateElement(
__( '30 day <a>money back guarantee</a>', 'woocommerce' ),
{
// eslint-disable-next-line jsx-a11y/anchor-has-content
a: <a href="https://woocommerce.com/refund-policy/" />,
}
);
const supportTitle = createInterpolateElement(
__( '<a>Support</a> teams across the world', 'woocommerce' ),
{
// eslint-disable-next-line jsx-a11y/anchor-has-content
a: <a href="https://woocommerce.com/docs/" />,
}
);
const paymentTitle = createInterpolateElement(
__( '<a>Safe & Secure</a> online payment', 'woocommerce' ),
{
// eslint-disable-next-line jsx-a11y/anchor-has-content
a: <a href="https://woocommerce.com/products/woocommerce-payments/" />,
}
);
function FooterContent(): JSX.Element {
return (
<div className="woocommerce-marketplace__footer">
<h2 className="woocommerce-marketplace__footer-title">
{ __(
'Grow your business with hundreds of solutions for your store.',
'woocommerce'
) }
</h2>
<div className="woocommerce-marketplace__footer-columns">
<IconWithText
icon={ check }
title={ refundPolicyTitle }
description={ __(
'For extensions and themes purchased from our Marketplace, we offer a full refund within 30 days of your date of purchase.',
'woocommerce'
) }
/>
<IconWithText
icon={ commentContent }
title={ supportTitle }
description={ __(
'We have happiness engineers around round the globe to help you at any given time.',
'woocommerce'
) }
/>
<IconWithText
icon={ lock }
title={ paymentTitle }
description={ __(
'Safety pay with WooCommerce payments, The only payment solution fully integrated to Woo.',
'woocommerce'
) }
/>
</div>
<div className="woocommerce-marketplace__footer-logo">
<img src={ WooIcon } alt="Woo Logo" aria-hidden="true" />
<span>{ __( 'Woo Marketplace', 'woocommerce' ) }</span>
</div>
</div>
);
}
export default function Footer(): JSX.Element {
return (
<WooFooterItem>
<FooterContent />
</WooFooterItem>
);
}

View File

@ -0,0 +1,29 @@
@import '../../stylesheets/_variables.scss';
.woocommerce-marketplace {
&__icon-group {
flex: 1;
max-width: 382px;
&-headline {
display: flex;
gap: $small-gap;
}
&-title {
color: #101517;
font-size: 14px;
font-weight: 600;
line-height: 20px;
margin: 0 0 8px;
}
&-description {
color: $wp-gray-50;
font-size: 13px;
font-weight: 400;
line-height: 20px;
margin: 0;
}
}
}

View File

@ -0,0 +1,37 @@
/**
* External dependencies
*/
import { Icon } from '@wordpress/icons';
import { ReactElement } from 'react';
/**
* Internal dependencies
*/
import './icon-with-text.scss';
export interface IconWithTextProps {
icon: JSX.Element;
title: ReactElement;
description: string;
}
export default function IconWithText( props: IconWithTextProps ): JSX.Element {
const { icon, title, description } = props;
return (
<div className="woocommerce-marketplace__icon-group">
<div className="woocommerce-marketplace__icon-group-headline">
<Icon
icon={ icon }
size={ 20 }
className="woocommerce-marketplace__icon-group-icon"
/>
<h3 className="woocommerce-marketplace__icon-group-title">
{ title }
</h3>
</div>
<p className="woocommerce-marketplace__icon-group-description">
{ description }
</p>
</div>
);
}

View File

@ -0,0 +1,40 @@
@import '../../stylesheets/_variables.scss';
.woocommerce-marketplace {
&__product-list-content {
display: grid;
gap: $medium-gap;
}
&__extension-card {
background-color: #3c3c3c;
height: 270px;
}
}
@media screen and (min-width: $breakpoint-medium) {
.woocommerce-marketplace {
&__product-list-content {
gap: $large-gap;
grid-template-columns: repeat(2, 1fr);
}
}
}
@media screen and (min-width: $breakpoint-large) {
.woocommerce-marketplace {
&__product-list-content {
gap: $large-gap;
grid-template-columns: repeat(3, 1fr);
}
}
}
@media screen and (min-width: $breakpoint-xlarge) {
.woocommerce-marketplace {
&__product-list-content {
grid-template-columns: repeat(4, 1fr);
}
}
}

View File

@ -0,0 +1,19 @@
/**
* External dependencies
*/
/**
* Internal dependencies
*/
import './product-list-content.scss';
export default function ProductListContent(): JSX.Element {
return (
<div className="woocommerce-marketplace__product-list-content">
<div className="woocommerce-marketplace__extension-card"></div>
<div className="woocommerce-marketplace__extension-card"></div>
<div className="woocommerce-marketplace__extension-card"></div>
<div className="woocommerce-marketplace__extension-card"></div>
</div>
);
}

View File

@ -0,0 +1,11 @@
.woo-marketplace__product-list-header {
color: $gray-900;
h2 {
font-size: 20px;
font-style: normal;
font-weight: 500;
line-height: 28px;
margin-top: 0;
}
}

View File

@ -0,0 +1,24 @@
/**
* External dependencies
*/
/**
* Internal dependencies
*/
import './product-list-header.scss';
interface ProductListHeaderProps {
title: string;
}
export default function ProductListHeader(
props: ProductListHeaderProps
): JSX.Element {
const { title } = props;
return (
<div className="woo-marketplace__product-list-header">
<h2>{ title }</h2>
</div>
);
}

View File

@ -0,0 +1,24 @@
/**
* External dependencies
*/
/**
* Internal dependencies
*/
import ProductListContent from '../product-list-content/product-list-content';
import ProductListHeader from '../product-list-header/product-list-header';
interface ProductListProps {
title: string;
}
export default function ProductList( props: ProductListProps ): JSX.Element {
const { title } = props;
return (
<div className="woocommerce-marketplace__product-list">
<ProductListHeader title={ title } />
<ProductListContent />
</div>
);
}

View File

@ -0,0 +1,36 @@
@import '../../stylesheets/_variables.scss';
.marketplace-search-form {
background-color: $gutenberg-gray-100;
padding: 9px 5px;
display: flex;
.search-form__search-input {
background-color: transparent;
border: none;
color: $gutenberg-gray-900;
display: inline;
fill: $woo-purple-50;
flex: 1;
font-size: 13px;
line-height: 20px;
::placeholder {
color: $gutenberg-gray-700;
}
}
input[type='text']:focus {
border: none;
box-shadow: none;
}
.search-form__search-button {
background-color: transparent;
border: none;
border-radius: 0;
cursor: pointer;
fill: currentColor;
font-size: 1em;
line-height: 0;
text-align: center;
}
}

View File

@ -0,0 +1,94 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Icon, search } from '@wordpress/icons';
/**
* Internal dependencies
*/
import './search.scss';
const searchPlaceholder = __( 'Search extensions and themes', 'woocommerce' );
const marketplaceAPI = 'https://woocommerce.com/wp-json';
export interface SearchProps {
locale?: string | 'en_US';
country?: string | undefined;
}
/**
* Search component.
*
* @param {SearchProps} props - Search props: locale and country.
* @return {JSX.Element} Search component.
*/
function Search( props: SearchProps ): JSX.Element {
const locale = props.locale ?? 'en_US';
const country = props.country ?? '';
const build_parameter_string = (
query_string: string,
query_country: string,
query_locale: string
): string => {
const params = new URLSearchParams();
params.append( 'term', query_string );
params.append( 'country', query_country );
params.append( 'locale', query_locale );
return params.toString();
};
const runSearch = () => {
const query = (
document.getElementById( 'search-query' ) as HTMLInputElement
).value.trim();
const params = build_parameter_string( query, country, locale );
fetch( marketplaceAPI + '?' + params, {
method: 'GET',
} )
.then( ( response ) => response.json() )
.then( ( response ) => {
return response;
} );
return [];
};
const handleKeyUp = ( event: { key: string } ) => {
if ( event.key === 'Enter' ) {
runSearch();
}
};
const renderSearch = () => {
return (
<div className="marketplace-search-form">
<label className="screen-reader-text" htmlFor="search-query">
{ searchPlaceholder }
</label>
<input
id="search-query"
className="search-form__search-input"
type="search"
name="search-query"
placeholder={ searchPlaceholder }
onKeyUp={ handleKeyUp }
/>
<button
id="wccom-header-search-button"
className="search-form__search-button"
aria-label={ __( 'Search', 'woocommerce' ) }
onClick={ runSearch }
>
<Icon icon={ search } size={ 32 } />
</button>
</div>
);
};
return <div className="marketplace-search-wrapper">{ renderSearch() }</div>;
}
export default Search;

View File

@ -1,6 +1,22 @@
.woocommerce-marketplace__tabs {
.woocommerce-marketplace__tab-button {
border-bottom: 3.5px solid transparent;
@import '../../stylesheets/_variables.scss';
.woocommerce-marketplace {
&__tabs {
margin: auto;
padding: 0 $content-spacing-small;
box-sizing: content-box;
}
&__tab-button {
border-bottom: 1.5px solid transparent;
border-radius: 0;
padding: 1.5rem 0;
margin: 0 1.5rem 0 0;
color: $mauve-light-12;
font-size: 13px;
font-style: normal;
font-weight: 600;
line-height: 16px;
&:focus:not(:disabled) {
box-shadow: none;
@ -11,3 +27,9 @@
}
}
}
@media screen and (min-width: $breakpoint-medium) {
.woocommerce-marketplace__tabs {
padding: 0 $content-spacing-large;
}
}

View File

@ -15,12 +15,14 @@ export default function Marketplace() {
const [ selectedTab, setSelectedTab ] = useState( DEFAULT_TAB_KEY );
return (
<>
<div className="woocommerce-marketplace">
<div className="woocommerce-marketplace__header">
<Tabs
selectedTab={ selectedTab }
setSelectedTab={ setSelectedTab }
/>
</div>
<Content selectedTab={ selectedTab } />
</>
</div>
);
}

View File

@ -1 +1,18 @@
/* This is for Stylelint. Delete this comment when we add styles here! */
@import './stylesheets/_variables.scss';
.woocommerce-admin-page__extensions {
background: #fff;
.woocommerce-layout__primary {
margin: 0;
}
.woocommerce-layout__main {
padding: 0;
}
}
.woocommerce-marketplace__header {
border-bottom: 1px solid $gutenberg-gray-300;
}

View File

@ -0,0 +1,28 @@
// Spacings
// Taken from base style system
// @wordpress/base-styles/_variables.scss
$small-gap: $grid-unit-10; // 8px
$medium-gap: $grid-unit-20; // 16px
$large-gap: $grid-unit-30; // 24px
$xlarge-gap: $grid-unit-40; // 32px
// Layout
$content-spacing-small: $grid-unit-20;
$content-spacing-large: $grid-unit-40;
$content-spacing-small: $medium-gap;
$content-spacing-large: $xlarge-gap;
$content-max-width: 1600px;
// Breakpoints
$breakpoint-medium: 769px;
$breakpoint-large: 1024px;
$breakpoint-xlarge: 1500px;
// Colours
$gutenberg-gray-100: #f0f0f0;
$gutenberg-gray-300: $gray-300;
$gutenberg-gray-700: #757575;
$gutenberg-gray-900: $gray-900;
$mauve-light-12: #1a1523;
$woo-purple-50: #7f54b3;
$wp-gray-50: #646970;