From e3f93ab9d6bdfbf97a8ba13b3f1b5a22b9031082 Mon Sep 17 00:00:00 2001 From: Hardeep Asrani Date: Sun, 24 Dec 2023 18:19:47 +0530 Subject: [PATCH 01/14] Initial design work --- .eslintrc | 66 ++++++++++++++++++++ package-lock.json | 19 ++++++ package.json | 4 ++ postcss.config.js | 2 +- src/App.js | 38 +++++++++++- src/components/Loader.js | 53 ++++++++++++++++ src/components/PageControl.js | 54 +++++++++++++++++ src/hooks/index.js | 111 ++++++++++++++++++++++++++++++++++ src/index.js | 5 +- src/parts/ColorPalette.js | 107 ++++++++++++++++++++++++++++++++ src/parts/Header.js | 16 +++++ src/parts/SiteDescription.js | 58 ++++++++++++++++++ src/parts/SiteTopic.js | 58 ++++++++++++++++++ src/steps.js | 43 +++++++++++++ src/store.js | 84 +++++++++++++++++++++++++ src/style.scss | 36 +++++++++++ src/utils.js | 52 ++++++++++++++++ tailwind.config.js | 14 ++++- 18 files changed, 811 insertions(+), 9 deletions(-) create mode 100644 .eslintrc create mode 100644 src/components/Loader.js create mode 100644 src/components/PageControl.js create mode 100644 src/hooks/index.js create mode 100644 src/parts/ColorPalette.js create mode 100644 src/parts/Header.js create mode 100644 src/parts/SiteDescription.js create mode 100644 src/parts/SiteTopic.js create mode 100644 src/steps.js create mode 100644 src/store.js create mode 100644 src/utils.js diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..87cb608 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,66 @@ +{ + "env": { + "browser": true, + "es2021": true + }, + "extends": "wordpress", + "parserOptions": { + "ecmaFeatures": { + "jsx": true + }, + "ecmaVersion": 2021, + "sourceType": "module" + }, + "ignorePatterns": [ "node_modules", "assets" ], + "rules": { + "indent": [ + "error", + "tab" + ], + "linebreak-style": [ + "error", + "unix" + ], + "quotes": [ + "error", + "single" + ], + "semi": [ + "error", + "always" + ], + "prefer-destructuring": [ + "warn", + { + "array": false, + "object": true + }, + { + "enforceForRenamedProperties": false + } + ], + "array-bracket-spacing": [ + "warn", + "always", + { + "arraysInArrays": false, + "objectsInArrays": false + } + ], + "key-spacing": [ + "warn", + { + "beforeColon": false, + "afterColon": true + } + ], + "object-curly-spacing": [ + "warn", + "always", + { + "arraysInObjects": true, + "objectsInObjects": false + } + ], + } +} diff --git a/package-lock.json b/package-lock.json index 3e75f59..d394996 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,8 +8,12 @@ "name": "quickwp", "version": "1.0.0", "license": "GPL-3.0-or-later", + "dependencies": { + "classnames": "^2.3.2" + }, "devDependencies": { "@wordpress/scripts": "^26.19.0", + "eslint-config-wordpress": "^2.0.0", "simple-git-hooks": "^2.9.0", "tailwindcss": "^3.4.0" } @@ -6253,6 +6257,11 @@ "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", "dev": true }, + "node_modules/classnames": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", + "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" + }, "node_modules/clean-webpack-plugin": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/clean-webpack-plugin/-/clean-webpack-plugin-3.0.0.tgz", @@ -8151,6 +8160,16 @@ "eslint": ">=7.0.0" } }, + "node_modules/eslint-config-wordpress": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-wordpress/-/eslint-config-wordpress-2.0.0.tgz", + "integrity": "sha512-9ydUZ1zORI3av5EOx4zQml8WpdDx1bAOZC4dLPcYGqVcdBol3dQ2L40e2ill52k/+I+rqUJppGzWK+zP7+lI1w==", + "deprecated": "This package has been deprecated, please use @wordpress/eslint-plugin or @wordpress/scripts", + "dev": true, + "engines": { + "node": ">=4.2.1" + } + }, "node_modules/eslint-import-resolver-node": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", diff --git a/package.json b/package.json index 2f7af84..cc937ef 100644 --- a/package.json +++ b/package.json @@ -30,10 +30,14 @@ "homepage": "https://github.com/Codeinwp/quickwp#readme", "devDependencies": { "@wordpress/scripts": "^26.19.0", + "eslint-config-wordpress": "^2.0.0", "simple-git-hooks": "^2.9.0", "tailwindcss": "^3.4.0" }, "simple-git-hooks": { "pre-commit": "npm run lint:js && composer run-script lint && composer run-script phpstan" + }, + "dependencies": { + "classnames": "^2.3.2" } } diff --git a/postcss.config.js b/postcss.config.js index 16e7961..0e0e5d7 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,3 +1,3 @@ module.exports = { - plugins: [ require( 'tailwindcss' ) ], + plugins: [ require( 'tailwindcss' ) ] }; diff --git a/src/App.js b/src/App.js index b9ba92b..3a32fa8 100644 --- a/src/App.js +++ b/src/App.js @@ -1,11 +1,43 @@ /** * WordPress dependencies. */ +import { __ } from '@wordpress/i18n'; + +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies. + */ +import { useIsSiteEditorLoading } from './hooks'; +import Loader from './components/Loader'; +import Header from './parts/Header'; + const App = () => { + const isEditorLoading = useIsSiteEditorLoading(); + + const currentStep = useSelect( ( select ) => + select( 'quickwp/data' ).getStep() + ); + const StepControls = currentStep?.view || null; + + if ( isEditorLoading ) { + return ( +
+ +
+ ); + } + return ( -
-

QuickWP

-

This is an example starter modal.

+
+
+
); }; diff --git a/src/components/Loader.js b/src/components/Loader.js new file mode 100644 index 0000000..f6d5f79 --- /dev/null +++ b/src/components/Loader.js @@ -0,0 +1,53 @@ +/** + * External dependencies. + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies. + */ +import { __ } from '@wordpress/i18n'; + +import { memo, useEffect, useMemo, useState } from '@wordpress/element'; + +/** + * Internal dependencies. + */ +import { PageControlIcon } from '../utils'; + +import STEPS from '../steps'; + +const Loader = () => { + const [ currentPage, setCurrentPage ] = useState( 0 ); + const numberOfPages = useMemo( () => STEPS.length, []); + + useEffect( () => { + const interval = setInterval( () => { + setCurrentPage( ( prevPage ) => + prevPage === numberOfPages - 1 ? 0 : prevPage + 1 + ); + }, 500 ); + + return () => clearInterval( interval ); + }, [ numberOfPages ]); + + return ( + + ); +}; + +export default memo( Loader ); diff --git a/src/components/PageControl.js b/src/components/PageControl.js new file mode 100644 index 0000000..07d307d --- /dev/null +++ b/src/components/PageControl.js @@ -0,0 +1,54 @@ +/** + * WordPress dependencies. + */ +import { __ } from '@wordpress/i18n'; + +import { Button } from '@wordpress/components'; + +import { useSelect, useDispatch } from '@wordpress/data'; + +/** + * Internal dependencies. + */ +import { PageControlIcon } from '../utils'; +import STEPS from '../steps'; + +const numberOfPages = STEPS.length; + +const PageControl = () => { + const currentStep = useSelect( ( select ) => + select( 'quickwp/data' ).getStep() + ); + const { setStep } = useDispatch( 'quickwp/data' ); + + const currentPage = STEPS.findIndex( + ( step ) => step.value === currentStep.value + ); + + return ( + + ); +}; + +export default PageControl; diff --git a/src/hooks/index.js b/src/hooks/index.js new file mode 100644 index 0000000..5ac4681 --- /dev/null +++ b/src/hooks/index.js @@ -0,0 +1,111 @@ +/** + * WordPress dependencies + */ +import { useEffect, useState } from '@wordpress/element'; +import { useSelect } from '@wordpress/data'; +import { decodeEntities } from '@wordpress/html-entities'; + +const MAX_LOADING_TIME = 10000; // 10 seconds + +// This is a copy of the useEditedEntityRecord function from the Gutenberg plugin. +export const useEditedEntityRecord = ( postType, postId ) => { + const { record, title, description, isLoaded, icon } = useSelect( + ( select ) => { + const { getEditedPostType, getEditedPostId } = + select( 'core/edit-site' ); + const { getEditedEntityRecord, hasFinishedResolution } = + select( 'core' ); + const { __experimentalGetTemplateInfo: getTemplateInfo } = + select( 'core/editor' ); + const usedPostType = postType ?? getEditedPostType(); + const usedPostId = postId ?? getEditedPostId(); + const _record = getEditedEntityRecord( + 'postType', + usedPostType, + usedPostId + ); + const _isLoaded = + usedPostId && + hasFinishedResolution( 'getEditedEntityRecord', [ + 'postType', + usedPostType, + usedPostId + ]); + const templateInfo = getTemplateInfo( _record ); + + return { + record: _record, + title: templateInfo.title, + description: templateInfo.description, + isLoaded: _isLoaded, + icon: templateInfo.icon + }; + }, + [ postType, postId ] + ); + + return { + isLoaded, + icon, + record, + getTitle: () => ( title ? decodeEntities( title ) : null ), + getDescription: () => + description ? decodeEntities( description ) : null + }; +}; + +// This is a copy of the useIsSiteEditorLoading function from the Gutenberg plugin. +export const useIsSiteEditorLoading = () => { + const { isLoaded: hasLoadedPost } = useEditedEntityRecord(); + const [ loaded, setLoaded ] = useState( false ); + const inLoadingPause = useSelect( + ( select ) => { + const hasResolvingSelectors = + select( 'core' ).hasResolvingSelectors(); + return ! loaded && ! hasResolvingSelectors; + }, + [ loaded ] + ); + + /* + * If the maximum expected loading time has passed, we're marking the + * editor as loaded, in order to prevent any failed requests from blocking + * the editor canvas from appearing. + */ + useEffect( () => { + let timeout; + + if ( ! loaded ) { + timeout = setTimeout( () => { + setLoaded( true ); + }, MAX_LOADING_TIME ); + } + + return () => { + clearTimeout( timeout ); + }; + }, [ loaded ]); + + useEffect( () => { + if ( inLoadingPause ) { + + /* + * We're using an arbitrary 1s timeout here to catch brief moments + * without any resolving selectors that would result in displaying + * brief flickers of loading state and loaded state. + * + * It's worth experimenting with different values, since this also + * adds 1s of artificial delay after loading has finished. + */ + const timeout = setTimeout( () => { + setLoaded( true ); + }, 1000 ); + + return () => { + clearTimeout( timeout ); + }; + } + }, [ inLoadingPause ]); + + return ! loaded || ! hasLoadedPost; +}; diff --git a/src/index.js b/src/index.js index 0accca0..ded242e 100644 --- a/src/index.js +++ b/src/index.js @@ -9,6 +9,7 @@ import { registerPlugin } from '@wordpress/plugins'; * Internal dependencies. */ import './style.scss'; +import './store'; import App from './App'; const Render = () => { @@ -22,6 +23,6 @@ const hasFlag = urlParams.get( 'quickwp' ); // If the quickwp query string is present, render the quickwp modal. if ( 'true' === hasFlag ) { registerPlugin( 'quickwp', { - render: Render, - } ); + render: Render + }); } diff --git a/src/parts/ColorPalette.js b/src/parts/ColorPalette.js new file mode 100644 index 0000000..f532415 --- /dev/null +++ b/src/parts/ColorPalette.js @@ -0,0 +1,107 @@ +/** + * WordPress dependencies. + */ +import { __ } from '@wordpress/i18n'; + +import { BlockPreview } from '@wordpress/block-editor'; + +import { Button, ColorIndicator, TextControl } from '@wordpress/components'; + +import { useDispatch, useSelect } from '@wordpress/data'; + +const palette = [ + { + slug: 'ti-bg', + color: '#FFFFFF', + name: 'Background' + }, + { + slug: 'ti-fg', + color: '#202020', + name: 'Text' + }, + { + slug: 'ti-accent', + color: '#325ce8', + name: 'Accent' + }, + { + slug: 'ti-accent-secondary', + color: '#1B47DA', + name: 'Accent Secondary' + }, + { + slug: 'ti-bg-inv', + color: '#1A1919', + name: 'Dark Background' + }, + { + slug: 'ti-bg-alt', + color: '#f7f7f3', + name: 'Background Alt' + }, + { + slug: 'ti-fg-alt', + color: '#FBFBFB', + name: 'Inverted Text' + } +]; + +const ColorPalette = () => { + const { onContinue } = useDispatch( 'quickwp/data' ); + + const { template } = useSelect( ( select ) => { + const { getBlocks } = select( 'core/block-editor' ); + + return { + template: getBlocks() + }; + }); + + return ( +
+
+

+ { __( + 'Let\'s give your site a color that fits to your brand. What is your primary brand color?', + 'quickwp' + ) } +

+ +
+ + + {} } + /> +
+ +
+ { palette.map( ( color ) => ( + + ) ) } +
+ + +
+ +
+ +
+
+ ); +}; + +export default ColorPalette; diff --git a/src/parts/Header.js b/src/parts/Header.js new file mode 100644 index 0000000..24f6765 --- /dev/null +++ b/src/parts/Header.js @@ -0,0 +1,16 @@ +/** + * Internal dependencies. + */ +import { Logo } from '../utils'; +import PageControl from '../components/PageControl'; + +const Header = () => { + return ( +
+ + +
+ ); +}; + +export default Header; diff --git a/src/parts/SiteDescription.js b/src/parts/SiteDescription.js new file mode 100644 index 0000000..0924054 --- /dev/null +++ b/src/parts/SiteDescription.js @@ -0,0 +1,58 @@ +/** + * WordPress dependencies. + */ +import { __ } from '@wordpress/i18n'; + +import { Button, TextareaControl } from '@wordpress/components'; + +import { useDispatch } from '@wordpress/data'; + +import { useState } from '@wordpress/element'; + +import { ENTER } from '@wordpress/keycodes'; + +const SiteDescription = () => { + const [ value, setValue ] = useState( '' ); + + const { onContinue } = useDispatch( 'quickwp/data' ); + + const onEnter = ( e ) => { + if ( ENTER === e.keyCode && !! value ) { + onContinue(); + } + }; + + return ( +
+

+ { __( + 'Great! We\'d love to learn more about your brand to create a tailor-made website just for you.', + 'quickwp' + ) } +

+ + + + +
+ ); +}; + +export default SiteDescription; diff --git a/src/parts/SiteTopic.js b/src/parts/SiteTopic.js new file mode 100644 index 0000000..5ae5259 --- /dev/null +++ b/src/parts/SiteTopic.js @@ -0,0 +1,58 @@ +/** + * WordPress dependencies. + */ +import { __ } from '@wordpress/i18n'; + +import { Button, TextControl } from '@wordpress/components'; + +import { useDispatch } from '@wordpress/data'; + +import { useState } from '@wordpress/element'; + +import { ENTER } from '@wordpress/keycodes'; + +const SiteTopic = () => { + const [ value, setValue ] = useState( '' ); + + const { onContinue } = useDispatch( 'quickwp/data' ); + + const onEnter = ( e ) => { + if ( ENTER === e.keyCode && !! value ) { + onContinue(); + } + }; + + return ( +
+

+ { __( + 'Welcome. What kind of site are you building?', + 'quickwp' + ) } +

+ + + + +
+ ); +}; + +export default SiteTopic; diff --git a/src/steps.js b/src/steps.js new file mode 100644 index 0000000..77d158d --- /dev/null +++ b/src/steps.js @@ -0,0 +1,43 @@ +/** + * WordPress dependencies. + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies. + */ +import SiteTopic from './parts/SiteTopic'; +import SiteDescription from './parts/SiteDescription'; +import ColorPalette from './parts/ColorPalette'; + +const STEPS = [ + { + value: 'site_topic', + label: __( 'Site Topic', 'quickwp' ), + view: SiteTopic + }, + { + value: 'site_description', + label: __( 'Site Description', 'quickwp' ), + view: SiteDescription + }, + { + value: 'color_palette', + label: __( 'Color Palette', 'quickwp' ), + view: ColorPalette + }, + { + value: 'image_suggestions', + label: __( 'Image Suggestions', 'quickwp' ) + }, + { + value: 'frontpage_template', + label: __( 'Front Page Template', 'quickwp' ) + }, + { + value: 'view_site', + label: __( 'View Site', 'quickwp' ) + } +]; + +export default STEPS; diff --git a/src/store.js b/src/store.js new file mode 100644 index 0000000..f50094a --- /dev/null +++ b/src/store.js @@ -0,0 +1,84 @@ +/** + * WordPress dependencies. + */ +import { createReduxStore, dispatch, register, select } from '@wordpress/data'; + +/** + * Internal dependencies. + */ +import STEPS from './steps'; + +const DEFAULT_STATE = { + step: 2 +}; + +const actions = { + setStep( step ) { + return { + type: 'SET_STEP', + step + }; + }, + nextStep() { + return ({ dispatch, select }) => { + const current = select.getStep(); + const stepIndex = STEPS.findIndex( + ( step ) => current.value === step.value + ); + const isLast = STEPS.length === stepIndex + 1; + const newStep = + STEPS[ isLast ? STEPS.length : stepIndex + 1 ]?.value; + + if ( isLast ) { + return; + } + + dispatch( actions.setStep( newStep ) ); + }; + }, + previousStep() { + return ({ dispatch, select }) => { + const current = select.getStep(); + const stepIndex = STEPS.findIndex( + ( step ) => current.value === step.value + ); + const isFirst = 0 === stepIndex; + const newStep = STEPS[ isFirst ? 0 : stepIndex - 1 ]?.value; + + dispatch( actions.setStep( newStep ) ); + }; + }, + onContinue() { + return ({ dispatch }) => { + dispatch( actions.nextStep() ); + }; + } +}; + +const store = createReduxStore( 'quickwp/data', { + reducer( state = DEFAULT_STATE, action ) { + switch ( action.type ) { + case 'SET_STEP': + const step = STEPS.findIndex( + ( step ) => step.value === action.step + ); + + return { + ...state, + step + }; + } + + return state; + }, + + actions, + + selectors: { + getStep( state ) { + return STEPS[ state.step ]; + } + } +}); + +register( store ); diff --git a/src/style.scss b/src/style.scss index da1a97c..2ddaa7d 100644 --- a/src/style.scss +++ b/src/style.scss @@ -1,2 +1,38 @@ +@import 'tailwindcss/base'; @import 'tailwindcss/components'; @import 'tailwindcss/utilities'; + +#quickwp { + --wp-admin-theme-color: #ffffff; + + .components-text-control__input { + @apply bg-transparent max-w-xl border-0 h-12 text-2xl not-italic font-normal text-fg shadow-none; + } + + .components-textarea-control__input { + @apply bg-transparent max-w-5xl h-24 border-0 text-2xl not-italic font-normal text-fg shadow-none resize-none; + } + + .components-text-control__input::placeholder, + .components-textarea-control__input::placeholder { + @apply text-fg/50 shadow-none; + } + + .components-button { + &.is-primary { + @apply bg-transparent w-fit h-auto border border-solid border-fg text-fg rounded-md px-6 py-4 transition-all text-lg not-italic font-medium; + + &:disabled { + @apply opacity-50; + } + } + } + + .block-editor-block-preview__content { + max-height: none !important; + + iframe { + max-height: none !important; + } + } +} diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..b5a7059 --- /dev/null +++ b/src/utils.js @@ -0,0 +1,52 @@ +/** + * WordPress dependencies + */ +import { SVG, Circle, G, Path } from '@wordpress/primitives'; + +export const PageControlIcon = ({ isFilled = false }) => { + return ( + + { isFilled ? ( + + ) : ( + + ) } + + ); +}; + +export const Logo = () => { + return ( + + + + ); +}; diff --git a/tailwind.config.js b/tailwind.config.js index 7eddf58..a10a29e 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,8 +1,16 @@ /** @type {import('tailwindcss').Config} */ module.exports = { - content: [ './src/*.js' ], + content: [ './src/style.scss', './src/*.js', './src/**/*.js' ], theme: { - extend: {}, + extend: { + colors: { + bg: '#000000', + fg: '#FFFFFF' + }, + maxHeight: { + '80vh': '80vh' + } + } }, - plugins: [], + plugins: [] }; From 619dcc771508a75088a200040cda9c09aa44abae Mon Sep 17 00:00:00 2001 From: Hardeep Asrani Date: Tue, 26 Dec 2023 01:40:49 +0530 Subject: [PATCH 02/14] Finish UI --- package-lock.json | 128 +++++++++++++++++++++++------- package.json | 1 + src/components/TemplatePreview.js | 17 ++++ src/parts/ColorPalette.js | 20 +++-- src/parts/ImageSuggestions.js | 94 ++++++++++++++++++++++ src/parts/Template.js | 95 ++++++++++++++++++++++ src/parts/ViewSite.js | 46 +++++++++++ src/steps.js | 12 ++- src/store.js | 4 +- src/style.scss | 16 ++++ tailwind.config.js | 11 ++- 11 files changed, 404 insertions(+), 40 deletions(-) create mode 100644 src/components/TemplatePreview.js create mode 100644 src/parts/ImageSuggestions.js create mode 100644 src/parts/Template.js create mode 100644 src/parts/ViewSite.js diff --git a/package-lock.json b/package-lock.json index d394996..f8118cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "GPL-3.0-or-later", "dependencies": { + "@wordpress/icons": "^9.39.0", "classnames": "^2.3.2" }, "devDependencies": { @@ -2009,7 +2010,6 @@ "version": "7.23.6", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.6.tgz", "integrity": "sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ==", - "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -3764,6 +3764,11 @@ "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", "dev": true }, + "node_modules/@types/prop-types": { + "version": "15.7.11", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" + }, "node_modules/@types/qs": { "version": "6.9.10", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.10.tgz", @@ -3776,12 +3781,35 @@ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", "dev": true }, + "node_modules/@types/react": { + "version": "18.2.45", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.45.tgz", + "integrity": "sha512-TtAxCNrlrBp8GoeEp1npd5g+d/OejJHFxS3OWmrPBMFaVQMSN0OFySozJio5BHxTuTeug00AVXVAjfDSfk+lUg==", + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.2.18", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.18.tgz", + "integrity": "sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw==", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", "dev": true }, + "node_modules/@types/scheduler": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==" + }, "node_modules/@types/semver": { "version": "7.5.6", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", @@ -4536,6 +4564,43 @@ "@playwright/test": ">=1" } }, + "node_modules/@wordpress/element": { + "version": "5.25.0", + "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-5.25.0.tgz", + "integrity": "sha512-8FFK1wJ/4n7Y7s3wWRJinoX5WSPbgnJJawYEc6f5Jc7cG+OddHiWZQkU94o6lnRRm0+cCarxMV8K8hNI2Jc7OQ==", + "dependencies": { + "@babel/runtime": "^7.16.0", + "@types/react": "^18.0.21", + "@types/react-dom": "^18.0.6", + "@wordpress/escape-html": "^2.48.0", + "change-case": "^4.1.2", + "is-plain-object": "^5.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@wordpress/element/node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@wordpress/escape-html": { + "version": "2.48.0", + "resolved": "https://registry.npmjs.org/@wordpress/escape-html/-/escape-html-2.48.0.tgz", + "integrity": "sha512-zWOJWyRYVddJeZHnAJzV8f58+0GukbIdp+EcbNuRVm+Q2rhv0BpyO1HJB3Z8ba35whtNcGWJibk9WqdMzBAMAw==", + "dependencies": { + "@babel/runtime": "^7.16.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@wordpress/eslint-plugin": { "version": "17.5.0", "resolved": "https://registry.npmjs.org/@wordpress/eslint-plugin/-/eslint-plugin-17.5.0.tgz", @@ -4663,6 +4728,19 @@ "node": ">=12" } }, + "node_modules/@wordpress/icons": { + "version": "9.39.0", + "resolved": "https://registry.npmjs.org/@wordpress/icons/-/icons-9.39.0.tgz", + "integrity": "sha512-5L0VseLEDtZv1P/weUmwiTLGYCnXuTLtpqLN/CUo7o3TqqrNIK7xubXlFVKd7zeUOpVQAG+S/BgnaWIXru9N5w==", + "dependencies": { + "@babel/runtime": "^7.16.0", + "@wordpress/element": "^5.25.0", + "@wordpress/primitives": "^3.46.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@wordpress/jest-console": { "version": "7.19.0", "resolved": "https://registry.npmjs.org/@wordpress/jest-console/-/jest-console-7.19.0.tgz", @@ -4749,6 +4827,19 @@ "prettier": ">=3" } }, + "node_modules/@wordpress/primitives": { + "version": "3.46.0", + "resolved": "https://registry.npmjs.org/@wordpress/primitives/-/primitives-3.46.0.tgz", + "integrity": "sha512-KCNr1PqswT9XZoar53HC0e94TF3Sd96DQGvueMZVUE2YPdD96p3EWJjDRhmya+U63yVdHec+35fPDm+aJQQDdQ==", + "dependencies": { + "@babel/runtime": "^7.16.0", + "@wordpress/element": "^5.25.0", + "classnames": "^2.3.1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@wordpress/scripts": { "version": "26.19.0", "resolved": "https://registry.npmjs.org/@wordpress/scripts/-/scripts-26.19.0.tgz", @@ -5990,7 +6081,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", - "dev": true, "dependencies": { "pascal-case": "^3.1.2", "tslib": "^2.0.3" @@ -6079,7 +6169,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", "integrity": "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==", - "dev": true, "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3", @@ -6106,7 +6195,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/change-case/-/change-case-4.1.2.tgz", "integrity": "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==", - "dev": true, "dependencies": { "camel-case": "^4.1.2", "capital-case": "^1.0.4", @@ -6495,7 +6583,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.4.tgz", "integrity": "sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==", - "dev": true, "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3", @@ -7096,6 +7183,11 @@ "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", "dev": true }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, "node_modules/cwd": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/cwd/-/cwd-0.10.0.tgz", @@ -7756,7 +7848,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", - "dev": true, "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" @@ -10014,7 +10105,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz", "integrity": "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==", - "dev": true, "dependencies": { "capital-case": "^1.0.4", "tslib": "^2.0.3" @@ -11869,8 +11959,7 @@ "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { "version": "3.14.1", @@ -12479,7 +12568,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -12491,7 +12579,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", - "dev": true, "dependencies": { "tslib": "^2.0.3" } @@ -13178,7 +13265,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", - "dev": true, "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" @@ -13831,7 +13917,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", - "dev": true, "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" @@ -13907,7 +13992,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", - "dev": true, "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" @@ -13917,7 +14001,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz", "integrity": "sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==", - "dev": true, "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" @@ -15364,7 +15447,6 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "dev": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -15376,8 +15458,6 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", - "dev": true, - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" @@ -15631,8 +15711,7 @@ "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/regenerator-transform": { "version": "0.15.2", @@ -16046,8 +16125,6 @@ "version": "0.23.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", - "dev": true, - "peer": true, "dependencies": { "loose-envify": "^1.1.0" } @@ -16159,7 +16236,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz", "integrity": "sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==", - "dev": true, "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3", @@ -16459,7 +16535,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", - "dev": true, "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" @@ -17706,8 +17781,7 @@ "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/tsutils": { "version": "3.21.0", @@ -18014,7 +18088,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz", "integrity": "sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==", - "dev": true, "dependencies": { "tslib": "^2.0.3" } @@ -18023,7 +18096,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz", "integrity": "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==", - "dev": true, "dependencies": { "tslib": "^2.0.3" } diff --git a/package.json b/package.json index cc937ef..7a0bb92 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "pre-commit": "npm run lint:js && composer run-script lint && composer run-script phpstan" }, "dependencies": { + "@wordpress/icons": "^9.39.0", "classnames": "^2.3.2" } } diff --git a/src/components/TemplatePreview.js b/src/components/TemplatePreview.js new file mode 100644 index 0000000..26525e1 --- /dev/null +++ b/src/components/TemplatePreview.js @@ -0,0 +1,17 @@ +/** + * WordPress dependencies. + */ +import { BlockPreview } from '@wordpress/block-editor'; + +const TemplatePreview = ({ template }) => { + return ( +
+ +
+ ); +}; + +export default TemplatePreview; diff --git a/src/parts/ColorPalette.js b/src/parts/ColorPalette.js index f532415..9f3f58a 100644 --- a/src/parts/ColorPalette.js +++ b/src/parts/ColorPalette.js @@ -3,11 +3,21 @@ */ import { __ } from '@wordpress/i18n'; -import { BlockPreview } from '@wordpress/block-editor'; +import { + Button, + ColorIndicator, + TextControl +} from '@wordpress/components'; -import { Button, ColorIndicator, TextControl } from '@wordpress/components'; +import { + useDispatch, + useSelect +} from '@wordpress/data'; -import { useDispatch, useSelect } from '@wordpress/data'; +/** + * Internal dependencies. + */ +import TemplatePreview from '../components/TemplatePreview'; const palette = [ { @@ -97,9 +107,7 @@ const ColorPalette = () => {
-
- -
+ ); }; diff --git a/src/parts/ImageSuggestions.js b/src/parts/ImageSuggestions.js new file mode 100644 index 0000000..12210d4 --- /dev/null +++ b/src/parts/ImageSuggestions.js @@ -0,0 +1,94 @@ +/** + * External dependencies. + */ +import classNames from 'classnames'; + +import { check } from '@wordpress/icons'; + +/** + * WordPress dependencies. + */ +import { __ } from '@wordpress/i18n'; + +import { + Button, + Icon +} from '@wordpress/components'; + +import { useDispatch } from '@wordpress/data'; + +import { useState } from '@wordpress/element'; + +const dummy = [ + 'https://mystock.themeisle.com/wp-content/uploads/2022/06/52172949534_aa8893fd8f_c.jpg?v=1', + 'https://mystock.themeisle.com/wp-content/uploads/2022/06/52171667257_e4c90f0a38_c.jpg?v=2', + 'https://mystock.themeisle.com/wp-content/uploads/2022/06/52172949534_aa8893fd8f_c.jpg?v=3', + 'https://mystock.themeisle.com/wp-content/uploads/2022/06/52172949534_aa8893fd8f_c.jpg?v=4', + 'https://mystock.themeisle.com/wp-content/uploads/2022/06/52172949534_aa8893fd8f_c.jpg?v=5', + 'https://mystock.themeisle.com/wp-content/uploads/2022/06/52171667257_e4c90f0a38_c.jpg?v=6', + 'https://mystock.themeisle.com/wp-content/uploads/2022/06/52171667257_e4c90f0a38_c.jpg?v=7', + 'https://mystock.themeisle.com/wp-content/uploads/2022/06/52172949534_aa8893fd8f_c.jpg?v=8', + 'https://mystock.themeisle.com/wp-content/uploads/2022/06/52171667257_e4c90f0a38_c.jpg?v=9' +]; + +const ImageSuggestions = () => { + const [ value, setValue ] = useState([]); + + const { onContinue } = useDispatch( 'quickwp/data' ); + + return ( +
+
+

+ { __( + 'Pick out images that you like, and we\'ll include them in the designs.', + 'quickwp' + ) } +

+ + +
+ +
+
+ { dummy.map( ( image, index ) => ( +
{ + if ( value.includes( image ) ) { + setValue( value.filter( ( item ) => item !== image ) ); + } else { + setValue([ ...value, image ]); + } + }} + > + { value.includes( image ) && ( +
+ +
+ ) } + + +
+ ) ) } +
+
+
+ ); +}; + +export default ImageSuggestions; diff --git a/src/parts/Template.js b/src/parts/Template.js new file mode 100644 index 0000000..1b0295f --- /dev/null +++ b/src/parts/Template.js @@ -0,0 +1,95 @@ +/** + * External dependencies. + */ +import classNames from 'classnames'; + +import { check } from '@wordpress/icons'; + +/** + * WordPress dependencies. + */ +import { __ } from '@wordpress/i18n'; + +import { + Button, + Icon +} from '@wordpress/components'; + +import { + useDispatch, + useSelect +} from '@wordpress/data'; + +import { useState } from '@wordpress/element'; + +/** + * Internal dependencies. + */ +import TemplatePreview from '../components/TemplatePreview'; + +const dummy = [ 1, 2, 3, 4 ]; + +const Template = () => { + const [ value, setValue ] = useState(); + + const { onContinue } = useDispatch( 'quickwp/data' ); + + const { template } = useSelect( ( select ) => { + const { getBlocks } = select( 'core/block-editor' ); + + return { + template: getBlocks() + }; + }); + + return ( +
+
+

+ { __( + 'Pick a layout for your homepage.', + 'quickwp' + ) } +

+ + +
+ +
+
+ { dummy.map( ( image, index ) => ( +
{ + setValue( value === image ? null : image ); + }} + > + { value === image && ( +
+ +
+ ) } + + +
+ ) ) } +
+
+
+ ); +}; + +export default Template; diff --git a/src/parts/ViewSite.js b/src/parts/ViewSite.js new file mode 100644 index 0000000..d76314b --- /dev/null +++ b/src/parts/ViewSite.js @@ -0,0 +1,46 @@ +/** + * WordPress dependencies. + */ +import { __ } from '@wordpress/i18n'; + +import { Button } from '@wordpress/components'; + +const ViewSite = () => { + return ( +
+
+

+ { __( + 'Your site is ready. Good job!', + 'quickwp' + ) } +

+ +

+ { __( + 'You can download the ZIP file and install it to your WordPress site.', + 'quickwp' + ) } +

+ +
+ + + +
+
+
+ ); +}; + +export default ViewSite; diff --git a/src/steps.js b/src/steps.js index 77d158d..eff486a 100644 --- a/src/steps.js +++ b/src/steps.js @@ -9,6 +9,9 @@ import { __ } from '@wordpress/i18n'; import SiteTopic from './parts/SiteTopic'; import SiteDescription from './parts/SiteDescription'; import ColorPalette from './parts/ColorPalette'; +import ImageSuggestions from './parts/ImageSuggestions'; +import Template from './parts/Template'; +import ViewSite from './parts/ViewSite'; const STEPS = [ { @@ -28,15 +31,18 @@ const STEPS = [ }, { value: 'image_suggestions', - label: __( 'Image Suggestions', 'quickwp' ) + label: __( 'Image Suggestions', 'quickwp' ), + view: ImageSuggestions }, { value: 'frontpage_template', - label: __( 'Front Page Template', 'quickwp' ) + label: __( 'Front Page Template', 'quickwp' ), + view: Template }, { value: 'view_site', - label: __( 'View Site', 'quickwp' ) + label: __( 'View Site', 'quickwp' ), + view: ViewSite } ]; diff --git a/src/store.js b/src/store.js index f50094a..578a8c1 100644 --- a/src/store.js +++ b/src/store.js @@ -1,7 +1,7 @@ /** * WordPress dependencies. */ -import { createReduxStore, dispatch, register, select } from '@wordpress/data'; +import { createReduxStore, register } from '@wordpress/data'; /** * Internal dependencies. @@ -9,7 +9,7 @@ import { createReduxStore, dispatch, register, select } from '@wordpress/data'; import STEPS from './steps'; const DEFAULT_STATE = { - step: 2 + step: 0 }; const actions = { diff --git a/src/style.scss b/src/style.scss index 2ddaa7d..d76976a 100644 --- a/src/style.scss +++ b/src/style.scss @@ -22,6 +22,22 @@ &.is-primary { @apply bg-transparent w-fit h-auto border border-solid border-fg text-fg rounded-md px-6 py-4 transition-all text-lg not-italic font-medium; + &:hover { + @apply bg-fg text-bg; + } + + &:disabled { + @apply opacity-50; + } + } + + &.is-secondary { + @apply bg-secondary w-fit h-auto text-fg rounded-md px-6 py-4 transition-all text-lg not-italic font-medium shadow-none; + + &:hover { + @apply bg-fg text-bg; + } + &:disabled { @apply opacity-50; } diff --git a/tailwind.config.js b/tailwind.config.js index a10a29e..1d2cde9 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -3,11 +3,20 @@ module.exports = { content: [ './src/style.scss', './src/*.js', './src/**/*.js' ], theme: { extend: { + aspectRatio: { + vert: [ 3, 4 ] + }, + boxShadow: { + selected: '-3px 3px 0px -1px #000' + }, colors: { bg: '#000000', - fg: '#FFFFFF' + fg: '#FFFFFF', + secondary: '#232323' }, maxHeight: { + 'md': '32rem', + '40vh': '40vh', '80vh': '80vh' } } From d9726a7aeb63bd5f34e545dce39820a032f10538 Mon Sep 17 00:00:00 2001 From: Hardeep Asrani Date: Tue, 9 Jan 2024 06:16:05 +0530 Subject: [PATCH 03/14] [WiP] Save current progress --- inc/class-api.php | 264 ++++++++++++++++++++ inc/class-main.php | 18 ++ phpstan.neon | 1 + quickwp.php | 1 + src/App.js | 34 ++- src/components/PageControl.js | 5 +- src/parts/ColorPalette.js | 71 +----- src/parts/ImageSuggestions.js | 41 ++- src/parts/SiteDescription.js | 47 ++-- src/parts/SiteTopic.js | 50 +++- src/store.js | 163 +++++++++++- src/style.scss | 4 + src/utils.js | 164 ++++++++++++ tests/php/static-analysis-stubs/quickwp.php | 3 +- 14 files changed, 751 insertions(+), 115 deletions(-) create mode 100644 inc/class-api.php diff --git a/inc/class-api.php b/inc/class-api.php new file mode 100644 index 0000000..18f4992 --- /dev/null +++ b/inc/class-api.php @@ -0,0 +1,264 @@ +register_route(); + } + + /** + * Get endpoint. + * + * @return string + */ + public function get_endpoint() { + return $this->namespace . '/' . $this->version; + } + + /** + * Register hooks and actions. + * + * @return void + */ + private function register_route() { + add_action( 'rest_api_init', array( $this, 'register_routes' ) ); + } + + /** + * Register REST API route + * + * @return void + */ + public function register_routes() { + $namespace = $this->namespace . '/' . $this->version; + + $routes = array( + 'send' => array( + 'methods' => \WP_REST_Server::CREATABLE, + 'args' => array( + 'step' => array( + 'required' => true, + 'type' => 'string', + ), + 'message' => array( + 'required' => false, + 'type' => 'string', + ), + ), + 'callback' => array( $this, 'send' ), + ), + 'status' => array( + 'methods' => \WP_REST_Server::READABLE, + 'args' => array( + 'thread_id' => array( + 'required' => true, + 'type' => 'string', + ), + 'run_id' => array( + 'required' => true, + 'type' => 'string', + ), + ), + 'callback' => array( $this, 'status' ), + ), + 'get' => array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get' ), + ), + 'images' => array( + 'methods' => \WP_REST_Server::READABLE, + 'args' => array( + 'query' => array( + 'required' => true, + 'type' => 'string', + ), + ), + 'callback' => array( $this, 'images' ), + ), + ); + + foreach ( $routes as $route => $args ) { + $args['permission_callback'] = function () { + return current_user_can( 'manage_options' ); + }; + + register_rest_route( $namespace, '/' . $route, $args ); + } + } + + /** + * Send data to the API. + * + * @param \WP_REST_Request $request Request. + * + * @return \WP_REST_Response + */ + public function send( \WP_REST_Request $request ) { + $data = $request->get_params(); + + $request = wp_remote_post( + QUICKWP_API . 'wizard/send', + array( + 'body' => array( + 'step' => $data['step'], + 'message' => $data['message'], + ), + ) + ); + + if ( is_wp_error( $request ) ) { + return new \WP_REST_Response( array( 'error' => $request->get_error_message() ), 500 ); + } + + /** + * Holds the response as a standard class object + * + * @var \stdClass $response + */ + $response = json_decode( wp_remote_retrieve_body( $request ) ); + + if ( ! isset( $response->id ) || ! $response->id ) { + return new \WP_REST_Response( array( 'error' => __( 'Error', 'quickwp' ) ), 500 ); + } + + return new \WP_REST_Response( $response, 200 ); + } + + /** + * Get status. + * + * @param \WP_REST_Request $request Request. + * + * @return \WP_REST_Response + */ + public function status( \WP_REST_Request $request ) { + $data = $request->get_params(); + + $request = wp_remote_get( // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_remote_get_wp_remote_get + QUICKWP_API . 'wizard/status', + array( + 'body' => array( + 'thread_id' => $data['thread_id'], + 'run_id' => $data['run_id'], + ), + ) + ); + + if ( is_wp_error( $request ) ) { + return new \WP_REST_Response( array( 'error' => $request->get_error_message() ), 500 ); + } + + /** + * Holds the response as a standard class object + * + * @var \stdClass $response + */ + $response = json_decode( wp_remote_retrieve_body( $request ) ); + + if ( ! isset( $response->id ) || ! $response->id ) { + return new \WP_REST_Response( array( 'error' => __( 'Error', 'quickwp' ) ), 500 ); + } + + return new \WP_REST_Response( $response, 200 ); + } + + /** + * Get data. + * + * @param \WP_REST_Request $request Request. + * + * @return \WP_REST_Response + */ + public function get( \WP_REST_Request $request ) { + $data = $request->get_params(); + + $request = wp_remote_get(// phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_remote_get_wp_remote_get + QUICKWP_API . 'wizard/get', + array( + 'body' => array( + 'thread_id' => $data['thread_id'], + ), + ) + ); + + if ( is_wp_error( $request ) ) { + return new \WP_REST_Response( array( 'error' => $request->get_error_message() ), 500 ); + } + + /** + * Holds the response as a standard class object + * + * @var \stdClass $response + */ + $response = json_decode( wp_remote_retrieve_body( $request ) ); + + if ( ! isset( $response->data ) || ! $response->data ) { + return new \WP_REST_Response( array( 'error' => __( 'Error', 'quickwp' ) ), 500 ); + } + + return new \WP_REST_Response( $response, 200 ); + } + + /** + * Get images. + * + * @param \WP_REST_Request $request Request. + * + * @return \WP_REST_Response + */ + public function images( \WP_REST_Request $request ) { + $data = $request->get_params(); + + $request = wp_remote_get(// phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_remote_get_wp_remote_get + QUICKWP_API . 'wizard/images', + array( + 'body' => array( + 'query' => $data['query'], + ), + ) + ); + + if ( is_wp_error( $request ) ) { + return new \WP_REST_Response( array( 'error' => $request->get_error_message() ), 500 ); + } + + /** + * Holds the response as a standard class object + * + * @var \stdClass $response + */ + $response = json_decode( wp_remote_retrieve_body( $request ) ); + + if ( ! isset( $response->photos ) || count( $response->photos ) === 0 ) { + return new \WP_REST_Response( array( 'error' => __( 'Error', 'quickwp' ) ), 500 ); + } + + return new \WP_REST_Response( $response, 200 ); + } +} diff --git a/inc/class-main.php b/inc/class-main.php index 54aec2d..932ce52 100644 --- a/inc/class-main.php +++ b/inc/class-main.php @@ -7,16 +7,26 @@ namespace ThemeIsle\QuickWP; +use ThemeIsle\QuickWP\API; + /** * Main class. */ class Main { + /** + * API instance. + * + * @var API + */ + private $api; /** * Constructor. */ public function __construct() { $this->register_hooks(); + + $this->api = new API(); } /** @@ -62,5 +72,13 @@ public function enqueue_assets() { ); wp_set_script_translations( 'quickwp', 'quickwp' ); + + wp_localize_script( + 'quickwp', + 'quickwp', + array( + 'api' => $this->api->get_endpoint(), + ) + ); } } diff --git a/phpstan.neon b/phpstan.neon index 743b918..4b001c4 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -4,5 +4,6 @@ parameters: - %currentWorkingDirectory%/inc bootstrapFiles: - %currentWorkingDirectory%/tests/php/static-analysis-stubs/quickwp.php + checkGenericClassInNonGenericObjectType: false includes: - %currentWorkingDirectory%/vendor/szepeviktor/phpstan-wordpress/extension.neon \ No newline at end of file diff --git a/quickwp.php b/quickwp.php index c935f7e..0509da0 100644 --- a/quickwp.php +++ b/quickwp.php @@ -21,6 +21,7 @@ define( 'QUICKWP_URL', plugins_url( '/', __FILE__ ) ); define( 'QUICKWP_PATH', __DIR__ ); define( 'QUICKWP_VERSION', '1.0.0' ); +define( 'QUICKWP_API', 'https://4ab6-103-217-244-109.ngrok-free.app/api/' ); $vendor_file = QUICKWP_PATH . '/vendor/autoload.php'; diff --git a/src/App.js b/src/App.js index 3a32fa8..2538258 100644 --- a/src/App.js +++ b/src/App.js @@ -15,9 +15,21 @@ import Header from './parts/Header'; const App = () => { const isEditorLoading = useIsSiteEditorLoading(); - const currentStep = useSelect( ( select ) => - select( 'quickwp/data' ).getStep() - ); + const { + currentStep, + hasError + } = useSelect( select => { + const { + getStep, + hasError + } = select( 'quickwp/data' ); + + return { + currentStep: getStep(), + hasError: hasError() + }; + }); + const StepControls = currentStep?.view || null; if ( isEditorLoading ) { @@ -31,6 +43,22 @@ const App = () => { ); } + if ( hasError ) { + return ( +
+

+ { __( + 'There has been an error. Please refresh the page and try again.', + 'quickwp' + ) } +

+
+ ); + } + return (
{ const currentStep = useSelect( ( select ) => select( 'quickwp/data' ).getStep() ); - const { setStep } = useDispatch( 'quickwp/data' ); const currentPage = STEPS.findIndex( ( step ) => step.value === currentStep.value @@ -43,7 +42,7 @@ const PageControl = () => { } aria-label={ STEPS[ page ]?.label } className="text-fg h-6 w-6 !min-w-0 !min-h-0" - onClick={ () => setStep( STEPS[ page ]?.value ) } + onClick={ () => {} } /> ) ) } diff --git a/src/parts/ColorPalette.js b/src/parts/ColorPalette.js index 9f3f58a..2b74682 100644 --- a/src/parts/ColorPalette.js +++ b/src/parts/ColorPalette.js @@ -5,8 +5,7 @@ import { __ } from '@wordpress/i18n'; import { Button, - ColorIndicator, - TextControl + ColorIndicator } from '@wordpress/components'; import { @@ -19,80 +18,36 @@ import { */ import TemplatePreview from '../components/TemplatePreview'; -const palette = [ - { - slug: 'ti-bg', - color: '#FFFFFF', - name: 'Background' - }, - { - slug: 'ti-fg', - color: '#202020', - name: 'Text' - }, - { - slug: 'ti-accent', - color: '#325ce8', - name: 'Accent' - }, - { - slug: 'ti-accent-secondary', - color: '#1B47DA', - name: 'Accent Secondary' - }, - { - slug: 'ti-bg-inv', - color: '#1A1919', - name: 'Dark Background' - }, - { - slug: 'ti-bg-alt', - color: '#f7f7f3', - name: 'Background Alt' - }, - { - slug: 'ti-fg-alt', - color: '#FBFBFB', - name: 'Inverted Text' - } -]; - const ColorPalette = () => { const { onContinue } = useDispatch( 'quickwp/data' ); - const { template } = useSelect( ( select ) => { + const { + palette, + template + } = useSelect( ( select ) => { const { getBlocks } = select( 'core/block-editor' ); + const { getColorPalette } = select( 'quickwp/data' ); return { + palette: getColorPalette(), template: getBlocks() }; }); + const onSubmit = async() => { + onContinue(); + }; + return (

{ __( - 'Let\'s give your site a color that fits to your brand. What is your primary brand color?', + 'Let\'s give your site a color that fits to your brand.', 'quickwp' ) }

-
- - - {} } - /> -
-
{ palette.map( ( color ) => ( { ) ) }
-
diff --git a/src/parts/ImageSuggestions.js b/src/parts/ImageSuggestions.js index 12210d4..2484e70 100644 --- a/src/parts/ImageSuggestions.js +++ b/src/parts/ImageSuggestions.js @@ -15,25 +15,24 @@ import { Icon } from '@wordpress/components'; -import { useDispatch } from '@wordpress/data'; +import { + useDispatch, + useSelect +} from '@wordpress/data'; import { useState } from '@wordpress/element'; -const dummy = [ - 'https://mystock.themeisle.com/wp-content/uploads/2022/06/52172949534_aa8893fd8f_c.jpg?v=1', - 'https://mystock.themeisle.com/wp-content/uploads/2022/06/52171667257_e4c90f0a38_c.jpg?v=2', - 'https://mystock.themeisle.com/wp-content/uploads/2022/06/52172949534_aa8893fd8f_c.jpg?v=3', - 'https://mystock.themeisle.com/wp-content/uploads/2022/06/52172949534_aa8893fd8f_c.jpg?v=4', - 'https://mystock.themeisle.com/wp-content/uploads/2022/06/52172949534_aa8893fd8f_c.jpg?v=5', - 'https://mystock.themeisle.com/wp-content/uploads/2022/06/52171667257_e4c90f0a38_c.jpg?v=6', - 'https://mystock.themeisle.com/wp-content/uploads/2022/06/52171667257_e4c90f0a38_c.jpg?v=7', - 'https://mystock.themeisle.com/wp-content/uploads/2022/06/52172949534_aa8893fd8f_c.jpg?v=8', - 'https://mystock.themeisle.com/wp-content/uploads/2022/06/52171667257_e4c90f0a38_c.jpg?v=9' -]; - const ImageSuggestions = () => { const [ value, setValue ] = useState([]); + const { images } = useSelect( select => { + const images = select( 'quickwp/data' ).getImages() || []; + + return { + images: images.slice( 0, 9 ) + }; + }); + const { onContinue } = useDispatch( 'quickwp/data' ); return ( @@ -53,24 +52,24 @@ const ImageSuggestions = () => {
- { dummy.map( ( image, index ) => ( + { images.map( ( image, index ) => (
{ - if ( value.includes( image ) ) { - setValue( value.filter( ( item ) => item !== image ) ); + if ( value.includes( image.id ) ) { + setValue( value.filter( ( item ) => item !== image.id ) ); } else { - setValue([ ...value, image ]); + setValue([ ...value, image.id ]); } }} > - { value.includes( image ) && ( + { value.includes( image.id ) && (
{
) ) } diff --git a/src/parts/SiteDescription.js b/src/parts/SiteDescription.js index 0924054..82b6fab 100644 --- a/src/parts/SiteDescription.js +++ b/src/parts/SiteDescription.js @@ -3,23 +3,41 @@ */ import { __ } from '@wordpress/i18n'; -import { Button, TextareaControl } from '@wordpress/components'; +import { + Button, + TextareaControl +} from '@wordpress/components'; -import { useDispatch } from '@wordpress/data'; +import { + useDispatch, + useSelect +} from '@wordpress/data'; -import { useState } from '@wordpress/element'; - -import { ENTER } from '@wordpress/keycodes'; +/** + * Internal dependencies. + */ +import { generateImages } from '../utils'; const SiteDescription = () => { - const [ value, setValue ] = useState( '' ); + const { siteDescription } = useSelect( select => { + return { + siteDescription: select( 'quickwp/data' ).getSiteDescription() + }; + }); - const { onContinue } = useDispatch( 'quickwp/data' ); + const { + onContinue, + setSiteDescription + } = useDispatch( 'quickwp/data' ); - const onEnter = ( e ) => { - if ( ENTER === e.keyCode && !! value ) { - onContinue(); + const onSubmit = async() => { + if ( 4 > siteDescription?.length ) { + return; } + + generateImages(); + + onContinue(); }; return ( @@ -37,17 +55,16 @@ const SiteDescription = () => { 'e.g. Our brand, LifeUp, specializes in life coaching seminars targeted towards businesses in the UK. In addition to group seminars, we offer personal training and one-on-one calls to cater to a variety of needs.', 'quickwp' ) } - value={ value } - onChange={ setValue } - onKeyDown={ onEnter } + value={ siteDescription } + onChange={ setSiteDescription } hideLabelFromVision={ true } autoFocus={ true } /> diff --git a/src/parts/SiteTopic.js b/src/parts/SiteTopic.js index 5ae5259..067006b 100644 --- a/src/parts/SiteTopic.js +++ b/src/parts/SiteTopic.js @@ -3,22 +3,48 @@ */ import { __ } from '@wordpress/i18n'; -import { Button, TextControl } from '@wordpress/components'; +import { + Button, + TextControl +} from '@wordpress/components'; -import { useDispatch } from '@wordpress/data'; - -import { useState } from '@wordpress/element'; +import { + useDispatch, + useSelect +} from '@wordpress/data'; import { ENTER } from '@wordpress/keycodes'; +/** + * Internal dependencies. + */ +import { generateColorPalette } from '../utils'; + const SiteTopic = () => { - const [ value, setValue ] = useState( '' ); + const { siteTopic } = useSelect( select => { + return { + siteTopic: select( 'quickwp/data' ).getSiteTopic() + }; + }); - const { onContinue } = useDispatch( 'quickwp/data' ); + const { + onContinue, + setSiteTopic + } = useDispatch( 'quickwp/data' ); + + const onSubmit = async() => { + if ( 4 > siteTopic?.length ) { + return; + } + + // At this point, we start the color palette generation process. + generateColorPalette(); + onContinue(); + }; const onEnter = ( e ) => { - if ( ENTER === e.keyCode && !! value ) { - onContinue(); + if ( ENTER === e.keyCode && !! siteTopic ) { + onSubmit(); } }; @@ -37,8 +63,8 @@ const SiteTopic = () => { 'e.g. Web Agency, Tech Reviewer', 'quickwp' ) } - value={ value } - onChange={ setValue } + value={ siteTopic } + onChange={ setSiteTopic } onKeyDown={ onEnter } hideLabelFromVision={ true } autoFocus={ true } @@ -46,8 +72,8 @@ const SiteTopic = () => { diff --git a/src/store.js b/src/store.js index 578a8c1..8814c5a 100644 --- a/src/store.js +++ b/src/store.js @@ -1,7 +1,16 @@ /** * WordPress dependencies. */ -import { createReduxStore, register } from '@wordpress/data'; +import apiFetch from '@wordpress/api-fetch'; + +import { + createReduxStore, + dispatch, + register, + select +} from '@wordpress/data'; + +import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies. @@ -9,7 +18,24 @@ import { createReduxStore, register } from '@wordpress/data'; import STEPS from './steps'; const DEFAULT_STATE = { - step: 0 + step: 0, + processes: { + 'color_palette': { + 'thread_id': null, + 'run_id': null, + hasLoaded: false + }, + 'images': { + 'thread_id': null, + 'run_id': null, + hasLoaded: false + } + }, + images: [], + siteTopic: null, + siteDescription: null, + hasError: false, + colorPalette: [] }; const actions = { @@ -52,6 +78,57 @@ const actions = { return ({ dispatch }) => { dispatch( actions.nextStep() ); }; + }, + setSiteTopic( siteTopic ) { + return { + type: 'SET_SITE_TOPIC', + siteTopic + }; + }, + setSiteDescription( siteDescription ) { + return { + type: 'SET_SITE_DESCRIPTION', + siteDescription + }; + }, + setColorPalette( colorPalette ) { + return { + type: 'SET_COLOR_PALETTE', + colorPalette + }; + }, + setError( hasError ) { + return { + type: 'SET_ERROR', + hasError + }; + }, + setImages( images ) { + return { + type: 'SET_IMAGES', + images + }; + }, + setThreadID( item, threadID ) { + return { + type: 'SET_THREAD_ID', + item, + threadID + }; + }, + setRunID( item, runID ) { + return { + type: 'SET_RUN_ID', + item, + runID + }; + }, + setProcessStatus( item, hasLoaded ) { + return { + type: 'SET_PROCESS_STATUS', + item, + hasLoaded + }; } }; @@ -67,6 +144,64 @@ const store = createReduxStore( 'quickwp/data', { ...state, step }; + case 'SET_SITE_TOPIC': + return { + ...state, + siteTopic: action.siteTopic + }; + case 'SET_SITE_DESCRIPTION': + return { + ...state, + siteDescription: action.siteDescription + }; + case 'SET_COLOR_PALETTE': + return { + ...state, + colorPalette: action.colorPalette + }; + case 'SET_ERROR': + return { + ...state, + hasError: action.hasError + }; + case 'SET_IMAGES': + return { + ...state, + images: action.images + }; + case 'SET_THREAD_ID': + return { + ...state, + processes: { + ...state.processes, + [ action.item ]: { + ...state.processes[ action.item ], + 'thread_id': action.threadID + } + } + }; + case 'SET_RUN_ID': + return { + ...state, + processes: { + ...state.processes, + [ action.item ]: { + ...state.processes[ action.item ], + 'run_id': action.runID + } + } + }; + case 'SET_PROCESS_STATUS': + return { + ...state, + processes: { + ...state.processes, + [ action.item ]: { + ...state.processes[ action.item ], + hasLoaded: action.hasLoaded + } + } + }; } return state; @@ -77,6 +212,30 @@ const store = createReduxStore( 'quickwp/data', { selectors: { getStep( state ) { return STEPS[ state.step ]; + }, + getSiteTopic( state ) { + return state.siteTopic; + }, + getSiteDescription( state ) { + return state.siteDescription; + }, + getColorPalette( state ) { + return state.colorPalette; + }, + hasError( state ) { + return state.hasError; + }, + getImages( state ) { + return state.images; + }, + getThreadID( state, item ) { + return state.processes[ item ]?.thread_id; + }, + getRunID( state, item ) { + return state.processes[ item ]?.run_id; + }, + getProcessStatus( state, item ) { + return state.processes[ item ]?.hasLoaded; } } }); diff --git a/src/style.scss b/src/style.scss index d76976a..850684d 100644 --- a/src/style.scss +++ b/src/style.scss @@ -29,6 +29,10 @@ &:disabled { @apply opacity-50; } + + .components-spinner { + @apply m-0 ml-3; + } } &.is-secondary { diff --git a/src/utils.js b/src/utils.js index b5a7059..5047ec9 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,8 +1,17 @@ /** * WordPress dependencies */ +import apiFetch from '@wordpress/api-fetch'; + +import { + dispatch, + select +} from '@wordpress/data'; + import { SVG, Circle, G, Path } from '@wordpress/primitives'; +import { addQueryArgs } from '@wordpress/url'; + export const PageControlIcon = ({ isFilled = false }) => { return ( { ); }; + +const sendEvent = async( data ) => { + const { + setRunID, + setThreadID + } = dispatch( 'quickwp/data' ); + + const response = await apiFetch({ + path: `${ window.quickwp.api }/send`, + method: 'POST', + data: { ...data } + }); + + setThreadID( data.step, response.thread_id ); + + setRunID( data.step, response.id ); +}; + +const getEvent = async( type ) => { + const threadID = select( 'quickwp/data' ).getThreadID( type ); + + const response = await apiFetch({ + path: addQueryArgs( `${ window.quickwp.api }/get`, { + 'thread_id': threadID + }) + }); + + return response; +}; + +const getEventStatus = async( type ) => { + const threadID = select( 'quickwp/data' ).getThreadID( type ); + const runID = select( 'quickwp/data' ).getRunID( type ); + const { setProcessStatus } = dispatch( 'quickwp/data' ); + + const response = await apiFetch({ + path: addQueryArgs( `${ window.quickwp.api }/status`, { + 'thread_id': threadID, + 'run_id': runID + }) + }); + + if ( 'completed' !== response.status ) { + return false; + } + + setProcessStatus( type, true ); + + return true; +}; + +const extractPalette = response => { + const runID = select( 'quickwp/data' ).getRunID( 'color_palette' ); + const { setError } = dispatch( 'quickwp/data' ); + + const { data } = response; + + const target = data.find( item => item.run_id === runID ); + + const jsonString = target.content[0].text.value; + + let matches = jsonString.match( /\[(.|\n)*?\]/ ); + + if ( matches && matches[0]) { + let jsonArrayString = matches[0]; + + let jsonObject; + try { + jsonObject = JSON.parse( jsonArrayString ); + } catch ( error ) { + setError( true ); + return false; + } + + return jsonObject; + } else { + setError( true ); + return false; + } +}; + +const fetchImages = async( request ) => { + const runID = select( 'quickwp/data' ).getRunID( 'images' ); + const { setImages } = dispatch( 'quickwp/data' ); + + const { data } = request; + + const target = data.find( item => item.run_id === runID ); + + const query = target.content[0].text.value; + + const response = await apiFetch({ + path: addQueryArgs( `${ window.quickwp.api }/images`, { + query + }) + }); + + + setImages( response?.photos ); +}; + +const awaitEvent = async( type ) => { + const hasResolved = await getEventStatus( type ); + + if ( ! hasResolved ) { + await new Promise( resolve => setTimeout( resolve, 3000 ) ); + await awaitEvent( type ); + return; + } +}; + +export const generateColorPalette = async() => { + const siteTopic = select( 'quickwp/data' ).getSiteTopic(); + + const { + setColorPalette, + setProcessStatus + } = dispatch( 'quickwp/data' ); + + await sendEvent({ + step: 'color_palette', + message: siteTopic + }); + + await awaitEvent( 'color_palette' ); + + const response = await getEvent( 'color_palette' ); + + const palette = extractPalette( response ); + + setColorPalette( palette ); + setProcessStatus( 'color_palette', true ); +}; + +export const generateImages = async() => { + const siteDescription = select( 'quickwp/data' ).getSiteDescription(); + + const { + setImages, + setProcessStatus + } = dispatch( 'quickwp/data' ); + + await sendEvent({ + step: 'images', + message: siteDescription + }); + + await awaitEvent( 'images' ); + + const response = await getEvent( 'images' ); + + await fetchImages( response ); + + setProcessStatus( 'images', true ); +}; diff --git a/tests/php/static-analysis-stubs/quickwp.php b/tests/php/static-analysis-stubs/quickwp.php index 109fd56..7f6783e 100644 --- a/tests/php/static-analysis-stubs/quickwp.php +++ b/tests/php/static-analysis-stubs/quickwp.php @@ -8,4 +8,5 @@ define( 'QUICKWP_BASEFILE', __FILE__ ); define( 'QUICKWP_URL', plugins_url( '/', __FILE__ ) ); define( 'QUICKWP_PATH', dirname( __FILE__ ) ); -define( 'QUICKWP_VERSION', '1.0.0' ); \ No newline at end of file +define( 'QUICKWP_VERSION', '1.0.0' ); +define( 'QUICKWP_API', 'quickwp/v1' ); \ No newline at end of file From 266ac0b499d6a9df915732826cda3078614c5b6a Mon Sep 17 00:00:00 2001 From: Hardeep Asrani Date: Tue, 9 Jan 2024 07:18:54 +0530 Subject: [PATCH 04/14] chore: save progress --- src/parts/ColorPalette.js | 23 +++++++++++++++++++---- src/parts/ImageSuggestions.js | 26 ++++++++++++++++++++++---- src/parts/SiteDescription.js | 7 ------- src/parts/SiteTopic.js | 7 ------- src/store.js | 22 ++++++++++++++-------- src/utils.js | 12 +++++++----- 6 files changed, 62 insertions(+), 35 deletions(-) diff --git a/src/parts/ColorPalette.js b/src/parts/ColorPalette.js index 2b74682..e049a30 100644 --- a/src/parts/ColorPalette.js +++ b/src/parts/ColorPalette.js @@ -5,7 +5,8 @@ import { __ } from '@wordpress/i18n'; import { Button, - ColorIndicator + ColorIndicator, + Spinner } from '@wordpress/components'; import { @@ -23,14 +24,20 @@ const ColorPalette = () => { const { palette, - template + template, + hasLoaded } = useSelect( ( select ) => { const { getBlocks } = select( 'core/block-editor' ); - const { getColorPalette } = select( 'quickwp/data' ); + + const { + getColorPalette, + getProcessStatus + } = select( 'quickwp/data' ); return { palette: getColorPalette(), - template: getBlocks() + template: getBlocks(), + hasLoaded: true === getProcessStatus( 'color_palette' ) }; }); @@ -38,6 +45,14 @@ const ColorPalette = () => { onContinue(); }; + if ( ! hasLoaded ) { + return ( +
+ +
+ ); + } + return (
diff --git a/src/parts/ImageSuggestions.js b/src/parts/ImageSuggestions.js index 2484e70..82c8ff4 100644 --- a/src/parts/ImageSuggestions.js +++ b/src/parts/ImageSuggestions.js @@ -12,7 +12,8 @@ import { __ } from '@wordpress/i18n'; import { Button, - Icon + Icon, + Spinner } from '@wordpress/components'; import { @@ -25,16 +26,33 @@ import { useState } from '@wordpress/element'; const ImageSuggestions = () => { const [ value, setValue ] = useState([]); - const { images } = useSelect( select => { - const images = select( 'quickwp/data' ).getImages() || []; + const { + images, + hasLoaded + } = useSelect( select => { + const { + getImages, + getProcessStatus + } = select( 'quickwp/data' ); + + const images = getImages() || []; return { - images: images.slice( 0, 9 ) + images: images.slice( 0, 9 ), + hasLoaded: true === getProcessStatus( 'images' ) }; }); const { onContinue } = useDispatch( 'quickwp/data' ); + if ( ! hasLoaded ) { + return ( +
+ +
+ ); + } + return (
diff --git a/src/parts/SiteDescription.js b/src/parts/SiteDescription.js index 82b6fab..be89138 100644 --- a/src/parts/SiteDescription.js +++ b/src/parts/SiteDescription.js @@ -13,11 +13,6 @@ import { useSelect } from '@wordpress/data'; -/** - * Internal dependencies. - */ -import { generateImages } from '../utils'; - const SiteDescription = () => { const { siteDescription } = useSelect( select => { return { @@ -35,8 +30,6 @@ const SiteDescription = () => { return; } - generateImages(); - onContinue(); }; diff --git a/src/parts/SiteTopic.js b/src/parts/SiteTopic.js index 067006b..82124ac 100644 --- a/src/parts/SiteTopic.js +++ b/src/parts/SiteTopic.js @@ -15,11 +15,6 @@ import { import { ENTER } from '@wordpress/keycodes'; -/** - * Internal dependencies. - */ -import { generateColorPalette } from '../utils'; - const SiteTopic = () => { const { siteTopic } = useSelect( select => { return { @@ -37,8 +32,6 @@ const SiteTopic = () => { return; } - // At this point, we start the color palette generation process. - generateColorPalette(); onContinue(); }; diff --git a/src/store.js b/src/store.js index 8814c5a..0e263b8 100644 --- a/src/store.js +++ b/src/store.js @@ -1,21 +1,16 @@ /** * WordPress dependencies. */ -import apiFetch from '@wordpress/api-fetch'; - import { createReduxStore, - dispatch, - register, - select + register } from '@wordpress/data'; -import { addQueryArgs } from '@wordpress/url'; - /** * Internal dependencies. */ import STEPS from './steps'; +import { generateColorPalette, generateImages } from './utils'; const DEFAULT_STATE = { step: 0, @@ -75,7 +70,18 @@ const actions = { }; }, onContinue() { - return ({ dispatch }) => { + return ({ dispatch, select }) => { + const current = select.getStep(); + + switch ( current.value ) { + case 'site_topic': + generateColorPalette(); + break; + case 'color_palette': + generateImages(); + break; + } + dispatch( actions.nextStep() ); }; }, diff --git a/src/utils.js b/src/utils.js index 5047ec9..e4af6ed 100644 --- a/src/utils.js +++ b/src/utils.js @@ -8,7 +8,12 @@ import { select } from '@wordpress/data'; -import { SVG, Circle, G, Path } from '@wordpress/primitives'; +import { + Circle, + G, + Path, + SVG +} from '@wordpress/primitives'; import { addQueryArgs } from '@wordpress/url'; @@ -196,10 +201,7 @@ export const generateColorPalette = async() => { export const generateImages = async() => { const siteDescription = select( 'quickwp/data' ).getSiteDescription(); - const { - setImages, - setProcessStatus - } = dispatch( 'quickwp/data' ); + const { setProcessStatus } = dispatch( 'quickwp/data' ); await sendEvent({ step: 'images', From ad397263ab0ac8f958e478d1e8081361318e9a91 Mon Sep 17 00:00:00 2001 From: Hardeep Asrani Date: Tue, 23 Jan 2024 01:12:35 +0530 Subject: [PATCH 05/14] [WiP] Save current progress --- inc/class-api.php | 184 ++++++++++++++++++-- inc/class-main.php | 6 +- quickwp.php | 12 +- src/components/TemplatePreview.js | 17 +- src/parts/ColorPalette.js | 51 +++++- src/parts/ImageSuggestions.js | 160 +++++++++++++---- src/parts/Template.js | 75 +++----- src/parts/ViewSite.js | 44 ++++- src/store.js | 91 ++++++++-- src/style.scss | 26 ++- src/utils.js | 104 ++++++++--- tailwind.config.js | 4 +- tests/php/static-analysis-stubs/quickwp.php | 10 +- 13 files changed, 623 insertions(+), 161 deletions(-) diff --git a/inc/class-api.php b/inc/class-api.php index 18f4992..6675d85 100644 --- a/inc/class-api.php +++ b/inc/class-api.php @@ -59,7 +59,7 @@ public function register_routes() { $namespace = $this->namespace . '/' . $this->version; $routes = array( - 'send' => array( + 'send' => array( 'methods' => \WP_REST_Server::CREATABLE, 'args' => array( 'step' => array( @@ -73,7 +73,7 @@ public function register_routes() { ), 'callback' => array( $this, 'send' ), ), - 'status' => array( + 'status' => array( 'methods' => \WP_REST_Server::READABLE, 'args' => array( 'thread_id' => array( @@ -87,11 +87,17 @@ public function register_routes() { ), 'callback' => array( $this, 'status' ), ), - 'get' => array( + 'get' => array( 'methods' => \WP_REST_Server::READABLE, + 'args' => array( + 'thread_id' => array( + 'required' => true, + 'type' => 'string', + ), + ), 'callback' => array( $this, 'get' ), ), - 'images' => array( + 'images' => array( 'methods' => \WP_REST_Server::READABLE, 'args' => array( 'query' => array( @@ -101,6 +107,16 @@ public function register_routes() { ), 'callback' => array( $this, 'images' ), ), + 'templates' => array( + 'methods' => \WP_REST_Server::READABLE, + 'args' => array( + 'thread_id' => array( + 'required' => true, + 'type' => 'string', + ), + ), + 'callback' => array( $this, 'templates' ), + ), ); foreach ( $routes as $route => $args ) { @@ -123,9 +139,10 @@ public function send( \WP_REST_Request $request ) { $data = $request->get_params(); $request = wp_remote_post( - QUICKWP_API . 'wizard/send', + QUICKWP_APP_API . 'wizard/send', array( - 'body' => array( + 'timeout' => 10, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout + 'body' => array( 'step' => $data['step'], 'message' => $data['message'], ), @@ -161,9 +178,10 @@ public function status( \WP_REST_Request $request ) { $data = $request->get_params(); $request = wp_remote_get( // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_remote_get_wp_remote_get - QUICKWP_API . 'wizard/status', + QUICKWP_APP_API . 'wizard/status', array( - 'body' => array( + 'timeout' => 10, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout + 'body' => array( 'thread_id' => $data['thread_id'], 'run_id' => $data['run_id'], ), @@ -199,9 +217,10 @@ public function get( \WP_REST_Request $request ) { $data = $request->get_params(); $request = wp_remote_get(// phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_remote_get_wp_remote_get - QUICKWP_API . 'wizard/get', + QUICKWP_APP_API . 'wizard/get', array( - 'body' => array( + 'timeout' => 10, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout + 'body' => array( 'thread_id' => $data['thread_id'], ), ) @@ -225,6 +244,107 @@ public function get( \WP_REST_Request $request ) { return new \WP_REST_Response( $response, 200 ); } + /** + * Get templates. + * + * @param \WP_REST_Request $request Request. + * + * @return \WP_REST_Response + */ + public function templates( \WP_REST_Request $request ) { + $data = $request->get_params(); + + $request = wp_remote_get(// phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_remote_get_wp_remote_get + QUICKWP_APP_API . 'wizard/get', + array( + 'timeout' => 10, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout + 'body' => array( + 'thread_id' => $data['thread_id'], + ), + ) + ); + + if ( is_wp_error( $request ) ) { + return new \WP_REST_Response( array( 'error' => $request->get_error_message() ), 500 ); + } + + /** + * Holds the response as a standard class object + * + * @var \stdClass $response + */ + $response = json_decode( wp_remote_retrieve_body( $request ) ); + + if ( ! isset( $response->data ) || ! $response->data ) { + return new \WP_REST_Response( array( 'error' => __( 'Error', 'quickwp' ) ), 500 ); + } + + $items = self::process_json_from_response( $response->data ); + + if ( ! $items ) { + return new \WP_REST_Response( array( 'error' => __( 'Error', 'quickwp' ) ), 500 ); + } + + $patterns_used = array(); + + foreach ( $items as $item ) { + if ( ! isset( $item['slug'] ) || ! isset( $item['order'] ) || ! isset( $item['strings'] ) ) { + continue; + } + + $patterns_used[] = array( + 'order' => $item['order'], + 'slug' => $item['slug'], + ); + + $strings = $item['strings']; + + foreach ( $strings as $string ) { + add_filter( + 'quickwp/' . $string['slug'], + function () use( $string ) { + return $string['value']; + } + ); + } + } + + usort( + $patterns_used, + function ( $a, $b ) { + return $a['order'] <=> $b['order']; + } + ); + + $theme_path = get_stylesheet_directory(); + + $filtered_patterns = array(); + + foreach ( $patterns_used as $pattern ) { + $pattern['slug'] = str_replace( 'quickwp/', '', $pattern['slug'] ); + + $pattern_path = $theme_path . '/patterns/' . $pattern['slug'] . '.php'; + + if ( ! file_exists( $pattern_path ) ) { + continue; + } + + ob_start(); + include $pattern_path; + $pattern_content = ob_get_clean(); + + $filtered_patterns[] = $pattern_content; + } + + return new \WP_REST_Response( + array( + 'status' => 'success', + 'data' => implode( '', $filtered_patterns ), + ), + 200 + ); + } + /** * Get images. * @@ -236,9 +356,10 @@ public function images( \WP_REST_Request $request ) { $data = $request->get_params(); $request = wp_remote_get(// phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_remote_get_wp_remote_get - QUICKWP_API . 'wizard/images', + QUICKWP_APP_API . 'wizard/images', array( - 'body' => array( + 'timeout' => 10, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout + 'body' => array( 'query' => $data['query'], ), ) @@ -259,6 +380,45 @@ public function images( \WP_REST_Request $request ) { return new \WP_REST_Response( array( 'error' => __( 'Error', 'quickwp' ) ), 500 ); } + + return new \WP_REST_Response( $response, 200 ); } + + /** + * Get JSON from response. + * + * @param array $data Response. + * + * @return array|bool + */ + private static function process_json_from_response( $data ) { + // Find the target item. + $target = current( $data ); + + if ( false === $target ) { + // Handle the case where the target is not found. + return false; + } + + // Extract the JSON string. + $json_string = $target->content[0]->text->value; + + try { + $json_object = json_decode( $json_string, true ); + return $json_object; + } catch ( \Exception $e) { + // If parsing failed, try to find a JSON array in the string. + preg_match( '/\[(.|\n)*\]/', $json_string, $matches ); + + if ( ! empty( $matches ) ) { + $json_array_string = $matches[0]; + $json_object = json_decode( $json_array_string, true ); + + return $json_object; + } + } + + return false; + } } diff --git a/inc/class-main.php b/inc/class-main.php index 932ce52..624934c 100644 --- a/inc/class-main.php +++ b/inc/class-main.php @@ -54,18 +54,18 @@ public function enqueue_assets() { return; } - $asset_file = include QUICKWP_PATH . '/build/index.asset.php'; + $asset_file = include QUICKWP_APP_PATH . '/build/index.asset.php'; wp_enqueue_style( 'quickwp', - QUICKWP_URL . 'build/style-index.css', + QUICKWP_APP_URL . 'build/style-index.css', array( 'wp-components' ), $asset_file['version'] ); wp_enqueue_script( 'quickwp', - QUICKWP_URL . 'build/index.js', + QUICKWP_APP_URL . 'build/index.js', $asset_file['dependencies'], $asset_file['version'], true diff --git a/quickwp.php b/quickwp.php index 0509da0..7cdd387 100644 --- a/quickwp.php +++ b/quickwp.php @@ -17,13 +17,13 @@ die; } -define( 'QUICKWP_BASEFILE', __FILE__ ); -define( 'QUICKWP_URL', plugins_url( '/', __FILE__ ) ); -define( 'QUICKWP_PATH', __DIR__ ); -define( 'QUICKWP_VERSION', '1.0.0' ); -define( 'QUICKWP_API', 'https://4ab6-103-217-244-109.ngrok-free.app/api/' ); +define( 'QUICKWP_APP_BASEFILE', __FILE__ ); +define( 'QUICKWP_APP_URL', plugins_url( '/', __FILE__ ) ); +define( 'QUICKWP_APP_PATH', __DIR__ ); +define( 'QUICKWP_APP_VERSION', '1.0.0' ); +define( 'QUICKWP_APP_API', 'https://aaf0-103-217-244-105.ngrok-free.app/api/' ); -$vendor_file = QUICKWP_PATH . '/vendor/autoload.php'; +$vendor_file = QUICKWP_APP_PATH . '/vendor/autoload.php'; if ( is_readable( $vendor_file ) ) { require_once $vendor_file; diff --git a/src/components/TemplatePreview.js b/src/components/TemplatePreview.js index 26525e1..f46a698 100644 --- a/src/components/TemplatePreview.js +++ b/src/components/TemplatePreview.js @@ -1,11 +1,24 @@ +/** + * External dependencies. + */ +import classNames from 'classnames'; + /** * WordPress dependencies. */ import { BlockPreview } from '@wordpress/block-editor'; -const TemplatePreview = ({ template }) => { +const TemplatePreview = ({ + template, + canScroll = false +}) => { return ( -
+
{ const { onContinue } = useDispatch( 'quickwp/data' ); const { + globalStylesId, + defaultStyles, palette, template, hasLoaded } = useSelect( ( select ) => { const { getBlocks } = select( 'core/block-editor' ); + const { + __experimentalGetCurrentGlobalStylesId, + __experimentalGetCurrentThemeBaseGlobalStyles + } = select( 'core' ); + const { getColorPalette, getProcessStatus } = select( 'quickwp/data' ); + const globalStylesId = __experimentalGetCurrentGlobalStylesId(); + return { + globalStylesId, + defaultStyles: __experimentalGetCurrentThemeBaseGlobalStyles(), palette: getColorPalette(), template: getBlocks(), hasLoaded: true === getProcessStatus( 'color_palette' ) }; }); + const { editEntityRecord } = useDispatch( 'core' ); + + useEffect( () => { + if ( hasLoaded && Boolean( palette.length ) ) { + const colorPalette = palette.map( color => { + const paletteColor = defaultStyles.settings.color.palette.theme.find( paletteColor => paletteColor.slug === color.slug ); + + if ( paletteColor ) { + return { + ...color, + name: paletteColor.name + }; + } + + return color; + }); + + const settings = { + color: { + palette: { + theme: [ + ...colorPalette + ] + } + } + }; + + editEntityRecord( 'root', 'globalStyles', globalStylesId, { + settings + }); + } + }, [ hasLoaded, palette ]); + const onSubmit = async() => { onContinue(); }; @@ -77,7 +123,10 @@ const ColorPalette = () => {
- +
); }; diff --git a/src/parts/ImageSuggestions.js b/src/parts/ImageSuggestions.js index 82c8ff4..10b1567 100644 --- a/src/parts/ImageSuggestions.js +++ b/src/parts/ImageSuggestions.js @@ -12,8 +12,10 @@ import { __ } from '@wordpress/i18n'; import { Button, + Disabled, Icon, - Spinner + Spinner, + TextControl } from '@wordpress/components'; import { @@ -23,27 +25,63 @@ import { import { useState } from '@wordpress/element'; +import { ENTER } from '@wordpress/keycodes'; + +/** + * Internal dependencies. + */ +import { requestImages } from '../utils'; + const ImageSuggestions = () => { const [ value, setValue ] = useState([]); + const [ search, setSearch ] = useState( '' ); + const [ isLoading, setIsLoading ] = useState( false ); const { images, + imageKeywords, + activeImageKeyword, hasLoaded } = useSelect( select => { const { getImages, + getImageKeywords, + getActiveImageKeyword, getProcessStatus } = select( 'quickwp/data' ); - const images = getImages() || []; + const activeImageKeyword = getActiveImageKeyword(); + + const images = getImages( activeImageKeyword ) || []; return { - images: images.slice( 0, 9 ), + images, + imageKeywords: getImageKeywords() || [], + activeImageKeyword, hasLoaded: true === getProcessStatus( 'images' ) }; }); - const { onContinue } = useDispatch( 'quickwp/data' ); + const { + onContinue, + setActiveImageKeyword + } = useDispatch( 'quickwp/data' ); + + const onSearch = async( query = search ) => { + if ( ! query || activeImageKeyword === query ) { + return; + } + + if ( query !== search ) { + setSearch( query ); + } + + setActiveImageKeyword( query ); + + setIsLoading( true ); + await requestImages( query ); + setIsLoading( false ); + }; if ( ! hasLoaded ) { return ( @@ -68,41 +106,87 @@ const ImageSuggestions = () => {
-
-
- { images.map( ( image, index ) => ( -
{ - if ( value.includes( image.id ) ) { - setValue( value.filter( ( item ) => item !== image.id ) ); - } else { - setValue([ ...value, image.id ]); - } - }} - > - { value.includes( image.id ) && ( -
- -
- ) } - - -
- ) ) } +
+ { + if ( ENTER === e.keyCode ) { + onSearch(); + } + }} + disabled={ isLoading } + className="is-dark" + /> + +
+ { imageKeywords.map( ( keyword ) => { + return ( + + ); + }) }
+ + { ( isLoading ) && ( +
+ +
+ ) } + + +
+ { images.map( image => ( +
{ + if ( value.includes( image.id ) ) { + setValue( value.filter( ( item ) => item !== image.id ) ); + } else { + setValue([ ...value, image.id ]); + } + }} + > + { value.includes( image.id ) && ( +
+ +
+ ) } + + +
+ ) ) } +
+
); diff --git a/src/parts/Template.js b/src/parts/Template.js index 1b0295f..f9a4ee9 100644 --- a/src/parts/Template.js +++ b/src/parts/Template.js @@ -1,18 +1,13 @@ -/** - * External dependencies. - */ -import classNames from 'classnames'; - -import { check } from '@wordpress/icons'; - /** * WordPress dependencies. */ import { __ } from '@wordpress/i18n'; +import { parse } from '@wordpress/blocks'; + import { Button, - Icon + Spinner } from '@wordpress/components'; import { @@ -20,28 +15,39 @@ import { useSelect } from '@wordpress/data'; -import { useState } from '@wordpress/element'; - /** * Internal dependencies. */ import TemplatePreview from '../components/TemplatePreview'; -const dummy = [ 1, 2, 3, 4 ]; - const Template = () => { - const [ value, setValue ] = useState(); - const { onContinue } = useDispatch( 'quickwp/data' ); - const { template } = useSelect( ( select ) => { - const { getBlocks } = select( 'core/block-editor' ); + const { + template, + hasLoaded + } = useSelect( ( select ) => { + const { + getHomepage, + getProcessStatus + } = select( 'quickwp/data' ); + + const homepage = getHomepage(); return { - template: getBlocks() + template: homepage ? parse( homepage ) : [], + hasLoaded: true === getProcessStatus( 'homepage' ) }; }); + if ( ! hasLoaded ) { + return ( +
+ +
+ ); + } + return (
@@ -57,37 +63,10 @@ const Template = () => {
-
-
- { dummy.map( ( image, index ) => ( -
{ - setValue( value === image ? null : image ); - }} - > - { value === image && ( -
- -
- ) } - - -
- ) ) } -
-
+
); }; diff --git a/src/parts/ViewSite.js b/src/parts/ViewSite.js index d76314b..622877d 100644 --- a/src/parts/ViewSite.js +++ b/src/parts/ViewSite.js @@ -3,9 +3,51 @@ */ import { __ } from '@wordpress/i18n'; +import apiFetch from '@wordpress/api-fetch'; + import { Button } from '@wordpress/components'; +import { useDispatch } from '@wordpress/data'; + +import { downloadBlob } from '@wordpress/blob'; + + const ViewSite = () => { + const { createErrorNotice } = useDispatch( 'core/notices' ); + + async function handleExport() { + try { + const response = await apiFetch({ + path: '/wp-block-editor/v1/export', + parse: false, + headers: { + Accept: 'application/zip' + } + }); + const blob = await response.blob(); + const contentDisposition = response.headers.get( + 'content-disposition' + ); + const contentDispositionMatches = + contentDisposition.match( /=(.+)\.zip/ ); + const fileName = contentDispositionMatches[ 1 ] ? + contentDispositionMatches[ 1 ] : + 'edit-site-export'; + + downloadBlob( fileName + '.zip', blob, 'application/zip' ); + } catch ( errorResponse ) { + let error = {}; + try { + error = await errorResponse.json(); + } catch ( e ) {} + const errorMessage = + error.message && 'unknown_error' !== error.code ? + error.message : + __( 'An error occurred while creating the site export.', 'quickwp' ); + + createErrorNotice( errorMessage, { type: 'snackbar' }); + } + } return (
@@ -26,7 +68,7 @@ const ViewSite = () => {
diff --git a/src/store.js b/src/store.js index 0e263b8..5ba1295 100644 --- a/src/store.js +++ b/src/store.js @@ -10,7 +10,11 @@ import { * Internal dependencies. */ import STEPS from './steps'; -import { generateColorPalette, generateImages } from './utils'; +import { + generateColorPalette, + generateImages, + generateHomepage +} from './utils'; const DEFAULT_STATE = { step: 0, @@ -24,13 +28,21 @@ const DEFAULT_STATE = { 'thread_id': null, 'run_id': null, hasLoaded: false + }, + 'homepage': { + 'thread_id': null, + 'run_id': null, + hasLoaded: false } }, - images: [], + colorPalette: [], + images: {}, + imageKeywords: [], + activeImageKeyword: null, + homepage: null, siteTopic: null, siteDescription: null, - hasError: false, - colorPalette: [] + hasError: false }; const actions = { @@ -77,9 +89,12 @@ const actions = { case 'site_topic': generateColorPalette(); break; - case 'color_palette': + case 'site_description': + generateHomepage(); generateImages(); break; + case 'color_palette': + break; } dispatch( actions.nextStep() ); @@ -103,16 +118,35 @@ const actions = { colorPalette }; }, - setError( hasError ) { + setImages( key, images ) { return { - type: 'SET_ERROR', - hasError + type: 'SET_IMAGES', + key, + images }; }, - setImages( images ) { + setImageKeywords( imageKeywords ) { return { - type: 'SET_IMAGES', - images + type: 'SET_IMAGE_KEYWORDS', + imageKeywords + }; + }, + setActiveImageKeyword( activeImageKeyword ) { + return { + type: 'SET_ACTIVE_IMAGE_KEYWORD', + activeImageKeyword + }; + }, + setHomepage( homepage ) { + return { + type: 'SET_HOMEPAGE', + homepage + }; + }, + setError( hasError ) { + return { + type: 'SET_ERROR', + hasError }; }, setThreadID( item, threadID ) { @@ -173,7 +207,25 @@ const store = createReduxStore( 'quickwp/data', { case 'SET_IMAGES': return { ...state, - images: action.images + images: { + ...state.images, + [ action.key ]: action.images + } + }; + case 'SET_IMAGE_KEYWORDS': + return { + ...state, + imageKeywords: action.imageKeywords + }; + case 'SET_ACTIVE_IMAGE_KEYWORD': + return { + ...state, + activeImageKeyword: action.activeImageKeyword + }; + case 'SET_HOMEPAGE': + return { + ...state, + homepage: action.homepage }; case 'SET_THREAD_ID': return { @@ -228,12 +280,21 @@ const store = createReduxStore( 'quickwp/data', { getColorPalette( state ) { return state.colorPalette; }, + getImages( state, key ) { + return state.images[ key ]; + }, + getImageKeywords( state ) { + return state.imageKeywords; + }, + getActiveImageKeyword( state ) { + return state.activeImageKeyword; + }, + getHomepage( state ) { + return state.homepage; + }, hasError( state ) { return state.hasError; }, - getImages( state ) { - return state.images; - }, getThreadID( state, item ) { return state.processes[ item ]?.thread_id; }, diff --git a/src/style.scss b/src/style.scss index 850684d..65ab77c 100644 --- a/src/style.scss +++ b/src/style.scss @@ -5,12 +5,20 @@ #quickwp { --wp-admin-theme-color: #ffffff; - .components-text-control__input { - @apply bg-transparent max-w-xl border-0 h-12 text-2xl not-italic font-normal text-fg shadow-none; - } + .components-base-control { + .components-text-control__input { + @apply bg-transparent max-w-xl border-0 h-12 text-2xl not-italic font-normal text-fg shadow-none; + } + + .components-textarea-control__input { + @apply bg-transparent max-w-5xl h-24 border-0 text-2xl not-italic font-normal text-fg shadow-none resize-none; + } - .components-textarea-control__input { - @apply bg-transparent max-w-5xl h-24 border-0 text-2xl not-italic font-normal text-fg shadow-none resize-none; + &.is-dark { + .components-text-control__input { + @apply text-fg-alt bg-bg-alt h-10 w-full max-w-full text-xs p-3; + } + } } .components-text-control__input::placeholder, @@ -46,6 +54,14 @@ @apply opacity-50; } } + + &.is-token { + @apply text-xs leading-none px-4 py-2; + + &.is-active { + @apply bg-fg text-bg; + } + } } .block-editor-block-preview__content { diff --git a/src/utils.js b/src/utils.js index e4af6ed..75e3738 100644 --- a/src/utils.js +++ b/src/utils.js @@ -7,11 +7,12 @@ import { dispatch, select } from '@wordpress/data'; +import { home } from '@wordpress/icons'; import { Circle, - G, Path, + Rect, SVG } from '@wordpress/primitives'; @@ -46,21 +47,25 @@ export const PageControlIcon = ({ isFilled = false }) => { export const Logo = () => { return ( - + xmlns="http://www.w3.org/2000/svg"> + + ); }; @@ -84,9 +89,10 @@ const sendEvent = async( data ) => { const getEvent = async( type ) => { const threadID = select( 'quickwp/data' ).getThreadID( type ); + const route = 'homepage' !== type ? 'get' : 'templates'; const response = await apiFetch({ - path: addQueryArgs( `${ window.quickwp.api }/get`, { + path: addQueryArgs( `${ window.quickwp.api }/${ route }`, { 'thread_id': threadID }) }); @@ -147,13 +153,31 @@ const extractPalette = response => { const fetchImages = async( request ) => { const runID = select( 'quickwp/data' ).getRunID( 'images' ); - const { setImages } = dispatch( 'quickwp/data' ); + const { + setActiveImageKeyword, + setImageKeywords + } = dispatch( 'quickwp/data' ); const { data } = request; const target = data.find( item => item.run_id === runID ); - const query = target.content[0].text.value; + let queries = target.content[0].text.value; + + queries = queries.split( ',' ); + + queries = queries.map( query => query.trim() ); + + const query = queries[0]; + + setImageKeywords( queries ); + setActiveImageKeyword( query ); + + await requestImages( query ); +}; + +export const requestImages = async( query ) => { + const { setImages } = dispatch( 'quickwp/data' ); const response = await apiFetch({ path: addQueryArgs( `${ window.quickwp.api }/images`, { @@ -161,16 +185,15 @@ const fetchImages = async( request ) => { }) }); - - setImages( response?.photos ); + setImages( query, response?.photos ); }; -const awaitEvent = async( type ) => { +const awaitEvent = async( type, interval = 5000 ) => { const hasResolved = await getEventStatus( type ); if ( ! hasResolved ) { - await new Promise( resolve => setTimeout( resolve, 3000 ) ); - await awaitEvent( type ); + await new Promise( resolve => setTimeout( resolve, interval ) ); + await awaitEvent( type, interval ); return; } }; @@ -208,7 +231,7 @@ export const generateImages = async() => { message: siteDescription }); - await awaitEvent( 'images' ); + await awaitEvent( 'images', 10000 ); const response = await getEvent( 'images' ); @@ -216,3 +239,36 @@ export const generateImages = async() => { setProcessStatus( 'images', true ); }; + +export const generateHomepage = async() => { + const siteTopic = select( 'quickwp/data' ).getSiteTopic(); + const siteDescription = select( 'quickwp/data' ).getSiteDescription(); + + const { + setError, + setProcessStatus, + setHomepage + } = dispatch( 'quickwp/data' ); + + await sendEvent({ + step: 'homepage', + message: `Website topic: ${ siteTopic } | Website description${ siteDescription }` + }); + + await awaitEvent( 'homepage', 10000 ); + + const response = await getEvent( 'homepage' ); + + if ( 'success' !== response?.status ) { + setError( true ); + return; + } + + let homepageTemplate = ''; + homepageTemplate += ''; + homepageTemplate += response.data; + homepageTemplate += ''; + + setHomepage( homepageTemplate ); + setProcessStatus( 'homepage', true ); +}; diff --git a/tailwind.config.js b/tailwind.config.js index 1d2cde9..d048af6 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -12,7 +12,9 @@ module.exports = { colors: { bg: '#000000', fg: '#FFFFFF', - secondary: '#232323' + secondary: '#232323', + 'bg-alt': '#2F2F2F', + 'fg-alt': '#E0E0E0' }, maxHeight: { 'md': '32rem', diff --git a/tests/php/static-analysis-stubs/quickwp.php b/tests/php/static-analysis-stubs/quickwp.php index 7f6783e..fcd5eda 100644 --- a/tests/php/static-analysis-stubs/quickwp.php +++ b/tests/php/static-analysis-stubs/quickwp.php @@ -5,8 +5,8 @@ * Adds QuickWP constants for PHPStan to use. */ -define( 'QUICKWP_BASEFILE', __FILE__ ); -define( 'QUICKWP_URL', plugins_url( '/', __FILE__ ) ); -define( 'QUICKWP_PATH', dirname( __FILE__ ) ); -define( 'QUICKWP_VERSION', '1.0.0' ); -define( 'QUICKWP_API', 'quickwp/v1' ); \ No newline at end of file +define( 'QUICKWP_APP_BASEFILE', __FILE__ ); +define( 'QUICKWP_APP_URL', plugins_url( '/', __FILE__ ) ); +define( 'QUICKWP_APP_PATH', dirname( __FILE__ ) ); +define( 'QUICKWP_APP_VERSION', '1.0.0' ); +define( 'QUICKWP_APP_API', 'quickwp/v1' ); \ No newline at end of file From 67869c5d708406f901565d3eb15d9d7a25235d52 Mon Sep 17 00:00:00 2001 From: Hardeep Asrani Date: Tue, 23 Jan 2024 01:12:43 +0530 Subject: [PATCH 06/14] [WiP] Save current progress --- inc/class-api.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/inc/class-api.php b/inc/class-api.php index 6675d85..7353183 100644 --- a/inc/class-api.php +++ b/inc/class-api.php @@ -390,7 +390,7 @@ public function images( \WP_REST_Request $request ) { * * @param array $data Response. * - * @return array|bool + * @return array|false */ private static function process_json_from_response( $data ) { // Find the target item. @@ -407,7 +407,7 @@ private static function process_json_from_response( $data ) { try { $json_object = json_decode( $json_string, true ); return $json_object; - } catch ( \Exception $e) { + } catch ( \Exception $e ) { // If parsing failed, try to find a JSON array in the string. preg_match( '/\[(.|\n)*\]/', $json_string, $matches ); From 967db6df0ce44b60d94ed4ca1459b16bc3f65629 Mon Sep 17 00:00:00 2001 From: Hardeep Asrani Date: Wed, 24 Jan 2024 01:17:04 +0530 Subject: [PATCH 07/14] [WiP] Save current progress --- inc/class-api.php | 15 +++++++- src/components/ColorPicker.js | 47 ++++++++++++++++++++++++ src/parts/ColorPalette.js | 25 ++++++++++--- src/parts/ImageSuggestions.js | 46 ++++++++++++++++-------- src/parts/Template.js | 10 +++++- src/parts/ViewSite.js | 21 +++++++++-- src/store.js | 67 +++++++++++++++++++++++++++++++++-- src/utils.js | 53 +++++++++++++++++++++------ 8 files changed, 247 insertions(+), 37 deletions(-) create mode 100644 src/components/ColorPicker.js diff --git a/inc/class-api.php b/inc/class-api.php index 7353183..8d2488b 100644 --- a/inc/class-api.php +++ b/inc/class-api.php @@ -303,10 +303,23 @@ public function templates( \WP_REST_Request $request ) { add_filter( 'quickwp/' . $string['slug'], function () use( $string ) { - return $string['value']; + return esc_html( $string['value'] ); } ); } + + if ( isset( $item['images'] ) ) { + $images = $item['images']; + + foreach ( $images as $image ) { + add_filter( + 'quickwp/' . $image['slug'], + function () use( $image ) { + return esc_url( $image['src'] ); + } + ); + } + } } usort( diff --git a/src/components/ColorPicker.js b/src/components/ColorPicker.js new file mode 100644 index 0000000..31e979d --- /dev/null +++ b/src/components/ColorPicker.js @@ -0,0 +1,47 @@ +/** + * WordPress dependencies. + */ +import { + ColorIndicator, + ColorPicker as WPColorPicker, + Popover +} from '@wordpress/components'; + +import { useState } from '@wordpress/element'; + +const ColorPicker = ({ + value, + onChange +}) => { + const [ isOpen, setIsOpen ] = useState( false ); + + return ( + { + if ( ! e.target.classList.contains( 'component-color-indicator' ) ) { + return; + } + + setIsOpen( ! isOpen ); + } } + > + { isOpen && ( + setIsOpen( false ) } + > + + + ) } + + ); +}; + +export default ColorPicker; diff --git a/src/parts/ColorPalette.js b/src/parts/ColorPalette.js index 158ca29..d6f90bf 100644 --- a/src/parts/ColorPalette.js +++ b/src/parts/ColorPalette.js @@ -5,7 +5,6 @@ import { __ } from '@wordpress/i18n'; import { Button, - ColorIndicator, Spinner } from '@wordpress/components'; @@ -19,6 +18,7 @@ import { useEffect } from '@wordpress/element'; /** * Internal dependencies. */ +import ColorPicker from '../components/ColorPicker'; import TemplatePreview from '../components/TemplatePreview'; const ColorPalette = () => { @@ -56,6 +56,8 @@ const ColorPalette = () => { const { editEntityRecord } = useDispatch( 'core' ); + const { setColorPalette } = useDispatch( 'quickwp/data' ); + useEffect( () => { if ( hasLoaded && Boolean( palette.length ) ) { const colorPalette = palette.map( color => { @@ -87,6 +89,21 @@ const ColorPalette = () => { } }, [ hasLoaded, palette ]); + const onChangeColor = ( value, slug ) => { + const newPalette = palette.map( color => { + if ( color.slug === slug ) { + return { + ...color, + color: value + }; + } + + return color; + }); + + setColorPalette( newPalette ); + }; + const onSubmit = async() => { onContinue(); }; @@ -111,9 +128,9 @@ const ColorPalette = () => {
{ palette.map( ( color ) => ( - onChangeColor( e, color.slug ) } /> ) ) }
diff --git a/src/parts/ImageSuggestions.js b/src/parts/ImageSuggestions.js index 10b1567..7ad4dfa 100644 --- a/src/parts/ImageSuggestions.js +++ b/src/parts/ImageSuggestions.js @@ -23,7 +23,10 @@ import { useSelect } from '@wordpress/data'; -import { useState } from '@wordpress/element'; +import { + useEffect, + useState +} from '@wordpress/element'; import { ENTER } from '@wordpress/keycodes'; @@ -33,38 +36,42 @@ import { ENTER } from '@wordpress/keycodes'; import { requestImages } from '../utils'; const ImageSuggestions = () => { - const [ value, setValue ] = useState([]); const [ search, setSearch ] = useState( '' ); const [ isLoading, setIsLoading ] = useState( false ); + const [ images, setImages ] = useState([]); const { - images, + queuedImages, imageKeywords, activeImageKeyword, + selectedImages, hasLoaded } = useSelect( select => { const { getImages, getImageKeywords, getActiveImageKeyword, + getSelectedImages, getProcessStatus } = select( 'quickwp/data' ); const activeImageKeyword = getActiveImageKeyword(); - const images = getImages( activeImageKeyword ) || []; + const queuedImages = getImages( activeImageKeyword ) || []; return { - images, + queuedImages, imageKeywords: getImageKeywords() || [], activeImageKeyword, + selectedImages: getSelectedImages() || [], hasLoaded: true === getProcessStatus( 'images' ) }; }); const { onContinue, - setActiveImageKeyword + setActiveImageKeyword, + toggleSelectedImage } = useDispatch( 'quickwp/data' ); const onSearch = async( query = search ) => { @@ -83,6 +90,21 @@ const ImageSuggestions = () => { setIsLoading( false ); }; + useEffect( () => { + const filteredImages = queuedImages.filter( queuedImage => { + return ! selectedImages.find( selectedImage => { + return selectedImage.id === queuedImage.id; + }); + }); + + const mergedImages = [ + ...selectedImages, + ...filteredImages + ]; + + setImages( mergedImages ); + }, [ queuedImages ]); + if ( ! hasLoaded ) { return (
@@ -159,18 +181,12 @@ const ImageSuggestions = () => { className={ classNames( 'flex flex-1 cursor-pointer', { - 'outline outline-offset-2 outline-2 outline-white grayscale': value.includes( image.id ) + 'outline outline-offset-2 outline-2 outline-white grayscale': selectedImages.includes( image ) } ) } - onClick={ () => { - if ( value.includes( image.id ) ) { - setValue( value.filter( ( item ) => item !== image.id ) ); - } else { - setValue([ ...value, image.id ]); - } - }} + onClick={ () => toggleSelectedImage( image ) } > - { value.includes( image.id ) && ( + { selectedImages.includes( image ) && (
{ const { template, + isSaving, hasLoaded } = useSelect( ( select ) => { const { getHomepage, + isSaving, getProcessStatus } = select( 'quickwp/data' ); @@ -36,6 +38,7 @@ const Template = () => { return { template: homepage ? parse( homepage ) : [], + isSaving: isSaving(), hasLoaded: true === getProcessStatus( 'homepage' ) }; }); @@ -58,7 +61,12 @@ const Template = () => { ) } -
diff --git a/src/parts/ViewSite.js b/src/parts/ViewSite.js index 622877d..e020be1 100644 --- a/src/parts/ViewSite.js +++ b/src/parts/ViewSite.js @@ -9,13 +9,27 @@ import { Button } from '@wordpress/components'; import { useDispatch } from '@wordpress/data'; -import { downloadBlob } from '@wordpress/blob'; +const downloadBlob = ( filename, content, contentType = '' ) => { + if ( ! filename || ! content ) { + return; + } + const file = new window.Blob([ content ], { type: contentType }); + const url = window.URL.createObjectURL( file ); + const anchorElement = document.createElement( 'a' ); + anchorElement.href = url; + anchorElement.download = filename; + anchorElement.style.display = 'none'; + document.body.appendChild( anchorElement ); + anchorElement.click(); + document.body.removeChild( anchorElement ); + window.URL.revokeObjectURL( url ); +}; const ViewSite = () => { const { createErrorNotice } = useDispatch( 'core/notices' ); - async function handleExport() { + const handleExport = async() => { try { const response = await apiFetch({ path: '/wp-block-editor/v1/export', @@ -47,7 +61,8 @@ const ViewSite = () => { createErrorNotice( errorMessage, { type: 'snackbar' }); } - } + }; + return (
diff --git a/src/store.js b/src/store.js index 5ba1295..3c9b6d8 100644 --- a/src/store.js +++ b/src/store.js @@ -13,7 +13,8 @@ import STEPS from './steps'; import { generateColorPalette, generateImages, - generateHomepage + generateHomepage, + saveChanges } from './utils'; const DEFAULT_STATE = { @@ -39,9 +40,11 @@ const DEFAULT_STATE = { images: {}, imageKeywords: [], activeImageKeyword: null, + selectedImages: [], homepage: null, siteTopic: null, siteDescription: null, + isSavimg: false, hasError: false }; @@ -90,11 +93,16 @@ const actions = { generateColorPalette(); break; case 'site_description': - generateHomepage(); generateImages(); break; case 'color_palette': break; + case 'image_suggestions': + generateHomepage(); + break; + case 'frontpage_template': + saveChanges(); + break; } dispatch( actions.nextStep() ); @@ -137,6 +145,29 @@ const actions = { activeImageKeyword }; }, + toggleSelectedImage( image ) { + return ({ dispatch, select }) => { + const selectedImages = select.getSelectedImages(); + + if ( selectedImages.includes( image ) ) { + dispatch( actions.removeSelectedImage( image ) ); + } else { + dispatch( actions.addSelectedImage( image ) ); + } + }; + }, + addSelectedImage( image ) { + return { + type: 'ADD_SELECTED_IMAGE', + image + }; + }, + removeSelectedImage( image ) { + return { + type: 'REMOVE_SELECTED_IMAGE', + image + }; + }, setHomepage( homepage ) { return { type: 'SET_HOMEPAGE', @@ -149,6 +180,12 @@ const actions = { hasError }; }, + setSaving( isSaving ) { + return { + type: 'SET_SAVING', + isSaving + }; + }, setThreadID( item, threadID ) { return { type: 'SET_THREAD_ID', @@ -204,6 +241,11 @@ const store = createReduxStore( 'quickwp/data', { ...state, hasError: action.hasError }; + case 'SET_SAVING': + return { + ...state, + isSaving: action.isSaving + }; case 'SET_IMAGES': return { ...state, @@ -222,6 +264,21 @@ const store = createReduxStore( 'quickwp/data', { ...state, activeImageKeyword: action.activeImageKeyword }; + case 'ADD_SELECTED_IMAGE': + return { + ...state, + selectedImages: [ + ...state.selectedImages, + action.image + ] + }; + case 'REMOVE_SELECTED_IMAGE': + return { + ...state, + selectedImages: state.selectedImages.filter( + ( image ) => image !== action.image + ) + }; case 'SET_HOMEPAGE': return { ...state, @@ -289,12 +346,18 @@ const store = createReduxStore( 'quickwp/data', { getActiveImageKeyword( state ) { return state.activeImageKeyword; }, + getSelectedImages( state ) { + return state.selectedImages; + }, getHomepage( state ) { return state.homepage; }, hasError( state ) { return state.hasError; }, + isSaving( state ) { + return state.isSaving; + }, getThreadID( state, item ) { return state.processes[ item ]?.thread_id; }, diff --git a/src/utils.js b/src/utils.js index 75e3738..1c62218 100644 --- a/src/utils.js +++ b/src/utils.js @@ -176,6 +176,16 @@ const fetchImages = async( request ) => { await requestImages( query ); }; +const awaitEvent = async( type, interval = 5000 ) => { + const hasResolved = await getEventStatus( type ); + + if ( ! hasResolved ) { + await new Promise( resolve => setTimeout( resolve, interval ) ); + await awaitEvent( type, interval ); + return; + } +}; + export const requestImages = async( query ) => { const { setImages } = dispatch( 'quickwp/data' ); @@ -188,16 +198,6 @@ export const requestImages = async( query ) => { setImages( query, response?.photos ); }; -const awaitEvent = async( type, interval = 5000 ) => { - const hasResolved = await getEventStatus( type ); - - if ( ! hasResolved ) { - await new Promise( resolve => setTimeout( resolve, interval ) ); - await awaitEvent( type, interval ); - return; - } -}; - export const generateColorPalette = async() => { const siteTopic = select( 'quickwp/data' ).getSiteTopic(); @@ -243,6 +243,15 @@ export const generateImages = async() => { export const generateHomepage = async() => { const siteTopic = select( 'quickwp/data' ).getSiteTopic(); const siteDescription = select( 'quickwp/data' ).getSiteDescription(); + const images = select( 'quickwp/data' ).getSelectedImages(); + const currentTemplate = select( 'core/edit-site' ).getEditedPostId(); + + const { editEntityRecord } = dispatch( 'core' ); + + const imagesAr = images.map( image => ({ + src: image.src.original, + alt: image.alt + }) ); const { setError, @@ -252,7 +261,7 @@ export const generateHomepage = async() => { await sendEvent({ step: 'homepage', - message: `Website topic: ${ siteTopic } | Website description${ siteDescription }` + message: `Website topic: ${ siteTopic } | Website description: ${ siteDescription } | Images: ${ JSON.stringify( imagesAr ) }` }); await awaitEvent( 'homepage', 10000 ); @@ -269,6 +278,28 @@ export const generateHomepage = async() => { homepageTemplate += response.data; homepageTemplate += ''; + editEntityRecord( 'postType', 'wp_template', currentTemplate, { + 'content': homepageTemplate + }); + setHomepage( homepageTemplate ); setProcessStatus( 'homepage', true ); }; + +export const saveChanges = async() => { + const { __experimentalGetDirtyEntityRecords } = select( 'core' ); + + const { saveEditedEntityRecord } = dispatch( 'core' ); + + const { setSaving } = dispatch( 'quickwp/data' ); + + const edits = __experimentalGetDirtyEntityRecords(); + + setSaving( true ); + + await Promise.all( edits.map( async edit => { + await saveEditedEntityRecord( edit.kind, edit.name, edit?.key ); + }) ); + + setSaving( false ); +}; From 4d423357710591ad99b42dca64b9308d158c3d69 Mon Sep 17 00:00:00 2001 From: Hardeep Asrani Date: Mon, 29 Jan 2024 19:11:58 +0530 Subject: [PATCH 08/14] fix: phpstan errors --- inc/class-api.php | 18 ++++++++++------ src/parts/ColorPalette.js | 1 + src/store.js | 4 ++-- src/utils.js | 43 ++++++++++++++++++++++++++++++++++----- 4 files changed, 53 insertions(+), 13 deletions(-) diff --git a/inc/class-api.php b/inc/class-api.php index 8d2488b..bd96ab3 100644 --- a/inc/class-api.php +++ b/inc/class-api.php @@ -401,16 +401,15 @@ public function images( \WP_REST_Request $request ) { /** * Get JSON from response. * - * @param array $data Response. + * @param array $data Response. * - * @return array|false + * @return array|false */ private static function process_json_from_response( $data ) { // Find the target item. $target = current( $data ); - if ( false === $target ) { - // Handle the case where the target is not found. + if ( false === $target || !isset($target->content) ) { return false; } @@ -419,7 +418,12 @@ private static function process_json_from_response( $data ) { try { $json_object = json_decode( $json_string, true ); - return $json_object; + + if ( is_array( $json_object ) ) { + return $json_object; + } + + return false; } catch ( \Exception $e ) { // If parsing failed, try to find a JSON array in the string. preg_match( '/\[(.|\n)*\]/', $json_string, $matches ); @@ -428,7 +432,9 @@ private static function process_json_from_response( $data ) { $json_array_string = $matches[0]; $json_object = json_decode( $json_array_string, true ); - return $json_object; + if ( is_array( $json_object ) ) { + return $json_object; + } } } diff --git a/src/parts/ColorPalette.js b/src/parts/ColorPalette.js index d6f90bf..d2ed9b1 100644 --- a/src/parts/ColorPalette.js +++ b/src/parts/ColorPalette.js @@ -129,6 +129,7 @@ const ColorPalette = () => {
{ palette.map( ( color ) => ( onChangeColor( e, color.slug ) } /> diff --git a/src/store.js b/src/store.js index 3c9b6d8..400d695 100644 --- a/src/store.js +++ b/src/store.js @@ -42,8 +42,8 @@ const DEFAULT_STATE = { activeImageKeyword: null, selectedImages: [], homepage: null, - siteTopic: null, - siteDescription: null, + siteTopic: '', + siteDescription: '', isSavimg: false, hasError: false }; diff --git a/src/utils.js b/src/utils.js index 1c62218..1018739 100644 --- a/src/utils.js +++ b/src/utils.js @@ -37,7 +37,7 @@ export const PageControlIcon = ({ isFilled = false }) => { fill="none" className="hover:fill-white" stroke="white" - stroke-width="2" + strokeWidth="2" /> ) } @@ -53,8 +53,8 @@ export const Logo = () => { fill="none" xmlns="http://www.w3.org/2000/svg"> @@ -70,6 +70,39 @@ export const Logo = () => { ); }; +/** + * Sometimes OpenAI requests fail, so we try to redo them if that happens. + */ +const retryApiFetch = async( params = [], maxAttempts = 3, delay = 500 ) => { + const { setError } = dispatch( 'quickwp/data' ); + + let attempts = 0; + + const makeRequest = async() => { + try { + + const response = await apiFetch({ + ...params + }); + + return response; + } catch ( error ) { + + attempts++; + + if ( attempts < maxAttempts ) { + await new Promise( resolve => setTimeout( resolve, delay ) ); + return makeRequest(); + } else { + setError( true ); + throw error; + } + } + }; + + return makeRequest(); +}; + const sendEvent = async( data ) => { const { setRunID, @@ -91,7 +124,7 @@ const getEvent = async( type ) => { const threadID = select( 'quickwp/data' ).getThreadID( type ); const route = 'homepage' !== type ? 'get' : 'templates'; - const response = await apiFetch({ + const response = await retryApiFetch({ path: addQueryArgs( `${ window.quickwp.api }/${ route }`, { 'thread_id': threadID }) @@ -189,7 +222,7 @@ const awaitEvent = async( type, interval = 5000 ) => { export const requestImages = async( query ) => { const { setImages } = dispatch( 'quickwp/data' ); - const response = await apiFetch({ + const response = await retryApiFetch({ path: addQueryArgs( `${ window.quickwp.api }/images`, { query }) From 49006c494423c57132812fbbb3c5489ae2ac433e Mon Sep 17 00:00:00 2001 From: Hardeep Asrani Date: Mon, 29 Jan 2024 19:17:31 +0530 Subject: [PATCH 09/14] chore: update distignore --- .distignore | 4 ++++ inc/class-api.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.distignore b/.distignore index 84dd374..753003b 100644 --- a/.distignore +++ b/.distignore @@ -8,6 +8,10 @@ phpcs.xml .eslintrc .gitattributes .releaserc.yml +.DS_Store +.editorconfig +.browserslistrc +postcss.config.js docker-compose.yml webpack.config.js CONTRIBUTING.md diff --git a/inc/class-api.php b/inc/class-api.php index bd96ab3..2973580 100644 --- a/inc/class-api.php +++ b/inc/class-api.php @@ -409,7 +409,7 @@ private static function process_json_from_response( $data ) { // Find the target item. $target = current( $data ); - if ( false === $target || !isset($target->content) ) { + if ( false === $target || ! isset( $target->content ) ) { return false; } From ca82b282f165f2bb43273e8ecba72dd405bd368c Mon Sep 17 00:00:00 2001 From: Hardeep Asrani Date: Tue, 30 Jan 2024 00:38:09 +0530 Subject: [PATCH 10/14] fix: rest get calls --- inc/class-api.php | 62 +++++++++++++++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 21 deletions(-) diff --git a/inc/class-api.php b/inc/class-api.php index 2973580..acc1a83 100644 --- a/inc/class-api.php +++ b/inc/class-api.php @@ -177,14 +177,19 @@ public function send( \WP_REST_Request $request ) { public function status( \WP_REST_Request $request ) { $data = $request->get_params(); - $request = wp_remote_get( // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_remote_get_wp_remote_get - QUICKWP_APP_API . 'wizard/status', + $api_url = QUICKWP_APP_API . 'wizard/status'; + + $query_params = array( + 'thread_id' => $data['thread_id'], + 'run_id' => $data['run_id'], + ); + + $request_url = add_query_arg( $query_params, $api_url ); + + $request = wp_safe_remote_get( + $request_url, array( 'timeout' => 10, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout - 'body' => array( - 'thread_id' => $data['thread_id'], - 'run_id' => $data['run_id'], - ), ) ); @@ -216,13 +221,18 @@ public function status( \WP_REST_Request $request ) { public function get( \WP_REST_Request $request ) { $data = $request->get_params(); - $request = wp_remote_get(// phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_remote_get_wp_remote_get - QUICKWP_APP_API . 'wizard/get', + $api_url = QUICKWP_APP_API . 'wizard/get'; + + $query_params = array( + 'thread_id' => $data['thread_id'], + ); + + $request_url = add_query_arg( $query_params, $api_url ); + + $request = wp_safe_remote_get( + $request_url, array( 'timeout' => 10, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout - 'body' => array( - 'thread_id' => $data['thread_id'], - ), ) ); @@ -254,13 +264,18 @@ public function get( \WP_REST_Request $request ) { public function templates( \WP_REST_Request $request ) { $data = $request->get_params(); - $request = wp_remote_get(// phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_remote_get_wp_remote_get - QUICKWP_APP_API . 'wizard/get', + $api_url = QUICKWP_APP_API . 'wizard/get'; + + $query_params = array( + 'thread_id' => $data['thread_id'], + ); + + $request_url = add_query_arg( $query_params, $api_url ); + + $request = wp_safe_remote_get( + $request_url, array( 'timeout' => 10, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout - 'body' => array( - 'thread_id' => $data['thread_id'], - ), ) ); @@ -368,13 +383,18 @@ function ( $a, $b ) { public function images( \WP_REST_Request $request ) { $data = $request->get_params(); - $request = wp_remote_get(// phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_remote_get_wp_remote_get - QUICKWP_APP_API . 'wizard/images', + $api_url = QUICKWP_APP_API . 'wizard/images'; + + $query_params = array( + 'query' => $data['query'], + ); + + $request_url = add_query_arg( $query_params, $api_url ); + + $request = wp_safe_remote_get( + $api_url, array( 'timeout' => 10, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout - 'body' => array( - 'query' => $data['query'], - ), ) ); From d9a834539f719e96300b7629a3af76edcdf81720 Mon Sep 17 00:00:00 2001 From: Hardeep Asrani Date: Wed, 31 Jan 2024 03:12:41 +0530 Subject: [PATCH 11/14] fix: various fixes --- inc/class-api.php | 11 +- inc/class-main.php | 59 ++++++- package-lock.json | 280 ++++++++++++++++++++++++++++++++++ package.json | 9 +- quickwp.php | 9 +- src/App.js | 23 ++- src/frontend/index.js | 76 +++++++++ src/frontend/style.scss | 38 +++++ src/parts/ImageSuggestions.js | 40 ++++- src/parts/ViewSite.js | 30 ++-- 10 files changed, 544 insertions(+), 31 deletions(-) create mode 100644 src/frontend/index.js create mode 100644 src/frontend/style.scss diff --git a/inc/class-api.php b/inc/class-api.php index acc1a83..0973407 100644 --- a/inc/class-api.php +++ b/inc/class-api.php @@ -329,8 +329,13 @@ function () use( $string ) { foreach ( $images as $image ) { add_filter( 'quickwp/' . $image['slug'], - function () use( $image ) { - return esc_url( $image['src'] ); + function ( $value ) use( $image ) { + // Check if image slug is a valid URL. + if ( filter_var( $image['src'], FILTER_VALIDATE_URL ) ) { + return esc_url( $image['src'] ); + } + + return $value; } ); } @@ -392,7 +397,7 @@ public function images( \WP_REST_Request $request ) { $request_url = add_query_arg( $query_params, $api_url ); $request = wp_safe_remote_get( - $api_url, + $request_url, array( 'timeout' => 10, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout ) diff --git a/inc/class-main.php b/inc/class-main.php index 624934c..2c8e63b 100644 --- a/inc/class-main.php +++ b/inc/class-main.php @@ -36,6 +36,12 @@ public function __construct() { */ private function register_hooks() { add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_assets' ) ); + + if ( QUICKWP_APP_GUIDED_MODE ) { + add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_assets' ) ); + add_action( 'admin_init', array( $this, 'guided_access' ) ); + add_action( 'show_admin_bar', '__return_false' ); + } } /** @@ -54,18 +60,18 @@ public function enqueue_assets() { return; } - $asset_file = include QUICKWP_APP_PATH . '/build/index.asset.php'; + $asset_file = include QUICKWP_APP_PATH . '/build/backend/index.asset.php'; wp_enqueue_style( 'quickwp', - QUICKWP_APP_URL . 'build/style-index.css', + QUICKWP_APP_URL . 'build/backend/style-index.css', array( 'wp-components' ), $asset_file['version'] ); wp_enqueue_script( 'quickwp', - QUICKWP_APP_URL . 'build/index.js', + QUICKWP_APP_URL . 'build/backend/index.js', $asset_file['dependencies'], $asset_file['version'], true @@ -77,8 +83,53 @@ public function enqueue_assets() { 'quickwp', 'quickwp', array( - 'api' => $this->api->get_endpoint(), + 'api' => $this->api->get_endpoint(), + 'siteUrl' => get_site_url(), + 'isGuidedMode' => QUICKWP_APP_GUIDED_MODE ) ); } + + /** + * Enqueue frontend assets. + * + * @return void + */ + public function enqueue_frontend_assets() { + $asset_file = include QUICKWP_APP_PATH . '/build/frontend/frontend.asset.php'; + + wp_enqueue_style( + 'quickwp-frontend', + QUICKWP_APP_URL . 'build/frontend/style-index.css', + array(), + $asset_file['version'] + ); + + wp_enqueue_script( + 'quickwp-frontend', + QUICKWP_APP_URL . 'build/frontend/frontend.js', + $asset_file['dependencies'], + $asset_file['version'], + true + ); + } + + /** + * Guided access. + * + * @return void + */ + public function guided_access() { + global $pagenow; + + if ( defined( 'DOING_AJAX' ) && DOING_AJAX || defined( 'REST_REQUEST' ) && REST_REQUEST ) { + return; + } + + // Allow access to themes.php page only + if ( 'site-editor.php' !== $pagenow ) { + wp_redirect( admin_url( 'site-editor.php?quickwp=true&canvas=edit' ) ); + exit; + } + } } diff --git a/package-lock.json b/package-lock.json index f8118cc..ef19050 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "devDependencies": { "@wordpress/scripts": "^26.19.0", "eslint-config-wordpress": "^2.0.0", + "npm-run-all": "^4.1.5", "simple-git-hooks": "^2.9.0", "tailwindcss": "^3.4.0" } @@ -12046,6 +12047,12 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -12468,6 +12475,52 @@ "uc.micro": "^1.0.1" } }, + "node_modules/load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/load-json-file/node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/load-json-file/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/load-json-file/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/loader-runner": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", @@ -12848,6 +12901,15 @@ "integrity": "sha512-yywVJy8ctVlN5lNPxsep5urnZ6TTclwPEyigM9M3Bi8vseJBOfqNrGWN/r8NzuIt3PovM323W04blJfGQfQSVg==", "dev": true }, + "node_modules/memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, "node_modules/meow": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", @@ -13261,6 +13323,12 @@ "node": ">= 0.4.0" } }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, "node_modules/no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", @@ -13515,6 +13583,189 @@ "node": ">=10" } }, + "node_modules/npm-run-all": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", + "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "memorystream": "^0.3.1", + "minimatch": "^3.0.4", + "pidtree": "^0.3.0", + "read-pkg": "^3.0.0", + "shell-quote": "^1.6.1", + "string.prototype.padend": "^3.0.0" + }, + "bin": { + "npm-run-all": "bin/npm-run-all/index.js", + "run-p": "bin/run-p/index.js", + "run-s": "bin/run-s/index.js" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/npm-run-all/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/npm-run-all/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/npm-run-all/node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/npm-run-all/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/npm-run-all/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "node_modules/npm-run-all/node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/npm-run-all/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", + "dev": true, + "dependencies": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/npm-run-all/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -14084,6 +14335,18 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pidtree": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", + "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -16880,6 +17143,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/string.prototype.padend": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.5.tgz", + "integrity": "sha512-DOB27b/2UTTD+4myKUFh+/fXWcu/UDyASIXfg+7VzoCNNGOfWvoyU/x5pvVHr++ztyt/oSYI1BcWBBG/hmlNjA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/string.prototype.trim": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", diff --git a/package.json b/package.json index 7a0bb92..5f3a736 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,9 @@ "version": "1.0.0", "description": "Build websites quickly.", "scripts": { - "build": "wp-scripts build", + "build": "npm-run-all build:*", + "build:backend": "wp-scripts build src/index.js --output-path=build/backend --output-filename=index.js", + "build:frontend": "wp-scripts build src/frontend/index.js --output-path=build/frontend --output-filename=frontend.js", "check-engines": "wp-scripts check-engines", "check-licenses": "wp-scripts check-licenses", "format": "wp-scripts format", @@ -13,7 +15,9 @@ "lint:pkg-json": "wp-scripts lint-pkg-json", "packages-update": "wp-scripts packages-update", "plugin-zip": "wp-scripts plugin-zip", - "start": "wp-scripts start", + "start": "npm-run-all --parallel start:*", + "start:backend": "wp-scripts start src/index.js --output-path=build/backend --output-filename=index.js", + "start:frontend": "wp-scripts start src/frontend/index.js --output-path=build/frontend --output-filename=frontend.js", "test:e2e": "wp-scripts test-e2e", "test:unit": "wp-scripts test-unit-js", "dist": "bash bin/dist.sh" @@ -31,6 +35,7 @@ "devDependencies": { "@wordpress/scripts": "^26.19.0", "eslint-config-wordpress": "^2.0.0", + "npm-run-all": "^4.1.5", "simple-git-hooks": "^2.9.0", "tailwindcss": "^3.4.0" }, diff --git a/quickwp.php b/quickwp.php index 7cdd387..a1c3633 100644 --- a/quickwp.php +++ b/quickwp.php @@ -21,7 +21,14 @@ define( 'QUICKWP_APP_URL', plugins_url( '/', __FILE__ ) ); define( 'QUICKWP_APP_PATH', __DIR__ ); define( 'QUICKWP_APP_VERSION', '1.0.0' ); -define( 'QUICKWP_APP_API', 'https://aaf0-103-217-244-105.ngrok-free.app/api/' ); + +if ( ! defined( 'QUICKWP_APP_API' ) ) { + define( 'QUICKWP_APP_API', 'https://aaf0-103-217-244-105.ngrok-free.app/api/' ); +} + +if ( ! defined( 'QUICKWP_APP_GUIDED_MODE' ) ) { + define( 'QUICKWP_APP_GUIDED_MODE', false ); +} $vendor_file = QUICKWP_APP_PATH . '/vendor/autoload.php'; diff --git a/src/App.js b/src/App.js index 2538258..11aff0f 100644 --- a/src/App.js +++ b/src/App.js @@ -3,7 +3,12 @@ */ import { __ } from '@wordpress/i18n'; -import { useSelect } from '@wordpress/data'; +import { + useDispatch, + useSelect +} from '@wordpress/data'; + +import { useEffect } from '@wordpress/element'; /** * Internal dependencies. @@ -15,21 +20,33 @@ import Header from './parts/Header'; const App = () => { const isEditorLoading = useIsSiteEditorLoading(); + const { toggle } = useDispatch( 'core/preferences' ); + const { currentStep, - hasError + hasError, + hasWelcome } = useSelect( select => { const { getStep, hasError } = select( 'quickwp/data' ); + const { get } = select( 'core/preferences' ); + return { currentStep: getStep(), - hasError: hasError() + hasError: hasError(), + hasWelcome: get( 'core/edit-site', 'welcomeGuide' ) }; }); + useEffect( () => { + if ( hasWelcome ) { + toggle( 'core/edit-site', 'welcomeGuide' ); + } + }, [ hasWelcome ]); + const StepControls = currentStep?.view || null; if ( isEditorLoading ) { diff --git a/src/frontend/index.js b/src/frontend/index.js new file mode 100644 index 0000000..f520333 --- /dev/null +++ b/src/frontend/index.js @@ -0,0 +1,76 @@ +/** + * WordPress dependencies. + */ +import apiFetch from '@wordpress/api-fetch'; + +/** + * Internal dependencies. + */ +import './style.scss'; + +const downloadBlob = ( filename, content, contentType = '' ) => { + if ( ! filename || ! content ) { + return; + } + + const file = new window.Blob([ content ], { type: contentType }); + const url = window.URL.createObjectURL( file ); + const anchorElement = document.createElement( 'a' ); + anchorElement.href = url; + anchorElement.download = filename; + anchorElement.style.display = 'none'; + document.body.appendChild( anchorElement ); + anchorElement.click(); + document.body.removeChild( anchorElement ); + window.URL.revokeObjectURL( url ); +}; + +const handleExport = async() => { + try { + const response = await apiFetch({ + path: '/wp-block-editor/v1/export', + parse: false, + headers: { + Accept: 'application/zip' + } + }); + const blob = await response.blob(); + const contentDisposition = response.headers.get( + 'content-disposition' + ); + const contentDispositionMatches = + contentDisposition.match( /=(.+)\.zip/ ); + const fileName = contentDispositionMatches[ 1 ] ? + contentDispositionMatches[ 1 ] : + 'edit-site-export'; + + downloadBlob( fileName + '.zip', blob, 'application/zip' ); + } catch ( errorResponse ) { + let error = {}; + try { + error = await errorResponse.json(); + } catch ( e ) {} + const errorMessage = + error.message && 'unknown_error' !== error.code ? + error.message : + __( 'An error occurred while creating the site export.', 'quickwp' ); + + console.error( errorMessage ); + } +}; + +const toolbar = document.createElement( 'div' ); + +toolbar.classList.add( 'quickwp-toolbar' ); + +const paragraph = document.createElement( 'p' ); +paragraph.textContent = 'You can download the ZIP file and install it to your WordPress site.'; +toolbar.appendChild( paragraph ); + +const button = document.createElement( 'button' ); +button.textContent = 'Download'; +toolbar.appendChild( button ); + +button.addEventListener( 'click', handleExport ); + +document.body.appendChild( toolbar ); diff --git a/src/frontend/style.scss b/src/frontend/style.scss new file mode 100644 index 0000000..aa3a142 --- /dev/null +++ b/src/frontend/style.scss @@ -0,0 +1,38 @@ +.quickwp-toolbar { + position: fixed; + display: flex; + background: #000; + height: 80px; + bottom: 0; + width: 100%; + box-shadow: 0px 0px 2px 0px rgba(0,0,0,0.75); + align-items: center; + justify-content: space-around; + gap: 12px; + + button { + width: fit-content; + cursor: pointer; + border-radius: 0.375rem; + border-width: 1px; + border-style: solid; + border-color: #FFF; + background-color: transparent; + padding-left: 1.5rem; + padding-right: 1.5rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + font-size: 1.125rem; + line-height: 1.75rem; + font-weight: 500; + font-style: normal; + color: #FFF; + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + + &:hover { + background-color: #FFF; + color: #000; + } + } +} \ No newline at end of file diff --git a/src/parts/ImageSuggestions.js b/src/parts/ImageSuggestions.js index 7ad4dfa..9412fdb 100644 --- a/src/parts/ImageSuggestions.js +++ b/src/parts/ImageSuggestions.js @@ -8,7 +8,10 @@ import { check } from '@wordpress/icons'; /** * WordPress dependencies. */ -import { __ } from '@wordpress/i18n'; +import { + __, + sprintf +} from '@wordpress/i18n'; import { Button, @@ -102,6 +105,10 @@ const ImageSuggestions = () => { ...filteredImages ]; + if ( mergedImages.length && JSON.stringify( mergedImages ) === JSON.stringify( images ) ) { + return; + } + setImages( mergedImages ); }, [ queuedImages ]); @@ -179,15 +186,17 @@ const ImageSuggestions = () => {
toggleSelectedImage( image ) } > { selectedImages.includes( image ) && ( - diff --git a/src/parts/ViewSite.js b/src/parts/ViewSite.js index e020be1..10b71a8 100644 --- a/src/parts/ViewSite.js +++ b/src/parts/ViewSite.js @@ -73,24 +73,28 @@ const ViewSite = () => { ) } -

- { __( - 'You can download the ZIP file and install it to your WordPress site.', - 'quickwp' - ) } -

+ { Boolean( window.quickwp.isGuidedMode ) && ( +

+ { __( + 'You can download the ZIP file and install it to your WordPress site.', + 'quickwp' + ) } +

+ ) }
- + { Boolean( window.quickwp.isGuidedMode ) && ( + + ) } From e537ccc65652160dc0943562eee34a59a150c539 Mon Sep 17 00:00:00 2001 From: Hardeep Asrani Date: Wed, 31 Jan 2024 03:13:01 +0530 Subject: [PATCH 12/14] fix: various fixes --- inc/class-main.php | 9 ++++----- phpstan.neon | 2 ++ tests/php/static-analysis-stubs/quickwp.php | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/inc/class-main.php b/inc/class-main.php index 2c8e63b..e5bd72e 100644 --- a/inc/class-main.php +++ b/inc/class-main.php @@ -37,10 +37,10 @@ public function __construct() { private function register_hooks() { add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_assets' ) ); - if ( QUICKWP_APP_GUIDED_MODE ) { + if ( defined( 'QUICKWP_APP_GUIDED_MODE' ) && QUICKWP_APP_GUIDED_MODE ) { add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_assets' ) ); add_action( 'admin_init', array( $this, 'guided_access' ) ); - add_action( 'show_admin_bar', '__return_false' ); + add_filter( 'show_admin_bar', '__return_false' ); // phpcs:ignore WordPressVIPMinimum.UserExperience.AdminBarRemoval.RemovalDetected } } @@ -85,7 +85,7 @@ public function enqueue_assets() { array( 'api' => $this->api->get_endpoint(), 'siteUrl' => get_site_url(), - 'isGuidedMode' => QUICKWP_APP_GUIDED_MODE + 'isGuidedMode' => defined( 'QUICKWP_APP_GUIDED_MODE' ) && QUICKWP_APP_GUIDED_MODE, ) ); } @@ -126,9 +126,8 @@ public function guided_access() { return; } - // Allow access to themes.php page only if ( 'site-editor.php' !== $pagenow ) { - wp_redirect( admin_url( 'site-editor.php?quickwp=true&canvas=edit' ) ); + wp_safe_redirect( admin_url( 'site-editor.php?quickwp=true&canvas=edit' ) ); exit; } } diff --git a/phpstan.neon b/phpstan.neon index 4b001c4..2dbc2a1 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -5,5 +5,7 @@ parameters: bootstrapFiles: - %currentWorkingDirectory%/tests/php/static-analysis-stubs/quickwp.php checkGenericClassInNonGenericObjectType: false + dynamicConstantNames: + - QUICKWP_APP_GUIDED_MODE includes: - %currentWorkingDirectory%/vendor/szepeviktor/phpstan-wordpress/extension.neon \ No newline at end of file diff --git a/tests/php/static-analysis-stubs/quickwp.php b/tests/php/static-analysis-stubs/quickwp.php index fcd5eda..752caee 100644 --- a/tests/php/static-analysis-stubs/quickwp.php +++ b/tests/php/static-analysis-stubs/quickwp.php @@ -9,4 +9,4 @@ define( 'QUICKWP_APP_URL', plugins_url( '/', __FILE__ ) ); define( 'QUICKWP_APP_PATH', dirname( __FILE__ ) ); define( 'QUICKWP_APP_VERSION', '1.0.0' ); -define( 'QUICKWP_APP_API', 'quickwp/v1' ); \ No newline at end of file +define( 'QUICKWP_APP_API', 'quickwp/v1' ); From 7afae81833b3ee9b031e027a88e22e89e0d2c01e Mon Sep 17 00:00:00 2001 From: Hardeep Asrani Date: Mon, 5 Feb 2024 09:03:04 +0530 Subject: [PATCH 13/14] chore: save progress --- inc/class-api.php | 184 +++++++++++++++++------------- inc/class-main.php | 58 ++++++++++ src/components/Loader.js | 4 +- src/components/TemplatePreview.js | 35 ++++-- src/parts/ColorPalette.js | 87 ++++++++++---- src/parts/ImageSuggestions.js | 36 +++--- src/parts/Template.js | 75 ++++++++---- src/store.js | 17 ++- src/style.scss | 10 +- src/utils.js | 56 ++++++--- tailwind.config.js | 8 +- 11 files changed, 396 insertions(+), 174 deletions(-) diff --git a/inc/class-api.php b/inc/class-api.php index 0973407..64c1acf 100644 --- a/inc/class-api.php +++ b/inc/class-api.php @@ -141,7 +141,7 @@ public function send( \WP_REST_Request $request ) { $request = wp_remote_post( QUICKWP_APP_API . 'wizard/send', array( - 'timeout' => 10, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout + 'timeout' => 20, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout 'body' => array( 'step' => $data['step'], 'message' => $data['message'], @@ -189,7 +189,7 @@ public function status( \WP_REST_Request $request ) { $request = wp_safe_remote_get( $request_url, array( - 'timeout' => 10, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout + 'timeout' => 20, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout ) ); @@ -232,7 +232,7 @@ public function get( \WP_REST_Request $request ) { $request = wp_safe_remote_get( $request_url, array( - 'timeout' => 10, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout + 'timeout' => 20, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout ) ); @@ -275,7 +275,7 @@ public function templates( \WP_REST_Request $request ) { $request = wp_safe_remote_get( $request_url, array( - 'timeout' => 10, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout + 'timeout' => 20, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout ) ); @@ -297,82 +297,25 @@ public function templates( \WP_REST_Request $request ) { $items = self::process_json_from_response( $response->data ); if ( ! $items ) { - return new \WP_REST_Response( array( 'error' => __( 'Error', 'quickwp' ) ), 500 ); + return new \WP_REST_Response( array( 'error' => __( 'Error Parsing JSON', 'quickwp' ) ), 500 ); } - $patterns_used = array(); - - foreach ( $items as $item ) { - if ( ! isset( $item['slug'] ) || ! isset( $item['order'] ) || ! isset( $item['strings'] ) ) { - continue; - } - - $patterns_used[] = array( - 'order' => $item['order'], - 'slug' => $item['slug'], - ); - - $strings = $item['strings']; - - foreach ( $strings as $string ) { - add_filter( - 'quickwp/' . $string['slug'], - function () use( $string ) { - return esc_html( $string['value'] ); - } - ); - } - - if ( isset( $item['images'] ) ) { - $images = $item['images']; + $result = array(); - foreach ( $images as $image ) { - add_filter( - 'quickwp/' . $image['slug'], - function ( $value ) use( $image ) { - // Check if image slug is a valid URL. - if ( filter_var( $image['src'], FILTER_VALIDATE_URL ) ) { - return esc_url( $image['src'] ); - } + foreach( $items as $item ) { + $pattern = self::extract_patterns( $item ); - return $value; - } - ); - } - } - } - - usort( - $patterns_used, - function ( $a, $b ) { - return $a['order'] <=> $b['order']; - } - ); - - $theme_path = get_stylesheet_directory(); - - $filtered_patterns = array(); - - foreach ( $patterns_used as $pattern ) { - $pattern['slug'] = str_replace( 'quickwp/', '', $pattern['slug'] ); - - $pattern_path = $theme_path . '/patterns/' . $pattern['slug'] . '.php'; - - if ( ! file_exists( $pattern_path ) ) { + if ( ! $pattern ) { continue; } - ob_start(); - include $pattern_path; - $pattern_content = ob_get_clean(); - - $filtered_patterns[] = $pattern_content; + $result[] = $pattern; } return new \WP_REST_Response( array( 'status' => 'success', - 'data' => implode( '', $filtered_patterns ), + 'data' => $result, ), 200 ); @@ -399,7 +342,7 @@ public function images( \WP_REST_Request $request ) { $request = wp_safe_remote_get( $request_url, array( - 'timeout' => 10, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout + 'timeout' => 20, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout ) ); @@ -414,12 +357,10 @@ public function images( \WP_REST_Request $request ) { */ $response = json_decode( wp_remote_retrieve_body( $request ) ); - if ( ! isset( $response->photos ) || count( $response->photos ) === 0 ) { + if ( ! isset( $response->photos ) ) { return new \WP_REST_Response( array( 'error' => __( 'Error', 'quickwp' ) ), 500 ); } - - return new \WP_REST_Response( $response, 200 ); } @@ -448,15 +389,12 @@ private static function process_json_from_response( $data ) { return $json_object; } - return false; + throw new \Exception( 'Invalid JSON' ); } catch ( \Exception $e ) { - // If parsing failed, try to find a JSON array in the string. - preg_match( '/\[(.|\n)*\]/', $json_string, $matches ); - - if ( ! empty( $matches ) ) { - $json_array_string = $matches[0]; - $json_object = json_decode( $json_array_string, true ); - + if ( substr( $json_string, 0, 7 ) === '```json' && substr( trim( $json_string ), -3 ) === '```' ) { + $cleaned_json = trim( str_replace( [ '```json', '```' ], '', $json_string ) ); + $json_object = json_decode( $cleaned_json, true ); + if ( is_array( $json_object ) ) { return $json_object; } @@ -465,4 +403,90 @@ private static function process_json_from_response( $data ) { return false; } + + /** + * Extract Patterns. + * + * @param array $items Items. + * + * @return array + */ + private static function extract_patterns( $items ) { + if ( ! isset( $items['slug'] ) || ! isset( $items['data'] ) ) { + return; + } + + $patterns_used = array(); + + foreach ( $items['data'] as $item ) { + if ( ! isset( $item['slug'] ) || ! isset( $item['order'] ) || ! isset( $item['strings'] ) ) { + continue; + } + + $patterns_used[] = array( + 'order' => $item['order'], + 'slug' => $item['slug'], + ); + + $strings = $item['strings']; + + foreach ( $strings as $string ) { + add_filter( + 'quickwp/' . $string['slug'], + function () use( $string ) { + return esc_html( $string['value'] ); + } + ); + } + + if ( isset( $item['images'] ) ) { + $images = $item['images']; + + foreach ( $images as $image ) { + add_filter( + 'quickwp/' . $image['slug'], + function ( $value ) use( $image ) { + if ( filter_var( $image['src'], FILTER_VALIDATE_URL ) && ( strpos( $image['src'], 'pexels.com' ) !== false ) ) { + return esc_url( $image['src'] ); + } + + return $value; + } + ); + } + } + } + + usort( + $patterns_used, + function ( $a, $b ) { + return $a['order'] <=> $b['order']; + } + ); + + $theme_path = get_stylesheet_directory(); + + $filtered_patterns = array(); + + foreach ( $patterns_used as $pattern ) { + $pattern['slug'] = str_replace( 'quickwp/', '', $pattern['slug'] ); + + $pattern_path = $theme_path . '/patterns/' . $pattern['slug'] . '.php'; + + if ( ! file_exists( $pattern_path ) ) { + continue; + } + + ob_start(); + include $pattern_path; + $pattern_content = ob_get_clean(); + + $filtered_patterns[] = $pattern_content; + } + + return array( + 'slug' => $items['slug'], + 'patterns' => implode( '', $filtered_patterns ), + ); + } } diff --git a/inc/class-main.php b/inc/class-main.php index e5bd72e..0e57d0d 100644 --- a/inc/class-main.php +++ b/inc/class-main.php @@ -42,6 +42,8 @@ private function register_hooks() { add_action( 'admin_init', array( $this, 'guided_access' ) ); add_filter( 'show_admin_bar', '__return_false' ); // phpcs:ignore WordPressVIPMinimum.UserExperience.AdminBarRemoval.RemovalDetected } + + add_action( 'wp_print_footer_scripts', array( $this, 'print_footer_scripts' ) ); } /** @@ -114,6 +116,62 @@ public function enqueue_frontend_assets() { ); } + /** + * Print footer scripts. + * + * @return void + */ + public function print_footer_scripts() { + if ( ! is_admin() ) { + return; + } + + $current_screen = get_current_screen(); + + if ( + ! current_user_can( 'manage_options' ) || + ! isset( $current_screen->id ) || + 'site-editor' !== $current_screen->id + ) { + return; + } + + ?> + + { { Array.from({ length: numberOfPages }).map( ( _, page ) => (
  • { + const parsedTemplate = useMemo( () => { + return Array.isArray( template ) ? template : parse( template ); + }, [ template ]); + return ( -
    +
    ); diff --git a/src/parts/ColorPalette.js b/src/parts/ColorPalette.js index d2ed9b1..26ad0c0 100644 --- a/src/parts/ColorPalette.js +++ b/src/parts/ColorPalette.js @@ -1,3 +1,10 @@ +/** + * External dependencies. + */ +import classNames from 'classnames'; + +import { rotateRight } from '@wordpress/icons'; + /** * WordPress dependencies. */ @@ -5,6 +12,8 @@ import { __ } from '@wordpress/i18n'; import { Button, + Disabled, + Icon, Spinner } from '@wordpress/components'; @@ -13,15 +22,21 @@ import { useSelect } from '@wordpress/data'; -import { useEffect } from '@wordpress/element'; +import { + useEffect, + useState +} from '@wordpress/element'; /** * Internal dependencies. */ +import { generateColorPalette } from '../utils'; import ColorPicker from '../components/ColorPicker'; import TemplatePreview from '../components/TemplatePreview'; const ColorPalette = () => { + const [ isRegenerating, setIsRegenerating ] = useState( false ); + const { onContinue } = useDispatch( 'quickwp/data' ); const { @@ -108,6 +123,12 @@ const ColorPalette = () => { onContinue(); }; + const onRegenerate = async() => { + setIsRegenerating( true ); + await generateColorPalette(); + setIsRegenerating( false ); + }; + if ( ! hasLoaded ) { return (
    @@ -117,33 +138,55 @@ const ColorPalette = () => { } return ( -
    -
    -

    - { __( - 'Let\'s give your site a color that fits to your brand.', - 'quickwp' - ) } -

    - -
    - { palette.map( ( color ) => ( - onChangeColor( e, color.slug ) } +
    + +
    +

    + { __( + 'Let\'s give your site a color that fits to your brand.', + 'quickwp' + ) } +

    + +
    + { palette.map( ( color ) => ( + onChangeColor( e, color.slug ) } + /> + ) ) } + + - ) ) } -
    +
    - -
    + +
    +
    ); diff --git a/src/parts/ImageSuggestions.js b/src/parts/ImageSuggestions.js index 9412fdb..2277504 100644 --- a/src/parts/ImageSuggestions.js +++ b/src/parts/ImageSuggestions.js @@ -121,8 +121,8 @@ const ImageSuggestions = () => { } return ( -
    -
    +
    +

    { __( 'Pick out images that you like, and we\'ll include them in the designs.', @@ -130,12 +130,6 @@ const ImageSuggestions = () => { ) }

    - -
    - -
    { } }} disabled={ isLoading } - className="is-dark" + className="is-light mt-4" />
    @@ -174,6 +168,16 @@ const ImageSuggestions = () => { }) }
    + +
    + +
    { ( isLoading ) && (
    @@ -181,25 +185,21 @@ const ImageSuggestions = () => { ) } -
    +
    { images.map( image => (
    toggleSelectedImage( image ) } > { selectedImages.includes( image ) && (
    ) } @@ -208,7 +208,7 @@ const ImageSuggestions = () => { className={ classNames( 'object-cover aspect-square', { - 'outline outline-offset-2 outline-2 outline-white': selectedImages.includes( image ) + 'outline outline-4 outline-active': selectedImages.includes( image ) } ) } src={ image.src.original } diff --git a/src/parts/Template.js b/src/parts/Template.js index 0493eb0..4fc1036 100644 --- a/src/parts/Template.js +++ b/src/parts/Template.js @@ -3,8 +3,6 @@ */ import { __ } from '@wordpress/i18n'; -import { parse } from '@wordpress/blocks'; - import { Button, Spinner @@ -21,27 +19,33 @@ import { import TemplatePreview from '../components/TemplatePreview'; const Template = () => { - const { onContinue } = useDispatch( 'quickwp/data' ); + const { + onContinue, + setSelectedHomepage + } = useDispatch( 'quickwp/data' ); const { - template, + templates, + selectedHomepage, isSaving, hasLoaded } = useSelect( ( select ) => { const { getHomepage, isSaving, - getProcessStatus + getProcessStatus, + getSelectedHomepage } = select( 'quickwp/data' ); - const homepage = getHomepage(); + const templates = getHomepage(); return { - template: homepage ? parse( homepage ) : [], + templates, + selectedHomepage: getSelectedHomepage(), isSaving: isSaving(), hasLoaded: true === getProcessStatus( 'homepage' ) }; - }); + }, []); if ( ! hasLoaded ) { return ( @@ -52,8 +56,8 @@ const Template = () => { } return ( -
    -
    +
    +

    { __( 'Pick a layout for your homepage.', @@ -61,20 +65,47 @@ const Template = () => { ) }

    - +
    + + + { selectedHomepage && ( + + ) } +
    - + { selectedHomepage ? ( +
    + template.slug === selectedHomepage ).content } + canScroll={ true } + /> +
    + ) : ( +
    + { templates.map( template => { + return ( + setSelectedHomepage( template.slug )} + className="aspect-vert" + /> + ); + }) } +
    + ) }
    ); }; diff --git a/src/store.js b/src/store.js index 400d695..681298e 100644 --- a/src/store.js +++ b/src/store.js @@ -41,7 +41,8 @@ const DEFAULT_STATE = { imageKeywords: [], activeImageKeyword: null, selectedImages: [], - homepage: null, + homepage: [], + selectedHomepage: null, siteTopic: '', siteDescription: '', isSavimg: false, @@ -174,6 +175,12 @@ const actions = { homepage }; }, + setSelectedHomepage( selectedHomepage ) { + return { + type: 'SET_SELECTED_HOMEPAGE', + selectedHomepage + }; + }, setError( hasError ) { return { type: 'SET_ERROR', @@ -284,6 +291,11 @@ const store = createReduxStore( 'quickwp/data', { ...state, homepage: action.homepage }; + case 'SET_SELECTED_HOMEPAGE': + return { + ...state, + selectedHomepage: action.selectedHomepage + }; case 'SET_THREAD_ID': return { ...state, @@ -352,6 +364,9 @@ const store = createReduxStore( 'quickwp/data', { getHomepage( state ) { return state.homepage; }, + getSelectedHomepage( state ) { + return state.selectedHomepage; + }, hasError( state ) { return state.hasError; }, diff --git a/src/style.scss b/src/style.scss index 65ab77c..9fd419e 100644 --- a/src/style.scss +++ b/src/style.scss @@ -14,9 +14,13 @@ @apply bg-transparent max-w-5xl h-24 border-0 text-2xl not-italic font-normal text-fg shadow-none resize-none; } - &.is-dark { + &.is-light { .components-text-control__input { - @apply text-fg-alt bg-bg-alt h-10 w-full max-w-full text-xs p-3; + @apply text-black bg-white h-10 w-full max-w-full text-xs p-3; + + &::placeholder { + @apply text-black/50; + } } } } @@ -59,7 +63,7 @@ @apply text-xs leading-none px-4 py-2; &.is-active { - @apply bg-fg text-bg; + @apply bg-active text-white; } } } diff --git a/src/utils.js b/src/utils.js index 1018739..88229fb 100644 --- a/src/utils.js +++ b/src/utils.js @@ -109,7 +109,7 @@ const sendEvent = async( data ) => { setThreadID } = dispatch( 'quickwp/data' ); - const response = await apiFetch({ + const response = await retryApiFetch({ path: `${ window.quickwp.api }/send`, method: 'POST', data: { ...data } @@ -138,7 +138,7 @@ const getEventStatus = async( type ) => { const runID = select( 'quickwp/data' ).getRunID( type ); const { setProcessStatus } = dispatch( 'quickwp/data' ); - const response = await apiFetch({ + const response = await retryApiFetch({ path: addQueryArgs( `${ window.quickwp.api }/status`, { 'thread_id': threadID, 'run_id': runID @@ -277,9 +277,6 @@ export const generateHomepage = async() => { const siteTopic = select( 'quickwp/data' ).getSiteTopic(); const siteDescription = select( 'quickwp/data' ).getSiteDescription(); const images = select( 'quickwp/data' ).getSelectedImages(); - const currentTemplate = select( 'core/edit-site' ).getEditedPostId(); - - const { editEntityRecord } = dispatch( 'core' ); const imagesAr = images.map( image => ({ src: image.src.original, @@ -306,26 +303,50 @@ export const generateHomepage = async() => { return; } - let homepageTemplate = ''; - homepageTemplate += ''; - homepageTemplate += response.data; - homepageTemplate += ''; + let homepageTemplates = []; - editEntityRecord( 'postType', 'wp_template', currentTemplate, { - 'content': homepageTemplate + response.data.forEach( item => { + let homepageTemplate = formatHomepage( item.patterns ); + + const template = { + slug: item.slug, + content: homepageTemplate + }; + + homepageTemplates.push( template ); }); - setHomepage( homepageTemplate ); + setHomepage( homepageTemplates ); setProcessStatus( 'homepage', true ); }; export const saveChanges = async() => { const { __experimentalGetDirtyEntityRecords } = select( 'core' ); - const { saveEditedEntityRecord } = dispatch( 'core' ); + const currentTemplate = select( 'core/edit-site' ).getEditedPostId(); + + const { + getHomepage, + getSelectedHomepage + } = select( 'quickwp/data' ); + + const { + editEntityRecord, + saveEditedEntityRecord + } = dispatch( 'core' ); const { setSaving } = dispatch( 'quickwp/data' ); + const homepage = getSelectedHomepage(); + + const templates = getHomepage(); + + const selectedHomepage = templates.find( template => template.slug === homepage ); + + editEntityRecord( 'postType', 'wp_template', currentTemplate, { + 'content': selectedHomepage.content + }); + const edits = __experimentalGetDirtyEntityRecords(); setSaving( true ); @@ -336,3 +357,12 @@ export const saveChanges = async() => { setSaving( false ); }; + +const formatHomepage = template => { + let homepageTemplate = ''; + homepageTemplate += ''; + homepageTemplate += template; + homepageTemplate += ''; + + return homepageTemplate; +}; diff --git a/tailwind.config.js b/tailwind.config.js index d048af6..5cd7db8 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -4,17 +4,15 @@ module.exports = { theme: { extend: { aspectRatio: { - vert: [ 3, 4 ] - }, - boxShadow: { - selected: '-3px 3px 0px -1px #000' + vert: '3/4' }, colors: { bg: '#000000', fg: '#FFFFFF', secondary: '#232323', 'bg-alt': '#2F2F2F', - 'fg-alt': '#E0E0E0' + 'fg-alt': '#E0E0E0', + active: '#4663F8' }, maxHeight: { 'md': '32rem', From 9af25770afe94800005bd17f29dd2c30db0911d6 Mon Sep 17 00:00:00 2001 From: Hardeep Asrani Date: Mon, 12 Feb 2024 17:42:56 +0530 Subject: [PATCH 14/14] feat: add event tracking and various changes --- inc/class-api.php | 247 +++++++++++++++++++++++------- inc/class-main.php | 1 + quickwp.php | 2 +- src/App.js | 3 + src/components/TemplatePreview.js | 32 +++- src/frontend/style.scss | 1 + src/parts/ColorPalette.js | 11 +- src/parts/ImageSuggestions.js | 16 +- src/parts/Template.js | 75 ++++----- src/parts/ViewSite.js | 29 +++- src/steps.js | 10 +- src/store.js | 76 +++++++-- src/utils.js | 196 ++++++++++++++++++------ 13 files changed, 504 insertions(+), 195 deletions(-) diff --git a/inc/class-api.php b/inc/class-api.php index 64c1acf..03ceb26 100644 --- a/inc/class-api.php +++ b/inc/class-api.php @@ -62,11 +62,15 @@ public function register_routes() { 'send' => array( 'methods' => \WP_REST_Server::CREATABLE, 'args' => array( - 'step' => array( + 'step' => array( 'required' => true, 'type' => 'string', ), - 'message' => array( + 'message' => array( + 'required' => false, + 'type' => 'string', + ), + 'template' => array( 'required' => false, 'type' => 'string', ), @@ -114,9 +118,27 @@ public function register_routes() { 'required' => true, 'type' => 'string', ), + 'images' => array( + 'required' => false, + 'type' => 'array', + ), ), 'callback' => array( $this, 'templates' ), ), + 'homepage' => array( + 'methods' => \WP_REST_Server::READABLE, + 'args' => array( + 'thread_id' => array( + 'required' => true, + 'type' => 'string', + ), + 'template' => array( + 'required' => true, + 'type' => 'string', + ), + ), + 'callback' => array( $this, 'homepage' ), + ), ); foreach ( $routes as $route => $args ) { @@ -138,14 +160,20 @@ public function register_routes() { public function send( \WP_REST_Request $request ) { $data = $request->get_params(); + $params = array( + 'step' => $data['step'], + 'message' => $data['message'], + ); + + if ( isset( $data['template'] ) ) { + $params['template'] = $data['template']; + } + $request = wp_remote_post( QUICKWP_APP_API . 'wizard/send', array( 'timeout' => 20, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout - 'body' => array( - 'step' => $data['step'], - 'message' => $data['message'], - ), + 'body' => $params, ) ); @@ -254,6 +282,101 @@ public function get( \WP_REST_Request $request ) { return new \WP_REST_Response( $response, 200 ); } + /** + * Get homepage. + * + * @param \WP_REST_Request $request Request. + * + * @return \WP_REST_Response + */ + public function homepage( \WP_REST_Request $request ) { + $data = $request->get_params(); + + $api_url = QUICKWP_APP_API . 'wizard/get'; + + $query_params = array( + 'thread_id' => $data['thread_id'], + ); + + $request_url = add_query_arg( $query_params, $api_url ); + + $request = wp_safe_remote_get( + $request_url, + array( + 'timeout' => 20, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout + ) + ); + + if ( is_wp_error( $request ) ) { + return new \WP_REST_Response( array( 'error' => $request->get_error_message() ), 500 ); + } + + /** + * Holds the response as a standard class object + * + * @var \stdClass $response + */ + $response = json_decode( wp_remote_retrieve_body( $request ) ); + + if ( ! isset( $response->data ) || ! $response->data ) { + return new \WP_REST_Response( array( 'error' => __( 'Error', 'quickwp' ) ), 500 ); + } + + $items = self::process_json_from_response( $response->data ); + + if ( ! $items ) { + return new \WP_REST_Response( array( 'error' => __( 'Error Parsing JSON', 'quickwp' ) ), 500 ); + } + + self::extract_data( $items ); + + $templates = apply_filters( 'quickwp_templates', array() ); + + if ( empty( $templates ) || ! isset( $templates['homepage'] ) ) { + return new \WP_REST_Response( array( 'error' => __( 'Error', 'quickwp' ) ), 500 ); + } + + $template = $templates['homepage'][ $data['template'] ]; + + $result = array(); + + $theme_path = get_stylesheet_directory(); + + $patterns = file_get_contents( $template ); //phpcs:ignore WordPressVIPMinimum.Performance.FetchingRemoteData.FileGetContentsUnknown + + if ( ! $patterns ) { + return new \WP_REST_Response( array( 'error' => __( 'Error', 'quickwp' ) ), 500 ); + } + + preg_match_all( '/"slug":"(.*?)"/', $patterns, $matches ); + $slugs = $matches[1]; + + $filtered_patterns = array(); + + foreach ( $slugs as $slug ) { + $slug = str_replace( 'quickwp/', '', $slug ); + $pattern_path = $theme_path . '/patterns/' . $slug . '.php'; + + if ( ! file_exists( $pattern_path ) ) { + continue; + } + + ob_start(); + include $pattern_path; + $pattern_content = ob_get_clean(); + + $filtered_patterns[] = $pattern_content; + } + + return new \WP_REST_Response( + array( + 'status' => 'success', + 'data' => implode( '', $filtered_patterns ), + ), + 200 + ); + } + /** * Get templates. * @@ -300,16 +423,65 @@ public function templates( \WP_REST_Request $request ) { return new \WP_REST_Response( array( 'error' => __( 'Error Parsing JSON', 'quickwp' ) ), 500 ); } + self::extract_data( $items ); + + $templates = apply_filters( 'quickwp_templates', array() ); + + if ( empty( $templates ) || ! isset( $templates['homepage'] ) ) { + return new \WP_REST_Response( array( 'error' => __( 'Error', 'quickwp' ) ), 500 ); + } + + $items = $templates['homepage']; + $result = array(); - foreach( $items as $item ) { - $pattern = self::extract_patterns( $item ); + $theme_path = get_stylesheet_directory(); + + foreach ( $items as $item => $path ) { + $pattern = file_get_contents( $path ); //phpcs:ignore WordPressVIPMinimum.Performance.FetchingRemoteData.FileGetContentsUnknown if ( ! $pattern ) { continue; } - $result[] = $pattern; + preg_match_all( '/"slug":"(.*?)"/', $pattern, $matches ); + $slugs = $matches[1]; + + $filtered_patterns = array(); + + foreach ( $slugs as $slug ) { + $slug = str_replace( 'quickwp/', '', $slug ); + $pattern_path = $theme_path . '/patterns/' . $slug . '.php'; + + if ( ! file_exists( $pattern_path ) ) { + continue; + } + + // Check if $data param has images and it counts more than 0. + if ( isset( $data['images'] ) && count( $data['images'] ) > 0 ) { + $images = $data['images']; + + add_filter( + 'quickwp/image', + function () use( $images ) { + // Get a random image from the array. + $image = $images[ array_rand( $images ) ]; + return esc_url( $image['src'] ); + } + ); + } + + ob_start(); + include $pattern_path; + $pattern_content = ob_get_clean(); + + $filtered_patterns[] = $pattern_content; + } + + $result[] = array( + 'slug' => $item, + 'patterns' => implode( '', $filtered_patterns ), + ); } return new \WP_REST_Response( @@ -369,6 +541,8 @@ public function images( \WP_REST_Request $request ) { * * @param array $data Response. * + * @throws \Exception Exception in case of invalid JSON. + * * @return array|false */ private static function process_json_from_response( $data ) { @@ -392,8 +566,8 @@ private static function process_json_from_response( $data ) { throw new \Exception( 'Invalid JSON' ); } catch ( \Exception $e ) { if ( substr( $json_string, 0, 7 ) === '```json' && substr( trim( $json_string ), -3 ) === '```' ) { - $cleaned_json = trim( str_replace( [ '```json', '```' ], '', $json_string ) ); - $json_object = json_decode( $cleaned_json, true ); + $cleaned_json = trim( str_replace( array( '```json', '```' ), '', $json_string ) ); + $json_object = json_decode( $cleaned_json, true ); if ( is_array( $json_object ) ) { return $json_object; @@ -405,29 +579,18 @@ private static function process_json_from_response( $data ) { } /** - * Extract Patterns. + * Extract Data. * * @param array $items Items. * - * @return array + * @return void */ - private static function extract_patterns( $items ) { - if ( ! isset( $items['slug'] ) || ! isset( $items['data'] ) ) { - return; - } - - $patterns_used = array(); - - foreach ( $items['data'] as $item ) { + private static function extract_data( $items ) { + foreach ( $items as $item ) { if ( ! isset( $item['slug'] ) || ! isset( $item['order'] ) || ! isset( $item['strings'] ) ) { continue; } - $patterns_used[] = array( - 'order' => $item['order'], - 'slug' => $item['slug'], - ); - $strings = $item['strings']; foreach ( $strings as $string ) { @@ -456,37 +619,5 @@ function ( $value ) use( $image ) { } } } - - usort( - $patterns_used, - function ( $a, $b ) { - return $a['order'] <=> $b['order']; - } - ); - - $theme_path = get_stylesheet_directory(); - - $filtered_patterns = array(); - - foreach ( $patterns_used as $pattern ) { - $pattern['slug'] = str_replace( 'quickwp/', '', $pattern['slug'] ); - - $pattern_path = $theme_path . '/patterns/' . $pattern['slug'] . '.php'; - - if ( ! file_exists( $pattern_path ) ) { - continue; - } - - ob_start(); - include $pattern_path; - $pattern_content = ob_get_clean(); - - $filtered_patterns[] = $pattern_content; - } - - return array( - 'slug' => $items['slug'], - 'patterns' => implode( '', $filtered_patterns ), - ); } } diff --git a/inc/class-main.php b/inc/class-main.php index 0e57d0d..5f5d54a 100644 --- a/inc/class-main.php +++ b/inc/class-main.php @@ -87,6 +87,7 @@ public function enqueue_assets() { array( 'api' => $this->api->get_endpoint(), 'siteUrl' => get_site_url(), + 'themeSlug' => get_template(), 'isGuidedMode' => defined( 'QUICKWP_APP_GUIDED_MODE' ) && QUICKWP_APP_GUIDED_MODE, ) ); diff --git a/quickwp.php b/quickwp.php index a1c3633..9c3fd20 100644 --- a/quickwp.php +++ b/quickwp.php @@ -23,7 +23,7 @@ define( 'QUICKWP_APP_VERSION', '1.0.0' ); if ( ! defined( 'QUICKWP_APP_API' ) ) { - define( 'QUICKWP_APP_API', 'https://aaf0-103-217-244-105.ngrok-free.app/api/' ); + define( 'QUICKWP_APP_API', 'https://quickwp.com/api/' ); } if ( ! defined( 'QUICKWP_APP_GUIDED_MODE' ) ) { diff --git a/src/App.js b/src/App.js index 11aff0f..bb18326 100644 --- a/src/App.js +++ b/src/App.js @@ -16,6 +16,7 @@ import { useEffect } from '@wordpress/element'; import { useIsSiteEditorLoading } from './hooks'; import Loader from './components/Loader'; import Header from './parts/Header'; +import { recordEvent } from './utils'; const App = () => { const isEditorLoading = useIsSiteEditorLoading(); @@ -45,6 +46,8 @@ const App = () => { if ( hasWelcome ) { toggle( 'core/edit-site', 'welcomeGuide' ); } + + recordEvent(); }, [ hasWelcome ]); const StepControls = currentStep?.view || null; diff --git a/src/components/TemplatePreview.js b/src/components/TemplatePreview.js index fceaab4..29bf6e4 100644 --- a/src/components/TemplatePreview.js +++ b/src/components/TemplatePreview.js @@ -10,7 +10,10 @@ import { parse } from '@wordpress/blocks'; import { BlockPreview } from '@wordpress/block-editor'; -import { useMemo } from '@wordpress/element'; +import { + useMemo, + useRef +} from '@wordpress/element'; const TemplatePreview = ({ template, @@ -19,12 +22,39 @@ const TemplatePreview = ({ className = '', onClick }) => { + const previewRef = useRef( null ); + const parsedTemplate = useMemo( () => { return Array.isArray( template ) ? template : parse( template ); }, [ template ]); + const scrollToBottom = () => { + const contentDocument = previewRef.current; + + if ( ! canScroll && contentDocument ) { + contentDocument.scrollTo({ + top: contentDocument.scrollHeight, + behavior: 'smooth' + }); + } + }; + + const scrollToTop = () => { + const contentDocument = previewRef.current; + + if ( ! canScroll && contentDocument ) { + contentDocument.scrollTo({ + top: 0, + behavior: 'smooth' + }); + } + }; + return (
    { template, hasLoaded } = useSelect( ( select ) => { - const { getBlocks } = select( 'core/block-editor' ); - const { __experimentalGetCurrentGlobalStylesId, __experimentalGetCurrentThemeBaseGlobalStyles @@ -55,7 +53,8 @@ const ColorPalette = () => { const { getColorPalette, - getProcessStatus + getProcessStatus, + getHomepage } = select( 'quickwp/data' ); const globalStylesId = __experimentalGetCurrentGlobalStylesId(); @@ -64,8 +63,8 @@ const ColorPalette = () => { globalStylesId, defaultStyles: __experimentalGetCurrentThemeBaseGlobalStyles(), palette: getColorPalette(), - template: getBlocks(), - hasLoaded: true === getProcessStatus( 'color_palette' ) + template: getHomepage() || [], + hasLoaded: true === getProcessStatus( 'color_palette' ) && true === getProcessStatus( 'homepage' ) }; }); @@ -174,7 +173,7 @@ const ColorPalette = () => { - - { selectedHomepage && ( - - ) } -
    + - { selectedHomepage ? ( -
    - template.slug === selectedHomepage ).content } - canScroll={ true } - /> -
    - ) : ( -
    - { templates.map( template => { - return ( - setSelectedHomepage( template.slug )} - className="aspect-vert" - /> - ); - }) } -
    - ) } +
    + { templates.map( template => { + return ( + setSelectedTemplate( template.slug )} + isSelected={ template.slug === selectedTemplate } + className="aspect-vert" + /> + ); + }) } +
    ); }; diff --git a/src/parts/ViewSite.js b/src/parts/ViewSite.js index 10b71a8..1cd18cd 100644 --- a/src/parts/ViewSite.js +++ b/src/parts/ViewSite.js @@ -5,9 +5,15 @@ import { __ } from '@wordpress/i18n'; import apiFetch from '@wordpress/api-fetch'; -import { Button } from '@wordpress/components'; +import { + Button, + Spinner +} from '@wordpress/components'; -import { useDispatch } from '@wordpress/data'; +import { + useDispatch, + useSelect +} from '@wordpress/data'; const downloadBlob = ( filename, content, contentType = '' ) => { if ( ! filename || ! content ) { @@ -29,6 +35,17 @@ const downloadBlob = ( filename, content, contentType = '' ) => { const ViewSite = () => { const { createErrorNotice } = useDispatch( 'core/notices' ); + const { + isSaving + } = useSelect( ( select ) => { + const { isSaving } = select( 'quickwp/data' ); + + + return { + isSaving: isSaving() + }; + }, []); + const handleExport = async() => { try { const response = await apiFetch({ @@ -63,6 +80,14 @@ const ViewSite = () => { } }; + if ( isSaving ) { + return ( +
    + +
    + ); + } + return (
    diff --git a/src/steps.js b/src/steps.js index eff486a..e60930d 100644 --- a/src/steps.js +++ b/src/steps.js @@ -24,11 +24,6 @@ const STEPS = [ label: __( 'Site Description', 'quickwp' ), view: SiteDescription }, - { - value: 'color_palette', - label: __( 'Color Palette', 'quickwp' ), - view: ColorPalette - }, { value: 'image_suggestions', label: __( 'Image Suggestions', 'quickwp' ), @@ -39,6 +34,11 @@ const STEPS = [ label: __( 'Front Page Template', 'quickwp' ), view: Template }, + { + value: 'color_palette', + label: __( 'Color Palette', 'quickwp' ), + view: ColorPalette + }, { value: 'view_site', label: __( 'View Site', 'quickwp' ), diff --git a/src/store.js b/src/store.js index 681298e..3f30dfe 100644 --- a/src/store.js +++ b/src/store.js @@ -1,3 +1,5 @@ +/* eslint-disable camelcase */ + /** * WordPress dependencies. */ @@ -13,7 +15,8 @@ import STEPS from './steps'; import { generateColorPalette, generateImages, - generateHomepage, + generateTemplates, + recordEvent, saveChanges } from './utils'; @@ -30,6 +33,11 @@ const DEFAULT_STATE = { 'run_id': null, hasLoaded: false }, + 'templates': { + 'thread_id': null, + 'run_id': null, + hasLoaded: false + }, 'homepage': { 'thread_id': null, 'run_id': null, @@ -41,10 +49,12 @@ const DEFAULT_STATE = { imageKeywords: [], activeImageKeyword: null, selectedImages: [], - homepage: [], - selectedHomepage: null, + templates: [], + selectedTemplate: null, + homepage: null, siteTopic: '', siteDescription: '', + sessionID: '', isSavimg: false, hasError: false }; @@ -89,6 +99,13 @@ const actions = { return ({ dispatch, select }) => { const current = select.getStep(); + const stepIndex = STEPS.findIndex( step => current.value === step.value ); + + recordEvent({ + step_id: stepIndex + 1, + step_status: 'completed' + }); + switch ( current.value ) { case 'site_topic': generateColorPalette(); @@ -96,12 +113,13 @@ const actions = { case 'site_description': generateImages(); break; - case 'color_palette': - break; case 'image_suggestions': - generateHomepage(); + generateTemplates( 'templates' ); break; case 'frontpage_template': + generateTemplates( 'homepage' ); + break; + case 'color_palette': saveChanges(); break; } @@ -169,16 +187,28 @@ const actions = { image }; }, + setTemplate( templates ) { + return { + type: 'SET_TEMPLATE', + templates + }; + }, + setSelectedTemplate( selectedTemplate ) { + return { + type: 'SET_SELECTED_TEMPLATE', + selectedTemplate + }; + }, setHomepage( homepage ) { return { type: 'SET_HOMEPAGE', homepage }; }, - setSelectedHomepage( selectedHomepage ) { + setSessionID( sessionID ) { return { - type: 'SET_SELECTED_HOMEPAGE', - selectedHomepage + type: 'SET_SESSION_ID', + sessionID }; }, setError( hasError ) { @@ -286,15 +316,20 @@ const store = createReduxStore( 'quickwp/data', { ( image ) => image !== action.image ) }; - case 'SET_HOMEPAGE': + case 'SET_TEMPLATE': return { ...state, - homepage: action.homepage + templates: action.templates + }; + case 'SET_SELECTED_TEMPLATE': + return { + ...state, + selectedTemplate: action.selectedTemplate }; - case 'SET_SELECTED_HOMEPAGE': + case 'SET_HOMEPAGE': return { ...state, - selectedHomepage: action.selectedHomepage + homepage: action.homepage }; case 'SET_THREAD_ID': return { @@ -318,6 +353,11 @@ const store = createReduxStore( 'quickwp/data', { } } }; + case 'SET_SESSION_ID': + return { + ...state, + sessionID: action.sessionID + }; case 'SET_PROCESS_STATUS': return { ...state, @@ -361,11 +401,17 @@ const store = createReduxStore( 'quickwp/data', { getSelectedImages( state ) { return state.selectedImages; }, + getTemplate( state ) { + return state.templates; + }, + getSelectedTemplate( state ) { + return state.selectedTemplate; + }, getHomepage( state ) { return state.homepage; }, - getSelectedHomepage( state ) { - return state.selectedHomepage; + getSessionID( state ) { + return state.sessionID; }, hasError( state ) { return state.hasError; diff --git a/src/utils.js b/src/utils.js index 88229fb..2a6f662 100644 --- a/src/utils.js +++ b/src/utils.js @@ -7,7 +7,6 @@ import { dispatch, select } from '@wordpress/data'; -import { home } from '@wordpress/icons'; import { Circle, @@ -116,17 +115,30 @@ const sendEvent = async( data ) => { }); setThreadID( data.step, response.thread_id ); - setRunID( data.step, response.id ); }; -const getEvent = async( type ) => { +const getEvent = async( type, params = {}) => { const threadID = select( 'quickwp/data' ).getThreadID( type ); - const route = 'homepage' !== type ? 'get' : 'templates'; + + let route = ''; + + switch ( type ) { + case 'homepage': + route = 'homepage'; + break; + case 'templates': + route = 'templates'; + break; + default: + route = 'get'; + break; + } const response = await retryApiFetch({ path: addQueryArgs( `${ window.quickwp.api }/${ route }`, { - 'thread_id': threadID + 'thread_id': threadID, + ...params }) }); @@ -273,12 +285,18 @@ export const generateImages = async() => { setProcessStatus( 'images', true ); }; -export const generateHomepage = async() => { +export const generateTemplates = async( type ) => { const siteTopic = select( 'quickwp/data' ).getSiteTopic(); const siteDescription = select( 'quickwp/data' ).getSiteDescription(); + const images = select( 'quickwp/data' ).getSelectedImages(); + const activeImageKeyword = select( 'quickwp/data' ).getActiveImageKeyword(); + const defaultImages = select( 'quickwp/data' ).getImages( activeImageKeyword ); + const homepage = select( 'quickwp/data' ).getSelectedTemplate(); + + const selectedImages = ( ! images.length && defaultImages.length ) ? defaultImages.slice( 0, 10 ) : images; - const imagesAr = images.map( image => ({ + let imagesAr = selectedImages.map( image => ({ src: image.src.original, alt: image.alt }) ); @@ -286,71 +304,93 @@ export const generateHomepage = async() => { const { setError, setProcessStatus, - setHomepage + setHomepage, + setTemplate } = dispatch( 'quickwp/data' ); - await sendEvent({ - step: 'homepage', - message: `Website topic: ${ siteTopic } | Website description: ${ siteDescription } | Images: ${ JSON.stringify( imagesAr ) }` - }); + let response; + + if ( 'homepage' === type ) { + await sendEvent({ + step: 'homepage', + message: `Website topic: ${ siteTopic } | Website description: ${ siteDescription } | Images: ${ JSON.stringify( imagesAr ) }`, + template: homepage + }); - await awaitEvent( 'homepage', 10000 ); + await awaitEvent( 'homepage', 10000 ); + + response = await getEvent( 'homepage', { + template: homepage + }); + } else { + await sendEvent({ + step: 'templates', + message: `Website topic: ${ siteTopic } | Website description: ${ siteDescription }` + }); - const response = await getEvent( 'homepage' ); + await awaitEvent( 'templates', 10000 ); + + response = await getEvent( 'templates', { + images: imagesAr + }); + } if ( 'success' !== response?.status ) { setError( true ); return; } - let homepageTemplates = []; + if ( 'homepage' === type ) { + const homepageTemplate = formatHomepage( response.data ); + + setHomepage( homepageTemplate ); + setProcessStatus( 'homepage', true ); + } else { + let homepageTemplates = []; - response.data.forEach( item => { - let homepageTemplate = formatHomepage( item.patterns ); + response.data.forEach( item => { + let homepageTemplate = formatHomepage( item.patterns ); - const template = { - slug: item.slug, - content: homepageTemplate - }; + const template = { + slug: item.slug, + content: homepageTemplate + }; - homepageTemplates.push( template ); - }); + homepageTemplates.push( template ); + }); - setHomepage( homepageTemplates ); - setProcessStatus( 'homepage', true ); + setTemplate( homepageTemplates ); + setProcessStatus( 'templates', true ); + } }; -export const saveChanges = async() => { - const { __experimentalGetDirtyEntityRecords } = select( 'core' ); - +const importTemplate = async() => { const currentTemplate = select( 'core/edit-site' ).getEditedPostId(); - const { - getHomepage, - getSelectedHomepage - } = select( 'quickwp/data' ); - - const { - editEntityRecord, - saveEditedEntityRecord - } = dispatch( 'core' ); + const { getHomepage } = select( 'quickwp/data' ); - const { setSaving } = dispatch( 'quickwp/data' ); + const { editEntityRecord } = dispatch( 'core' ); - const homepage = getSelectedHomepage(); + const homepage = getHomepage(); - const templates = getHomepage(); + await editEntityRecord( 'postType', 'wp_template', currentTemplate, { + 'content': homepage + }); +}; - const selectedHomepage = templates.find( template => template.slug === homepage ); +export const saveChanges = async() => { + const { __experimentalGetDirtyEntityRecords } = select( 'core' ); - editEntityRecord( 'postType', 'wp_template', currentTemplate, { - 'content': selectedHomepage.content - }); + const { saveEditedEntityRecord } = dispatch( 'core' ); - const edits = __experimentalGetDirtyEntityRecords(); + const { setSaving } = dispatch( 'quickwp/data' ); setSaving( true ); + importTemplate(); + + const edits = __experimentalGetDirtyEntityRecords(); + await Promise.all( edits.map( async edit => { await saveEditedEntityRecord( edit.kind, edit.name, edit?.key ); }) ); @@ -359,10 +399,72 @@ export const saveChanges = async() => { }; const formatHomepage = template => { + const slug = window.quickwp.themeSlug; let homepageTemplate = ''; - homepageTemplate += ''; + homepageTemplate += ''; homepageTemplate += template; - homepageTemplate += ''; + homepageTemplate += ''; return homepageTemplate; }; + +export const recordEvent = async( data = {}) => { + if ( ! Boolean( window.quickwp.isGuidedMode ) ) { + return; + } + + const { setSessionID } = dispatch( 'quickwp/data' ); + const { getSessionID } = select( 'quickwp/data' ); + + const trackingId = getSessionID(); + + try { + const response = await fetch( + 'https://api.themeisle.com/tracking/onboarding', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + _id: trackingId, + data: { + slug: 'quickwp', + // eslint-disable-next-line camelcase + license_id: 'free', + site: '', + ...data + } + }) + } + ); + + if ( ! response.ok ) { + console.error( `HTTP error! Status: ${ response.status }` ); + return false; + } + + const jsonResponse = await response.json(); + + const validCodes = [ 'success', 'invalid' ]; // Add valid codes to this array + + if ( ! validCodes.includes( jsonResponse.code ) ) { + return false; + } + + if ( 'invalid' === jsonResponse.code ) { + console.error( jsonResponse.message ); + return false; + } + const responseData = jsonResponse.data; + + if ( responseData?.id && '' === trackingId ) { + setSessionID( responseData.id ); + } + + return responseData.id || false; + } catch ( error ) { + console.error( error ); + return false; + } +};