diff --git a/.phpstorm.meta.php b/.phpstorm.meta.php index eaab4582520..ed02ec2d0e7 100644 --- a/.phpstorm.meta.php +++ b/.phpstorm.meta.php @@ -17,6 +17,7 @@ 'admin.paired_browsing' => \AmpProject\AmpWP\Admin\PairedBrowsing::class, 'admin.plugin_row_meta' => \AmpProject\AmpWP\Admin\PluginRowMeta::class, 'admin.polyfills' => \AmpProject\AmpWP\Admin\Polyfills::class, + 'admin.user_rest_endpoint_extension' => \AmpProject\AmpWP\Admin\UserRESTEndpointExtension::class, 'admin.validation_counts' => \AmpProject\AmpWP\Admin\ValidationCounts::class, 'amp_slug_customization_watcher' => \AmpProject\AmpWP\AmpSlugCustomizationWatcher::class, 'background_task_deactivator' => \AmpProject\AmpWP\BackgroundTask\BackgroundTaskDeactivator::class, diff --git a/assets/src/components/amp-setting-toggle/index.js b/assets/src/components/amp-setting-toggle/index.js index 23cc7f2b3f6..05ddbb579c2 100644 --- a/assets/src/components/amp-setting-toggle/index.js +++ b/assets/src/components/amp-setting-toggle/index.js @@ -2,6 +2,7 @@ * External dependencies */ import PropTypes from 'prop-types'; +import classnames from 'classnames'; /** * WordPress dependencies @@ -19,14 +20,27 @@ import './style.css'; * * @param {Object} props Component props. * @param {boolean} props.checked Whether the toggle is on. + * @param {boolean} props.compact Whether the toggle is compact/small. * @param {boolean} props.disabled Whether the toggle is disabled. * @param {Function} props.onChange Change handler. * @param {string} props.text Toggle text. * @param {Object|string} props.title Toggle title. */ -export function AMPSettingToggle( { checked, disabled = false, onChange, text, title } ) { +export function AMPSettingToggle( { + checked, + compact = false, + disabled = false, + onChange, + text, + title, +} ) { return ( -
+
+ + + ); +} + +NavMenu.propTypes = { + links: PropTypes.arrayOf( + PropTypes.shape( { + url: PropTypes.string, + label: PropTypes.string, + isActive: PropTypes.bool, + } ), + ), + onClick: PropTypes.func, +}; diff --git a/assets/src/components/nav-menu/style.scss b/assets/src/components/nav-menu/style.scss new file mode 100644 index 00000000000..b572044f35d --- /dev/null +++ b/assets/src/components/nav-menu/style.scss @@ -0,0 +1,64 @@ +.nav-menu__item { + margin: 0; + + & + & { + border-top: 1px solid var(--amp-settings-color-border); + } +} + +.amp .nav-menu__link { + align-items: center; + color: var(--amp-settings-color-muted); + display: flex; + flex-flow: row nowrap; + font-size: 14px; + justify-content: space-between; + padding: 0.75rem 1rem; + text-decoration: none; + transition: background-color 120ms ease; + + &.nav-menu__link--active, + &:hover { + background-color: var(--very-light-gray); + color: var(--amp-settings-color-muted); + } + + &:focus { + box-shadow: none; + } + + &::after { + border: 2px solid; + border-bottom: none; + border-left: none; + content: ""; + display: block; + flex: 0 0 auto; + height: 8px; + margin-left: 1rem; + transform: rotateZ(45deg); + width: 8px; + } +} + +// Nav menu as a selectable component. +.nav-menu.selectable { + padding: 0; + + .nav-menu__list { + margin: 0; + padding: 0 1rem; + } + + .nav-menu__link { + margin: 0 -1rem; + } + + .nav-menu__item:first-child .nav-menu__link { + border-radius: 8px 8px 0 0; + } + + .nav-menu__item:last-child .nav-menu__link { + border-radius: 0 0 8px 8px; + } +} diff --git a/assets/src/components/nav-menu/test/__snapshots__/nav-menu.js.snap b/assets/src/components/nav-menu/test/__snapshots__/nav-menu.js.snap new file mode 100644 index 00000000000..182c4042302 --- /dev/null +++ b/assets/src/components/nav-menu/test/__snapshots__/nav-menu.js.snap @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NavMenu matches the snapshot 1`] = ` + +`; diff --git a/assets/src/components/nav-menu/test/nav-menu.js b/assets/src/components/nav-menu/test/nav-menu.js new file mode 100644 index 00000000000..78ba617d620 --- /dev/null +++ b/assets/src/components/nav-menu/test/nav-menu.js @@ -0,0 +1,101 @@ + +/** + * External dependencies + */ +import { act } from 'react-dom/test-utils'; +import { create } from 'react-test-renderer'; + +/** + * WordPress dependencies + */ +import { render } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { NavMenu } from '../index'; + +let container; + +const links = [ + { + url: 'https://example.com/foo', + label: 'Foo', + isActive: false, + }, + { + url: 'https://example.com/bar', + label: 'Bar', + isActive: true, + }, +]; + +describe( 'NavMenu', () => { + beforeEach( () => { + container = document.createElement( 'div' ); + document.body.appendChild( container ); + } ); + + afterEach( () => { + document.body.removeChild( container ); + container = null; + } ); + + it( 'matches the snapshot', () => { + const wrapper = create( ); + + expect( wrapper.toJSON() ).toMatchSnapshot(); + } ); + + it( 'renders a nav menu with a list of links', () => { + act( () => { + render( + , + container, + ); + } ); + + expect( container.querySelector( 'nav' ) ).not.toBeNull(); + expect( container.querySelector( 'ul' ) ).not.toBeNull(); + expect( container.querySelectorAll( 'li' ) ).toHaveLength( 2 ); + } ); + + it( 'contains correct links', () => { + act( () => { + render( + , + container, + ); + } ); + + expect( container.querySelector( 'nav' ).textContent ).toBe( 'FooBar' ); + expect( container.querySelectorAll( 'a' ) ).toHaveLength( 2 ); + expect( container.querySelectorAll( 'a[href="https://example.com/foo"]' ) ).toHaveLength( 1 ); + expect( container.querySelectorAll( 'a[href="https://example.com/bar"]' ) ).toHaveLength( 1 ); + expect( container.querySelectorAll( 'a[class*="--active"]' ) ).toHaveLength( 1 ); + expect( container.querySelector( 'a[class*="--active"]' ).getAttribute( 'href' ) ).toBe( 'https://example.com/bar' ); + } ); + + it( 'calls the handler function on click', () => { + const handler = jest.fn(); + + act( () => { + render( + , + container, + ); + } ); + + act( + () => { + container.querySelector( 'a' ).click(); + }, + ); + + expect( handler ).toHaveBeenCalledTimes( 1 ); + + const [ event, link ] = handler.mock.calls[ 0 ]; + expect( event.type ).toBe( 'click' ); + expect( link ).toBe( links[ 0 ] ); + } ); +} ); diff --git a/assets/src/components/phone/index.js b/assets/src/components/phone/index.js index 2e1f4934660..1c4ed55de68 100644 --- a/assets/src/components/phone/index.js +++ b/assets/src/components/phone/index.js @@ -7,17 +7,22 @@ import PropTypes from 'prop-types'; * Internal dependencies */ import './style.css'; +import { Loading } from '../loading'; /** * Component resembling a phone with a screen. * - * @param {Object} props Component props. - * @param {any} props.children The elements to display in the screen. + * @param {Object} props Component props. + * @param {any} props.children The elements to display in the screen. + * @param {boolean} props.isLoading Flag indicating if the content is loading. */ -export function Phone( { children } ) { +export function Phone( { children, isLoading = false } ) { return ( -
-
+
+
+
+ +
{ children }
@@ -26,4 +31,5 @@ export function Phone( { children } ) { Phone.propTypes = { children: PropTypes.any, + isLoading: PropTypes.bool, }; diff --git a/assets/src/components/phone/style.css b/assets/src/components/phone/style.css index 8ad1355cb4a..e9cec3c3529 100644 --- a/assets/src/components/phone/style.css +++ b/assets/src/components/phone/style.css @@ -24,9 +24,32 @@ width: 43px; } -.phone-inner { +.phone__inner { display: flex; - background-color: var(--color-gray-medium); + background-color: var(--amp-settings-color-dark-gray); flex-grow: 1; overflow: hidden; + position: relative; +} + +.phone__overlay { + align-items: center; + background-color: var(--amp-settings-color-dark-gray); + bottom: 0; + display: flex; + justify-content: center; + left: 0; + opacity: 0; + pointer-events: none; + position: absolute; + right: 0; + top: 0; + transition: opacity 0.3s ease, visibility 0.3s ease; + visibility: hidden; +} + +.phone.is-loading .phone__overlay { + opacity: 1; + pointer-events: auto; + visibility: visible; } diff --git a/assets/src/components/redirect-toggle/test/__snapshots__/redirect-toggle.js.snap b/assets/src/components/redirect-toggle/test/__snapshots__/redirect-toggle.js.snap index e1911a5ef7b..fe49df26ef0 100644 --- a/assets/src/components/redirect-toggle/test/__snapshots__/redirect-toggle.js.snap +++ b/assets/src/components/redirect-toggle/test/__snapshots__/redirect-toggle.js.snap @@ -2,7 +2,7 @@ exports[`RedirectToggle matches snapshot 1`] = `
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/assets/src/components/svg/icon-laptop-search.js b/assets/src/components/svg/icon-laptop-search.js new file mode 100644 index 00000000000..2dd0d4307fa --- /dev/null +++ b/assets/src/components/svg/icon-laptop-search.js @@ -0,0 +1,41 @@ +/** + * External dependencies + */ +import PropTypes from 'prop-types'; + +/** + * WordPress dependencies + */ +import { useInstanceId } from '@wordpress/compose'; + +const INTRINSIC_ICON_WIDTH = 42; +const INTRINSIC_ICON_HEIGHT = 29; +const INTRINSIC_STROKE_WIDTH = 2; + +export function IconLaptopSearch( { width = INTRINSIC_ICON_WIDTH, ...props } ) { + const clipPathId = `clip-icon-laptop-search-${ useInstanceId( IconLaptopSearch ) }`; + const strokeWidth = INTRINSIC_STROKE_WIDTH * ( INTRINSIC_ICON_WIDTH / width ); + + return ( + + + + + + + + + + + + + + + + + + ); +} +IconLaptopSearch.propTypes = { + width: PropTypes.number, +}; diff --git a/assets/src/components/svg/icon-laptop-toggles.js b/assets/src/components/svg/icon-laptop-toggles.js new file mode 100644 index 00000000000..bc06a62d5a4 --- /dev/null +++ b/assets/src/components/svg/icon-laptop-toggles.js @@ -0,0 +1,26 @@ +/** + * WordPress dependencies + */ +import { useInstanceId } from '@wordpress/compose'; + +export function IconLaptopToggles( props ) { + const clipPathId = `clip-icon-laptop-toggles-${ useInstanceId( IconLaptopToggles ) }`; + + return ( + + + + + + + + + + + + + + + + ); +} diff --git a/assets/src/components/user-context-provider/index.js b/assets/src/components/user-context-provider/index.js index 5e6ccaa6495..2d7c78c96a9 100644 --- a/assets/src/components/user-context-provider/index.js +++ b/assets/src/components/user-context-provider/index.js @@ -20,19 +20,28 @@ export const User = createContext(); /** * Context provider user data. * - * @param {Object} props Component props. - * @param {?any} props.children Component children. - * @param {boolean} props.onlyFetchIfPluginIsConfigured Flag indicating whether the users data should be fetched only if the plugin is fully configured (i.e. the Onboarding Wizard has been completed). - * @param {string} props.userOptionDeveloperTools The key of the option to use from the settings endpoint. - * @param {string} props.usersResourceRestPath The REST path for interacting with the `users` resources. + * @param {Object} props Component props. + * @param {?any} props.children Component children. + * @param {boolean} props.onlyFetchIfPluginIsConfigured Flag indicating whether the users data should be fetched only if the plugin is fully configured (i.e. the Onboarding Wizard has been completed). + * @param {string} props.userFieldReviewPanelDismissedForTemplateMode The key of the option to use from the settings endpoint. + * @param {string} props.userOptionDeveloperTools The key of the option to use from the settings endpoint. + * @param {string} props.usersResourceRestPath The REST path for interacting with the `users` resources. */ -export function UserContextProvider( { children, onlyFetchIfPluginIsConfigured = true, userOptionDeveloperTools, usersResourceRestPath } ) { +export function UserContextProvider( { + children, + onlyFetchIfPluginIsConfigured = true, + userFieldReviewPanelDismissedForTemplateMode, + userOptionDeveloperTools, + usersResourceRestPath, +} ) { const { originalOptions, fetchingOptions } = useContext( Options ); const { plugin_configured: pluginConfigured } = originalOptions; const [ fetchingUser, setFetchingUser ] = useState( false ); const [ developerToolsOption, setDeveloperToolsOption ] = useState( null ); + const [ reviewPanelDismissedForTemplateMode, setReviewPanelDismissedForTemplateMode ] = useState( null ); const [ originalDeveloperToolsOption, setOriginalDeveloperToolsOption ] = useState( null ); const [ savingDeveloperToolsOption, setSavingDeveloperToolsOption ] = useState( false ); + const [ savingReviewPanelDismissedForTemplateMode, setSavingReviewPanelDismissedForTemplateMode ] = useState( false ); const [ didSaveDeveloperToolsOption, setDidSaveDeveloperToolsOption ] = useState( false ); const { setAsyncError } = useAsyncError(); @@ -82,6 +91,10 @@ export function UserContextProvider( { children, onlyFetchIfPluginIsConfigured = setOriginalDeveloperToolsOption( fetchedUser[ userOptionDeveloperTools ] ); setDeveloperToolsOption( fetchedUser[ userOptionDeveloperTools ] ); + + if ( userFieldReviewPanelDismissedForTemplateMode ) { + setReviewPanelDismissedForTemplateMode( fetchedUser[ userFieldReviewPanelDismissedForTemplateMode ] ); + } } catch ( e ) { setAsyncError( e ); return; @@ -89,10 +102,10 @@ export function UserContextProvider( { children, onlyFetchIfPluginIsConfigured = setFetchingUser( false ); } )(); - }, [ onlyFetchIfPluginIsConfigured, fetchingOptions, fetchingUser, originalDeveloperToolsOption, pluginConfigured, setAsyncError, userOptionDeveloperTools, usersResourceRestPath ] ); + }, [ onlyFetchIfPluginIsConfigured, fetchingOptions, fetchingUser, originalDeveloperToolsOption, pluginConfigured, setAsyncError, userFieldReviewPanelDismissedForTemplateMode, userOptionDeveloperTools, usersResourceRestPath ] ); /** - * Sends the option back to the REST endpoint to be saved. + * Sends the dev tools option back to the REST endpoint to be saved. */ const saveDeveloperToolsOption = useCallback( async () => { if ( ! hasDeveloperToolsOptionChange ) { @@ -125,6 +138,39 @@ export function UserContextProvider( { children, onlyFetchIfPluginIsConfigured = setSavingDeveloperToolsOption( false ); }, [ hasDeveloperToolsOptionChange, developerToolsOption, setAsyncError, userOptionDeveloperTools, usersResourceRestPath ] ); + /** + * Sends the template mode for which the "Review" panel is dismissed back to + * the REST endpoint to be saved. + */ + const saveReviewPanelDismissedForTemplateMode = useCallback( async ( templateMode ) => { + if ( savingReviewPanelDismissedForTemplateMode || ! userFieldReviewPanelDismissedForTemplateMode ) { + return; + } + + // Update the local state immediately. + setReviewPanelDismissedForTemplateMode( templateMode ); + setSavingReviewPanelDismissedForTemplateMode( true ); + + try { + await apiFetch( { + method: 'post', + path: `${ usersResourceRestPath }/me`, + data: { + [ userFieldReviewPanelDismissedForTemplateMode ]: templateMode, + }, + } ); + + if ( true === hasUnmounted.current ) { + return; + } + } catch ( e ) { + setAsyncError( e ); + return; + } + + setSavingReviewPanelDismissedForTemplateMode( false ); + }, [ savingReviewPanelDismissedForTemplateMode, setAsyncError, userFieldReviewPanelDismissedForTemplateMode, usersResourceRestPath ] ); + return ( @@ -148,6 +197,7 @@ export function UserContextProvider( { children, onlyFetchIfPluginIsConfigured = UserContextProvider.propTypes = { children: PropTypes.any, onlyFetchIfPluginIsConfigured: PropTypes.bool, + userFieldReviewPanelDismissedForTemplateMode: PropTypes.string, userOptionDeveloperTools: PropTypes.string.isRequired, usersResourceRestPath: PropTypes.string.isRequired, }; diff --git a/assets/src/css/elements.css b/assets/src/css/elements.css index 7b331556050..332f6e989c2 100644 --- a/assets/src/css/elements.css +++ b/assets/src/css/elements.css @@ -28,7 +28,7 @@ } .amp p { - font-size: 16px; + font-size: var(--amp-settings-font-size); } .amp, diff --git a/assets/src/css/variables.css b/assets/src/css/variables.css index 622c7f64818..3c906a1e4dd 100644 --- a/assets/src/css/variables.css +++ b/assets/src/css/variables.css @@ -5,7 +5,9 @@ */ :root { --amp-settings-color-black: #212121; + --amp-settings-color-dark-gray: #333; --amp-settings-color-brand: #2459e7; + --amp-settings-color-muted: #48525c; --gray: #6c7781; --amp-settings-color-border: #e8e8e8; --very-light-gray: #fafafc; @@ -16,4 +18,5 @@ --color-valid: #46b450; --amp-settings-color-danger: #dc3232; --color-gray-medium: rgba(0, 0, 0, 0.54); + --amp-settings-font-size: 16px; } diff --git a/assets/src/onboarding-wizard/__mocks__/amp-settings.js b/assets/src/onboarding-wizard/__mocks__/amp-settings.js index cbdd1e3857b..b291caa4804 100644 --- a/assets/src/onboarding-wizard/__mocks__/amp-settings.js +++ b/assets/src/onboarding-wizard/__mocks__/amp-settings.js @@ -4,7 +4,7 @@ module.exports = { CURRENT_THEME: { name: 'Twenty Twenty', }, - FINISH_LINK: 'http://site.test/wp-admin/?page=amp-options', + SETTINGS_LINK: 'http://site.test/wp-admin/?page=amp-options', OPTIONS_REST_PATH: 'http://site.test/wp-json/amp/v1/options', READER_THEMES_REST_PATH: 'http://site.test/wp-json/amp/v1/reader-themes', UPDATES_NONCE: '', diff --git a/assets/src/onboarding-wizard/components/navigation-context-provider.js b/assets/src/onboarding-wizard/components/navigation-context-provider.js index 8bad19582d1..51ba502f9f1 100644 --- a/assets/src/onboarding-wizard/components/navigation-context-provider.js +++ b/assets/src/onboarding-wizard/components/navigation-context-provider.js @@ -23,7 +23,7 @@ export const Navigation = createContext(); * @param {Array} props.pages Pages in the app. */ export function NavigationContextProvider( { children, pages } ) { - const [ activePageIndex, setActivePageIndex ] = useState( 0 ); + const [ currentPage, setCurrentPage ] = useState( pages[ 0 ] ); const [ canGoForward, setCanGoForward ] = useState( true ); // Allow immediately moving forward on first page. @todo This may need to change in 2.1. const { editedOptions } = useContext( Options ); @@ -37,7 +37,7 @@ export function NavigationContextProvider( { children, pages } ) { return pages.filter( ( page ) => 'theme-selection' !== page.slug ); }, [ pages, themeSupport ] ); - const currentPage = adaptedPages[ activePageIndex ]; + const activePageIndex = adaptedPages.findIndex( ( adaptedPage ) => adaptedPage.slug === currentPage.slug ); const isLastPage = activePageIndex === adaptedPages.length - 1; @@ -45,7 +45,7 @@ export function NavigationContextProvider( { children, pages } ) { * Navigates back to the previous page. */ const moveBack = () => { - setActivePageIndex( activePageIndex - 1 ); + setCurrentPage( adaptedPages[ activePageIndex - 1 ] ); setCanGoForward( true ); }; @@ -57,7 +57,7 @@ export function NavigationContextProvider( { children, pages } ) { return; } - setActivePageIndex( activePageIndex + 1 ); + setCurrentPage( adaptedPages[ activePageIndex + 1 ] ); setCanGoForward( false ); // Each page is responsible for setting this to true. }; @@ -72,7 +72,6 @@ export function NavigationContextProvider( { children, pages } ) { moveBack, moveForward, pages: adaptedPages, - setActivePageIndex, setCanGoForward, } } diff --git a/assets/src/onboarding-wizard/components/template-mode-override-context-provider.js b/assets/src/onboarding-wizard/components/template-mode-override-context-provider.js index c20d3c8fa7e..d0909656741 100644 --- a/assets/src/onboarding-wizard/components/template-mode-override-context-provider.js +++ b/assets/src/onboarding-wizard/components/template-mode-override-context-provider.js @@ -14,6 +14,7 @@ import { createContext, useState, useEffect, useContext } from '@wordpress/eleme import { Options } from '../../components/options-context-provider'; import { ReaderThemes } from '../../components/reader-themes-context-provider'; import { User } from '../../components/user-context-provider'; +import { READER } from '../../common/constants'; import { Navigation } from './navigation-context-provider'; export const TemplateModeOverride = createContext(); @@ -26,13 +27,14 @@ export const TemplateModeOverride = createContext(); */ export function TemplateModeOverrideContextProvider( { children } ) { const { editedOptions, originalOptions, updateOptions, readerModeWasOverridden, setReaderModeWasOverridden } = useContext( Options ); - const { activePageIndex, currentPage: { slug: currentPageSlug }, setActivePageIndex } = useContext( Navigation ); + const { currentPage } = useContext( Navigation ); const { selectedTheme, currentTheme } = useContext( ReaderThemes ); const { developerToolsOption, fetchingUser, originalDeveloperToolsOption } = useContext( User ); const [ respondedToDeveloperToolsOptionChange, setRespondedToDeveloperToolsOptionChange ] = useState( false ); const [ mostRecentlySelectedThemeSupport, setMostRecentlySelectedThemeSupport ] = useState( null ); const [ technicalQuestionChangedAtLeastOnce, setTechnicalQuestionChangedAtLeastOnce ] = useState( false ); + const { slug: currentPageSlug } = currentPage || {}; const { theme_support: themeSupport } = editedOptions || {}; const { theme_support: originalThemeSupport } = originalOptions || {}; @@ -60,17 +62,15 @@ export function TemplateModeOverrideContextProvider( { children } ) { * Override with transitional if the user has selected reader mode and their currently active theme is the same as the selected reader theme. */ useEffect( () => { - if ( 'summary' === currentPageSlug && 'reader' === themeSupport && selectedTheme.name === currentTheme.name ) { + if ( 'done' === currentPageSlug && READER === themeSupport && selectedTheme.name === currentTheme.name ) { if ( ! readerModeWasOverridden ) { updateOptions( { theme_support: 'transitional' } ); setReaderModeWasOverridden( true ); - setActivePageIndex( activePageIndex - 1 ); } else { setReaderModeWasOverridden( false ); } } }, [ - activePageIndex, selectedTheme.name, currentTheme.name, themeSupport, @@ -78,7 +78,6 @@ export function TemplateModeOverrideContextProvider( { children } ) { readerModeWasOverridden, updateOptions, setReaderModeWasOverridden, - setActivePageIndex, ] ); /** @@ -119,7 +118,7 @@ export function TemplateModeOverrideContextProvider( { children } ) { ] ); return ( - + { children } ); diff --git a/assets/src/onboarding-wizard/index.js b/assets/src/onboarding-wizard/index.js index ea7db292c5e..4571a468e65 100644 --- a/assets/src/onboarding-wizard/index.js +++ b/assets/src/onboarding-wizard/index.js @@ -12,7 +12,7 @@ import { APP_ROOT_ID, CLOSE_LINK, CURRENT_THEME, - FINISH_LINK, + SETTINGS_LINK, OPTIONS_REST_PATH, READER_THEMES_REST_PATH, UPDATES_NONCE, @@ -57,7 +57,7 @@ export function Providers( { children } ) { { render( - + , root, ); diff --git a/assets/src/onboarding-wizard/pages/done/index.js b/assets/src/onboarding-wizard/pages/done/index.js new file mode 100644 index 00000000000..525f4a6f0ab --- /dev/null +++ b/assets/src/onboarding-wizard/pages/done/index.js @@ -0,0 +1,191 @@ +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { useContext, useEffect } from '@wordpress/element'; + +/** + * External dependencies + */ +import { SETTINGS_LINK } from 'amp-settings'; // From WP inline script. + +/** + * Internal dependencies + */ +import './style.scss'; +import { + AMPNotice, + NOTICE_SIZE_LARGE, + NOTICE_TYPE_SUCCESS, + NOTICE_TYPE_INFO, +} from '../../../components/amp-notice'; +import { Navigation } from '../../components/navigation-context-provider'; +import { Options } from '../../../components/options-context-provider'; +import { ReaderThemes } from '../../../components/reader-themes-context-provider'; +import { User } from '../../../components/user-context-provider'; +import { IconLaptopToggles } from '../../../components/svg/icon-laptop-toggles'; +import { IconLaptopSearch } from '../../../components/svg/icon-laptop-search'; +import { AMPSettingToggle } from '../../../components/amp-setting-toggle'; +import { NavMenu } from '../../../components/nav-menu'; +import { READER, STANDARD, TRANSITIONAL } from '../../../common/constants'; +import { Preview } from './preview'; +import { Saving } from './saving'; +import { usePreview } from './use-preview'; + +/** + * Final screen, where data is saved. + */ +export function Done() { + const { + didSaveOptions, + editedOptions: { theme_support: themeSupport, reader_theme: readerTheme }, + hasOptionsChanges, + readerModeWasOverridden, + saveOptions, + savingOptions, + } = useContext( Options ); + const { didSaveDeveloperToolsOption, saveDeveloperToolsOption, savingDeveloperToolsOption } = useContext( User ); + const { canGoForward, setCanGoForward } = useContext( Navigation ); + const { downloadedTheme, downloadingTheme, downloadingThemeError } = useContext( ReaderThemes ); + const { previewLinks, setActivePreviewLink, previewUrl, isPreviewingAMP, toggleIsPreviewingAMP } = usePreview(); + + /** + * Allow the finish button to be enabled. + */ + useEffect( () => { + if ( ! canGoForward ) { + setCanGoForward( true ); + } + }, [ setCanGoForward, canGoForward ] ); + + /** + * Triggers saving of options on arrival to this screen. + */ + useEffect( () => { + if ( ! didSaveOptions && ! savingOptions ) { + saveOptions(); + } + }, [ didSaveOptions, saveOptions, savingOptions ] ); + + /** + * Triggers saving of user options on arrival of this screen. + */ + useEffect( () => { + if ( ! didSaveDeveloperToolsOption && ! savingDeveloperToolsOption ) { + saveDeveloperToolsOption(); + } + }, [ didSaveDeveloperToolsOption, savingDeveloperToolsOption, saveDeveloperToolsOption ] ); + + if ( savingOptions || savingDeveloperToolsOption || downloadingTheme || hasOptionsChanges ) { + return ; + } + + return ( +
+

+ { __( 'Done', 'amp' ) } +

+
+

+ + { __( 'Review', 'amp' ) } +

+ { READER === themeSupport && downloadedTheme === readerTheme && ( + + { __( 'Your Reader theme was automatically installed', 'amp' ) } + + ) } + { readerModeWasOverridden && ( + + { __( 'Because you selected a Reader theme that is the same as your site\'s active theme, your site has automatically been switched to Transitional template mode.', 'amp' ) } + + ) } +

+ { __( 'Your site is ready to bring great experiences to your users!', 'amp' ) } +

+ { STANDARD === themeSupport && ( +

+ { __( 'In Standard mode there is a single AMP version of your site. Browse your site here to ensure it meets your expectations.', 'amp' ) } +

+ ) } + { TRANSITIONAL === themeSupport && ( + <> +

+ { __( 'In Transitional mode AMP and non-AMP versions of your site are served using your currently active theme.', 'amp' ) } +

+

+ { __( 'Browse your site here to ensure it meets your expectations, and toggle the AMP setting to compare both versions.', 'amp' ) } +

+ + ) } + { READER === themeSupport && ( + <> +

+ { __( 'In Reader mode AMP is served using your selected Reader theme, and pages for your non-AMP site are served using your primary theme. Browse your site here to ensure it meets your expectations, and toggle the AMP setting to compare both versions.', 'amp' ) } +

+

+ { __( 'As a last step, use the Customizer to tailor the Reader theme as needed.', 'amp' ) } +

+ + ) } +
+ { + e.preventDefault(); + setActivePreviewLink( link ); + } } + /> +
+
+
+ { READER === themeSupport && downloadingThemeError && ( + + { __( 'There was an error downloading your Reader theme. As a result, your site is currently using the legacy reader theme. Please install your chosen theme manually.', 'amp' ) } + + ) } + { STANDARD !== themeSupport && ( + + ) } + +
+
+

+ + { __( 'Need help?', 'amp' ) } +

+
    + { /* dangerouslySetInnerHTML reason: Injection of a link. */ } +
  • support forums', 'amp' ), + 'https://wordpress.org/support/plugin/amp/#new-topic-0', + ), + } } /> + { /* dangerouslySetInnerHTML reason: Injection of a link. */ } +
  • in settings', 'amp' ), + SETTINGS_LINK, + ), + } } /> + { /* dangerouslySetInnerHTML reason: Injection of a link. */ } +
  • Learn more how the AMP plugin works', 'amp' ), + 'https://amp-wp.org/documentation/how-the-plugin-works/', + ), + } } /> +
+
+
+ ); +} diff --git a/assets/src/onboarding-wizard/pages/done/preview.js b/assets/src/onboarding-wizard/pages/done/preview.js new file mode 100644 index 00000000000..fe7e7aefa30 --- /dev/null +++ b/assets/src/onboarding-wizard/pages/done/preview.js @@ -0,0 +1,63 @@ +/** + * External dependencies + */ +import PropTypes from 'prop-types'; + +/** + * WordPress dependencies + */ +import { useEffect, useRef, useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { Phone } from '../../../components/phone'; + +/** + * Page preview component. + * + * @param {Object} props Component props. + * @param {string} props.url URL of the page to be previewed. + */ +export function Preview( { url } ) { + const iframeRef = useRef( null ); + const [ isLoading, setIsLoading ] = useState( true ); + + useEffect( () => { + if ( ! iframeRef.current ) { + return null; + } + + const iframe = iframeRef.current; + const onLoad = () => setIsLoading( false ); + + iframe.addEventListener( 'load', onLoad ); + + return () => { + iframe.removeEventListener( 'load', onLoad ); + }; + }, [] ); + + useEffect( () => { + if ( url ) { + setIsLoading( true ); + } + }, [ url ] ); + + return ( + +