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 (
+
-
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 (
+
+ { Array.from({ length: numberOfPages }).map( ( _, page ) => (
+ -
+
+
+ ) ) }
+
+ );
+};
+
+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 (
+
+ { Array.from({ length: numberOfPages }).map( ( _, page ) => (
+ -
+
+ ) ) }
+
+ );
+};
+
+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 (
+
+ );
+};
+
+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: []
};