);
};
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 (
-
- );
-};
-
-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 (
-
-
-
-
-
- Neve
- Neve Pro
-
- {featureData.map((item, index) => (
-
- ))}
-
-
-
-
-
- {__(
- 'Get access to all Pro features and power-up your website',
- 'neve'
- )}
-
-
- {__('Get Neve Pro Now', 'neve')}
-
- {__('(opens in a new tab)', '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'
+ )}
+
+
+
+
+
+ {__('View Pro Plans', 'neve')}
+
+
+
+ {__('Contact Support', '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 && (
-
-
- {__('(opens in a new tab)', 'neve')}
-
-
- {__('Join our Facebook Group', 'neve')}
-
- )}
-
-
- {!whiteLabel && (
-
-
- {__('Learn More', 'neve')}
-
-
- )}
-
-
-
-
- {__('(opens in a new tab)', 'neve')}
-
- {__('Contact Support', 'neve')}
-
-
-
- {!whiteLabel && (
-
-
- {__('Learn More', 'neve')}
-
-
- )}
- {!whiteLabel && (
-
- setTab('changelog')}>
- {__('View Changelog', 'neve')}
-
-
- )}
-
- );
-};
-
-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 (
+
+ {__('Upgrade', 'neve')}
+
+ );
+ }
+
+ 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 (
+ setTab(id)}
+ >
+
+ {label}
+
+ );
+ })}
+
+
+ );
+};
+
+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 && (
+ <>
+ ({
+ ...acc,
+ [v]: l,
+ }),
+ {}
+ )}
+ onChange={(newValue) => {
+ updateSetting(key, newValue);
+ }}
+ />
+
+ {value && description && (
+ {description}
+ )}
+ >
+ )}
+
+ );
+ })}
+ >
+ );
+};
+
+export default AccessRestriction;
diff --git a/assets/apps/dashboard/src/Components/Content/Settings/GeneralTabContent.js b/assets/apps/dashboard/src/Components/Content/Settings/GeneralTabContent.js
new file mode 100644
index 0000000000..74127f88f5
--- /dev/null
+++ b/assets/apps/dashboard/src/Components/Content/Settings/GeneralTabContent.js
@@ -0,0 +1,197 @@
+/* global neveDash */
+import { __ } from '@wordpress/i18n';
+import {
+ LucideImage,
+ LucideLock,
+ LucideMenu,
+ LucideMonitorDown,
+ LucideTags,
+ LucideType,
+} from 'lucide-react';
+import useLicenseData from '../../../Hooks/useLicenseData';
+import { NEVE_HAS_VALID_PRO } from '../../../utils/constants';
+import TextControl from '../../Controls/TextControl';
+import ToggleControl from '../../Controls/ToggleControl';
+import OptionGroup from './OptionGroup';
+import ControlWrap from '../../Controls/ControlWrap';
+import Toggle from '../../Common/Toggle';
+import Select from '../../Common/Select';
+
+const DUMMY_SETTINGS_ARGS = {
+ enable_featured_image_taxonomy: {
+ icon: LucideImage,
+ label: __('Featured image for taxonomy', 'neve'),
+ description: __(
+ 'Enable featured images for categories and tags.',
+ 'neve'
+ ),
+ },
+ enable_mega_menu: {
+ icon: LucideMenu,
+ label: __('Enable Mega Menu', 'neve'),
+ description: __(
+ 'Enable Mega menu fuctionality in the menu dashboard page.',
+ 'neve'
+ ),
+ },
+ typekit_id: {
+ icon: LucideType,
+ label: __('Embed Typekit', 'neve'),
+ type: 'text',
+ placeholder: __('Paste your Typekit code here', 'neve'),
+ description: __(
+ 'Add your Typekit embed code here to use custom fonts.',
+ 'neve'
+ ),
+ },
+ neve_access_restriction: {
+ icon: LucideLock,
+ label: __('Enable Content Restriction', 'neve'),
+ description: __(
+ 'Restrict content access on Posts, Pages, and set a default login page.',
+ 'neve'
+ ),
+ },
+};
+
+if (NEVE_HAS_VALID_PRO) {
+ DUMMY_SETTINGS_ARGS.typekit_loading_method = {
+ icon: LucideMonitorDown,
+ label: __('Typekit loading method', 'neve'),
+ description: __(
+ 'Choose if Typekit fonts should be loaded with JavaScript or CSS. JavaScript is usually faster.',
+ 'neve'
+ ),
+ };
+
+ DUMMY_SETTINGS_ARGS.featured_image_taxonomies = {
+ icon: LucideTags,
+ description: __(
+ 'Enable featured images for the following taxonomies.',
+ 'neve'
+ ),
+ };
+
+ DUMMY_SETTINGS_ARGS.neve_access_restriction = {
+ ...DUMMY_SETTINGS_ARGS.neve_access_restriction,
+ description: __(
+ 'Adds a metabox to control content restriction on the post/taxonomy edit page.',
+ 'neve'
+ ),
+ };
+}
+
+const AccessRestrictionDummySettings = () => {
+ const { icon, label, description } =
+ DUMMY_SETTINGS_ARGS.neve_access_restriction;
+
+ const contentTypes = [
+ __('Posts', 'neve'),
+ __('Pages', 'neve'),
+ __('Categories', 'neve'),
+ ];
+
+ return (
+
+
+ {contentTypes.map((type) => (
+
+ ))}
+
+
+
+
+
+
+ );
+};
+
+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' ? (
+
+ ) : (
+
+ )}
+
+ );
+ }
+ )}
+
+
+ );
+ })}
+
+
+
+ {__('Save', 'neve')}
+
+
+
+ );
+};
+
+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 (
+
+ );
+};
+
+export default () => {
+ const { isLicenseValid, licenseTier } = useLicenseData();
+
+ const showPlaceholder = !isLicenseValid || licenseTier < 3;
+
+ return (
+ <>
+
+
+ {__('White Label Settings', 'neve')}
+
+
+ {showPlaceholder && (
+
+
+ {__('Available in Agency Plan', 'neve')}
+
+ )}
+
+
+ {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 && (
+
+ )}
+
+ {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 && (
+
+ {loading && (
+
+ )}
+ {buttonText}
+
+ )}
+ {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 && (
+
+ )}
+
+ );
+};
+
+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 (
-
- >
- )}
- >
- );
-};
-
-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 (
-