Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Boost: Add foundation pages UI #39832

Merged
merged 19 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { __ } from '@wordpress/i18n';
import Meta from './meta/meta';
import SettingsItem from '$features/ui/settings-item/settings-item';

const FoundationPages = () => {
return (
<SettingsItem
title={ __( 'Foundation Pages', 'jetpack-boost' ) }
description={
<p>
{ __(
'List the most important pages of your site. They will be optimized. The Page Speed scores are based on the first foundation page.',
'jetpack-boost'
) }
</p>
}
>
<Meta />
</SettingsItem>
);
};

export default FoundationPages;
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useDataSync } from '@automattic/jetpack-react-data-sync-client';
import { z } from 'zod';

/**
* Hook to get the Foundation Pages.
*/
export function useFoundationPages(): [ string[], ( newValue: string[] ) => void ] {
const [ { data }, { mutate } ] = useDataSync(
'jetpack_boost_ds',
'foundation_pages_list',
z.array( z.string() )
);

function updatePages( newValue: string[] ) {
mutate( newValue );
}

return [ data || [], updatePages ];
}

const FoundationPagesProperties = z.object( { max_pages: z.number() } );
type FoundationPagesProperties = z.infer< typeof FoundationPagesProperties >;

export function useFoundationPagesProperties(): FoundationPagesProperties | undefined {
const [ { data } ] = useDataSync(
'jetpack_boost_ds',
'foundation_pages_properties',
FoundationPagesProperties
);

return data;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
.wrapper {
font-size: 14px;
line-height: 22px;

:global(button ~ button) {
margin-left: 20px !important;
}

.head {
display: flex;
flex-direction: row;
align-items: flex-start;

:global(svg) {
fill: var(--jetpack-green-40);
}

.summary {
flex-grow: 1;
margin-right: 5px;
color: var(--gray-40);
}
}

.body {
margin-top: 16px;

.section {
padding: 16px;
border-radius: 4px;
background-color: var(--gray-0);

& ~ .section {
margin-top: 16px;
}

&.has-error {
textarea {
border-color: var( --red-40 );
}

.error-message {
display: block;
color: var( --red-40 );
}

.error {
color: var( --red-40 );
}
}

.title {
margin-bottom: 16px;
font-size: 16px;
line-height: 1;
font-weight: 600;
}

textarea {
display: block;
margin-bottom: 8px;
width: 100%;
padding: 12px 16px;
border-radius: 4px;
border: 1px solid var( --gray-10 );
background: var( --primary-white );
color: #000;
}

.error-message {
display: none;
}

.description {
margin-top: 8px;
font-size: 14px;
line-height: 1.5;
color: var( --gray-60 );
font-weight: 400;
}

label {
display: block;
font-size: 16px;
line-height: 1.5;

&:not(:last-child) {
margin-bottom: 8px;
}
}
}

.button {
margin-top: 16px;
}
}

.see-logs-link {
font-size: 16px;
line-height: 1.5;
}

.logging-toggle {
margin-right: 1em;
float: left;
}

.clearfix {
clear: both;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { Button, Notice } from '@automattic/jetpack-components';
import { __, _n, sprintf } from '@wordpress/i18n';
import ChevronDown from '$svg/chevron-down';
import ChevronUp from '$svg/chevron-up';
import React, { useEffect, useState } from 'react';
import clsx from 'clsx';
import styles from './meta.module.scss';
import { useFoundationPages, useFoundationPagesProperties } from '../lib/stores/foundation-pages';
import { createInterpolateElement } from '@wordpress/element';
import { recordBoostEvent } from '$lib/utils/analytics';
import getSupportLink from '$lib/utils/get-support-link';

const Meta = () => {
const [ isExpanded, setIsExpanded ] = useState( false );
const [ foundationPages, setFoundationPages ] = useFoundationPages();
const foundationPagesProperties = useFoundationPagesProperties();

const updateFoundationPages = ( newValue: string ) => {
const newItems = newValue.split( '\n' ).map( line => line.trim() );

setFoundationPages( newItems );
};

let content = null;

if ( foundationPagesProperties !== undefined ) {
content = (
<List
items={ foundationPages.join( '\n' ) }
setItems={ updateFoundationPages }
maxItems={ foundationPagesProperties.max_pages }
/>
);
} else {
content = (
<Notice
level="warning"
title={ __( 'Failed to load', 'jetpack-boost' ) }
hideCloseButton={ true }
>
<p>
{ createInterpolateElement(
__(
'Refresh the page and try again. If the issue persists, please <link>contact support</link>.',
'jetpack-boost'
),
{
link: (
// eslint-disable-next-line jsx-a11y/anchor-has-content
<a
href={ getSupportLink() }
target="_blank"
rel="noopener noreferrer"
onClick={ () => {
recordBoostEvent( 'foundation_pages_properties_failed', {} );
} }
/>
),
}
) }
</p>
</Notice>
);
}

return (
<div className={ styles.wrapper } data-testid="foundation-pages-meta">
<div className={ styles.head }>
<div className={ styles.summary }>
{ foundationPagesProperties &&
sprintf(
/* translators: %1$d is the number of foundation pages added, %2$d is the maximum number allowed */
__( '%1$d / %2$d added', 'jetpack-boost' ),
foundationPages.length,
foundationPagesProperties.max_pages
) }
</div>
<div className={ styles.actions }>
<Button
variant="link"
size="small"
weight="regular"
iconSize={ 16 }
icon={ isExpanded ? <ChevronUp /> : <ChevronDown /> }
onClick={ () => setIsExpanded( ! isExpanded ) }
>
{ __( 'Show Options', 'jetpack-boost' ) }
</Button>
</div>
</div>
{ isExpanded && <div className={ styles.body }>{ content }</div> }
</div>
);
};

type ListProps = {
items: string;
setItems: ( newValue: string ) => void;
maxItems: number;
};

const List: React.FC< ListProps > = ( { items, setItems, maxItems } ) => {
const [ inputValue, setInputValue ] = useState( items );
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [ inputInvalid, setInputInvalid ] = useState( false );

const inputRows = Math.min( maxItems, 10 );

const validateInputValue = ( value: string ) => {
setInputValue( value );
setInputInvalid( ! validateItems( value ) );
};

const validateItems = ( value: string ) => {
const lines = value
.split( '\n' )
.map( line => line.trim() )
.filter( line => line.trim() !== '' );

// There should always be at least one foundation page.
if ( lines.length === 0 ) {
return false;
}

// Check if the number of items exceeds maxItems
if ( lines.length > maxItems ) {
return false;
}

return true;
};

useEffect( () => {
setInputValue( items );
}, [ items ] );

function save() {
setItems( inputValue );
}

return (
<div
className={ clsx( styles.section, {
[ styles[ 'has-error' ] ]: inputInvalid,
} ) }
>
<textarea
value={ inputValue }
rows={ inputRows }
onChange={ e => validateInputValue( e.target.value ) }
id="jb-foundation-pages"
/>
{ inputInvalid && (
<p className={ styles.error }>
{ sprintf(
/* translators: %d is the maximum number of foundation page URLs. */
_n(
'You must provide %d foundation page URL.',
'You must provide between 1 and %d foundation page URLs.',
maxItems,
'jetpack-boost'
),
maxItems
) }
</p>
) }
<Button
disabled={ items === inputValue || inputInvalid }
onClick={ save }
className={ styles.button }
>
{ __( 'Save', 'jetpack-boost' ) }
</Button>
</div>
);
};

export default Meta;
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,7 @@
margin-right: 2em;
min-width: 36px;
}

.content {
width: 100%;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
.wrapper {
margin-bottom: 2em;
display: flex;
margin-left: auto;
margin-right: auto;


a {
text-decoration: underline;
}

h2 {
font-size: 22px;
margin-top: 0;
}

p {
margin: 1em 0;
}

.content {
width: 100%;
}

.content h3:focus-visible {
outline: none;
}
}
Loading
Loading