diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index c0c31e96cc..60a282ee2e 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -27,7 +27,7 @@ jobs: - name: Get yarn cache directory path id: yarn-cache-dir-path run: echo "::set-output name=dir::$(yarn cache dir)" - - uses: actions/cache@v2 + - uses: actions/cache@v3 id: yarn-cache with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} @@ -39,7 +39,7 @@ jobs: run: | echo "::set-output name=dir::$(composer config cache-files-dir)" - name: Configure Composer cache - uses: actions/cache@v1 + uses: actions/cache@v3 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} diff --git a/assets/apps/dashboard/src/Components/App.js b/assets/apps/dashboard/src/Components/App.js index 1e356e1da9..98702eb4ea 100644 --- a/assets/apps/dashboard/src/Components/App.js +++ b/assets/apps/dashboard/src/Components/App.js @@ -1,61 +1,59 @@ +import Container from '../Layout/Container'; +import { fetchOptions } from '../utils/rest'; +import Sidebar from './Content/Sidebar/Sidebar'; import Header from './Header'; import Notifications from './Notifications'; -import TabsContent from './TabsContent'; -import Sidebar from './Sidebar'; -import Loading from './Loading'; +import SkeletonLoader from './SkeletonLoader'; import Snackbar from './Snackbar'; -import { fetchOptions } from '../utils/rest'; +import TransitionWrapper from './Common/TransitionWrapper'; -import { withDispatch, withSelect } from '@wordpress/data'; -import { compose } from '@wordpress/compose'; -import { useState, Fragment, useEffect } from '@wordpress/element'; -import Deal from './Deal'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { useEffect, useState } from '@wordpress/element'; +import { tabs } from '../utils/common'; +import { NEVE_STORE } from '../utils/constants'; -const App = ({ setSettings, toast, currentTab, setTab }) => { +const App = () => { const [loading, setLoading] = useState(true); + + const { setSettings, setTab } = useDispatch(NEVE_STORE); + + const { currentTab } = useSelect((select) => { + const { getTab } = select(NEVE_STORE); + return { + currentTab: getTab(), + }; + }); + useEffect(() => { fetchOptions().then((r) => { setSettings(r); setLoading(false); }); }, []); + if (loading) { - return ; + return ; } return ( - -
-
-
-
- - {'starter-sites' !== currentTab && } - -
- {'starter-sites' !== currentTab && - 'custom-layouts' !== currentTab && ( - - )} -
-
- {toast && } - +
+
+ + {/**/} + {'starter-sites' !== currentTab && } + + +
{tabs[currentTab].render(setTab)}
+ + {!['starter-sites', 'settings'].includes(currentTab) && ( + + + + )} +
+ + +
); }; -export default compose( - withDispatch((dispatch) => { - const { setSettings, setTab } = dispatch('neve-dashboard'); - return { - setSettings: (object) => setSettings(object), - setTab: (tab) => setTab(tab), - }; - }), - withSelect((select) => { - const { getToast, getTab } = select('neve-dashboard'); - return { - toast: getToast(), - currentTab: getTab(), - }; - }) -)(App); +export default App; diff --git a/assets/apps/dashboard/src/Components/Common/Button.js b/assets/apps/dashboard/src/Components/Common/Button.js new file mode 100644 index 0000000000..5cf509adc5 --- /dev/null +++ b/assets/apps/dashboard/src/Components/Common/Button.js @@ -0,0 +1,81 @@ +import cn from 'classnames'; +import { LoaderCircle } from 'lucide-react'; +import PropTypes from 'prop-types'; + +const Button = (props) => { + const { + href, + onClick, + className = '', + isSubmit, + isPrimary, + isSecondary, + isLink, + children, + disabled, + loading, + target, + } = props; + + const classNames = cn([ + 'flex items-center px-3 py-2 transition-colors duration-150 text-sm border gap-2', + { + rounded: !className.includes('rounded'), + 'border-transparent bg-blue-600 text-white': isPrimary, + 'border-blue-600 text-blue-600': isSecondary, + 'border-transparent text-gray-600': isLink, + 'cursor-not-allowed opacity-50': disabled, + 'hover:bg-blue-700 hover:text-white': !disabled && isPrimary, + 'hover:bg-blue-600 hover:text-white': !disabled && isSecondary, + 'hover:text-gray-900': !disabled && isLink, + }, + className, + ]); + + const passedProps = { + className: classNames, + disabled, + onClick, + }; + + if (isSubmit) { + passedProps.type = 'submit'; + } + + if (href) { + passedProps.href = href; + } + + if (target) { + passedProps.target = target; + + if (target === '_blank') { + passedProps.rel = 'noopener noreferrer'; + } + } + + const TAG = href && !onClick ? 'a' : 'button'; + + return ( + + {loading && } + + {children} + + ); +}; + +Button.propTypes = { + href: PropTypes.string, + onClick: PropTypes.func, + className: PropTypes.string, + isSubmit: PropTypes.bool, + isPrimary: PropTypes.bool, + isSecondary: PropTypes.bool, + isLink: PropTypes.bool, + children: PropTypes.node, + disabled: PropTypes.bool, + target: PropTypes.oneOf(['_blank', '_self', '_parent', '_top']), +}; + +export default Button; diff --git a/assets/apps/dashboard/src/Components/Common/Link.js b/assets/apps/dashboard/src/Components/Common/Link.js new file mode 100644 index 0000000000..147f59b602 --- /dev/null +++ b/assets/apps/dashboard/src/Components/Common/Link.js @@ -0,0 +1,36 @@ +import { __ } from '@wordpress/i18n'; + +import { LucideExternalLink } from 'lucide-react'; +import cn from 'classnames'; + +export default ({ text, url, isExternal, className }) => { + const linkClasses = cn([ + 'text-blue-600 hover:text-blue-700 hover:underline text-sm', + className, + { + 'inline-flex gap-1.5 items-center': isExternal, + }, + ]); + if (!isExternal) { + return ( + + {text} + + ); + } + + return ( + + {text} + + {__('(opens in a new tab)', 'neve')} + + + + ); +}; diff --git a/assets/apps/dashboard/src/Components/Common/Multiselect.js b/assets/apps/dashboard/src/Components/Common/Multiselect.js new file mode 100644 index 0000000000..f960bed48a --- /dev/null +++ b/assets/apps/dashboard/src/Components/Common/Multiselect.js @@ -0,0 +1,122 @@ +import { useRef, useState, useEffect } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import cn from 'classnames'; +import { Check, ChevronDown } from 'lucide-react'; + +const MultiSelect = ({ value, label, disabled, choices = {}, onChange }) => { + const [isOpen, setIsOpen] = useState(false); + + const dropdownRef = useRef(null); + + const closeDropdown = (e) => { + if (dropdownRef.current && !dropdownRef.current.contains(e.target)) { + setIsOpen(false); + } + }; + + useEffect(() => { + if (isOpen) { + document.addEventListener('click', closeDropdown); + } else { + document.removeEventListener('click', closeDropdown); + } + + return () => { + document.removeEventListener('click', closeDropdown); + }; + }, [isOpen]); + + const handleChange = (optionValue) => { + const nextValues = value.includes(optionValue) + ? value.filter((v) => v !== optionValue) + : [...value, optionValue]; + onChange(nextValues); + }; + + return ( +
+ {label && ( + + {label} + + )} +
+ + + {isOpen && ( +
+
+ {Object.entries(choices).map( + ([optionValue, optionLabel]) => ( + + ) + )} +
+
+ )} +
+
+ ); +}; + +export default MultiSelect; diff --git a/assets/apps/dashboard/src/Components/Common/Notice.js b/assets/apps/dashboard/src/Components/Common/Notice.js new file mode 100644 index 0000000000..1c038a78d3 --- /dev/null +++ b/assets/apps/dashboard/src/Components/Common/Notice.js @@ -0,0 +1,79 @@ +import { useEffect } from '@wordpress/element'; +import cn from 'classnames'; +import { + LucideCircleAlert, + LucideCircleCheck, + LucideCircleX, + LucideInfo, +} from 'lucide-react'; + +const Notice = ({ + children, + className = '', + + // Type variants + isSuccess = false, + isWarning = false, + isError = false, + + // Optional custom icon + icon: CustomIcon, + isAutoDismiss: autoDismiss = 0, + onDismiss = () => {}, +}) => { + useEffect(() => { + if (autoDismiss < 1000) return; + + const dismissTimeout = setTimeout(() => { + onDismiss(); + }, autoDismiss); + + return () => { + clearTimeout(dismissTimeout); + }; + }, [autoDismiss, onDismiss]); + + if (!children) return null; + + const getTypeClasses = () => { + if (isSuccess) return 'border-lime-300 bg-lime-50 text-lime-800'; + if (isWarning) return 'border-orange-300 bg-orange-50 text-yellow-800'; + if (isError) return 'border-red-300 bg-red-50 text-red-800'; + return 'border-sky-300 bg-sky-50 text-sky-800'; + }; + + const getIcon = () => { + if (CustomIcon) return CustomIcon; + if (isSuccess) return LucideCircleCheck; + if (isWarning) return LucideCircleAlert; + if (isError) return LucideCircleX; + return LucideInfo; + }; + + const iconColor = { + 'text-lime-500': isSuccess, + 'text-orange-500': isWarning, + 'text-red-500': isError, + 'text-sky-500': !isSuccess && !isWarning && !isError, + }; + + const Icon = getIcon(); + + return ( +
+ + +
+ {children &&
{children}
} +
+
+ ); +}; + +export default Notice; diff --git a/assets/apps/dashboard/src/Components/Notification.js b/assets/apps/dashboard/src/Components/Common/Notification.js similarity index 59% rename from assets/apps/dashboard/src/Components/Notification.js rename to assets/apps/dashboard/src/Components/Common/Notification.js index b8cf8a94b4..900a55220b 100644 --- a/assets/apps/dashboard/src/Components/Notification.js +++ b/assets/apps/dashboard/src/Components/Common/Notification.js @@ -1,26 +1,59 @@ /* global neveDash */ -import classnames from 'classnames'; - -import { sprintf, __ } from '@wordpress/i18n'; -import { useState } from '@wordpress/element'; -import { external } from '@wordpress/icons'; -import { Button, Dashicon, Icon, Tooltip } from '@wordpress/components'; - -const Notification = ({ data, slug }) => { - // eslint-disable-next-line no-unused-vars +import cn from 'classnames'; + +import { useState, useEffect } from '@wordpress/element'; +import { __, sprintf } from '@wordpress/i18n'; + +import { + LucideCircleCheck, + LucideCircleX, + LucideExternalLink, +} from 'lucide-react'; +import Card from '../../Layout/Card'; +import Button from './Button'; +import Tooltip from './Tooltip'; +import TransitionInOut from './TransitionInOut'; + +const Notification = ({ data }) => { const [hidden, setHidden] = useState(false); const { text, cta, type, update, url, targetBlank } = data; const { canInstallPlugins } = neveDash; const [inProgress, setInProgress] = useState(false); const [done, setDone] = useState(false); const [errorMessage, setErrorMessage] = useState(null); - const classes = classnames([ - 'notification', - slug, - type && !done ? type : '', + + useEffect(() => { + let timeout; + if (done === 'done') { + timeout = setTimeout(() => { + setHidden(true); + }, 5000); + } + + return () => { + clearTimeout(timeout); + }; + }, [done]); + + const classes = cn([ + 'text-white !p-3 rounded flex flex-col md:flex-row items-center justify-between gap-4', + { + 'bg-blue-500': type === 'info' || (!type && !done), + 'bg-amber-500': 'warning' === type && !done, + 'bg-emerald-500': 'success' === type || 'done' === done, + 'bg-red-500': + ('error' === type && 'done' !== done) || 'error' === done, + }, + ]); + + const buttonClasses = cn([ + 'bg-white border-white hover:opacity-90 text-blue-500 font-medium', { - 'success hidden': 'done' === done, - error: 'error' === done, + '!text-blue-600': type === 'info' || (!type && !done), + '!text-amber-600': 'warning' === type && !done, + '!text-emerald-600': 'success' === type || 'done' === done, + '!text-red-600': + ('error' === type && 'done' !== done) || 'error' === done, }, ]); @@ -119,23 +152,16 @@ const Notification = ({ data, slug }) => { } return ( ); }; @@ -152,10 +178,6 @@ const Notification = ({ data, slug }) => { __('Neve Pro', 'neve') ) )} - position="top center" - style={{ - opacity: 1, - }} > {ctaContent()} @@ -165,17 +187,21 @@ const Notification = ({ data, slug }) => { const UpdateNotification = () => { return ( -
- {!done &&

{text}

} + <> + {!done && ( +

+ {text} +

+ )} {'done' === done && ( -

- +

+ {__('Done!', 'neve')}

)} {'error' === done && ( -

- +

+ {errorMessage || __( 'An error occurred. Please reload the page and try again.', @@ -184,33 +210,38 @@ const Notification = ({ data, slug }) => {

)} {wrappedButtonContent} -
+ ); }; - const LinkNotification = () => { - return ( -
-

- {url && cta && ( - - )} -

- ); - }; - - if (update) { - return ; - } + const LinkNotification = () => ( + <> +

+ {url && cta && ( + + )} + + ); - return ; + return ( + + + {update ? : } + + + ); }; export default Notification; diff --git a/assets/apps/dashboard/src/Components/Common/Pill.js b/assets/apps/dashboard/src/Components/Common/Pill.js new file mode 100644 index 0000000000..6d4137c8c8 --- /dev/null +++ b/assets/apps/dashboard/src/Components/Common/Pill.js @@ -0,0 +1,23 @@ +import cn from 'classnames'; + +export default ({ children, type = 'primary', className }) => { + if (!children) { + return null; + } + + const typeClasses = { + primary: 'bg-blue-100 text-blue-700', + secondary: 'bg-gray-100 text-gray-700', + success: 'bg-lime-100 text-lime-700', + error: 'bg-red-100 text-red-700', + warning: 'bg-yellow-100 text-yellow-700', + }; + + const classes = cn([ + typeClasses[type], + 'px-1.5 py-0.5 text-sm font-medium rounded leading-none uppercase', + className, + ]); + + return {children}; +}; diff --git a/assets/apps/dashboard/src/Components/Common/Select.js b/assets/apps/dashboard/src/Components/Common/Select.js new file mode 100644 index 0000000000..157e634061 --- /dev/null +++ b/assets/apps/dashboard/src/Components/Common/Select.js @@ -0,0 +1,96 @@ +import cn from 'classnames'; +import { LoaderCircle, LucideChevronDown } from 'lucide-react'; + +import { + Field, + Label, + Listbox, + ListboxButton, + ListboxOption, + ListboxOptions, +} from '@headlessui/react'; +import { __ } from '@wordpress/i18n'; + +export default ({ + label, + value, + onChange, + disabled = false, + loading, + choices, +}) => { + return ( + + {label && ( + + )} +

+ {loading && ( + + )} + + {({ open }) => ( + <> + + {choices[value] || + __('Select an option', 'neve')} + + + + + {Object.entries(choices).map( + ([optionValue, optionLabel]) => ( + + {optionLabel} + + ) + )} + + + )} + +
+ + ); +}; diff --git a/assets/apps/dashboard/src/Components/Common/TextInput.js b/assets/apps/dashboard/src/Components/Common/TextInput.js new file mode 100644 index 0000000000..6c178baa1a --- /dev/null +++ b/assets/apps/dashboard/src/Components/Common/TextInput.js @@ -0,0 +1,52 @@ +import { Description, Field, Input, Label } from '@headlessui/react'; +import { Fragment } from '@wordpress/element'; +import cn from 'classnames'; + +const TextInput = ({ + value, + label, + disabled, + onChange, + name, + className = '', + type = 'text', + description, +}) => { + let TagName = 'input'; + + if (type === 'textarea') { + TagName = 'textarea'; + } + + return ( + + {label && ( + + )} + {description && ( + + {description} + + )} + + + + + ); +}; + +export default TextInput; diff --git a/assets/apps/dashboard/src/Components/Common/Toast.js b/assets/apps/dashboard/src/Components/Common/Toast.js new file mode 100644 index 0000000000..009b11beab --- /dev/null +++ b/assets/apps/dashboard/src/Components/Common/Toast.js @@ -0,0 +1,65 @@ +import cn from 'classnames'; + +import { useEffect, useState } from '@wordpress/element'; +import { + LucideCircleAlert, + LucideCircleCheck, + LucideCircleX, + LucideInfo, +} from 'lucide-react'; +import TransitionInOut from './TransitionInOut'; + +const Toast = ({ message, dismiss, time, type = 'info', className }) => { + const [show, setShow] = useState(false); + + useEffect(() => { + setShow(true); + + const timeBeforeHide = time || 2000; + const timeBeforeDismiss = timeBeforeHide + 1000; + + const hideTimeout = setTimeout(() => { + setShow(false); + }, timeBeforeHide); + + const dismissTimeout = setTimeout(() => { + if (dismiss) dismiss(''); + }, timeBeforeDismiss); + + return () => { + clearTimeout(hideTimeout); + clearTimeout(dismissTimeout); + }; + }, []); + + const iconMap = { + info: LucideInfo, + error: LucideCircleX, + success: LucideCircleCheck, + warning: LucideCircleAlert, + }; + + const classes = cn( + 'px-2 py-1.5 flex items-center text-sm border rounded gap-2', + { + 'bg-sky-50 text-sky-800': type === 'info', + 'bg-red-50 text-red-800': type === 'error', + 'bg-lime-50 text-lime-800': type === 'success', + 'bg-orange-50 text-orange-800': type === 'warning', + }, + className + ); + + const ICON = iconMap[type]; + + return ( + +
+ + {message} +
+
+ ); +}; + +export default Toast; diff --git a/assets/apps/dashboard/src/Components/Common/Toggle.js b/assets/apps/dashboard/src/Components/Common/Toggle.js new file mode 100644 index 0000000000..9387af341b --- /dev/null +++ b/assets/apps/dashboard/src/Components/Common/Toggle.js @@ -0,0 +1,68 @@ +import { Switch, Label, Field, Description } from '@headlessui/react'; +import cn from 'classnames'; + +export default ({ + checked, + onToggle, + label, + disabled = false, + className, + labelBefore = false, + labelClassName = '', + description, +}) => { + const switchClasses = cn( + 'group inline-flex h-6 w-11 items-center rounded-full bg-gray-300 transition data-[checked]:bg-blue-600', + { + 'cursor-not-allowed opacity-50': disabled, + } + ); + + const wrapClasses = cn('flex items-center gap-3', className); + + const labelClasses = cn( + { + 'font-medium': !labelClassName.includes('font-'), + 'text-sm': !labelClassName.includes('text-'), + 'text-gray-600': !labelClassName.includes('text-'), + }, + labelClassName + ); + + return ( + +
+ {label && labelBefore && ( + + )} + + + + + + {label && !labelBefore && ( + + )} +
+ + {description && ( + + {description} + + )} +
+ ); +}; diff --git a/assets/apps/dashboard/src/Components/Common/Tooltip.js b/assets/apps/dashboard/src/Components/Common/Tooltip.js new file mode 100644 index 0000000000..0306d820af --- /dev/null +++ b/assets/apps/dashboard/src/Components/Common/Tooltip.js @@ -0,0 +1,20 @@ +import { useState } from '@wordpress/element'; + +export default ({ text, children }) => { + const [isVisible, setIsVisible] = useState(false); + + return ( +
setIsVisible(true)} + onMouseLeave={() => setIsVisible(false)} + > + {children} + {isVisible && ( +
+ {text} +
+ )} +
+ ); +}; diff --git a/assets/apps/dashboard/src/Components/Common/TransitionInOut.js b/assets/apps/dashboard/src/Components/Common/TransitionInOut.js new file mode 100644 index 0000000000..65fdc7906e --- /dev/null +++ b/assets/apps/dashboard/src/Components/Common/TransitionInOut.js @@ -0,0 +1,14 @@ +import { Transition } from '@headlessui/react'; + +export default ({ children, show }) => { + const className = `transition + data-[closed]:transform data-[closed]:-translate-y-2 data-[closed]:opacity-0 + data-[open]:transform data-[open]:translate-y-0 data-[open]:opacity-100 + duration-300 ease-in-out`; + + return ( + +
{children}
+
+ ); +}; diff --git a/assets/apps/dashboard/src/Components/Common/TransitionWrapper.js b/assets/apps/dashboard/src/Components/Common/TransitionWrapper.js new file mode 100644 index 0000000000..f9ce051bf9 --- /dev/null +++ b/assets/apps/dashboard/src/Components/Common/TransitionWrapper.js @@ -0,0 +1,43 @@ +import { Transition } from '@headlessui/react'; +import cn from 'classnames'; + +export default ({ children, from = 'bottom', className }) => { + const directionClasses = { + left: { + enterFrom: '-translate-x-2', + enterTo: 'translate-x-0', + }, + right: { + enterFrom: 'translate-x-2', + enterTo: 'translate-x-0', + }, + top: { + enterFrom: '-translate-y-2', + enterTo: 'translate-y-0', + }, + bottom: { + enterFrom: 'translate-y-2', + enterTo: 'translate-y-0', + }, + }; + + const animationData = directionClasses[from] || directionClasses.bottom; + + const transitionClasses = { + enter: 'ease-out duration-150', + enterFrom: `opacity-0 ${animationData.enterFrom}`, + enterTo: `opacity-100 ${animationData.enterTo}`, + }; + + return ( + + {children} + + ); +}; diff --git a/assets/apps/dashboard/src/Components/Content/Changelog.js b/assets/apps/dashboard/src/Components/Content/Changelog.js index e5846395ea..ff0e9873a7 100644 --- a/assets/apps/dashboard/src/Components/Content/Changelog.js +++ b/assets/apps/dashboard/src/Components/Content/Changelog.js @@ -1,114 +1,172 @@ /* global neveDash */ -import Accordion from '../Accordion'; -import classnames from 'classnames'; - +import cn from 'classnames'; +import { Clock, Crown, Rocket, Bug, Zap, CheckCircle } from 'lucide-react'; import { __ } from '@wordpress/i18n'; -import { Fragment, useState } from '@wordpress/element'; +import { useState } from '@wordpress/element'; + +import Card from '../../Layout/Card'; +import Button from '../Common/Button'; +import Pill from '../Common/Pill'; +import TransitionWrapper from '../Common/TransitionWrapper'; + +const TAB_CHOICES = { + FREE: 'free', + PRO: 'pro', +}; + +const CHANGE_TYPES = { + features: { + icon: , + label: __('Features', 'neve'), + }, + fixes: { + icon: , + label: __('Bug Fixes', 'neve'), + }, + tweaks: { + icon: , + label: __('Tweaks', 'neve'), + }, +}; + +const TabButton = ({ active, onClick, children }) => { + return ( + + ); +}; + +const ChangelogEntry = ({ data }) => { + const { version, tweaks, fixes, features, date } = data; + + const renderChangeList = (type, items) => { + if (!items?.length) return null; + const { icon, label } = CHANGE_TYPES[type]; + + return ( +
+
+ {icon} + + {label} + +
+
    + {items.map((item, index) => { + return ( +
  • + + +
  • + ); + })} +
+
+ ); + }; + + return ( +
+
+
+
+

+ {__('Version', 'neve')} {version} +

+ + {date} + +
+
+
+ {Object.entries({ features, fixes, tweaks }).map( + ([type, items]) => renderChangeList(type, items) + )} +
+
+
+ ); +}; const Changelog = () => { const { changelog, changelogPro } = neveDash; - const [showForPro, setShowForPro] = useState(false); + + const [shown, setShown] = useState(4); + const [activeTab, setActiveTab] = useState(TAB_CHOICES.FREE); + const changelogData = + activeTab === TAB_CHOICES.FREE ? changelog : changelogPro; return ( -
+ {changelogPro && ( -
- {__('Show changelog for', 'neve')} - { - setShowForPro(false); - }} - > - {__('Neve', 'neve')} - - { - setShowForPro(true); - }} + +
+
+ + + {__('Recent Updates', 'neve')} + +
+
+ setActiveTab(TAB_CHOICES.FREE)} + > + {__('Free Version', 'neve')} + + setActiveTab(TAB_CHOICES.PRO)} + > +
+ + {__('Pro Version', 'neve')} +
+
+
+
+
+ )} + + + {changelogData.slice(0, shown).map((entry) => { + const { version, tweaks, fixes, features } = entry; + + if ((!tweaks && !fixes && !features) || !version) { + return null; + } + + return ; + })} + + + {changelogData.length > shown && ( +
)} - {(showForPro ? changelogPro : changelog).map((entry, index) => { - const { date, version, tweaks, fixes, features } = entry; - if (!tweaks && !fixes && !features) { - return null; - } - const title = ( - - v{version} -{' '} - {date} - - ); - - return ( - - {features && ( -
-
- - {__('Features', 'neve')} - -
-
    - {features.map((feature, indexFeature) => ( -
  • - ))} -
-
- )} - {fixes && ( -
-
- - {__('Bug Fixes', 'neve')} - -
-
    - {fixes.map((fix, indexFixes) => ( -
  • - ))} -
-
- )} - {tweaks && ( -
-
- - {__('Tweaks', 'neve')} - -
-
    - {tweaks.map((tweak, indexTweak) => ( -
  • - ))} -
-
- )} -
- ); - })} -
+
); }; diff --git a/assets/apps/dashboard/src/Components/Content/CustomLayoutsUnavailable.js b/assets/apps/dashboard/src/Components/Content/CustomLayoutsUnavailable.js deleted file mode 100644 index 53b4781c28..0000000000 --- a/assets/apps/dashboard/src/Components/Content/CustomLayoutsUnavailable.js +++ /dev/null @@ -1,85 +0,0 @@ -/* global neveDash */ -import { __ } from '@wordpress/i18n'; -import { Button } from '@wordpress/components'; -import { compose } from '@wordpress/compose'; -import { withSelect } from '@wordpress/data'; -import { useEffect } from '@wordpress/element'; - -const CustomLayoutsUnavailable = ({ license, setTab }) => { - const { customLayoutsNeveProURL, assets } = neveDash; - - const hasPro = neveDash.pro || neveDash.hasOldPro; - const secondButtonMessage = hasPro - ? __('Activate', 'neve') - : __('Free vs Pro', 'neve'); - const navigateToFreeVsPro = () => { - setTab('free-pro'); - }; - - const navigateToProActivate = () => { - setTab('pro'); - }; - - useEffect(() => { - if (license && 'valid' === license.valid) { - setTab('pro'); - window.location.href = 'edit.php?post_type=neve_custom_layouts'; - } - }, [license]); - - return ( -
-
-
- Neve -

{__('Custom Layouts', 'neve')}

-

- {__( - 'Get access to all Pro features and power-up your website', - 'neve' - )} -

- - - -
-
- ); -}; - -export default compose( - withSelect((select) => { - const { getLicense } = select('neve-dashboard'); - return { - license: getLicense(), - }; - }) -)(CustomLayoutsUnavailable); diff --git a/assets/apps/dashboard/src/Components/Content/FreePro.js b/assets/apps/dashboard/src/Components/Content/FreePro.js index 828c98a37b..0eef316bd3 100644 --- a/assets/apps/dashboard/src/Components/Content/FreePro.js +++ b/assets/apps/dashboard/src/Components/Content/FreePro.js @@ -1,47 +1,142 @@ /* global neveDash */ -import FeatureRow from '../FeatureRow'; import { __ } from '@wordpress/i18n'; -import { Button } from '@wordpress/components'; -const Pro = () => { - const { featureData } = neveDash; - return ( -
- - - - - - - {featureData.map((item, index) => ( - - ))} - -
- NeveNeve Pro
- -
-

- {__( - 'Get access to all Pro features and power-up your website', - 'neve' - )} -

- +import { + CheckCircle2, + XCircle, + HelpCircle, + ArrowRight, + BookOpen, +} from 'lucide-react'; + +import Card from '../../Layout/Card'; +import Tooltip from '../Common/Tooltip'; +import Button from '../Common/Button'; +import TransitionWrapper from '../Common/TransitionWrapper'; + +const FreeProCard = () => ( + +
+
{__('Feature', 'neve')}
+
+
+ {__('Free', 'neve')} +
+
+ {__('Pro', 'neve')} +
+ + {neveDash.featureData.map(({ section, items }) => ( +
+
+

+ {section} +

+
+ + {items.map((item, index) => ( +
+
+
+
+ + {item.title} + + {item.tooltip && ( + + + + )} +
+

+ {item.description} +

+
+
+
+ {item.free ? ( + + ) : ( + + )} +
+
+ +
+
+
+
+ ))} +
+ ))} +
+); + +const UpsellCard = () => { + return ( + +
+

+ {__('Need help deciding?', 'neve')} +

+
+

+ {__( + 'Our support team is happy to answer your questions about specific Pro features and help you determine if they match your needs.', + 'neve' + )} +

+
+
+ {__( + 'Average response time: ~8 hours during business days', + 'neve' + )} +
+
+
+
+ + +
+
+
); }; -export default Pro; +export default () => { + return ( +
+ + + + + + +
+ ); +}; diff --git a/assets/apps/dashboard/src/Components/Content/Help.js b/assets/apps/dashboard/src/Components/Content/Help.js deleted file mode 100644 index b48d63443d..0000000000 --- a/assets/apps/dashboard/src/Components/Content/Help.js +++ /dev/null @@ -1,134 +0,0 @@ -/* global neveDash */ -import Card from '../Card'; - -import { __ } from '@wordpress/i18n'; -import { Fragment } from '@wordpress/element'; -import { Button, Icon, ExternalLink } from '@wordpress/components'; - -const Help = (props) => { - const { setTab } = props; - - let { docsURL, codexURL, supportURL, whiteLabel, assets } = neveDash; - const { supportCardDescription, docsCardDescription } = neveDash.strings; - - if (whiteLabel && whiteLabel.agencyURL) { - supportURL = whiteLabel.agencyURL; - docsURL = whiteLabel.agencyURL; - } - - return ( - - {!whiteLabel && ( - - - {__('Learn More', 'neve')} - - - )} - - {!whiteLabel && ( - - {__('Go to Neve Codex', 'neve')} - - )} - - {__('Go to docs', 'neve')} - - {!whiteLabel && ( - - )} - - - {!whiteLabel && ( - - - {__('Learn More', 'neve')} - - - )} - - - - - - {!whiteLabel && ( - - - {__('Learn More', 'neve')} - - - )} - {!whiteLabel && ( - - - - )} - - ); -}; - -export default Help; diff --git a/assets/apps/dashboard/src/Components/Content/ModuleGrid.js b/assets/apps/dashboard/src/Components/Content/ModuleGrid.js new file mode 100644 index 0000000000..850a9ab140 --- /dev/null +++ b/assets/apps/dashboard/src/Components/Content/ModuleGrid.js @@ -0,0 +1,171 @@ +/* global neveDash */ +import { useDispatch, useSelect } from '@wordpress/data'; +import { useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { LoaderCircle, LucideCheck, LucideSettings } from 'lucide-react'; + +import useLicenseData from '../../Hooks/useLicenseData'; +import Card from '../../Layout/Card'; +import { + NEVE_HAS_PRO, + NEVE_MODULE_ICON_MAP, + NEVE_STORE, +} from '../../utils/constants'; +import Link from '../Common/Link'; +import Pill from '../Common/Pill'; +import Toggle from '../Common/Toggle'; +import Tooltip from '../Common/Tooltip'; +import { changeOption } from '../../utils/rest'; +import Button from '../Common/Button'; + +const ModuleToggle = ({ slug, moduleData }) => { + const [loading, setLoading] = useState(false); + + const { licenseTier, isLicenseValid } = useLicenseData(); + const { changeModuleStatus, setToast } = useDispatch(NEVE_STORE); + const { moduleStatus } = useSelect((select) => { + const { getModuleStatus } = select(NEVE_STORE); + + return { + moduleStatus: getModuleStatus(slug) || false, + }; + }); + + if (!NEVE_HAS_PRO) { + return ( + + + {__('Pro', 'neve')} + + + ); + } + + const { nicename, availabilityLevel } = moduleData; + const { upgradeLinks } = neveDash; + + if (!isLicenseValid || licenseTier < availabilityLevel) { + return ( + + ); + } + + const handleToggle = (value) => { + setLoading(true); + changeModuleStatus(slug, value); + + changeOption(slug, value, true).then((r) => { + if (r.success) { + setLoading(false); + setToast( + (value + ? __('Module Activated', 'neve') + : __('Module Deactivated.', 'neve')) + ` (${nicename})` + ); + return; + } + changeModuleStatus(slug, !value); + setLoading(false); + setToast( + __('Could not activate module. Please try again.', 'neve') + ); + }); + }; + + return ( +
+ {loading && } + +
+ ); +}; + +const ModuleCard = ({ moduleData, slug }) => { + const { nicename, description, documentation, hide, byline } = moduleData; + const CardIcon = NEVE_MODULE_ICON_MAP[slug] || LucideSettings; + + if (hide) { + return null; + } + + return ( + } + title={nicename} + className="bg-white p-6 rounded-lg shadow-sm" + afterTitle={} + > +

+ {description}{' '} + {documentation && documentation.url && ( + + )} +

+ + {byline && ( +

+ + {byline} +

+ )} +
+ ); +}; + +const ModulesHeader = () => { + const { isLicenseValid } = useLicenseData(); + + return ( +
+

+ {__('Neve Pro Modules', 'neve')} +

+ {!isLicenseValid && ( + + )} +
+ ); +}; + +export default () => { + const unorderedModuels = Object.entries(neveDash.modules); + + const orderedModules = unorderedModuels.sort((a, b) => { + if (a[1].order && b[1].order) { + return a[1].order - b[1].order; + } + return 0; + }); + + return ( + <> + +
+ {orderedModules.map(([slug, moduleData]) => ( + + ))} +
+ + ); +}; diff --git a/assets/apps/dashboard/src/Components/Content/Plugins.js b/assets/apps/dashboard/src/Components/Content/Plugins.js deleted file mode 100644 index beab1d0386..0000000000 --- a/assets/apps/dashboard/src/Components/Content/Plugins.js +++ /dev/null @@ -1,27 +0,0 @@ -import PluginCard from '../PluginCard'; - -import { withSelect } from '@wordpress/data'; -import { Fragment } from '@wordpress/element'; - -const Header = ({ plugins }) => { - if (!plugins) { - return null; - } - - return ( - - {Object.keys(plugins).map((slug) => { - return ( - - ); - })} - - ); -}; - -export default withSelect((select) => { - const { getPlugins } = select('neve-dashboard'); - return { - plugins: getPlugins(), - }; -})(Header); diff --git a/assets/apps/dashboard/src/Components/Content/Pro.js b/assets/apps/dashboard/src/Components/Content/Pro.js deleted file mode 100644 index 27144e9911..0000000000 --- a/assets/apps/dashboard/src/Components/Content/Pro.js +++ /dev/null @@ -1,26 +0,0 @@ -/* global neveDash */ -import ModuleCard from '../ModuleCard'; - -const Pro = () => { - const { modules, hasOldPro, strings } = neveDash; - - if (hasOldPro) { - return ( -
-
-

{strings.updateOldPro}

-
-
- ); - } - - return ( -
- {Object.keys(modules).map((id, index) => { - return ; - })} -
- ); -}; - -export default Pro; diff --git a/assets/apps/dashboard/src/Components/Content/Settings.js b/assets/apps/dashboard/src/Components/Content/Settings.js new file mode 100644 index 0000000000..0d95b1e749 --- /dev/null +++ b/assets/apps/dashboard/src/Components/Content/Settings.js @@ -0,0 +1,140 @@ +/* global neveDash */ +import { useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import cn from 'classnames'; +import { + CircleFadingArrowUp, + LucideBriefcase, + LucideGauge, + LucidePuzzle, + LucideSettings, +} from 'lucide-react'; + +import { useSelect } from '@wordpress/data'; +import Card from '../../Layout/Card'; +import { + NEVE_HAS_PRO, + NEVE_SHOW_WHITELABEL, + NEVE_STORE, +} from '../../utils/constants'; +import Notice from '../Common/Notice'; +import TransitionWrapper from '../Common/TransitionWrapper'; +import GeneralTabContent from './Settings/GeneralTabContent'; +import ManageModulesTabContent from './Settings/ManageModulesTabContent'; +import PerformanceTabContent from './Settings/PerformanceTabContent'; +import WhiteLabelTabContent from './Settings/WhiteLabelTabContent'; + +const NAV_ITEMS = [ + { + id: 'general', + label: __('General', 'neve'), + icon: LucideSettings, + }, + { + id: 'performance', + label: __('Performance', 'neve'), + icon: LucideGauge, + }, + { + id: 'white-label', + label: __('White Label', 'neve'), + icon: LucideBriefcase, + }, + { + id: 'manage-modules', + label: __('Manage Modules', 'neve'), + icon: LucidePuzzle, + }, +]; + +const Menu = ({ tab, setTab }) => { + const { whiteLabelStatus } = useSelect((select) => { + const { getModuleStatus } = select(NEVE_STORE); + + return { + whiteLabelStatus: getModuleStatus('white_label') || false, + }; + }); + + const menuItems = NAV_ITEMS.filter(({ id }) => { + if (id === 'manage-modules') return NEVE_HAS_PRO; + + if (id === 'white-label') { + return !NEVE_HAS_PRO || (whiteLabelStatus && NEVE_SHOW_WHITELABEL); + } + + return true; + }); + + return ( + + + {menuItems.map(({ id, label, icon }) => { + const Icon = icon; + const classes = cn( + 'w-full flex items-center px-4 py-3 text-left', + { + 'text-gray-600 hover:bg-gray-50': tab !== id, + 'bg-blue-50 text-blue-600': tab === id, + } + ); + + return ( + + ); + })} + + + ); +}; + +const Settings = () => { + const { hasOldPro, strings } = neveDash; + + const [tab, setTab] = useState(NAV_ITEMS[0].id); + + if (hasOldPro) { + return ( + {strings.updateOldPro} + ); + } + + return ( +
+
+ +
+ + {tab === 'general' && ( + + + + )} + {tab === 'performance' && ( + + + + )} + {tab === 'white-label' && ( + + + + )} + {tab === 'manage-modules' && ( + + + + )} + +
+ ); +}; + +export default Settings; diff --git a/assets/apps/dashboard/src/Components/Content/Settings/AccessRestriction.js b/assets/apps/dashboard/src/Components/Content/Settings/AccessRestriction.js new file mode 100644 index 0000000000..cba6a517bc --- /dev/null +++ b/assets/apps/dashboard/src/Components/Content/Settings/AccessRestriction.js @@ -0,0 +1,195 @@ +/* global neveAccessRestriction */ + +import apiFetch from '@wordpress/api-fetch'; +import { useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { LucideLoaderCircle } from 'lucide-react'; +import Notice from '../../Common/Notice'; +import Select from '../../Common/Select'; +import Toggle from '../../Common/Toggle'; +import ControlWrap from '../../Controls/ControlWrap'; +import { NEVE_STORE } from '../../../utils/constants'; +import { useDispatch } from '@wordpress/data'; + +export const saveOption = (value) => { + return new Promise((resolve) => { + apiFetch({ + path: neveAccessRestriction.settingsRoute, + method: 'POST', + data: { settings: value }, + }) + .then((responseRaw) => { + const response = JSON.parse(responseRaw); + const status = response.status === 'success'; + resolve({ success: status }); + }) + .catch(() => { + resolve({ success: false }); + }); + }); +}; + +const AccessRestriction = ({ optionData }) => { + const [settings, setSettings] = useState(neveAccessRestriction.options); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(''); + + const { setToast } = useDispatch(NEVE_STORE); + + const updateContentTypeStatus = (slug, status) => { + const newSettings = { ...settings }; + + newSettings.content_types[slug].enabled = status; + + setSettings(newSettings); + saveAsync(newSettings); + }; + + const updateSetting = (slug, value) => { + const newSettings = { + ...settings, + [slug]: value, + }; + + setSettings(newSettings); + saveAsync(newSettings); + }; + + const saveAsync = (newSettings = null) => { + const settingsToSave = newSettings || settings; + setSaving(true); + setError(''); + saveOption(JSON.stringify(settingsToSave)) + .then((r) => { + if (!r.success) { + setError( + __('An error occurred. Please try again.', 'neve') + ); + setToast(false); + return; + } + setToast(true); + neveAccessRestriction.options = newSettings; + }) + .finally(() => { + setSaving(false); + }); + }; + + return ( + + + {__('Saving', 'neve')}... +
+ ) : null + } + > +
+ { + return callbackSettings[callbackKey].enabled; + }} + settings={neveAccessRestriction.options.content_types} + /> +
+ +
+ +
+ + {'' !== error && ( + + {error} + + )} + + ); +}; + +const defaultValueCallback = (settings, key) => settings[key]; + +const Fields = ({ + type, + updateSetting, + settings, + valueCallback = defaultValueCallback, +}) => { + const { fields } = neveAccessRestriction.fields[type]; + + return ( + <> + {Object.keys(fields).map((key, index) => { + const { type: fieldType, label, description } = fields[key]; + + if (fields[key].parent) { + const parent = fields[key].parent; + + if (settings[parent.fieldKey] !== parent.fieldValue) { + return null; + } + } + + const value = valueCallback(settings, key); + + return ( +
+ {'toggle' === fieldType && ( + <> + { + const status = newValue ? 'yes' : 'no'; + updateSetting(key, status); + }} + /> + {value && description && ( +

{description}

+ )} + + )} + {'select' === fieldType && ( + <> + +
+ + ); +}; + +const DummySettings = () => { + return ( + <> + {Object.entries(DUMMY_SETTINGS_ARGS).map(([id, setting]) => { + if (id === 'neve_access_restriction') { + return ; + } + + if (!setting.type) { + return ; + } + if (setting.type === 'text') { + return ; + } + + return
{setting.label}
; + })} + + ); +}; + +// Modules that should not be displayed in the general tab. +const EXCLUDED_MODULES = ['performance_features']; + +const ProModuleSettings = () => { + const { modules } = neveDash; + + const displayedModules = Object.entries(modules) + .filter( + ([key, moduleArgs]) => + !EXCLUDED_MODULES.includes(key) && + moduleArgs.options && + moduleArgs.options.length > 0 + ) + .map(([key, moduleArgs]) => { + return [key, moduleArgs.options]; + }); + + return ( + <> + {displayedModules.map(([key, optionGroups]) => { + if (!optionGroups || optionGroups.length < 1) { + return null; + } + + return optionGroups.map(({ options }, idx) => { + if (!options || Object.keys(options).length < 1) { + return null; + } + + return ( + + ); + }); + })} + + ); +}; + +export default () => { + const { isLicenseValid } = useLicenseData(); + + return ( + <> +

+ {__('General Settings', 'neve')} +

+ +
+ {(isLicenseValid && ) || } +
+ + ); +}; diff --git a/assets/apps/dashboard/src/Components/Content/Settings/ManageModulesTabContent.js b/assets/apps/dashboard/src/Components/Content/Settings/ManageModulesTabContent.js new file mode 100644 index 0000000000..8a077600b0 --- /dev/null +++ b/assets/apps/dashboard/src/Components/Content/Settings/ManageModulesTabContent.js @@ -0,0 +1,5 @@ +import ModuleGrid from '../ModuleGrid'; + +export default () => { + return ; +}; diff --git a/assets/apps/dashboard/src/Components/Content/Settings/OptionGroup.js b/assets/apps/dashboard/src/Components/Content/Settings/OptionGroup.js new file mode 100644 index 0000000000..a273a8eab7 --- /dev/null +++ b/assets/apps/dashboard/src/Components/Content/Settings/OptionGroup.js @@ -0,0 +1,123 @@ +import { useSelect } from '@wordpress/data'; +import MultiselectControl from '../../Controls/MultiselectControl'; +import SelectControl from '../../Controls/SelectControl'; +import TextControl from '../../Controls/TextControl'; +import ToggleControl from '../../Controls/ToggleControl'; +import { NEVE_STORE } from '../../../utils/constants'; +import { useEffect } from '@wordpress/element'; +import AccessRestriction from './AccessRestriction'; +import ControlWrap from '../../Controls/ControlWrap'; + +export default ({ options, overrides = {}, module }) => { + const { getProOption, getModuleStatus } = useSelect(NEVE_STORE); + + if (!getModuleStatus(module)) { + return null; + } + + return ( + <> + {Object.entries(options).map(([slug, optionData]) => { + if ( + optionData.depends_on && + getProOption(optionData.depends_on) !== true + ) { + return null; + } + + if (slug === 'enable_local_fonts') { + return null; + } + + if (overrides[slug]) { + optionData = { ...optionData, ...overrides[slug] }; + } + + const { type } = optionData; + + switch (type) { + case 'toggle': + return ( + + ); + + case 'text': + return ( + + ); + + case 'select': + return ( + + ); + case 'multi_select': + return ( + + ); + + case 'react': + return slug === 'neve_access_restriction' ? ( + + ) : ( + + + + ); + + default: + return null; + } + })} + + ); +}; + +const ReactPlaceholder = ({ slug }) => { + useEffect(() => { + window.dispatchEvent( + new window.CustomEvent('neve-dashboard-react-placeholder', { + detail: { + slug, + }, + }) + ); + }, []); + + return
; +}; diff --git a/assets/apps/dashboard/src/Components/Content/Settings/PerformanceTabContent.js b/assets/apps/dashboard/src/Components/Content/Settings/PerformanceTabContent.js new file mode 100644 index 0000000000..536e468d8a --- /dev/null +++ b/assets/apps/dashboard/src/Components/Content/Settings/PerformanceTabContent.js @@ -0,0 +1,100 @@ +/* global neveDash */ +import { __ } from '@wordpress/i18n'; +import { LucideCode, LucideSmile, LucideText, LucideZap } from 'lucide-react'; +import ToggleControl from '../../Controls/ToggleControl'; +import useLicenseData from '../../../Hooks/useLicenseData'; +import OptionGroup from './OptionGroup'; + +const LOCAL_HOSTING_OPTION = 'enable_local_fonts'; + +const DUMMY_SETTINGS_ARGS = { + enable_emoji_removal: { + icon: LucideSmile, + label: __('Emoji Removal', 'neve'), + description: __( + 'Remove emoji scripts to improve page load time.', + 'neve' + ), + }, + enable_embedded_removal: { + icon: LucideCode, + label: __('Embed Removal', 'neve'), + description: __('Remove embed scripts for better performance.', 'neve'), + }, + enable_lazy_content: { + icon: LucideZap, + label: __('Lazy Rendering', 'neve'), + description: __( + 'Enable lazy rendering for better initial page load.', + 'neve' + ), + }, +}; + +const DummySettings = () => { + return ( + <> + {Object.values(DUMMY_SETTINGS_ARGS).map((setting, index) => { + return ( + + ); + })} + + ); +}; + +const ProModuleSettings = () => { + const optionGroups = neveDash?.modules?.performance_features.options || []; + + if (optionGroups.length < 1) { + return null; + } + + return optionGroups.map(({ options }, idx) => { + if (!options || Object.keys(options).length < 1) { + return null; + } + + return ( + + ); + }); +}; + +export default () => { + const { isLicenseValid } = useLicenseData(); + + return ( + <> +

+ {__('Performance Settings', 'neve')} +

+ +
+ + + {(isLicenseValid && ) || } +
+ + ); +}; diff --git a/assets/apps/dashboard/src/Components/Content/Settings/WhiteLabelTabContent.js b/assets/apps/dashboard/src/Components/Content/Settings/WhiteLabelTabContent.js new file mode 100644 index 0000000000..63d0fcdc29 --- /dev/null +++ b/assets/apps/dashboard/src/Components/Content/Settings/WhiteLabelTabContent.js @@ -0,0 +1,313 @@ +/* global neveDash */ +import { __ } from '@wordpress/i18n'; +import useLicenseData from '../../../Hooks/useLicenseData'; +import { + CircleFadingArrowUp, + LucideBuilding, + LucideMessageCircleQuestion, + LucidePlug, + LucideSettings, + LucideToggleRight, + LucideWallpaper, +} from 'lucide-react'; +import TextInput from '../../Common/TextInput'; +import { useReducer, useState } from '@wordpress/element'; +import Button from '../../Common/Button'; +import ControlWrap from '../../Controls/ControlWrap'; +import Toggle from '../../Common/Toggle'; +import Notice from '../../Common/Notice'; +import { changeOption } from '../../../utils/rest'; +import { useDispatch } from '@wordpress/data'; +import { NEVE_STORE } from '../../../utils/constants'; + +const DUMMY_SECTIONS = { + agency: { + title: __('Agency Branding', 'neve'), + icon: LucideBuilding, + fields: { + author_name: { + type: 'text', + label: __('Agency Author', 'neve'), + }, + author_url: { + type: 'text', + label: __('Agency Author URL', 'neve'), + }, + starter_sites: { + type: 'toggle', + label: __('Hide Sites Library', 'neve'), + }, + my_library: { + type: 'toggle', + label: __('Hide My Library', 'neve'), + }, + }, + }, + theme: { + title: __('Theme Branding', 'neve'), + icon: LucideWallpaper, + fields: { + theme_name: { + type: 'text', + label: __('Theme Name', 'neve'), + }, + theme_description: { + type: 'textarea', + label: __('Theme Description', 'neve'), + }, + screenshot_url: { + type: 'text', + label: __('Screenshot URL', 'neve'), + }, + }, + }, + plugin: { + title: __('Plugin Branding', 'neve'), + icon: LucidePlug, + fields: { + plugin_name: { + type: 'text', + label: __('Plugin Name', 'neve'), + }, + plugin_description: { + type: 'textarea', + label: __('Plugin Description', 'neve'), + }, + }, + }, + sidebar: { + title: __('Enable White Label', 'neve'), + icon: LucideToggleRight, + fields: { + white_label: { + type: 'toggle', + label: __('Hide Options from Dashboard', 'neve'), + }, + license: { + type: 'toggle', + label: __('Enable License Hiding', 'neve'), + }, + }, + }, +}; + +const PlaceholderComponent = () => { + return ( +
+ {Object.entries(DUMMY_SECTIONS).map(([id, section]) => { + const ICON = section.icon; + + return ( + +
+ {Object.entries(section.fields).map( + ([sid, setting]) => { + return ( +
+ {setting.type === 'toggle' ? ( + + ) : ( + + )} +
+ ); + } + )} +
+
+ ); + })} + +
+ +
+
+ ); +}; + +const WhiteLabelSettings = () => { + const { strings } = neveDash; + + const { fields, optionKey, options } = neveDash.whiteLabelData; + + const allFields = Object.values(fields) + .map((all) => all.fields) + .reduce((acc, val) => ({ ...acc, ...val }), {}); + + // remap values from options if the field type is toggle parse for '0' and '1'. + const formDefaults = Object.keys(allFields).reduce((acc, key) => { + if (allFields[key].type === 'toggle') { + acc[key] = options[key] === '1'; + } else { + acc[key] = options[key]; + } + return acc; + }, {}); + + const [loading, setLoading] = useState(false); + const [formData, setFormData] = useReducer((state, action) => { + return { + ...state, + [action.name]: action.value, + }; + }, formDefaults); + + const { setToast } = useDispatch(NEVE_STORE); + + if (!neveDash.whiteLabelData) { + return ( + {strings.updateOldPro} + ); + } + + const handleSubmit = (e) => { + e.preventDefault(); + + setLoading(true); + + changeOption(optionKey, JSON.stringify(formData), false, false) + .then((r) => { + if (r.success) { + setToast(__('White Label settings saved.', 'neve')); + neveDash.whiteLabelData.options = formData; + return; + } + + setToast(r.message ? r.message : false); + }) + .catch((err) => { + setToast(err.message ? err.message : false); + }) + .finally(() => { + setLoading(false); + }); + }; + + return ( +
+ {Object.entries(fields).map(([id, section]) => { + // We replace the icon as there's no way to retro-fit it from PRO. + section.icon = DUMMY_SECTIONS[id]?.icon || LucideSettings; + + return ( + +
+ {Object.entries(section.fields).map( + ([sid, setting]) => { + return ( +
+ {[ + 'text', + 'url', + 'textarea', + ].includes(setting.type) && ( + + setFormData({ + name: sid, + value: e.target + .value, + }) + } + value={formData[sid]} + /> + )} + {setting.type === 'toggle' && ( + + setFormData({ + name: sid, + value, + }) + } + /> + )} +
+ ); + } + )} +
+
+ ); + })} +
+ +
+
+ ); +}; + +export default () => { + const { isLicenseValid, licenseTier } = useLicenseData(); + + const showPlaceholder = !isLicenseValid || licenseTier < 3; + + return ( + <> +
+

+ {__('White Label Settings', 'neve')} +

+ + {showPlaceholder && ( + + )} +
+ + {showPlaceholder && } + {!showPlaceholder && !neveDash.whiteLabelData && ( + + {__( + 'Please reload this page in order to view the White Label Settings', + 'neve' + )} + + )} + {!showPlaceholder && !!neveDash.whiteLabelData && ( + + )} + + ); +}; diff --git a/assets/apps/dashboard/src/Components/Content/Sidebar/LicenseCard.js b/assets/apps/dashboard/src/Components/Content/Sidebar/LicenseCard.js new file mode 100644 index 0000000000..4f04f9abb3 --- /dev/null +++ b/assets/apps/dashboard/src/Components/Content/Sidebar/LicenseCard.js @@ -0,0 +1,165 @@ +/* global neveDash */ +import { fetchOptions, send } from '../../../utils/rest'; +import Toast from '../../Common/Toast'; + +import { useDispatch } from '@wordpress/data'; +import { useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { LucideCircleCheck, LucideCircleX } from 'lucide-react'; +import useLicenseData from '../../../Hooks/useLicenseData'; +import Card from '../../../Layout/Card'; +import { NEVE_STORE } from '../../../utils/constants'; +import Button from '../../Common/Button'; +import Pill from '../../Common/Pill'; + +const LicenseCard = () => { + const { proApi } = neveDash; + + const { changeLicense, setSettings } = useDispatch(NEVE_STORE); + + const { license, isLicenseValid } = useLicenseData(); + + const [key, setKey] = useState(isLicenseValid ? license.key || '' : ''); + const [status, setStatus] = useState(false); + + const [toast, setToast] = useState(''); + const [toastType, setToastType] = useState('success'); + + const { valid, expiration } = license; + const { whiteLabel, strings } = neveDash; + const { licenseCardHeading, licenseCardDescription } = strings; + + const toggleLicense = () => { + const toDo = 'valid' === valid ? 'deactivate' : 'activate'; + setStatus('activate' === toDo ? 'activating' : 'deactivating'); + send(proApi + '/toggle_license', { key, action: toDo }).then( + (response) => { + setToastType(response.success ? 'success' : 'error'); + setKey('activate' === toDo ? key : ''); + setToast(response.message); + setStatus(false); + if (response.license) { + changeLicense(response.license); + fetchOptions().then((r) => { + setSettings(r); + }); + } + } + ); + }; + + if (whiteLabel && whiteLabel.hideLicense) { + return null; + } + + const getStatusLabel = () => { + const statusLabelMap = { + activating: __('Activating', 'neve'), + deactivating: __('Deactivating', 'neve'), + activate: __('Activate', 'neve'), + deactivate: __('Deactivate', 'neve'), + }; + + if (!status) { + return 'valid' === valid + ? __('Deactivate', 'neve') + : __('Activate', 'neve'); + } + + return statusLabelMap[status]; + }; + + return ( + +
+ {!whiteLabel && licenseCardDescription && ( +

+ )} +

{ + e.preventDefault(); + toggleLicense(); + }} + > + { + const keyToSet = e.target.value.replace(/\s+/g, ''); + setKey(keyToSet); + }} + value={ + 'valid' === valid + ? '******************************' + + key.slice(-5) + : key + } + placeholder={__('Enter License Key', 'neve')} + /> + +
+ {toast && ( + + )} + {'expired' === valid || + ('valid' === valid && ( +
+ + {valid === 'valid' ? ( + <> + + {__('Valid', 'neve')} + + ) : ( + <> + + {__('Expired', 'neve')} + + )} + + {expiration && ( + <> + + + {'valid' === valid + ? __('Expires', 'neve') + : __('Expired', 'neve')} + + + + {expiration} + + + + )} +
+ ))} +
+
+ ); +}; + +export default LicenseCard; diff --git a/assets/apps/dashboard/src/Components/Content/Sidebar/PluginsCard.js b/assets/apps/dashboard/src/Components/Content/Sidebar/PluginsCard.js new file mode 100644 index 0000000000..83e8625b16 --- /dev/null +++ b/assets/apps/dashboard/src/Components/Content/Sidebar/PluginsCard.js @@ -0,0 +1,139 @@ +/* global neveDash */ +import usePluginActions from '../../../Hooks/usePluginActions'; +import Card from '../../../Layout/Card'; +import { + NEVE_HIDE_PLUGINS, + NEVE_PLUGIN_ICON_MAP, +} from '../../../utils/constants'; + +import { useSelect } from '@wordpress/data'; +import { useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import cn from 'classnames'; +import { LoaderCircle, LucidePuzzle } from 'lucide-react'; +import Pill from '../../Common/Pill'; +import Toast from '../../Common/Toast'; +import TransitionInOut from '../../Common/TransitionInOut'; + +const PluginCard = ({ slug, data }) => { + const ICON = NEVE_PLUGIN_ICON_MAP[slug] || LucidePuzzle; + + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); + + const { title, description } = data; + + const { doPluginAction, loading, buttonText } = usePluginActions( + slug, + true + ); + + const isPluginActive = useSelect((select) => { + const { getPlugins } = select('neve-dashboard'); + + const plugins = getPlugins(); + + return plugins[slug].cta === 'deactivate'; + }); + + if (isPluginActive && !success) { + return null; + } + + const handleClick = async () => { + setError(null); + + const result = await doPluginAction(); + + if (result.success) { + setSuccess(true); + + return; + } + + if (!result.success) { + setError(result.error); + } + }; + + return ( +
+
+
+ +

+ {title} +

+ + {!success && ( + + )} + {success && ( +
+ + + {__('Active', 'neve')} + + +
+ )} +
+

+ {description} +

+ + {error && ( +
+ +
+ )} +
+
+ ); +}; + +const PluginsCard = ({ grid = false }) => { + const { plugins } = neveDash; + + if (NEVE_HIDE_PLUGINS || plugins.length < 1) { + return null; + } + + const contentClasses = cn({ + 'space-y-3': !grid, + 'grid gap-4 grid-cols-2': grid, + }); + + return ( + +
+ {Object.entries(plugins).map(([slug, args]) => ( + + ))} +
+
+ ); +}; + +export default PluginsCard; diff --git a/assets/apps/dashboard/src/Components/Content/Sidebar/Sidebar.js b/assets/apps/dashboard/src/Components/Content/Sidebar/Sidebar.js new file mode 100644 index 0000000000..e6d8eb3844 --- /dev/null +++ b/assets/apps/dashboard/src/Components/Content/Sidebar/Sidebar.js @@ -0,0 +1,150 @@ +import { changeOption } from '../../../utils/rest'; +import SupportCard from './SupportCard'; +import LicenseCard from './LicenseCard'; +import { __ } from '@wordpress/i18n'; +import { useState } from '@wordpress/element'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { + NEVE_HAS_PRO, + NEVE_IS_WHITELABEL, + NEVE_STORE, +} from '../../../utils/constants'; +import Card from '../../../Layout/Card'; +import Link from '../../Common/Link'; +import Toggle from '../../Common/Toggle'; +import PluginsCard from './PluginsCard'; +import { LucideLoaderCircle } from 'lucide-react'; + +const ReviewCard = () => ( + +

+ {__( + 'Are you are enjoying Neve? We would love to hear your feedback.', + 'neve' + )} +

+ + +
+); + +const ContributingCard = () => { + const loggerEnabled = useSelect((select) => { + const { getOption } = select(NEVE_STORE); + + return getOption('neve_logger_flag'); + }); + + const [tracking, setTracking] = useState('yes' === loggerEnabled); + const [loading, setLoading] = useState(false); + + const { setToast, setLogger } = useDispatch(NEVE_STORE); + + const handleTrackingChange = (value) => { + setLoading(true); + setTracking(value); + changeOption('neve_logger_flag', value ? 'yes' : 'no', false, false) + .then((r) => { + if (!r.success) { + setToast( + __('Could not update option. Please try again.', 'neve') + ); + setTracking(!value); + return; + } + setLogger(value ? 'yes' : 'no'); + setToast(__('Option Updated', 'neve')); + }) + .catch(() => { + setToast( + __('Could not update option. Please try again.', 'neve') + ); + setTracking(!value); + }) + .finally(() => { + setLoading(false); + }); + }; + + return ( + +

+ {__( + 'Become a contributor by opting in to our anonymous data tracking. We guarantee no sensitive data is collected.', + 'neve' + )} +

+ + + + + {__('Allow Anonymous Tracking', 'neve')} + {loading && ( + + )} +
+ } + /> + + ); +}; + +const CommunityCard = () => { + return ( + +

+ {__( + 'Share opinions, ask questions and help each other on our Neve community!', + 'neve' + )} +

+ + +
+ ); +}; + +const Sidebar = () => { + return ( +
+ {NEVE_HAS_PRO && } + + {NEVE_HAS_PRO && } + + {!NEVE_IS_WHITELABEL && } + + {!NEVE_HAS_PRO && } + + {!NEVE_IS_WHITELABEL && ( + <> + + + + )} +
+ ); +}; + +export default Sidebar; diff --git a/assets/apps/dashboard/src/Components/Content/Sidebar/SupportCard.js b/assets/apps/dashboard/src/Components/Content/Sidebar/SupportCard.js new file mode 100644 index 0000000000..b3eb4320d1 --- /dev/null +++ b/assets/apps/dashboard/src/Components/Content/Sidebar/SupportCard.js @@ -0,0 +1,43 @@ +/* global neveDash */ +import { useSelect } from '@wordpress/data'; +import { NEVE_IS_WHITELABEL, NEVE_STORE } from '../../../utils/constants'; +import Link from '../../Common/Link'; + +const SupportCard = () => { + const { license } = useSelect((select) => { + const { getLicense } = select(NEVE_STORE); + return { + license: getLicense(), + }; + }); + + if (!license || !license.valid || 'valid' !== license.valid) { + return null; + } + const { supportData } = license; + + if (!supportData || !supportData.text || !supportData.url) { + return null; + } + + let supportUrl = supportData.url; + + if (NEVE_IS_WHITELABEL) { + if (!neveDash.whiteLabel?.agencyURL) { + return null; + } + + supportUrl = neveDash.whiteLabel.agencyURL; + } + + return ( + + ); +}; + +export default SupportCard; diff --git a/assets/apps/dashboard/src/Components/Content/Start.js b/assets/apps/dashboard/src/Components/Content/Start.js deleted file mode 100644 index bd24e6769a..0000000000 --- a/assets/apps/dashboard/src/Components/Content/Start.js +++ /dev/null @@ -1,178 +0,0 @@ -/* global neveDash */ -import Card from '../Card'; -import { tabs } from '../../utils/common'; - -import { __ } from '@wordpress/i18n'; -import { Fragment } from '@wordpress/element'; -import { Button, ExternalLink } from '@wordpress/components'; -import { withSelect } from '@wordpress/data'; - -const Start = (props) => { - const { setTab, tier } = props; - const { pro, whiteLabel, customizerShortcuts, tpcOnboardingURL } = neveDash; - const starterSitesHidden = whiteLabel && whiteLabel.hideStarterSites; - - const renderCustomizerLinks = () => { - const split = Math.ceil(customizerShortcuts.length / 2); - const parts = [ - customizerShortcuts.slice(0, split), - customizerShortcuts.slice(split), - ]; - return ( -
- {parts.map((column, index) => { - return ( -
- {column.map((item, indexColumn) => { - return ( - - - {indexColumn !== column.length - 1 && ( -
- )} -
- ); - })} -
- ); - })} -
- ); - }; - - return ( - <> - {!starterSitesHidden && ( - - {!neveDash.isValidLicense && ( -

{neveDash.strings.starterSitesCardUpsellMessage}

- )} - -
- {tabs['starter-sites'] ? ( - - ) : ( - - )} - {!neveDash.isValidLicense && ( - - )} -
-
- )} - - {renderCustomizerLinks()} - - - {!whiteLabel && ( - <> - - - - - {tier !== 3 && ( - - {__('Discover Templates Cloud', 'neve')} - - )} - {tier === 3 && ( - - {__('Learn how to use Templates Cloud', 'neve')} - - )} - - - )} - {!pro && ( - <> - - - {__('Learn more', 'neve')} - - - - - {__('Learn more', 'neve')} - - - - )} - - ); -}; - -export default withSelect((select) => { - const { getLicenseTier } = select('neve-dashboard'); - return { - tier: getLicenseTier(), - }; -})(Start); diff --git a/assets/apps/dashboard/src/Components/Content/StarterSitesUnavailable.js b/assets/apps/dashboard/src/Components/Content/StarterSitesUnavailable.js index d17430307c..12cc52d042 100644 --- a/assets/apps/dashboard/src/Components/Content/StarterSitesUnavailable.js +++ b/assets/apps/dashboard/src/Components/Content/StarterSitesUnavailable.js @@ -1,52 +1,79 @@ /* global neveDash */ -import InstallActivate from '../Plugin/InstallActivate'; import { withSelect } from '@wordpress/data'; +import { useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; -const StarterSitesUnavailable = ({ templatesPluginData }) => { - const { tpcPath, tpcAdminURL, canInstallPlugins, assets } = neveDash; - const activateRedirect = - tpcAdminURL + (canInstallPlugins ? '&onboarding=yes' : ''); - const currentState = templatesPluginData?.cta || 'install'; +import InstallActivate from '../Plugin/InstallActivate'; +import Card from '../../Layout/Card'; +import Container from '../../Layout/Container'; +import TransitionWrapper from '../Common/TransitionWrapper'; + +const BackgroundPlaceholder = () => { + const [show, setShow] = useState(false); + + const handleImageLoaded = () => { + setShow(true); + }; return ( -
-
-
- { - window.location.href = activateRedirect; - }} - successUpdate={() => { - window.location.href = activateRedirect; - }} - description={ - <> -

- {'deactivate' === currentState - ? neveDash.strings - .starterSitesUnavailableUpdate - : neveDash.strings - .starterSitesUnavailableActive} -

-
- - } +
+ + {__('Starter -
+
); }; +const StarterSitesUnavailable = ({ templatesPluginData }) => { + const { tpcPath, tpcAdminURL, canInstallPlugins } = neveDash; + const currentState = templatesPluginData?.cta || 'install'; + const activateRedirect = `${tpcAdminURL}${ + canInstallPlugins ? '&onboarding=yes' : '' + }`; + const description = { + __html: + 'deactivate' === currentState + ? neveDash.strings.starterSitesUnavailableUpdate + : neveDash.strings.starterSitesUnavailableActive, + }; + + const redirectToStarterSites = () => { + window.location.href = activateRedirect; + }; + + return ( + + + + +

+ + + + + + ); +}; + export default withSelect((select) => { const { getPlugins } = select('neve-dashboard'); return { diff --git a/assets/apps/dashboard/src/Components/Content/Welcome.js b/assets/apps/dashboard/src/Components/Content/Welcome.js new file mode 100644 index 0000000000..ba714004a3 --- /dev/null +++ b/assets/apps/dashboard/src/Components/Content/Welcome.js @@ -0,0 +1,38 @@ +/* global neveDash */ +import { __ } from '@wordpress/i18n'; +import { LucidePanelsTopLeft } from 'lucide-react'; + +import Card from '../../Layout/Card'; +import { NEVE_HAS_PRO } from '../../utils/constants'; +import Link from '../Common/Link'; +import TransitionWrapper from '../Common/TransitionWrapper'; +import ModuleGrid from './ModuleGrid'; +import PluginsCard from './Sidebar/PluginsCard'; + +export default () => ( + + + {!NEVE_HAS_PRO && } + {NEVE_HAS_PRO && } + +); + +const CustomizerShortcutsCard = () => ( + } + > +

+ {neveDash.customizerShortcuts.map(({ text, link, description }) => ( +
+ + {description && ( + + {description} + + )} +
+ ))} +
+
+); diff --git a/assets/apps/dashboard/src/Components/Controls/ControlWrap.js b/assets/apps/dashboard/src/Components/Controls/ControlWrap.js new file mode 100644 index 0000000000..6a74602764 --- /dev/null +++ b/assets/apps/dashboard/src/Components/Controls/ControlWrap.js @@ -0,0 +1,52 @@ +import { __ } from '@wordpress/i18n'; +import cn from 'classnames'; +import { LucideSettings } from 'lucide-react'; +import Pill from '../Common/Pill'; + +export default ({ + icon, + label, + description, + locked, + afterTitle = null, + className, + children, +}) => { + const ICON = icon || LucideSettings; + + return ( +
+
+
+
+ {icon !== null && ( + + )} +

{label}

+ {locked && ( + + {__('Pro', 'neve')} + + )} +
+ + {description && ( +

{description}

+ )} +
+ + {afterTitle && ( + + {afterTitle} + + )} +
+ {children &&
{children}
} +
+ ); +}; diff --git a/assets/apps/dashboard/src/Components/Controls/MultiselectControl.js b/assets/apps/dashboard/src/Components/Controls/MultiselectControl.js new file mode 100644 index 0000000000..06feb141b3 --- /dev/null +++ b/assets/apps/dashboard/src/Components/Controls/MultiselectControl.js @@ -0,0 +1,80 @@ +import { useDispatch, useSelect } from '@wordpress/data'; +import { useState } from '@wordpress/element'; +import { LoaderCircle } from 'lucide-react'; +import { NEVE_STORE } from '../../utils/constants'; +import { changeOption } from '../../utils/rest'; +import MultiSelect from '../Common/Multiselect'; +import ControlWrap from './ControlWrap'; + +const MultiSelectControl = ({ + icon, + label, + description, + option, + isPro = true, + disabled, + locked, + choices = {}, +}) => { + const [loading, setLoading] = useState(false); + + const { changeModuleOption, setToast } = useDispatch(NEVE_STORE); + const value = useSelect((select) => { + const { getOption, getProOption } = select(NEVE_STORE); + + return isPro ? getProOption(option) : getOption(option); + }); + + const handleChange = (nextValues) => { + setLoading(true); + + window.tiTrk?.with('neve').set(option, { + feature: 'module-settings', + featureComponent: option, + featureValue: nextValues, + }); + + changeModuleOption(option, nextValues); + + changeOption(option, nextValues) + .then((r) => { + if (r.success) { + setToast(true); + return; + } + changeModuleOption(option, value); + setToast(false); + }) + .finally(() => { + setLoading(false); + }); + }; + + return ( + + {loading && ( + + )} + + + + } + /> + ); +}; + +export default MultiSelectControl; diff --git a/assets/apps/dashboard/src/Components/Controls/SelectControl.js b/assets/apps/dashboard/src/Components/Controls/SelectControl.js new file mode 100644 index 0000000000..15a3ff7ed3 --- /dev/null +++ b/assets/apps/dashboard/src/Components/Controls/SelectControl.js @@ -0,0 +1,66 @@ +import { useDispatch, useSelect } from '@wordpress/data'; +import { useState } from '@wordpress/element'; +import { NEVE_STORE } from '../../utils/constants'; +import { changeOption as changeSetting } from '../../utils/rest'; + +import Select from '../Common/Select'; +import ControlWrap from './ControlWrap'; + +export default ({ + icon, + label, + description, + option, + isPro = true, + disabled, + locked, + choices, +}) => { + const [loading, setLoading] = useState(false); + + const { changeModuleOption, setToast } = useDispatch(NEVE_STORE); + const value = useSelect((select) => { + const { getOption, getProOption } = select(NEVE_STORE); + + return isPro ? getProOption(option) : getOption(option); + }); + + const handleChange = (nextValue) => { + setLoading(true); + window.tiTrk?.with('neve').set(option, { + feature: 'module-settings', + featureComponent: option, + featureValue: nextValue, + }); + changeSetting(option, nextValue) + .then((r) => { + if (r.success) { + changeModuleOption(option, nextValue); + setToast(true); + return; + } + setToast(false); + }) + .finally(() => { + setLoading(false); + }); + }; + + return ( + + } + > + ); +}; diff --git a/assets/apps/dashboard/src/Components/Controls/TextControl.js b/assets/apps/dashboard/src/Components/Controls/TextControl.js new file mode 100644 index 0000000000..d1652585a1 --- /dev/null +++ b/assets/apps/dashboard/src/Components/Controls/TextControl.js @@ -0,0 +1,90 @@ +import { useDispatch, useSelect } from '@wordpress/data'; +import { useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { NEVE_STORE } from '../../utils/constants'; +import { changeOption } from '../../utils/rest'; +import Button from '../Common/Button'; +import TextInput from '../Common/TextInput'; +import ControlWrap from './ControlWrap'; + +export default ({ + icon, + label, + description, + option, + isPro = true, + disabled, + locked, + placeholder = '', +}) => { + const { changeModuleOption, setToast } = useDispatch(NEVE_STORE); + const value = useSelect((select) => { + const { getOption, getProOption } = select(NEVE_STORE); + + return isPro ? getProOption(option) : getOption(option); + }); + + const [loading, setLoading] = useState(false); + + return ( + { + e.preventDefault(); + setLoading(true); + changeOption(option, value) + .then((r) => { + if (r.success) { + setToast(true); + return; + } + setToast(r.message ? r.message : false); + }) + .finally(() => { + setLoading(false); + }); + }} + > +
+ { + changeModuleOption(option, e.target.value); + if (option === 'typekit_id') { + window.tiTrk?.with('neve').add({ + feature: 'typekit-fonts', + featureComponent: 'typekit-id', + featureValue: 'enabled', + }); + } + }} + /> + + +
+ + } + /> + ); +}; diff --git a/assets/apps/dashboard/src/Components/Controls/ToggleControl.js b/assets/apps/dashboard/src/Components/Controls/ToggleControl.js new file mode 100644 index 0000000000..7af6fc7ff9 --- /dev/null +++ b/assets/apps/dashboard/src/Components/Controls/ToggleControl.js @@ -0,0 +1,78 @@ +import { useDispatch, useSelect } from '@wordpress/data'; +import { useState } from '@wordpress/element'; +import { LucideLoaderCircle } from 'lucide-react'; + +import Toggle from '../Common/Toggle'; +import ControlWrap from './ControlWrap'; + +import { NEVE_STORE } from '../../utils/constants'; +import { changeOption } from '../../utils/rest'; + +export default ({ + icon, + label, + description, + option, + isPro = true, + disabled, + locked, +}) => { + const [loading, setLoading] = useState(false); + + const value = useSelect((select) => { + const { getOption, getProOption } = select(NEVE_STORE); + + return isPro ? getProOption(option) : getOption(option); + }); + + const { setToast, changeModuleOption } = useDispatch(NEVE_STORE); + + const onToggleChange = (nextValue) => { + setLoading(true); + window.tiTrk?.with('neve').set(option, { + feature: 'module-settings', + featureComponent: option, + featureValue: nextValue, + }); + changeModuleOption(option, nextValue); + changeOption(option, nextValue) + .then((r) => { + if (!r.success) { + changeModuleOption(option, !nextValue); + setToast(false); + + return; + } + + setToast(true); + }) + .finally(() => { + setLoading(false); + }); + }; + + return ( + + {loading && ( + + )} + + + } + > + ); +}; diff --git a/assets/apps/dashboard/src/Components/FeatureRow.js b/assets/apps/dashboard/src/Components/FeatureRow.js deleted file mode 100644 index eb27cb1c8d..0000000000 --- a/assets/apps/dashboard/src/Components/FeatureRow.js +++ /dev/null @@ -1,122 +0,0 @@ -/* global neveDash */ -import { Dashicon, ExternalLink } from '@wordpress/components'; -import { useState, createInterpolateElement } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; -import classnames from 'classnames'; - -const FeatureRow = ({ item }) => { - const { title, description, inLite, docsLink, presentational } = item; - const [tooltip, toggleTooltip] = useState(false); - - const showTooltip = () => toggleTooltip(true); - const hideTooltip = () => toggleTooltip(false); - - const renderFeatureTitle = () => { - if (!presentational) { - return title; - } - - return ( - - {title} - - ); - }; - - const renderInfoIcon = () => { - if (!presentational) { - return ; - } - }; - - const renderNeveLiteIndicator = () => { - if (!presentational) { - return ( - - - - ); - } - }; - - const renderNeveProIndicator = () => { - if (!presentational) { - return ( - - - - ); - } - }; - - return ( - - -
-

{renderFeatureTitle()}

- { - e.preventDefault(); - showTooltip(); - }} - onMouseLeave={(e) => { - e.preventDefault(); - hideTooltip(); - }} - onFocus={(e) => { - e.preventDefault(); - showTooltip(); - }} - onBlur={(e) => { - e.preventDefault(); - hideTooltip(); - }} - > - {renderInfoIcon()} - - {tooltip && ( -
-
-

- {description + ' '} - {docsLink && - typeof createInterpolateElement !== - 'undefined' && - createInterpolateElement( - __( - 'More details here.', - 'neve' - ), - { - external_link: ( - - #dumptext - - ), - } - )} -

-
-
- )} -
-
- - {renderNeveLiteIndicator()} - {renderNeveProIndicator()} - - ); -}; - -export default FeatureRow; diff --git a/assets/apps/dashboard/src/Components/Header.js b/assets/apps/dashboard/src/Components/Header.js index 4161b102cf..fea8408f2b 100644 --- a/assets/apps/dashboard/src/Components/Header.js +++ b/assets/apps/dashboard/src/Components/Header.js @@ -1,17 +1,175 @@ /* global neveDash */ -import { addUrlHash, getTabHash, tabs } from '../utils/common'; -import classnames from 'classnames'; +import cn from 'classnames'; +import { getTabHash, tabs } from '../utils/common'; +import { Fragment, useEffect } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -import { useEffect } from '@wordpress/element'; -const Header = (props) => { +import { useDispatch, useSelect } from '@wordpress/data'; +import { LucideBookOpen, LucideFileText } from 'lucide-react'; +import useLicenseData from '../Hooks/useLicenseData'; +import Container from '../Layout/Container'; +import { NEVE_IS_WHITELABEL, NEVE_STORE } from '../utils/constants'; +import Button from './Common/Button'; +import Pill from './Common/Pill'; + +const HeaderTopBar = ({ currentTab, setTab }) => { + const NAV_BUTTONS = { + help: { + label: __('Documentation', 'neve'), + icon: , + url: neveDash.docsURL, + }, + changelog: { + label: __('Changelog', 'neve'), + icon: , + }, + }; + + const { isLicenseValid } = useLicenseData(); + + if (NEVE_IS_WHITELABEL) { + const hasWhiteLabelAgencyURL = !!neveDash.whiteLabel?.agencyURL; + + NAV_BUTTONS.changelog.hide = true; + + if (hasWhiteLabelAgencyURL) { + NAV_BUTTONS.help.url = neveDash.whiteLabel?.agencyURL; + } else { + NAV_BUTTONS.help.hide = true; + } + } + + return ( +
+ +
+
+ {!NEVE_IS_WHITELABEL && ( + {__('Neve + )} + + {neveDash.strings.header} + + + {isLicenseValid + ? __('Pro', 'neve') + : __('Free', 'neve')} + + + {neveDash.version} + +
+
+ {Object.entries(NAV_BUTTONS).map( + ([slug, { label, icon, url, hide }], index) => { + if (hide) { + return null; + } + + const props = {}; + + if (!url) { + props.onClick = (e) => { + e.preventDefault(); + setTab(slug); + }; + if (currentTab === slug) { + props.className = '!text-blue-600'; + } + } else { + props.href = url; + props.target = '_blank'; + } + + return ( + + {index > 0 && ( +
+ )} + + + ); + } + )} +
+
+ +
+ ); +}; + +const Navigation = ({ setTab, currentTab }) => { + return ( +
+ + + +
+ ); +}; + +const Header = () => { + const { currentTab } = useSelect((select) => { + const { getTab } = select(NEVE_STORE); + return { + currentTab: getTab(), + }; + }); + + const { setTab } = useDispatch(NEVE_STORE); + const setTabToCurrentHash = () => { const hash = getTabHash(); if (null === hash) { return; } - props.setTab(hash); + setTab(hash); }; useEffect(() => { @@ -23,54 +181,19 @@ const Header = (props) => { }; }, []); - const renderHead = () => { - return ( -
-

{neveDash.strings.header}

- {neveDash.version} - {!neveDash.whiteLabel && ( - {__('Neve - )} -
- ); - }; - - const renderNavigation = () => { - const { currentTab, setTab } = props; - return ( - - ); + const handleTabSwitch = (slug) => { + setTab(slug); + window.location.hash = slug; }; return ( -
-
- {renderHead()} - {renderNavigation(props)} +
+
+ +
); diff --git a/assets/apps/dashboard/src/Components/LicenseCard.js b/assets/apps/dashboard/src/Components/LicenseCard.js deleted file mode 100644 index a2de9e6e81..0000000000 --- a/assets/apps/dashboard/src/Components/LicenseCard.js +++ /dev/null @@ -1,172 +0,0 @@ -/* global neveDash */ -import { send, fetchOptions } from '../utils/rest'; -import Toast from './Toast'; -import classnames from 'classnames'; - -import { __ } from '@wordpress/i18n'; -import { Button, Dashicon } from '@wordpress/components'; -import { Fragment, useState } from '@wordpress/element'; -import { withDispatch, withSelect } from '@wordpress/data'; -import { compose } from '@wordpress/compose'; - -const LicenseCard = ({ isVisible, setSettings, changeLicense, license }) => { - const { proApi } = neveDash; - const [key, setKey] = useState( - license && 'valid' === license.valid ? license.key || '' : '' - ); - const [status, setStatus] = useState(false); - - // const [ expiration, setExpiration ] = useState(license.expiration || ''); - const [toast, setToast] = useState(''); - const [toastType, setToastType] = useState('success'); - - const { valid, expiration } = license; - const { whiteLabel, strings } = neveDash; - const { licenseCardHeading, licenseCardDescription } = strings; - - if (!isVisible) { - return null; - } - const toggleLicense = () => { - const toDo = 'valid' === valid ? 'deactivate' : 'activate'; - setStatus('activate' === toDo ? 'activating' : 'deactivating'); - send(proApi + '/toggle_license', { key, action: toDo }).then( - (response) => { - setToastType(response.success ? 'success' : 'error'); - setKey('activate' === toDo ? key : ''); - setToast(response.message); - setStatus(false); - if (response.license) { - changeLicense(response.license); - fetchOptions().then((r) => { - setSettings(r); - }); - } - } - ); - }; - - if (whiteLabel && whiteLabel.hideLicense) { - return null; - } - let statusLabel = ''; - if (!status) { - if ('valid' === valid) { - statusLabel = __('Deactivate', 'neve'); - } else { - statusLabel = __('Activate', 'neve'); - } - } - if ('activating' === status) { - statusLabel = __('Activating', 'neve'); - statusLabel = __('Deactivating', 'neve'); - } - - return ( - - ); -}; - -export default compose( - withDispatch((dispatch) => { - const { changeLicense, setSettings } = dispatch('neve-dashboard'); - return { - setSettings: (object) => setSettings(object), - changeLicense: (data) => { - changeLicense(data); - }, - }; - }), - withSelect((select) => { - const { getLicense } = select('neve-dashboard'); - return { - license: getLicense(), - }; - }) -)(LicenseCard); diff --git a/assets/apps/dashboard/src/Components/Loading.js b/assets/apps/dashboard/src/Components/Loading.js deleted file mode 100644 index 48214147a2..0000000000 --- a/assets/apps/dashboard/src/Components/Loading.js +++ /dev/null @@ -1,136 +0,0 @@ -/* global neveDash */ -/* eslint jsx-a11y/heading-has-content: 0 */ -const Loading = () => { - return ( -
-
-
-
-

- v2.6.2 - {!neveDash.whiteLabel && ( -
- )} -
- -

-
-
-
- {neveDash.notifications && ( -
- {Object.keys(neveDash.notifications).map( - (notification, index) => { - return ( -
- ); - } - )} -
- )} -
-
-
-
-

-

-
-

-

-

-

-
-
-
-
-

-

-
-
-
- -
- -
- -
- -
-
- -
- -
- -
- -
-
-
-
-
-
-
-

-

-
-

-

-
-
-
-
- {!neveDash.whiteLabel && ( - - )} -
-
-
- ); -}; - -export default Loading; diff --git a/assets/apps/dashboard/src/Components/ModuleCard.js b/assets/apps/dashboard/src/Components/ModuleCard.js deleted file mode 100644 index a39dda77d1..0000000000 --- a/assets/apps/dashboard/src/Components/ModuleCard.js +++ /dev/null @@ -1,277 +0,0 @@ -/* global neveDash, CustomEvent */ -/*eslint camelcase: ["error", {allow: ["required_actions"]}]*/ -import Accordion from './Accordion'; -import InputForm from './Options/InputForm'; -import Select from './Options/Select'; -import Toggle from './Options/Toggle'; -import MultiSelectOption from './Options/MultiSelect'; -import { changeOption } from '../utils/rest'; -import classnames from 'classnames'; -import MultiInstallActivate from './Plugin/MultiInstallActivate'; - -import { - Button, - ToggleControl, - Dashicon, - ExternalLink, -} from '@wordpress/components'; -import { withSelect, withDispatch } from '@wordpress/data'; -import { Fragment, useState, useEffect } from '@wordpress/element'; -import { compose } from '@wordpress/compose'; -import { __ } from '@wordpress/i18n'; - -const ReactPlaceholder = ({ slug }) => { - useEffect(() => { - window.dispatchEvent( - new CustomEvent('neve-dashboard-react-placeholder', { - detail: { - slug, - }, - }) - ); - }, []); - - return
; -}; - -const ModuleCard = ({ - slug, - setToast, - getOption, - changeModuleStatus, - getModuleStatus, - tier, -}) => { - const [loading, setLoading] = useState(false); - - const { - nicename, - description, - availabilityLevel, - options, - links, - documentation, - // eslint-disable-next-line camelcase - required_actions, - manageableDependentPlugins, - manageablePluginsLabels, - dependentPlugins, - } = neveDash.modules[slug]; - const { upgradeLinks } = neveDash; - - const isToggleEnabled = (toggleSlug) => { - return getOption(toggleSlug); - }; - - const renderOptionsAccordions = () => { - return options.map((group, index) => { - const { label, options: optionGroup } = group; - return ( - -
- {Object.keys(optionGroup).map( - (optionSlug, indexGroup) => { - const { - label: labelGroup, - type, - placeholder, - documentation: documentationOption, - choices, - depends_on: dependsOn, - } = optionGroup[optionSlug]; - - return ( - - {'text' === type && ( - - )} - {'toggle' === type && ( - - )} - {'select' === type && ( -