From 7e536239b06d6707a336d0797a1fd0ffbaf09aec Mon Sep 17 00:00:00 2001 From: Nikita Date: Fri, 4 Aug 2023 08:55:12 +0300 Subject: [PATCH] split popup to multiple components --- src/popup/components/content/index.js | 170 +++++++++++++ src/popup/components/content/style.scss | 82 +++++++ src/popup/components/footer/index.js | 75 ++++++ src/popup/components/footer/style.scss | 40 ++++ src/popup/components/input/index.js | 117 +++++++++ src/popup/components/input/style.scss | 43 ++++ src/popup/index.js | 306 ++---------------------- src/popup/style.scss | 162 ------------- 8 files changed, 553 insertions(+), 442 deletions(-) create mode 100644 src/popup/components/content/index.js create mode 100644 src/popup/components/content/style.scss create mode 100644 src/popup/components/footer/index.js create mode 100644 src/popup/components/footer/style.scss create mode 100644 src/popup/components/input/index.js create mode 100644 src/popup/components/input/style.scss diff --git a/src/popup/components/content/index.js b/src/popup/components/content/index.js new file mode 100644 index 0000000..59c36ca --- /dev/null +++ b/src/popup/components/content/index.js @@ -0,0 +1,170 @@ +import './style.scss'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useRef, useEffect, RawHTML } from '@wordpress/element'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { Button } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import LoadingText from '../loading-text'; +import Notice from '../notice'; +import { ReactComponent as PopupPostTitleAboutIcon } from '../../../icons/popup-post-title-about.svg'; +import { ReactComponent as PopupPostAboutIcon } from '../../../icons/popup-post-about.svg'; +import { ReactComponent as PopupOutlineAboutIcon } from '../../../icons/popup-outline-about.svg'; +import { ReactComponent as PopupParagraphAboutIcon } from '../../../icons/popup-paragraph-about.svg'; +import { ReactComponent as PopupListAboutIcon } from '../../../icons/popup-list-about.svg'; +import { ReactComponent as PopupTableAboutIcon } from '../../../icons/popup-table-about.svg'; + +const commands = [ + { + type: 'category', + label: __('Post Presets', 'mind'), + }, + { + type: 'request', + label: __('Post title about…', 'mind'), + request: __('Write a post title about ', 'mind'), + icon: , + }, + { + type: 'request', + label: __('Post about…', 'mind'), + request: __('Write a blog post about ', 'mind'), + icon: , + }, + { + type: 'request', + label: __('Outline about…', 'mind'), + request: __('Write a blog post outline about ', 'mind'), + icon: , + }, + + { + type: 'category', + label: __('Content Presets', 'mind'), + }, + { + type: 'request', + label: __('Paragraph about…', 'mind'), + request: __('Create a paragraph about ', 'mind'), + icon: , + }, + { + type: 'request', + label: __('List about…', 'mind'), + request: __('Create a list about ', 'mind'), + icon: , + }, + { + type: 'request', + label: __('Table about…', 'mind'), + request: __('Create a table about ', 'mind'), + icon: , + }, +]; + +export default function Content() { + const ref = useRef(); + + const { setInput, setScreen } = useDispatch('mind/popup'); + + const { isOpen, input, screen, loading, response, error } = useSelect( + (select) => { + const { + isOpen: checkIsOpen, + getInput, + getContext, + getScreen, + getLoading, + getResponse, + getError, + } = select('mind/popup'); + + return { + isOpen: checkIsOpen(), + input: getInput(), + context: getContext(), + screen: getScreen(), + loading: getLoading(), + response: getResponse(), + error: getError(), + }; + } + ); + + function focusInput() { + if (ref?.current) { + const inputEl = ref.current.querySelector('input'); + + if (inputEl) { + inputEl.focus(); + } + } + } + + // Set focus on Input. + useEffect(() => { + if (isOpen && !loading && ref?.current) { + focusInput(); + } + }, [isOpen, loading, ref]); + + // Open request page if something is in input. + useEffect(() => { + if (screen === '' && input) { + setScreen('request'); + } + }, [screen, input, setScreen]); + + return ( +
+ {screen === '' ? ( +
+ {commands.map((data) => { + if (data.type === 'category') { + return ( + + {data.label} + + ); + } + + return ( + + ); + })} +
+ ) : null} + + {screen === 'request' && ( +
+ {loading && ( + + {__('Waiting for AI response', 'mind')} + + )} + {!loading && response && {response}} + {!loading && error && {error}} +
+ )} +
+ ); +} diff --git a/src/popup/components/content/style.scss b/src/popup/components/content/style.scss new file mode 100644 index 0000000..095910c --- /dev/null +++ b/src/popup/components/content/style.scss @@ -0,0 +1,82 @@ +$padding: 10px; + +.mind-popup-content { + overflow: auto; + flex: 1; + padding: $padding $padding * 2; + + &:empty, + &:has(.mind-popup-request:empty) { + padding: 0; + margin-bottom: -1px; + } + + .mind-popup-request { + ol, + ul { + list-style: auto; + padding-left: 15px; + } + + table { + border-collapse: collapse; + width: 100%; + + td, + th { + border: 1px solid; + padding: 0.5em; + white-space: pre-wrap; + min-width: 1px; + } + } + } +} + +// Prompts. +.mind-popup-commands { + display: flex; + flex-direction: column; + margin-left: -$padding; + margin-right: -$padding; + + .mind-popup-commands-category { + position: relative; + padding: $padding; + padding-top: 28px; + color: #7f7f7f; + + &:first-child { + padding-top: $padding; + } + + &:not(:first-child)::before { + content: ""; + display: block; + position: absolute; + top: 8px; + left: -10px; + right: -10px; + border-top: 1px solid #e8e7e7; + } + } + + .mind-popup-commands-button { + display: flex; + align-items: center; + gap: 10px; + background-color: transparent; + color: #000; + border: none; + text-align: left; + padding: $padding; + border-radius: 5px; + height: auto; + min-height: 40px; + + &:hover, + &:focus { + background-color: rgba(#000, 5%); + } + } +} diff --git a/src/popup/components/footer/index.js b/src/popup/components/footer/index.js new file mode 100644 index 0000000..da4b772 --- /dev/null +++ b/src/popup/components/footer/index.js @@ -0,0 +1,75 @@ +import './style.scss'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Button } from '@wordpress/components'; +import { useSelect, useDispatch } from '@wordpress/data'; + +export default function Input(props) { + const { onInsert } = props; + + const { close, reset, setError, requestAI } = useDispatch('mind/popup'); + + const { input, loading, response } = useSelect((select) => { + const { getInput, getContext, getScreen, getLoading, getResponse } = + select('mind/popup'); + + return { + input: getInput(), + context: getContext(), + screen: getScreen(), + loading: getLoading(), + response: getResponse(), + }; + }); + + const showFooter = response || (input && !loading && !response); + + if (!showFooter) { + return null; + } + + return ( +
+
+ {input && !loading && !response && ( + + )} + {response && ( + <> + + + + + )} +
+
+ ); +} diff --git a/src/popup/components/footer/style.scss b/src/popup/components/footer/style.scss new file mode 100644 index 0000000..2bd1925 --- /dev/null +++ b/src/popup/components/footer/style.scss @@ -0,0 +1,40 @@ +$padding: 10px; + +.mind-popup-footer { + display: flex; + justify-content: space-between; + align-items: center; + padding: $padding $padding * 2; + background-color: #f9f9f9; + border-top: 1px solid #e8e7e7; +} +.mind-popup-footer-actions { + display: flex; + margin: -5px -15px; + margin-left: auto; + gap: 5px; + + button { + display: flex; + gap: 8px; + height: 28px; + border-radius: 5px; + font-weight: 500; + + &:hover, + &:focus { + background-color: rgba(0, 0, 0, 5%); + color: #121212; + } + + kbd { + font: inherit; + font-weight: 400; + border-radius: 3px; + padding: 3px 4px; + margin-right: -8px; + color: rgba(0, 0, 0, 50%); + background-color: rgba(0, 0, 0, 8%); + } + } +} diff --git a/src/popup/components/input/index.js b/src/popup/components/input/index.js new file mode 100644 index 0000000..400ba99 --- /dev/null +++ b/src/popup/components/input/index.js @@ -0,0 +1,117 @@ +import './style.scss'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useRef, useEffect } from '@wordpress/element'; +import { TextControl } from '@wordpress/components'; +import { useSelect, useDispatch } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import TOOLBAR_ICON from '../../../utils/icon'; + +export default function Input(props) { + const { onInsert } = props; + + const ref = useRef(); + + const { reset, setInput, setScreen, requestAI } = useDispatch('mind/popup'); + + const { isOpen, input, context, screen, loading, response } = useSelect( + (select) => { + const { + isOpen: checkIsOpen, + getInput, + getContext, + getScreen, + getLoading, + getResponse, + } = select('mind/popup'); + + return { + isOpen: checkIsOpen(), + input: getInput(), + context: getContext(), + screen: getScreen(), + loading: getLoading(), + response: getResponse(), + }; + } + ); + + let contextLabel = context; + + switch (context) { + case 'selected-blocks': + contextLabel = __('Selected Blocks'); + break; + case 'post-title': + contextLabel = __('Post Title'); + break; + // no default + } + + function onKeyDown(e) { + // Go back to starter screen. + if (screen !== '' && e.key === 'Backspace' && !e.target.value) { + reset(); + return; + } + + // Insert request to post. + if (response && e.key === 'Enter') { + onInsert(); + return; + } + + // Send request to AI. + if (screen === 'request' && e.key === 'Enter') { + requestAI(); + } + } + + function focusInput() { + if (ref?.current) { + const inputEl = ref.current.querySelector('input'); + + if (inputEl) { + inputEl.focus(); + } + } + } + + // Set focus on Input. + useEffect(() => { + if (isOpen && !loading && ref?.current) { + focusInput(); + } + }, [isOpen, loading, ref]); + + // Open request page if something is in input. + useEffect(() => { + if (screen === '' && input) { + setScreen('request'); + } + }, [screen, input, setScreen]); + + return ( +
+ {TOOLBAR_ICON} + { + setInput(val); + }} + onKeyDown={onKeyDown} + disabled={loading} + /> + {contextLabel ? ( + {contextLabel} + ) : null} +
+ ); +} diff --git a/src/popup/components/input/style.scss b/src/popup/components/input/style.scss new file mode 100644 index 0000000..9994137 --- /dev/null +++ b/src/popup/components/input/style.scss @@ -0,0 +1,43 @@ +$padding: 10px; + +.mind-popup-input { + display: flex; + align-items: center; + margin-bottom: 0; + border-bottom: 1px solid #e8e7e7; + + > svg { + position: absolute; + left: $padding * 2; + pointer-events: none; + } + + > .components-base-control { + flex: 1; + } + + .components-base-control__field { + margin-bottom: 0; + } + .components-base-control__field input { + border: none !important; + box-shadow: none !important; + padding: $padding * 2; + padding-left: $padding * 5; + font-size: 1.15em; + color: #151515; + + &::placeholder { + color: #a3a3a3; + } + } + + .mind-popup-input-context { + font-weight: 400; + border-radius: 3px; + padding: 3px 10px; + margin-right: 10px; + color: rgb(0, 0, 0, 60%); + background-color: rgba(0, 0, 0, 8%); + } +} diff --git a/src/popup/index.js b/src/popup/index.js index 710ac6c..f9dcd7b 100644 --- a/src/popup/index.js +++ b/src/popup/index.js @@ -6,9 +6,8 @@ import './style.scss'; /** * WordPress dependencies */ -import { __ } from '@wordpress/i18n'; -import { createRoot, useRef, useEffect, RawHTML } from '@wordpress/element'; -import { Modal, Button, TextControl } from '@wordpress/components'; +import { createRoot } from '@wordpress/element'; +import { Modal } from '@wordpress/components'; import { useSelect, useDispatch } from '@wordpress/data'; import { rawHandler } from '@wordpress/blocks'; import domReady from '@wordpress/dom-ready'; @@ -16,110 +15,35 @@ import domReady from '@wordpress/dom-ready'; /** * Internal dependencies */ -import TOOLBAR_ICON from '../utils/icon'; +import Input from './components/input'; import LoadingLine from './components/loading-line'; -import LoadingText from './components/loading-text'; -import Notice from './components/notice'; - -import { ReactComponent as PopupPostTitleAboutIcon } from '../icons/popup-post-title-about.svg'; -import { ReactComponent as PopupPostAboutIcon } from '../icons/popup-post-about.svg'; -import { ReactComponent as PopupOutlineAboutIcon } from '../icons/popup-outline-about.svg'; -import { ReactComponent as PopupParagraphAboutIcon } from '../icons/popup-paragraph-about.svg'; -import { ReactComponent as PopupListAboutIcon } from '../icons/popup-list-about.svg'; -import { ReactComponent as PopupTableAboutIcon } from '../icons/popup-table-about.svg'; +import Content from './components/content'; +import Footer from './components/footer'; const POPUP_CONTAINER_CLASS = 'mind-popup-container'; -const commands = [ - { - type: 'category', - label: __('Post Presets', 'mind'), - }, - { - type: 'request', - label: __('Post title about…', 'mind'), - request: __('Write a post title about ', 'mind'), - icon: , - }, - { - type: 'request', - label: __('Post about…', 'mind'), - request: __('Write a blog post about ', 'mind'), - icon: , - }, - { - type: 'request', - label: __('Outline about…', 'mind'), - request: __('Write a blog post outline about ', 'mind'), - icon: , - }, - - { - type: 'category', - label: __('Content Presets', 'mind'), - }, - { - type: 'request', - label: __('Paragraph about…', 'mind'), - request: __('Create a paragraph about ', 'mind'), - icon: , - }, - { - type: 'request', - label: __('List about…', 'mind'), - request: __('Create a list about ', 'mind'), - icon: , - }, - { - type: 'request', - label: __('Table about…', 'mind'), - request: __('Create a table about ', 'mind'), - icon: , - }, -]; - -export default function Popup(props) { - const { onClose } = props; - - const ref = useRef(); - +export default function Popup() { const { setHighlightBlocks } = useDispatch('mind/blocks'); - const { close, reset, setInput, setScreen, setError, requestAI } = - useDispatch('mind/popup'); + const { close, reset } = useDispatch('mind/popup'); - const { - isOpen, - input, - context, - insertionPlace, - screen, - loading, - response, - error, - } = useSelect((select) => { - const { - isOpen: checkIsOpen, - getInput, - getContext, - getInsertionPlace, - getScreen, - getLoading, - getResponse, - getError, - } = select('mind/popup'); + const { isOpen, insertionPlace, loading, response } = useSelect( + (select) => { + const { + isOpen: checkIsOpen, + getInsertionPlace, + getLoading, + getResponse, + } = select('mind/popup'); - return { - isOpen: checkIsOpen(), - input: getInput(), - context: getContext(), - insertionPlace: getInsertionPlace(), - screen: getScreen(), - loading: getLoading(), - response: getResponse(), - error: getError(), - }; - }); + return { + isOpen: checkIsOpen(), + insertionPlace: getInsertionPlace(), + loading: getLoading(), + response: getResponse(), + }; + } + ); const { selectedClientIds } = useSelect((select) => { const { getSelectedBlockClientIds } = select('core/block-editor'); @@ -131,36 +55,8 @@ export default function Popup(props) { }; }, []); - let contextLabel = context; - - switch (context) { - case 'selected-blocks': - contextLabel = __('Selected Blocks'); - break; - case 'post-title': - contextLabel = __('Post Title'); - break; - // no default - } - const { insertBlocks, replaceBlocks } = useDispatch('core/block-editor'); - function focusInput() { - if (ref?.current) { - const inputEl = ref.current.querySelector( - '.mind-popup-input input' - ); - - if (inputEl) { - inputEl.focus(); - } - } - } - - function copyToClipboard() { - window.navigator.clipboard.writeText(response); - } - function insertResponse() { const parsedBlocks = rawHandler({ HTML: response }); @@ -184,177 +80,27 @@ export default function Popup(props) { reset(); close(); - - if (onClose) { - onClose(); - } - } - - // Set focus on Input. - useEffect(() => { - if (isOpen && !loading && ref?.current) { - focusInput(); - } - }, [isOpen, loading, ref]); - - // Open request page if something is in input. - useEffect(() => { - if (screen === '' && input) { - setScreen('request'); - } - }, [screen, input, setScreen]); - - function onKeyDown(e) { - // Go back to starter screen. - if (screen !== '' && e.key === 'Backspace' && !e.target.value) { - reset(); - return; - } - - // Insert request to post. - if (response && e.key === 'Enter') { - onInsert(); - return; - } - - // Send request to AI. - if (screen === 'request' && e.key === 'Enter') { - requestAI(); - } } if (!isOpen) { return null; } - const showFooter = response || (input && !loading && !response); - return ( { reset(); close(); - - if (onClose) { - onClose(); - } }} __experimentalHideHeader > -
- {TOOLBAR_ICON} - { - setInput(val); - }} - onKeyDown={onKeyDown} - disabled={loading} - /> - {contextLabel ? ( - - {contextLabel} - - ) : ( - '' - )} -
+ {loading && } -
- {screen === '' ? ( -
- {commands.map((data) => { - if (data.type === 'category') { - return ( - - {data.label} - - ); - } - - return ( - - ); - })} -
- ) : null} - - {screen === 'request' && ( -
- {loading && ( - - {__('Waiting for AI response', 'mind')} - - )} - {!loading && response && {response}} - {!loading && error && ( - {error} - )} -
- )} -
- {showFooter && ( -
-
- {input && !loading && !response && ( - - )} - {response && ( - <> - - - - - )} -
-
- )} + +