initial commit

This commit is contained in:
2024-04-29 13:12:44 +05:45
commit 34887303c5
19300 changed files with 5268802 additions and 0 deletions

View File

@ -0,0 +1,69 @@
import { useEffect, useCallback } from '@wordpress/element'
import { General as GeneralApi } from '@extendify/api/General'
import MainWindow from '@extendify/pages/MainWindow'
import { useGlobalStore } from '@extendify/state/GlobalState'
import { useTemplatesStore } from '@extendify/state/Templates'
import { useUserStore } from '@extendify/state/User'
import '@extendify/utility-control'
import { useTaxonomyStore } from './state/Taxonomies'
export default function ExtendifyLibrary({ show = false }) {
const open = useGlobalStore((state) => state.open)
const setReady = useGlobalStore((state) => state.setReady)
const setOpen = useGlobalStore((state) => state.setOpen)
const showLibrary = useCallback(() => setOpen(true), [setOpen])
const hideLibrary = useCallback(() => setOpen(false), [setOpen])
const initTemplateData = useTemplatesStore(
(state) => state.initTemplateData,
)
const fetchTaxonomies = useTaxonomyStore((state) => state.fetchTaxonomies)
// When the uuid of the user comes back from the database, we can
// assume that the state object is ready. This is important to check
// as the library may be "open" when loaded, but not ready.
const userHasHydrated = useUserStore((state) => state._hasHydrated)
const taxonomiesReady = useTemplatesStore(
(state) => Object.keys(state.taxonomyDefaultState).length > 0,
)
useEffect(() => {
if (!open) return
fetchTaxonomies().then(() => {
useTemplatesStore.getState().setupDefaultTaxonomies()
})
}, [open, fetchTaxonomies])
useEffect(() => {
if (userHasHydrated && taxonomiesReady) {
initTemplateData()
setReady(true)
}
}, [userHasHydrated, taxonomiesReady, initTemplateData, setReady])
useEffect(() => {
const search = new URLSearchParams(window.location.search)
if (show || search.has('ext-open')) {
setOpen(true)
}
}, [show, setOpen])
useEffect(() => {
GeneralApi.metaData().then((data) => {
useGlobalStore.setState({
metaData: data,
})
})
}, [])
// Let the visibility to be controlled from outside the application
useEffect(() => {
window.addEventListener('extendify::open-library', showLibrary)
window.addEventListener('extendify::close-library', hideLibrary)
return () => {
window.removeEventListener('extendify::open-library', showLibrary)
window.removeEventListener('extendify::close-library', hideLibrary)
}
}, [hideLibrary, showLibrary])
return <MainWindow />
}

View File

@ -0,0 +1,18 @@
import { useTemplatesStore } from '@extendify/state/Templates'
import { useUserStore } from '@extendify/state/User'
import { Axios as api } from './axios'
export const General = {
metaData() {
return api.get('meta-data')
},
ping(action) {
const categories =
useTemplatesStore.getState()?.searchParams?.taxonomies ?? []
return api.post('simple-ping', {
action,
categories,
sdk_partner: useUserStore.getState()?.sdkPartner ?? '',
})
},
}

View File

@ -0,0 +1,19 @@
import { Axios as api } from './axios'
export const Plugins = {
getInstalled() {
return api.get('plugins')
},
installAndActivate(plugins = []) {
const formData = new FormData()
formData.append('plugins', JSON.stringify(plugins))
return api.post('plugins', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
},
getActivated() {
return api.get('active-plugins')
},
}

View File

@ -0,0 +1,16 @@
import { Axios as api } from './axios'
export const SiteSettings = {
getData() {
return api.get('site-settings')
},
setData(data) {
const formData = new FormData()
formData.append('data', JSON.stringify(data))
return api.post('site-settings', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
},
}

View File

@ -0,0 +1,7 @@
import { Axios as api } from './axios'
export const Taxonomies = {
async get() {
return await api.get('taxonomies')
},
}

View File

@ -0,0 +1,76 @@
import { useTemplatesStore } from '@extendify/state/Templates'
import { useUserStore } from '@extendify/state/User'
import { Axios as api } from './axios'
let count = 0
export const Templates = {
async get(searchParams, options = {}) {
count++
const defaultpageSize = searchParams.type === 'pattern' ? '8' : '4'
const taxonomyType =
searchParams.type === 'pattern' ? 'patternType' : 'layoutType'
const args = Object.assign(
{
filterByFormula: prepareFilterFormula(
searchParams,
taxonomyType,
),
pageSize: defaultpageSize,
categories: searchParams.taxonomies,
search: searchParams.search,
type: searchParams.type,
offset: '',
initial: count === 1,
request_count: count,
sdk_partner: useUserStore.getState().sdkPartner ?? '',
},
options,
)
return await api.post('templates', args)
},
// TODO: Refactor this later to combine the following three
maybeImport(template) {
const categories =
useTemplatesStore.getState()?.searchParams?.taxonomies ?? []
return api.post(`templates/${template.id}`, {
template_id: template?.id,
categories,
maybe_import: true,
type: template.fields?.type,
sdk_partner: useUserStore.getState().sdkPartner ?? '',
pageSize: '1',
template_name: template.fields?.title,
})
},
import(template) {
const categories =
useTemplatesStore.getState()?.searchParams?.taxonomies ?? []
return api.post(`templates/${template.id}`, {
template_id: template.id,
categories,
imported: true,
basePattern:
template.fields?.basePattern ??
template.fields?.baseLayout ??
'',
type: template.fields.type,
sdk_partner: useUserStore.getState().sdkPartner ?? '',
pageSize: '1',
template_name: template.fields?.title,
})
},
}
const prepareFilterFormula = ({ taxonomies }, type) => {
const siteType = taxonomies?.siteType?.slug
const formula = [
`{type}="${type.replace('Type', '')}"`,
`{siteType}="${siteType}"`,
]
if (taxonomies[type]?.slug) {
formula.push(`{${type}}="${taxonomies[type].slug}"`)
}
return `AND(${formula.join(', ')})`.replace(/\r?\n|\r/g, '')
}

View File

@ -0,0 +1,67 @@
import { Axios as api } from './axios'
export const User = {
async getData() {
// Zustand changed their persist middleware to bind to the store
// so api was undefined here. That's why using fetch for this one request.
const data = await fetch(`${window.extendifyData.root}/user`, {
method: 'GET',
headers: {
'X-WP-Nonce': window.extendifyData.nonce,
'X-Requested-With': 'XMLHttpRequest',
'X-Extendify': true,
},
})
return await data.json()
},
getMeta(key) {
return api.get('user-meta', {
params: {
key,
},
})
},
authenticate(email, key) {
const formData = new FormData()
formData.append('email', email)
formData.append('key', key)
return api.post('login', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
},
register(email) {
const formData = new FormData()
formData.append('data', email)
return api.post('register', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
},
setData(data) {
const formData = new FormData()
formData.append('data', JSON.stringify(data))
return api.post('user', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
},
deleteData() {
return api.post('clear-user')
},
registerMailingList(email) {
const formData = new FormData()
formData.append('email', email)
return api.post('register-mailing-list', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
},
allowedImports() {
return api.get('max-free-imports')
},
}

View File

@ -0,0 +1,73 @@
import axios from 'axios'
import { useUserStore } from '@extendify/state/User'
const Axios = axios.create({
baseURL: window.extendifyData.root,
headers: {
'X-WP-Nonce': window.extendifyData.nonce,
'X-Requested-With': 'XMLHttpRequest',
'X-Extendify': true,
},
})
function findResponse(response) {
return Object.prototype.hasOwnProperty.call(response, 'data')
? response.data
: response
}
function handleErrors(error) {
if (!error.response) {
return
}
console.error(error.response)
// TODO: add a global error message system
return Promise.reject(findResponse(error.response))
}
function addDefaults(request) {
const userState = useUserStore.getState()
const remainingImports = userState.apiKey
? 'unlimited'
: userState.remainingImports()
if (request.data) {
request.data.remaining_imports = remainingImports
request.data.entry_point = userState.entryPoint
request.data.total_imports = userState.imports
request.data.participating_tests = userState.activeTestGroups()
}
return request
}
function checkDevMode(request) {
request.headers['X-Extendify-Dev-Mode'] =
window.location.search.indexOf('DEVMODE') > -1
request.headers['X-Extendify-Local-Mode'] =
window.location.search.indexOf('LOCALMODE') > -1
return request
}
function checkForSoftError(response) {
if (Object.prototype.hasOwnProperty.call(response, 'soft_error')) {
window.dispatchEvent(
new CustomEvent('extendify::softerror-encountered', {
detail: response.soft_error,
bubbles: true,
}),
)
}
return response
}
Axios.interceptors.response.use(
(response) => checkForSoftError(findResponse(response)),
(error) => handleErrors(error),
)
// TODO: setup a pipe function instead of this nested pattern
Axios.interceptors.request.use(
(request) => checkDevMode(addDefaults(request)),
(error) => error,
)
export { Axios }

View File

@ -0,0 +1,179 @@
/* Adding CSS classes should be done with consideration and rarely */
@tailwind base;
@tailwind components;
@tailwind utilities;
.extendify {
--tw-ring-inset: var(--tw-empty, /*!*/ /*!*/);
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: transparent;
--tw-ring-color: var(--wp-admin-theme-color);
}
.extendify *,
.extendify *:after,
.extendify *:before {
box-sizing: border-box;
border: 0 solid #e5e7eb;
}
.extendify .button-focus {
@apply outline-none focus:shadow-none focus:ring-wp focus:ring-wp-theme-500;
}
.extendify select.button-focus,
.extendify input.button-focus {
@apply focus:outline-none focus:border-transparent focus:shadow-none;
}
div.extendify button.extendify-skip-to-sr-link:focus {
@apply fixed top-0 z-high bg-white p-4;
}
.button-extendify-main {
@apply button-focus cursor-pointer whitespace-nowrap rounded bg-extendify-main p-1.5 px-3 text-base text-white no-underline transition duration-200 hover:bg-extendify-main-dark hover:text-white focus:text-white active:bg-gray-900 active:text-white;
}
#extendify-search-input:focus ~ svg,
#extendify-search-input:not(:placeholder-shown) ~ svg {
@apply hidden;
}
#extendify-search-input::-webkit-textfield-decoration-container {
@apply mr-3;
}
/* WP tweaks and overrides */
.extendify .components-panel__body > .components-panel__body-title {
/* Override WP aggressive boder:none and border:0 */
border-bottom: 1px solid #e0e0e0 !important;
@apply bg-transparent;
}
.extendify .components-modal__header {
@apply border-b border-gray-300;
}
/* A shim to ensure live previews w/o iframes display properly */
.block-editor-block-preview__content
.block-editor-block-list__layout.is-root-container
> .ext {
@apply max-w-none;
}
/* Ensure our patterns display fullwidth on old themes + < 5.9 */
.block-editor-block-list__layout.is-root-container
.ext.block-editor-block-list__block {
@apply max-w-full;
}
.block-editor-block-preview__content-iframe
.block-editor-block-list__layout.is-root-container
.ext.block-editor-block-list__block {
@apply max-w-none;
}
.extendify .block-editor-block-preview__container {
/* no important */
@apply opacity-0;
animation: extendifyOpacityIn 200ms cubic-bezier(0.694, 0, 0.335, 1)
forwards 0ms;
}
/* Remove excess margin for top-level patterns on < 5.9 */
.extendify .is-root-container > [data-block],
.extendify .is-root-container > [data-align="full"],
.extendify .is-root-container > [data-align="full"] > .wp-block {
@apply my-0 !important;
}
/* Remove excess margin for top-level patterns on 5.9+ */
.editor-styles-wrapper:not(.block-editor-writing-flow)
> .is-root-container
:where(.wp-block)[data-align="full"] {
@apply my-0 !important;
}
@keyframes extendifyOpacityIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.extendify .with-light-shadow::after {
content: "";
@apply absolute inset-0 border-0 shadow-inner-sm;
}
.extendify .with-light-shadow:hover::after {
@apply shadow-inner-md;
}
/* Fallback as FireFox does not support backdrop filter */
@supports not (
(-webkit-backdrop-filter: saturate(2) blur(24px)) or
(backdrop-filter: saturate(2) blur(24px))
) {
div.extendify .bg-extendify-transparent-white {
@apply bg-gray-100;
}
}
/* Style contentType/pageType control in sidebar */
.components-panel__body.ext-type-control .components-panel__body-toggle {
@apply pl-0 pr-0;
}
.components-panel__body.ext-type-control .components-panel__body-title {
@apply m-0 border-b-0 px-5;
}
.components-panel__body.ext-type-control
.components-panel__body-title
.components-button {
@apply m-0 border-b-0 py-2 text-xss font-medium uppercase text-extendify-gray;
}
.components-panel__body.ext-type-control
.components-button
.components-panel__arrow {
@apply right-0 text-extendify-gray;
}
.extendify .animate-pulse {
animation: extendifyPulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes extendifyPulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.is-template--inactive::before,
.is-template--in-review::before {
content: "";
@apply absolute top-0 left-0 bottom-0 right-0 z-40 h-full w-full border-8 border-solid border-extendify-secondary;
}
.is-template--inactive::before {
border-color: #fdeab6;
}
.extendify-tooltip-default:not(.is-without-arrow)[data-y-axis="bottom"]::after {
border-bottom-color: #1e1e1e;
}
.extendify-tooltip-default:not(.is-without-arrow)::before {
@apply border-transparent;
}
.extendify-tooltip-default:not(.is-without-arrow) .components-popover__content {
min-width: 250px;
@apply border-transparent bg-gray-900 p-4 text-white;
}
.extendify-bottom-arrow::after {
content: "";
bottom: -15px;
@apply absolute inline-block h-0 w-0 -translate-y-px transform border-8 border-transparent;
border-top-color: #1e1e1e !important;
}

View File

@ -0,0 +1,45 @@
import { rawHandler } from '@wordpress/blocks'
import { render } from '@wordpress/element'
import ExtendifyLibrary from '@extendify/ExtendifyLibrary'
import '@extendify/blocks/blocks'
import '@extendify/buttons'
import '@extendify/listeners'
import { useWantedTemplateStore } from '@extendify/state/Importing'
import { injectTemplateBlocks } from '@extendify/util/templateInjection'
window._wpLoadBlockEditor &&
window.wp.domReady(() => {
// Insert into the editor (note: Modal opens in a portal)
const extendify = Object.assign(document.createElement('div'), {
id: 'extendify-root',
})
document.body.append(extendify)
render(<ExtendifyLibrary />, extendify)
// Add an extra div to use for utility modals, etc
extendify.parentNode.insertBefore(
Object.assign(document.createElement('div'), {
id: 'extendify-util',
}),
extendify.nextSibling,
)
// Insert a template on page load if it exists in localstorage
// Note 6/28/21 - this was moved to after the render to possibly
// fix a bug where imports would go from 3->0.
if (useWantedTemplateStore.getState().importOnLoad) {
const template = useWantedTemplateStore.getState().wantedTemplate
setTimeout(() => {
injectTemplateBlocks(
rawHandler({ HTML: template.fields.code }),
template,
)
}, 0)
}
// Reset template state after checking if we need an import
useWantedTemplateStore.setState({
importOnLoad: false,
wantedTemplate: {},
})
})

View File

@ -0,0 +1,14 @@
/**
* WordPress dependencies
*/
import { registerBlockCollection } from '@wordpress/blocks'
import { Icon } from '@wordpress/components'
import { brandBlockIcon } from '@extendify/components/icons'
/**
* Function to register a block collection for our block(s).
*/
registerBlockCollection('extendify', {
title: 'Extendify',
icon: <Icon icon={brandBlockIcon} />,
})

View File

@ -0,0 +1,2 @@
import './block-category.js'
import './library/block.js'

View File

@ -0,0 +1,140 @@
import { registerBlockType } from '@wordpress/blocks'
import { useDispatch } from '@wordpress/data'
import { useEffect } from '@wordpress/element'
import { __, _x, sprintf } from '@wordpress/i18n'
import {
Icon,
gallery,
postAuthor,
mapMarker,
button,
cover,
overlayText,
} from '@wordpress/icons'
import { brandBlockIcon } from '@extendify/components/icons'
import { setModalVisibility } from '@extendify/util/general'
import metadata from './block.json'
export const openModal = (source) => setModalVisibility(source, 'open')
registerBlockType(metadata, {
icon: brandBlockIcon,
category: 'extendify',
example: {
attributes: {
preview: window.extendifyData.asset_path + '/preview.png',
},
},
variations: [
{
name: 'gallery',
icon: <Icon icon={gallery} />,
category: 'extendify',
attributes: { search: 'gallery' },
title: __('Gallery Patterns', 'extendify'),
description: __('Add gallery patterns and layouts.', 'extendify'),
keywords: [__('slideshow', 'extendify'), __('images', 'extendify')],
},
{
name: 'team',
icon: <Icon icon={postAuthor} />,
category: 'extendify',
attributes: { search: 'team' },
title: __('Team Patterns', 'extendify'),
description: __('Add team patterns and layouts.', 'extendify'),
keywords: [
_x('crew', 'As in team', 'extendify'),
__('colleagues', 'extendify'),
__('members', 'extendify'),
],
},
{
name: 'hero',
icon: <Icon icon={cover} />,
category: 'extendify',
attributes: { search: 'hero' },
title: _x(
'Hero Patterns',
'Hero being a hero/top section of a webpage',
'extendify',
),
description: __('Add hero patterns and layouts.', 'extendify'),
keywords: [__('heading', 'extendify'), __('headline', 'extendify')],
},
{
name: 'text',
icon: <Icon icon={overlayText} />,
category: 'extendify',
attributes: { search: 'text' },
title: _x(
'Text Patterns',
'Relating to patterns that feature text only',
'extendify',
),
description: __('Add text patterns and layouts.', 'extendify'),
keywords: [__('simple', 'extendify'), __('paragraph', 'extendify')],
},
{
name: 'about',
icon: <Icon icon={mapMarker} />,
category: 'extendify',
attributes: { search: 'about' },
title: _x(
'About Page Patterns',
'Add patterns relating to an about us page',
'extendify',
),
description: __('About patterns and layouts.', 'extendify'),
keywords: [__('who we are', 'extendify'), __('team', 'extendify')],
},
{
name: 'call-to-action',
icon: <Icon icon={button} />,
category: 'extendify',
attributes: { search: 'call-to-action' },
title: __('Call to Action Patterns', 'extendify'),
description: __(
'Add call to action patterns and layouts.',
'extendify',
),
keywords: [
_x('cta', 'Initialism for call to action', 'extendify'),
__('callout', 'extendify'),
__('buttons', 'extendify'),
],
},
],
edit: function Edit({ clientId, attributes }) {
const { removeBlock } = useDispatch('core/block-editor')
useEffect(() => {
if (attributes.preview) {
return
}
if (attributes.search) {
addTermToSearchParams(attributes.search)
}
openModal('library-block')
removeBlock(clientId)
}, [clientId, attributes, removeBlock])
return (
<img
style={{ display: 'block', maxWidth: '100%' }}
src={attributes.preview}
alt={sprintf(
__('%s Pattern Library', 'extendify'),
'Extendify',
)}
/>
)
},
})
const addTermToSearchParams = (term) => {
const params = new URLSearchParams(window.location.search)
params.append('ext-patternType', term)
window.history.replaceState(
null,
null,
window.location.pathname + '?' + params.toString(),
)
}

View File

@ -0,0 +1,17 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 2,
"name": "extendify/library",
"title": "Pattern Library",
"description": "Add block patterns and full page layouts with the Extendify Library.",
"keywords": ["template", "layouts"],
"textdomain": "extendify",
"attributes": {
"preview": {
"type": "string"
},
"search": {
"type": "string"
}
}
}

View File

@ -0,0 +1,101 @@
import { PluginSidebarMoreMenuItem } from '@wordpress/edit-post'
import { render } from '@wordpress/element'
import { __ } from '@wordpress/i18n'
import { Icon } from '@wordpress/icons'
import { registerPlugin } from '@wordpress/plugins'
import LibraryAccessModal from '@extendify/components/LibraryAccessModal'
import { CtaButton, MainButtonWrapper } from '@extendify/components/MainButtons'
import { brandMark } from '@extendify/components/icons/'
const userState = window.extendifyData?.user?.state
const isAdmin = () => window.extendifyData.user === null || userState?.isAdmin
const isGlobalLibraryEnabled = () =>
window.extendifyData.sitesettings === null ||
window.extendifyData?.sitesettings?.state?.enabled
const isLibraryEnabled = () =>
window.extendifyData.user === null
? isGlobalLibraryEnabled()
: userState?.enabled
// Add the MAIN button when Gutenberg is available and ready
if (window._wpLoadBlockEditor) {
const finish = window.wp.data.subscribe(() => {
requestAnimationFrame(() => {
if (!isGlobalLibraryEnabled() && !isAdmin()) {
return
}
if (document.getElementById('extendify-templates-inserter')) {
return
}
if (!document.querySelector('.edit-post-header-toolbar')) {
return
}
const buttonContainer = Object.assign(
document.createElement('div'),
{ id: 'extendify-templates-inserter' },
)
document
.querySelector('.edit-post-header-toolbar')
.append(buttonContainer)
render(<MainButtonWrapper />, buttonContainer)
if (!isLibraryEnabled()) {
document
.getElementById('extendify-templates-inserter-btn')
.classList.add('hidden')
}
finish()
})
})
}
// The CTA button inside patterns
if (window._wpLoadBlockEditor) {
window.wp.data.subscribe(() => {
requestAnimationFrame(() => {
if (!isGlobalLibraryEnabled() && !isAdmin()) {
return
}
if (!document.querySelector('[id$=patterns-view]')) {
return
}
if (document.getElementById('extendify-cta-button')) {
return
}
const ctaButtonContainer = Object.assign(
document.createElement('div'),
{ id: 'extendify-cta-button-container' },
)
document
.querySelector('[id$=patterns-view]')
.prepend(ctaButtonContainer)
render(<CtaButton />, ctaButtonContainer)
})
})
}
// This will add a button to enable or disable the library button
const LibraryEnableDisable = () => {
function setOpenSiteSettingsModal() {
const util = document.getElementById('extendify-util')
render(<LibraryAccessModal />, util)
}
return (
<>
<PluginSidebarMoreMenuItem
onClick={setOpenSiteSettingsModal}
icon={<Icon icon={brandMark} size={24} />}>
{' '}
{__('Extendify', 'extendify')}
</PluginSidebarMoreMenuItem>
</>
)
}
// Load this button always, which is used to enable or disable
window._wpLoadBlockEditor &&
registerPlugin('extendify-settings-enable-disable', {
render: LibraryEnableDisable,
})

View File

@ -0,0 +1,37 @@
import { useEffect, useState } from '@wordpress/element'
import { __, sprintf } from '@wordpress/i18n'
import { CopyToClipboard } from 'react-copy-to-clipboard'
/** Overlay for pattern import button */
export const DevButtonOverlay = ({ template }) => {
const basePatternId = template?.fields?.basePattern?.length
? template?.fields?.basePattern[0]
: ''
const [idText, setIdText] = useState(basePatternId)
useEffect(() => {
if (!basePatternId?.length || idText === basePatternId) return
setTimeout(() => setIdText(basePatternId), 1000)
}, [idText, basePatternId])
if (!basePatternId) return null
return (
<div className="absolute bottom-0 left-0 z-50 mb-4 ml-4 flex items-center space-x-2 opacity-0 transition duration-100 group-hover:opacity-100 space-x-0.5">
<CopyToClipboard
text={template?.fields?.basePattern}
onCopy={() => setIdText(__('Copied!', 'extendify'))}>
<button className="text-sm rounded-md border border-black bg-white py-1 px-2.5 font-medium text-black no-underline m-0 cursor-pointer">
{sprintf(__('Base: %s', 'extendify'), idText)}
</button>
</CopyToClipboard>
<a
target="_blank"
className="text-sm rounded-md border border-black bg-white py-1 px-2.5 font-medium text-black no-underline m-0"
href={template?.fields?.editURL}
rel="noreferrer">
{__('Edit', 'extendify')}
</a>
</div>
)
}

View File

@ -0,0 +1,109 @@
import { safeHTML } from '@wordpress/dom'
import { useEffect, memo, useRef } from '@wordpress/element'
import { __, _n, sprintf } from '@wordpress/i18n'
import { Icon } from '@wordpress/icons'
import classnames from 'classnames'
import { General } from '@extendify/api/General'
import { User as UserApi } from '@extendify/api/User'
import { useUserStore } from '@extendify/state/User'
import { growthArrow } from './icons'
import { alert, download } from './icons/'
export const ImportCounter = memo(function ImportCounter() {
const remainingImports = useUserStore((state) => state.remainingImports)
const allowedImports = useUserStore((state) => state.allowedImports)
const count = remainingImports()
const status = count > 0 ? 'has-imports' : 'no-imports'
const buttonRef = useRef()
useEffect(() => {
if (allowedImports < 1 || !allowedImports) {
const fallback = 5
UserApi.allowedImports()
.then((allowedImports) => {
allowedImports = /^[1-9]\d*$/.test(allowedImports)
? allowedImports
: fallback
useUserStore.setState({ allowedImports })
})
.catch(() =>
useUserStore.setState({ allowedImports: fallback }),
)
}
}, [allowedImports])
if (!allowedImports) {
return null
}
return (
// tabIndex for group focus animations
<div tabIndex="0" className="group relative">
<a
target="_blank"
ref={buttonRef}
rel="noreferrer"
className={classnames(
'button-focus hidden w-full justify-between rounded py-3 px-4 text-sm text-white no-underline sm:flex',
{
'bg-wp-theme-500 hover:bg-wp-theme-600': count > 0,
'bg-extendify-alert': !count,
},
)}
onClick={async () => await General.ping('import-counter-click')}
href={`https://www.extendify.com/pricing/?utm_source=${encodeURIComponent(
window.extendifyData.sdk_partner,
)}&utm_medium=library&utm_campaign=import-counter&utm_content=get-more&utm_term=${status}&utm_group=${useUserStore
.getState()
.activeTestGroupsUtmValue()}`}>
<span className="flex items-center space-x-2 text-xs no-underline">
<Icon icon={count > 0 ? download : alert} size={14} />
<span>
{sprintf(
_n('%s Import', '%s Imports', count, 'extendify'),
count,
)}
</span>
</span>
<span className="outline-none flex items-center text-sm font-medium text-white no-underline">
{__('Get more', 'extendify')}
<Icon icon={growthArrow} size={24} className="-mr-1.5" />
</span>
</a>
<div
className="extendify-bottom-arrow invisible absolute top-0 w-full -translate-y-full transform opacity-0 shadow-md transition-all delay-200 duration-300 ease-in-out group-hover:visible group-hover:-top-2.5 group-hover:opacity-100 group-focus:visible group-focus:-top-2.5 group-focus:opacity-100"
tabIndex="-1">
<a
href={`https://www.extendify.com/pricing/?utm_source=${encodeURIComponent(
window.extendifyData.sdk_partner,
)}&utm_medium=library&utm_campaign=import-counter-tooltip&utm_content=get-50-off&utm_term=${status}&utm_group=${useUserStore
.getState()
.activeTestGroupsUtmValue()}`}
className="block bg-gray-900 text-white p-4 no-underline rounded bg-cover"
onClick={async () =>
await General.ping('import-counter-tooltip-click')
}
style={{
backgroundImage: `url(${window.extendifyData.asset_path}/logo-tips.png)`,
backgroundSize: '100% 100%',
}}>
<span
dangerouslySetInnerHTML={{
__html: safeHTML(
sprintf(
__(
'%1$sGet %2$s off%3$s Extendify Pro when you upgrade today!',
'extendify',
),
'<strong>',
'50%',
'</strong>',
),
),
}}
/>
</a>
</div>
</div>
)
})

View File

@ -0,0 +1,205 @@
import { BlockPreview } from '@wordpress/block-editor'
import { rawHandler } from '@wordpress/blocks'
import { useEffect, useState, useRef, useMemo } from '@wordpress/element'
import { __, sprintf } from '@wordpress/i18n'
import classNames from 'classnames'
import { Templates as TemplatesApi } from '@extendify/api/Templates'
import { useIsDevMode } from '@extendify/hooks/helpers'
import { AuthorizationCheck, Middleware } from '@extendify/middleware'
import { useGlobalStore } from '@extendify/state/GlobalState'
import { useUserStore } from '@extendify/state/User'
import { injectTemplateBlocks } from '@extendify/util/templateInjection'
import { DevButtonOverlay } from './DevHelpers'
import { NoImportModal } from './modals/NoImportModal'
import { ProModal } from './modals/ProModal'
const canImportMiddleware = Middleware([
'hasRequiredPlugins',
'hasPluginsActivated',
])
export function ImportTemplateBlock({ template, maxHeight }) {
const importButtonRef = useRef(null)
const once = useRef(false)
const hasAvailableImports = useUserStore(
(state) => state.hasAvailableImports,
)
const loggedIn = useUserStore((state) => state.apiKey.length)
const setOpen = useGlobalStore((state) => state.setOpen)
const pushModal = useGlobalStore((state) => state.pushModal)
const removeAllModals = useGlobalStore((state) => state.removeAllModals)
const blocks = useMemo(
() => rawHandler({ HTML: template.fields.code }),
[template.fields.code],
)
const [loaded, setLoaded] = useState(false)
const devMode = useIsDevMode()
const [topValue, setTopValue] = useState(0)
const focusTrapInnerBlocks = () => {
if (once.current) return
once.current = true
Array.from(
importButtonRef.current.querySelectorAll(
'a, button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])',
),
).forEach((el) => el.setAttribute('tabIndex', '-1'))
}
const importTemplates = async () => {
await canImportMiddleware.check(template)
AuthorizationCheck(canImportMiddleware)
.then(() => {
setTimeout(() => {
injectTemplateBlocks(blocks, template)
.then(() => removeAllModals())
.then(() => setOpen(false))
.then(() => canImportMiddleware.reset())
}, 100)
})
.catch(() => {})
}
const handleKeyDown = (event) => {
if (['Enter', 'Space', ' '].includes(event.key)) {
event.stopPropagation()
event.preventDefault()
importTemplate()
}
}
const importTemplate = () => {
// Make a note that they attempted to import
TemplatesApi.maybeImport(template)
if (template?.fields?.pro && !loggedIn) {
pushModal(<ProModal />)
return
}
if (!hasAvailableImports()) {
pushModal(<NoImportModal />)
return
}
importTemplates()
}
// Trigger resize event on the live previews to add
// Grammerly/Loom/etc compatability
// TODO: This can probably be removed after WP 5.9
useEffect(() => {
const rafIds = []
const timeouts = []
let rafId1, rafId2, rafId3, rafId4
rafId1 = window.requestAnimationFrame(() => {
rafId2 = window.requestAnimationFrame(() => {
importButtonRef.current
.querySelectorAll('iframe')
.forEach((frame) => {
const inner = frame.contentWindow.document.body
const rafId = window.requestAnimationFrame(() => {
const maybeRoot =
inner.querySelector('.is-root-container')
if (maybeRoot) {
const height = maybeRoot?.offsetHeight
if (height) {
rafId4 = window.requestAnimationFrame(
() => {
frame.contentWindow.dispatchEvent(
new Event('resize'),
)
},
)
const id = window.setTimeout(() => {
frame.contentWindow.dispatchEvent(
new Event('resize'),
)
}, 2000)
timeouts.push(id)
}
}
frame.contentWindow.dispatchEvent(
new Event('resize'),
)
})
rafIds.push(rafId)
})
rafId3 = window.requestAnimationFrame(() => {
window.dispatchEvent(new Event('resize'))
setLoaded(true)
})
})
})
return () => {
;[...rafIds, rafId1, rafId2, rafId3, rafId4].forEach((id) =>
window.cancelAnimationFrame(id),
)
timeouts.forEach((id) => window.clearTimeout(id))
}
}, [])
useEffect(() => {
if (!Number.isInteger(maxHeight)) return
const button = importButtonRef.current
const handleIn = () => {
// The live component changes over time so easier to query on demand
const height = button.offsetHeight
button.style.transitionDuration = height * 1.5 + 'ms'
setTopValue(Math.abs(height - maxHeight) * -1)
}
const handleOut = () => {
const height = button.offsetHeight
button.style.transitionDuration = height / 1.5 + 'ms'
setTopValue(0)
}
button.addEventListener('focus', handleIn)
button.addEventListener('mouseenter', handleIn)
button.addEventListener('blur', handleOut)
button.addEventListener('mouseleave', handleOut)
return () => {
button.removeEventListener('focus', handleIn)
button.removeEventListener('mouseenter', handleIn)
button.removeEventListener('blur', handleOut)
button.removeEventListener('mouseleave', handleOut)
}
}, [maxHeight])
return (
<div className="group relative">
<div
role="button"
tabIndex="0"
aria-label={sprintf(
__('Press to import %s', 'extendify'),
template?.fields?.type,
)}
style={{ maxHeight }}
className="button-focus relative m-0 cursor-pointer overflow-hidden bg-gray-100 ease-in-out"
onFocus={focusTrapInnerBlocks}
onClick={importTemplate}
onKeyDown={handleKeyDown}>
<div
ref={importButtonRef}
style={{ top: topValue, transitionProperty: 'all' }}
className={classNames('with-light-shadow relative', {
[`is-template--${template.fields.status}`]:
template?.fields?.status && devMode,
'p-6 md:p-8': Number.isInteger(maxHeight),
})}>
<BlockPreview
blocks={blocks}
live={false}
viewportWidth={1400}
/>
</div>
</div>
{/* Show dev info after the preview is loaded to trigger observer */}
{devMode && loaded && <DevButtonOverlay template={template} />}
{template?.fields?.pro && (
<div className="pointer-events-none absolute top-4 right-4 z-20 rounded-md border border-none bg-white bg-wp-theme-500 py-1 px-2.5 font-medium text-white no-underline shadow-sm">
{__('Pro', 'extendify')}
</div>
)}
</div>
)
}

View File

@ -0,0 +1,111 @@
import { Modal } from '@wordpress/components'
import { ToggleControl } from '@wordpress/components'
import { useSelect } from '@wordpress/data'
import { unmountComponentAtNode, useState, useEffect } from '@wordpress/element'
import { __ } from '@wordpress/i18n'
import { useSiteSettingsStore } from '@extendify/state/SiteSettings'
import { useUserStore } from '@extendify/state/User'
const LibraryAccessModal = () => {
const isAdmin = useSelect((select) =>
select('core').canUser('create', 'users'),
)
const [libraryforMyself, setLibraryforMyself] = useState(
useUserStore.getState().enabled,
)
const [libraryforEveryone, setLibraryforEveryone] = useState(
useSiteSettingsStore.getState().enabled,
)
const closeModal = () => {
const util = document.getElementById('extendify-util')
unmountComponentAtNode(util)
}
useEffect(() => {
hideButton(!libraryforMyself)
}, [libraryforMyself])
function hideButton(state = true) {
const button = document.getElementById(
'extendify-templates-inserter-btn',
)
if (!button) return
if (state) {
button.classList.add('hidden')
} else {
button.classList.remove('hidden')
}
}
async function saveUser(value) {
await useUserStore.setState({ enabled: value })
}
async function saveSetting(value) {
await useSiteSettingsStore.setState({ enabled: value })
}
async function saveToggle(state, type) {
if (type === 'global') {
await saveSetting(state)
} else {
await saveUser(state)
}
}
function handleToggle(type) {
if (type === 'global') {
setLibraryforEveryone((state) => {
saveToggle(!state, type)
return !state
})
} else {
setLibraryforMyself((state) => {
hideButton(!state)
saveToggle(!state, type)
return !state
})
}
}
return (
<Modal
title={__('Extendify Settings', 'extendify')}
onRequestClose={closeModal}>
<ToggleControl
label={
isAdmin
? __('Enable the library for myself', 'extendify')
: __('Enable the library', 'extendify')
}
help={__(
'Publish with hundreds of patterns & page layouts',
'extendify',
)}
checked={libraryforMyself}
onChange={() => handleToggle('user')}
/>
{isAdmin && (
<>
<br />
<ToggleControl
label={__(
'Allow all users to publish with the library',
)}
help={__(
'Everyone publishes with patterns & page layouts',
'extendify',
)}
checked={libraryforEveryone}
onChange={() => handleToggle('global')}
/>
</>
)}
</Modal>
)
}
export default LibraryAccessModal

View File

@ -0,0 +1,112 @@
import { Button } from '@wordpress/components'
import { useState, useEffect, useRef } from '@wordpress/element'
import { __ } from '@wordpress/i18n'
import { Icon } from '@wordpress/icons'
import { General } from '@extendify/api/General'
import { useTestGroup } from '@extendify/hooks/useTestGroup'
import { useGlobalStore } from '@extendify/state/GlobalState'
import { useUserStore } from '@extendify/state/User'
import { openModal } from '@extendify/util/general'
import { brandMark } from './icons'
import { NewImportsPopover } from './popovers/NewImportsPopover'
export const MainButtonWrapper = () => {
const [showTooltip, setShowTooltip] = useState(false)
const once = useRef(false)
const buttonRef = useRef()
const loggedIn = useUserStore((state) => state.apiKey.length)
const hasImported = useUserStore((state) => state.imports > 0)
const open = useGlobalStore((state) => state.open)
const hasPendingNewImports = useUserStore(
(state) => state.allowedImports === 0,
)
const uuid = useUserStore((state) => state.uuid)
const buttonText = useTestGroup('main-button-text', ['A', 'B', 'C'], true)
const [libraryButtonText, setLibraryButtonText] = useState()
const handleTooltipClose = async () => {
await General.ping('mb-tooltip-closed')
setShowTooltip(false)
// If they close the tooltip, we can set the allowed imports
// to -1 and when it opens it will fetch and update. Meanwhile,
// -1 will be ignored by the this component.
useUserStore.setState({
allowedImports: -1,
})
}
useEffect(() => {
if (!uuid) return
const text = () => {
switch (buttonText) {
case 'A':
return __('Library', 'extendify')
case 'B':
return __('Add section', 'extendify')
case 'C':
return __('Add template', 'extendify')
}
}
setLibraryButtonText(text())
}, [buttonText, uuid])
useEffect(() => {
if (open) {
setShowTooltip(false)
once.current = true
}
if (!loggedIn && hasPendingNewImports && hasImported) {
once.current || setShowTooltip(true)
once.current = true
}
}, [loggedIn, hasPendingNewImports, hasImported, open])
return (
<>
<MainButton buttonRef={buttonRef} text={libraryButtonText} />
{showTooltip && (
<NewImportsPopover
anchorRef={buttonRef}
onClick={async () => {
await General.ping('mb-tooltip-pressed')
openModal('main-button-tooltip')
}}
onPressX={handleTooltipClose}
/>
)}
</>
)
}
const MainButton = ({ buttonRef, text }) => {
return (
<Button
isPrimary
ref={buttonRef}
style={{ padding: '12px' }}
onClick={() => openModal('main-button')}
id="extendify-templates-inserter-btn"
icon={
<Icon
style={{ marginRight: '4px' }}
icon={brandMark}
size={24}
/>
}>
{text}
</Button>
)
}
export const CtaButton = () => {
return (
<Button
id="extendify-cta-button"
style={{
margin: '1rem 1rem 0',
width: 'calc(100% - 2rem)',
justifyContent: ' center',
}}
onClick={() => openModal('patterns-cta')}
isSecondary>
{__('Discover patterns in Extendify Library', 'extendify')}
</Button>
)
}

View File

@ -0,0 +1,90 @@
import { safeHTML } from '@wordpress/dom'
import { useEffect, memo } from '@wordpress/element'
import { __, _n, _x, sprintf } from '@wordpress/i18n'
import { Icon } from '@wordpress/icons'
import classNames from 'classnames'
import { General } from '@extendify/api/General'
import { User as UserApi } from '@extendify/api/User'
import { useUserStore } from '@extendify/state/User'
import { brandMark } from './icons'
export const SidebarNotice = memo(function SidebarNotice() {
const remainingImports = useUserStore((state) => state.remainingImports)
const allowedImports = useUserStore((state) => state.allowedImports)
const count = remainingImports()
const link = `https://www.extendify.com/pricing/?utm_source=${encodeURIComponent(
window.extendifyData.sdk_partner,
)}&utm_medium=library&utm_campaign=import-counter&utm_content=get-more&utm_term=${
count > 0 ? 'has-imports' : 'no-imports'
}&utm_group=${useUserStore.getState().activeTestGroupsUtmValue()}`
useEffect(() => {
if (allowedImports < 1 || !allowedImports) {
const fallback = 5
UserApi.allowedImports()
.then((allowedImports) => {
allowedImports = /^[1-9]\d*$/.test(allowedImports)
? allowedImports
: fallback
useUserStore.setState({ allowedImports })
})
.catch(() =>
useUserStore.setState({ allowedImports: fallback }),
)
}
}, [allowedImports])
if (!allowedImports) {
return null
}
return (
<a
target="_blank"
className="absolute bottom-4 left-0 mx-5 block bg-white rounded border-solid border border-gray-200 p-4 text-left no-underline group button-focus"
rel="noreferrer"
onClick={async () => await General.ping('fp-sb-click')}
href={link}>
<span className="flex -ml-1.5 space-x-1.5">
<Icon icon={brandMark} />
<span className="mb-1 text-gray-800 font-medium text-sm">
{__('Free Plan', 'extendify')}
</span>
</span>
<span className="text-gray-700 block ml-6 mb-1.5">
{sprintf(
_n(
'You have %s free pattern and layout import remaining this month.',
'You have %s free pattern and layout imports remaining this month.',
count,
'extendify',
),
count,
)}
</span>
<span
className={classNames(
'block font-semibold ml-6 text-sm group-hover:underline',
{
'text-red-500': count < 2,
'text-wp-theme-500': count > 1,
},
)}
dangerouslySetInnerHTML={{
__html: safeHTML(
sprintf(
_x(
'Upgrade today %s',
'The replacement string is a right arrow and context is not lost if removed.',
'extendify',
),
`<span class="text-base">
${String.fromCharCode(8250)}
</span>`,
),
),
}}
/>
</a>
)
})

View File

@ -0,0 +1,241 @@
import { useEffect, useState, useRef, useMemo } from '@wordpress/element'
import { __ } from '@wordpress/i18n'
import classNames from 'classnames'
import Fuse from 'fuse.js'
import { useTemplatesStore } from '@extendify/state/Templates'
import { useUserStore } from '@extendify/state/User'
const searchMemo = new Map()
export const SiteTypeSelector = ({ value, setValue, terms }) => {
const preferredOptionsHistory = useUserStore(
(state) =>
state.preferredOptionsHistory?.siteType?.filter((t) => t.slug) ??
{},
)
const searchParams = useTemplatesStore((state) => state.searchParams)
const [expanded, setExpanded] = useState(false)
const searchRef = useRef()
const [fuzzy, setFuzzy] = useState({})
const [tempValue, setTempValue] = useState('')
const [visibleChoices, setVisibleChoices] = useState([])
const examples = useMemo(() => {
return terms
.filter((t) => t?.featured)
.sort((a, b) => {
if (a.slug < b.slug) return -1
if (a.slug > b.slug) return 1
return 0
})
}, [terms])
const updateSearch = (term) => {
setTempValue(term)
filter(term)
}
const filter = (term = '') => {
if (searchMemo.has(term)) {
setVisibleChoices(searchMemo.get(term))
return
}
const results = fuzzy.search(term)
searchMemo.set(
term,
results?.length ? results.map((t) => t.item) : examples,
)
setVisibleChoices(searchMemo.get(term))
}
const showRecent = () =>
visibleChoices === examples &&
Object.keys(preferredOptionsHistory).length > 0
const unknown = value.slug === 'unknown' || !value?.slug
useEffect(() => {
setFuzzy(
new Fuse(terms, {
keys: ['slug', 'title', 'keywords'],
minMatchCharLength: 2,
threshold: 0.3,
}),
)
}, [terms])
useEffect(() => {
if (!tempValue?.length) setVisibleChoices(examples)
}, [examples, tempValue])
useEffect(() => {
expanded && searchRef.current.focus()
}, [expanded])
const contentHeader = (description) => {
return (
<>
<span className="flex flex-col text-left">
<span className="mb-1 text-sm">
{__('Site Type', 'extendify')}
</span>
<span className="text-xs font-light">{description}</span>
</span>
<span className="flex items-center space-x-4">
{unknown && !expanded && (
<svg
className="text-wp-alert-red"
aria-hidden="true"
focusable="false"
width="21"
height="21"
viewBox="0 0 21 21"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
className="stroke-current"
d="M10.9982 4.05371C7.66149 4.05371 4.95654 6.75866 4.95654 10.0954C4.95654 13.4321 7.66149 16.137 10.9982 16.137C14.3349 16.137 17.0399 13.4321 17.0399 10.0954C17.0399 6.75866 14.3349 4.05371 10.9982 4.05371V4.05371Z"
strokeWidth="1.25"
/>
<path
className="fill-current"
d="M10.0205 12.8717C10.0205 12.3287 10.4508 11.8881 10.9938 11.8881C11.5368 11.8881 11.9774 12.3287 11.9774 12.8717C11.9774 13.4147 11.5368 13.8451 10.9938 13.8451C10.4508 13.8451 10.0205 13.4147 10.0205 12.8717Z"
/>
<path
className="fill-current"
d="M11.6495 10.2591C11.6086 10.6177 11.3524 10.9148 10.9938 10.9148C10.625 10.9148 10.3791 10.6074 10.3483 10.2591L10.0205 7.31855C9.95901 6.81652 10.4918 6.34521 10.9938 6.34521C11.4959 6.34521 12.0286 6.81652 11.9774 7.31855L11.6495 10.2591Z"
/>
</svg>
)}
<svg
className={classNames('stroke-current text-gray-700', {
'-translate-x-1 rotate-90 transform': expanded,
})}
aria-hidden="true"
focusable="false"
width="8"
height="13"
viewBox="0 0 8 13"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M1.24194 11.5952L6.24194 6.09519L1.24194 0.595215"
strokeWidth="1.5"
/>
</svg>
</span>
</>
)
}
const choicesList = (choices, title = __('Suggestions', 'extendify')) => {
if (choices === examples) {
title = __('Examples', 'extendify')
}
return (
<>
<h4 className="mt-4 mb-2 text-left text-xss font-medium uppercase text-gray-700">
{title}
</h4>
<ul className="m-0">
{choices.map((item) => {
const label = item?.title ?? item.slug
const current =
searchParams?.taxonomies?.siteType?.slug ===
item.slug
return (
<li
key={item.slug + item?.title}
className="m-0 mb-1">
<button
type="button"
className={classNames(
'm-0 w-full cursor-pointer bg-transparent pl-0 text-left text-sm hover:text-wp-theme-500',
{ 'text-gray-800': !current },
)}
onClick={() => {
setExpanded(false)
setValue(item)
}}>
{label}
</button>
</li>
)
})}
</ul>
</>
)
}
return (
<div className="w-full rounded bg-extendify-transparent-black">
<button
type="button"
onClick={() => setExpanded((expanded) => !expanded)}
className="button-focus m-0 flex w-full cursor-pointer items-center justify-between rounded bg-transparent p-4 text-gray-800 hover:bg-extendify-transparent-black-100">
{contentHeader(
expanded
? __('What kind of site is this?', 'extendify')
: value?.title ?? value.slug ?? 'Unknown',
)}
</button>
{expanded && (
<div className="max-h-96 overflow-y-auto p-4 pt-0">
<div className="relative my-2">
<label htmlFor="site-type-search" className="sr-only">
{__('Search', 'extendify')}
</label>
<input
ref={searchRef}
id="site-type-search"
value={tempValue ?? ''}
onChange={(event) =>
updateSearch(event.target.value)
}
type="text"
className="button-focus m-0 w-full rounded border-0 bg-white p-3.5 py-2.5 text-sm"
placeholder={__('Search', 'extendify')}
/>
<svg
className="pointer-events-none absolute top-2 right-2 hidden lg:block"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
role="img"
aria-hidden="true"
focusable="false">
<path d="M13.5 6C10.5 6 8 8.5 8 11.5c0 1.1.3 2.1.9 3l-3.4 3 1 1.1 3.4-2.9c1 .9 2.2 1.4 3.6 1.4 3 0 5.5-2.5 5.5-5.5C19 8.5 16.5 6 13.5 6zm0 9.5c-2.2 0-4-1.8-4-4s1.8-4 4-4 4 1.8 4 4-1.8 4-4 4z"></path>
</svg>
</div>
{tempValue?.length > 1 && visibleChoices === examples && (
<p className="text-left">
{__('Nothing found...', 'extendify')}
</p>
)}
{showRecent() && (
<div className="mb-8">
{choicesList(
preferredOptionsHistory,
__('Recent', 'extendify'),
)}
</div>
)}
{visibleChoices?.length > 0 && (
<div>{choicesList(visibleChoices)}</div>
)}
{unknown ? null : (
<button
type="button"
className="mt-4 w-full cursor-pointer bg-transparent pl-0 text-left text-sm text-wp-theme-500 hover:text-wp-theme-500"
onClick={() => {
setExpanded(false)
setValue({ slug: '', title: 'Unknown' })
}}>
{__('Reset', 'extendify')}
</button>
)}
</div>
)}
</div>
)
}

View File

@ -0,0 +1,49 @@
import { PanelBody, PanelRow } from '@wordpress/components'
import classNames from 'classnames'
import { useTemplatesStore } from '@extendify/state/Templates'
import { getTaxonomyName } from '@extendify/util/general'
export default function TaxonomySection({ taxType, taxonomies, taxLabel }) {
const updateTaxonomies = useTemplatesStore(
(state) => state.updateTaxonomies,
)
const searchParams = useTemplatesStore((state) => state.searchParams)
if (!taxonomies?.length > 0) return null
return (
<PanelBody
title={getTaxonomyName(taxLabel ?? taxType)}
className="ext-type-control p-0"
initialOpen={true}>
<PanelRow>
<div className="relative w-full overflow-hidden">
<ul className="m-0 w-full px-5 py-1">
{taxonomies.map((tax) => {
const isCurrentTax =
searchParams?.taxonomies[taxType]?.slug ===
tax?.slug
return (
<li className="m-0 w-full" key={tax.slug}>
<button
type="button"
className="button-focus m-0 flex w-full cursor-pointer items-center justify-between bg-transparent px-0 py-2 text-left text-sm leading-none transition duration-200 hover:text-wp-theme-500"
onClick={() =>
updateTaxonomies({ [taxType]: tax })
}>
<span
className={classNames({
'text-wp-theme-500':
isCurrentTax,
})}>
{tax?.title ?? tax.slug}
</span>
</button>
</li>
)
})}
</ul>
</div>
</PanelRow>
</PanelBody>
)
}

View File

@ -0,0 +1,37 @@
import { __ } from '@wordpress/i18n'
import classNames from 'classnames'
import { useGlobalStore } from '@extendify/state/GlobalState'
import { useTemplatesStore } from '@extendify/state/Templates'
export const TypeSelect = ({ className }) => {
const updateType = useTemplatesStore((state) => state.updateType)
const currentType = useGlobalStore(
(state) => state?.currentType ?? 'pattern',
)
return (
<div className={className}>
<h4 className="sr-only">{__('Type select', 'extendify')}</h4>
<button
type="button"
className={classNames({
'button-focus m-0 min-w-sm cursor-pointer rounded-tl-sm rounded-bl-sm border border-black py-2.5 px-4 text-xs leading-none': true,
'bg-gray-900 text-white': currentType === 'pattern',
'bg-transparent text-black': currentType !== 'pattern',
})}
onClick={() => updateType('pattern')}>
<span className="">{__('Patterns', 'extendify')}</span>
</button>
<button
type="button"
className={classNames({
'outline-none button-focus m-0 -ml-px min-w-sm cursor-pointer items-center rounded-tr-sm rounded-br-sm border border-black py-2.5 px-4 text-xs leading-none': true,
'bg-gray-900 text-white': currentType === 'template',
'bg-transparent text-black': currentType !== 'template',
})}
onClick={() => updateType('template')}>
<span className="">{__('Page Layouts', 'extendify')}</span>
</button>
</div>
)
}

View File

@ -0,0 +1,15 @@
export { default as alert } from './library/alert'
export { default as brandBlockIcon } from './library/brand-block-icon'
export { default as brandMark } from './library/brand-mark'
export { default as brandLogo } from './library/brand-logo'
export { default as diamond } from './library/diamond'
export { default as download } from './library/download'
export { default as download2 } from './library/download2'
export { default as featured } from './library/featured'
export { default as growthArrow } from './library/growth-arrow'
export { default as layouts } from './library/layouts'
export { default as patterns } from './library/patterns'
export { default as success } from './library/success'
export { default as support } from './library/support'
export { default as star } from './library/star'
export { default as user } from './library/user'

View File

@ -0,0 +1,25 @@
/**
* WordPress dependencies
*/
import { Path, SVG } from '@wordpress/primitives'
const alert = (
<SVG viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg">
<Path
d="M7.32457 0.907043C3.98785 0.907043 1.2829 3.61199 1.2829 6.94871C1.2829 10.2855 3.98785 12.9904 7.32457 12.9904C10.6613 12.9904 13.3663 10.2855 13.3663 6.94871C13.3663 3.61199 10.6613 0.907043 7.32457 0.907043V0.907043Z"
stroke="currentColor"
strokeWidth="1.25"
fill="none"
/>
<Path
d="M6.34684 9.72526C6.34684 9.18224 6.77716 8.74168 7.32018 8.74168C7.8632 8.74168 8.30377 9.18224 8.30377 9.72526C8.30377 10.2683 7.8632 10.6986 7.32018 10.6986C6.77716 10.6986 6.34684 10.2683 6.34684 9.72526Z"
fill="currentColor"
/>
<Path
d="M7.9759 7.11261C7.93492 7.47121 7.67878 7.76834 7.32018 7.76834C6.95134 7.76834 6.70544 7.46097 6.6747 7.11261L6.34684 4.1721C6.28537 3.67006 6.81814 3.19876 7.32018 3.19876C7.82222 3.19876 8.35499 3.67006 8.30377 4.1721L7.9759 7.11261Z"
fill="currentColor"
/>
</SVG>
)
export default alert

View File

@ -0,0 +1,17 @@
/**
* WordPress dependencies
*/
import { Path, SVG } from '@wordpress/primitives'
const brandBlockIcon = (
<SVG fill="none" viewBox="0 0 25 24" xmlns="http://www.w3.org/2000/svg">
<Path
clipRule="evenodd"
d="m14.4063 2h4.1856c1.1856 0 1.6147.12701 2.0484.36409.4336.23802.7729.58706 1.0049 1.03111.2319.445.3548.8853.3548 2.10175v4.29475c0 1.2165-.1238 1.6567-.3548 2.1017-.232.445-.5722.7931-1.0049 1.0312-.1939.1064-.3873.1939-.6476.2567v3.4179c0 1.8788-.1912 2.5588-.5481 3.246-.3582.6873-.8836 1.2249-1.552 1.5925-.6697.3676-1.3325.5623-3.1634.5623h-6.46431c-1.83096 0-2.49367-.1962-3.16346-.5623-.6698-.3676-1.19374-.9067-1.552-1.5925s-.54943-1.3672-.54943-3.246v-6.63138c0-1.87871.19117-2.55871.54801-3.24597.35827-.68727.88362-1.22632 1.55342-1.59393.66837-.36615 1.3325-.56231 3.16346-.56231h2.76781c.0519-.55814.1602-.86269.3195-1.16946.232-.445.5721-.79404 1.0058-1.03206.4328-.23708.8628-.36409 2.0483-.36409zm-2.1512 2.73372c0-.79711.6298-1.4433 1.4067-1.4433h5.6737c.777 0 1.4068.64619 1.4068 1.4433v5.82118c0 .7971-.6298 1.4433-1.4068 1.4433h-5.6737c-.7769 0-1.4067-.6462-1.4067-1.4433z"
fill="currentColor"
fillRule="evenodd"
/>
</SVG>
)
export default brandBlockIcon

View File

@ -0,0 +1,32 @@
/**
* WordPress dependencies
*/
import { Path, SVG, G } from '@wordpress/primitives'
const brandLogo = (
<SVG
fill="none"
width="150"
height="30"
viewBox="0 0 2524 492"
xmlns="http://www.w3.org/2000/svg">
<G fill="currentColor">
<Path d="m609.404 378.5c-24.334 0-46-5.5-65-16.5-18.667-11.333-33.334-26.667-44-46-10.667-19.667-16-42.167-16-67.5 0-25.667 5.166-48.333 15.5-68 10.333-19.667 24.833-35 43.5-46 18.666-11.333 40-17 64-17 25 0 46.5 5.333 64.5 16 18 10.333 31.833 24.833 41.5 43.5 10 18.667 15 41 15 67v18.5l-212 .5 1-39h150.5c0-17-5.5-30.667-16.5-41-10.667-10.333-25.167-15.5-43.5-15.5-14.334 0-26.5 3-36.5 9-9.667 6-17 15-22 27s-7.5 26.667-7.5 44c0 26.667 5.666 46.833 17 60.5 11.666 13.667 28.833 20.5 51.5 20.5 16.666 0 30.333-3.167 41-9.5 11-6.333 18.166-15.333 21.5-27h56.5c-5.334 27-18.667 48.167-40 63.5-21 15.333-47.667 23-80 23z" />
<path d="m797.529 372h-69.5l85-121-85-126h71l54.5 84 52.5-84h68.5l-84 125.5 81.5 121.5h-70l-53-81.5z" />
<path d="m994.142 125h155.998v51h-155.998zm108.498 247h-61v-324h61z" />
<path d="m1278.62 378.5c-24.33 0-46-5.5-65-16.5-18.66-11.333-33.33-26.667-44-46-10.66-19.667-16-42.167-16-67.5 0-25.667 5.17-48.333 15.5-68 10.34-19.667 24.84-35 43.5-46 18.67-11.333 40-17 64-17 25 0 46.5 5.333 64.5 16 18 10.333 31.84 24.833 41.5 43.5 10 18.667 15 41 15 67v18.5l-212 .5 1-39h150.5c0-17-5.5-30.667-16.5-41-10.66-10.333-25.16-15.5-43.5-15.5-14.33 0-26.5 3-36.5 9-9.66 6-17 15-22 27s-7.5 26.667-7.5 44c0 26.667 5.67 46.833 17 60.5 11.67 13.667 28.84 20.5 51.5 20.5 16.67 0 30.34-3.167 41-9.5 11-6.333 18.17-15.333 21.5-27h56.5c-5.33 27-18.66 48.167-40 63.5-21 15.333-47.66 23-80 23z" />
<path d="m1484.44 372h-61v-247h56.5l5 32c7.67-12.333 18.5-22 32.5-29 14.34-7 29.84-10.5 46.5-10.5 31 0 54.34 9.167 70 27.5 16 18.333 24 43.333 24 75v152h-61v-137.5c0-20.667-4.66-36-14-46-9.33-10.333-22-15.5-38-15.5-19 0-33.83 6-44.5 18-10.66 12-16 28-16 48z" />
<path d="m1798.38 378.5c-24 0-44.67-5.333-62-16-17-11-30.34-26.167-40-45.5-9.34-19.333-14-41.833-14-67.5s4.66-48.333 14-68c9.66-20 23.5-35.667 41.5-47s39.33-17 64-17c17.33 0 33.16 3.5 47.5 10.5 14.33 6.667 25.33 16.167 33 28.5v-156.5h60.5v372h-56l-4-38.5c-7.34 14-18.67 25-34 33-15 8-31.84 12-50.5 12zm13.5-56c14.33 0 26.66-3 37-9 10.33-6.333 18.33-15.167 24-26.5 6-11.667 9-24.833 9-39.5 0-15-3-28-9-39-5.67-11.333-13.67-20.167-24-26.5-10.34-6.667-22.67-10-37-10-14 0-26.17 3.333-36.5 10-10.34 6.333-18.34 15.167-24 26.5-5.34 11.333-8 24.333-8 39s2.66 27.667 8 39c5.66 11.333 13.66 20.167 24 26.5 10.33 6.333 22.5 9.5 36.5 9.5z" />
<path d="m1996.45 372v-247h61v247zm30-296.5c-10.34 0-19.17-3.5-26.5-10.5-7-7.3333-10.5-16.1667-10.5-26.5s3.5-19 10.5-26c7.33-6.99999 16.16-10.49998 26.5-10.49998 10.33 0 19 3.49999 26 10.49998 7.33 7 11 15.6667 11 26s-3.67 19.1667-11 26.5c-7 7-15.67 10.5-26 10.5z" />
<path d="m2085.97 125h155v51h-155zm155.5-122.5v52c-3.33 0-6.83 0-10.5 0-3.33 0-6.83 0-10.5 0-15.33 0-25.67 3.6667-31 11-5 7.3333-7.5 17.1667-7.5 29.5v277h-60.5v-277c0-22.6667 3.67-40.8333 11-54.5 7.33-14 17.67-24.1667 31-30.5 13.33-6.66666 28.83-10 46.5-10 5 0 10.17.166671 15.5.5 5.67.333329 11 .99999 16 2z" />
<path d="m2330.4 125 80.5 228-33 62.5-112-290.5zm-58 361.5v-50.5h36.5c8 0 15-1 21-3 6-1.667 11.34-5 16-10 5-5 9.17-12.333 12.5-22l102.5-276h63l-121 302c-9 22.667-20.33 39.167-34 49.5-13.66 10.333-30.66 15.5-51 15.5-8.66 0-16.83-.5-24.5-1.5-7.33-.667-14.33-2-21-4z" />
<path
clipRule="evenodd"
d="m226.926 25.1299h83.271c23.586 0 32.123 2.4639 40.751 7.0633 8.628 4.6176 15.378 11.389 19.993 20.0037 4.615 8.6329 7.059 17.1746 7.059 40.7738v83.3183c0 23.599-2.463 32.141-7.059 40.774-4.615 8.633-11.383 15.386-19.993 20.003-3.857 2.065-7.704 3.764-12.884 4.981v66.308c0 36.447-3.803 49.639-10.902 62.972-7.128 13.333-17.579 23.763-30.877 30.894-13.325 7.132-26.51 10.909-62.936 10.909h-128.605c-36.4268 0-49.6113-3.805-62.9367-10.909-13.3254-7.131-23.749-17.589-30.8765-30.894-7.12757-13.304-10.9308-26.525-10.9308-62.972v-128.649c0-36.447 3.80323-49.639 10.9026-62.972 7.1275-13.333 17.5793-23.7909 30.9047-30.9224 13.2972-7.1034 26.5099-10.9088 62.9367-10.9088h55.064c1.033-10.8281 3.188-16.7362 6.357-22.6877 4.615-8.6329 11.382-15.4043 20.01-20.0219 8.61-4.5994 17.165-7.0633 40.751-7.0633zm-42.798 53.0342c0-15.464 12.53-28 27.986-28h112.877c15.457 0 27.987 12.536 27.987 28v112.9319c0 15.464-12.53 28-27.987 28h-112.877c-15.456 0-27.986-12.536-27.986-28z"
fillRule="evenodd"
/>
</G>
</SVG>
)
export default brandLogo

View File

@ -0,0 +1,17 @@
/**
* WordPress dependencies
*/
import { Path, SVG } from '@wordpress/primitives'
const brandMark = (
<SVG fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<Path
clipRule="evenodd"
d="m13.505 4h3.3044c.936 0 1.2747.10161 1.6171.29127.3424.19042.6102.46965.7934.82489.1831.356.2801.70824.2801 1.6814v3.43584c0 .9731-.0977 1.3254-.2801 1.6814-.1832.356-.4517.6344-.7934.8248-.153.0852-.3057.1552-.5112.2054v2.7344c0 1.503-.151 2.047-.4327 2.5968-.2828.5498-.6976.9799-1.2252 1.274-.5288.294-1.052.4498-2.4975.4498h-5.10341c-1.44549 0-1.96869-.1569-2.49747-.4498-.52878-.2941-.94242-.7254-1.22526-1.274-.28284-.5487-.43376-1.0938-.43376-2.5968v-5.3051c0-1.50301.15092-2.04701.43264-2.59682.28284-.54981.6976-.98106 1.22638-1.27514.52767-.29293 1.05198-.44985 2.49747-.44985h2.18511c.041-.44652.1265-.69015.2522-.93557.1832-.356.4517-.63523.7941-.82565.3417-.18966.6812-.29127 1.6171-.29127zm-1.6984 2.18698c0-.63769.4973-1.15464 1.1106-1.15464h4.4793c.6133 0 1.1106.51695 1.1106 1.15464v4.65692c0 .6377-.4973 1.1547-1.1106 1.1547h-4.4793c-.6133 0-1.1106-.517-1.1106-1.1547z"
fill="currentColor"
fillRule="evenodd"
/>
</SVG>
)
export default brandMark

View File

@ -0,0 +1,20 @@
/**
* WordPress dependencies
*/
import { SVG } from '@wordpress/primitives'
const alert = (
<SVG
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg">
<path
d="m11.9893 2.59931c-.1822.00285-.3558.07789-.4827.20864s-.1967.30653-.1941.48871v1.375c-.0013.0911.0156.18155.0495.26609.034.08454.0844.16149.1484.22637s.1402.11639.2242.15156c.0841.03516.1743.05327.2654.05327s.1813-.01811.2654-.05327c.084-.03517.1603-.08668.2242-.15156.064-.06488.1144-.14183.1484-.22637s.0508-.17499.0495-.26609v-1.375c.0013-.09202-.0158-.18337-.0505-.26863-.0346-.08526-.086-.1627-.1511-.22773s-.1426-.11633-.2279-.15085c-.0853-.03453-.1767-.05158-.2687-.05014zm-5.72562.46013c-.1251.00033-.24775.0348-.35471.09968-.10697.06488-.19421.15771-.25232.2685-.05812.1108-.0849.23534-.07747.36023.00744.12488.0488.24537.11964.34849l.91667 1.375c.04939.07667.11354.14274.18872.19437.07517.05164.15987.0878.24916.10639.08928.01858.18137.01922.27091.00187.08953-.01734.17472-.05233.2506-.10292.07589-.05059.14095-.11577.1914-.19174.05045-.07598.08528-.16123.10246-.2508.01719-.08956.01638-.18165-.00237-.2709s-.05507-.17388-.10684-.24897l-.91666-1.375c-.06252-.09667-.14831-.1761-.2495-.231-.1012-.0549-.21456-.08351-.32969-.0832zm11.45212 0c-.1117.00307-.2209.03329-.3182.08804-.0973.05474-.1798.13237-.2404.22616l-.9167 1.375c-.0518.07509-.0881.15972-.1068.24897-.0188.08925-.0196.18134-.0024.2709.0172.08957.052.17482.1024.2508.0505.07597.1156.14115.1914.19174.0759.05059.1611.08558.2506.10292.0896.01735.1817.01671.271-.00187.0892-.01859.1739-.05475.2491-.10639.0752-.05163.1393-.1177.1887-.19437l.9167-1.375c.0719-.10456.1135-.22698.1201-.3537s-.022-.25281-.0826-.36429c-.0606-.11149-.1508-.20403-.2608-.26738-.11-.06334-.2353-.09502-.3621-.09153zm-9.61162 3.67472c-.09573-.00001-.1904.01998-.27795.05867-.08756.03869-.16607.09524-.23052.16602l-4.58333 5.04165c-.11999.1319-.18407.3052-.17873.4834.00535.1782.0797.3473.20738.4718l8.47917 8.25c.1284.1251.3006.1951.4798.1951.1793 0 .3514-.07.4798-.1951l8.4792-8.25c.1277-.1245.202-.2936.2074-.4718.0053-.1782-.0588-.3515-.1788-.4834l-4.5833-5.04165c-.0644-.07078-.1429-.12733-.2305-.16602s-.1822-.05868-.278-.05867h-3.877zm.30436 1.375h2.21646l-2.61213 3.48314c-.04258.0557-.07639.1176-.10026.1835h-2.83773zm4.96646 0h2.2165l3.3336 3.66664h-2.8368c-.0241-.066-.0582-.1278-.1011-.1835zm-1.375.45833 2.4063 3.20831h-4.81254zm-6.78637 4.58331h2.70077c.00665.0188.01412.0374.02238.0555l2.11442 4.6505zm4.20826 0h5.15621l-2.5781 5.6719zm6.66371 0h2.7008l-4.8376 4.706 2.1144-4.6505c.0083-.0181.0158-.0367.0224-.0555z"
fill="#000"
/>
</SVG>
)
export default alert

View File

@ -0,0 +1,21 @@
/**
* WordPress dependencies
*/
import { Path, SVG } from '@wordpress/primitives'
const download = (
<SVG viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<Path
d="M7.32457 0.907043C3.98785 0.907043 1.2829 3.61199 1.2829 6.94871C1.2829 10.2855 3.98785 12.9904 7.32457 12.9904C10.6613 12.9904 13.3663 10.2855 13.3663 6.94871C13.3663 3.61199 10.6613 0.907043 7.32457 0.907043V0.907043Z"
stroke="white"
strokeWidth="1.25"
/>
<Path
d="M7.32458 10.0998L4.82458 7.59977M7.32458 10.0998V3.79764V10.0998ZM7.32458 10.0998L9.82458 7.59977L7.32458 10.0998Z"
stroke="white"
strokeWidth="1.25"
/>
</SVG>
)
export default download

View File

@ -0,0 +1,18 @@
/**
* WordPress dependencies
*/
import { SVG } from '@wordpress/primitives'
const download2 = (
<SVG viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M7.93298 20.2773L17.933 20.2773C18.1982 20.2773 18.4526 20.172 18.6401 19.9845C18.8276 19.7969 18.933 19.5426 18.933 19.2773C18.933 19.0121 18.8276 18.7578 18.6401 18.5702C18.4526 18.3827 18.1982 18.2773 17.933 18.2773L7.93298 18.2773C7.66777 18.2773 7.41341 18.3827 7.22588 18.5702C7.03834 18.7578 6.93298 19.0121 6.93298 19.2773C6.93298 19.5426 7.03834 19.7969 7.22588 19.9845C7.41341 20.172 7.66777 20.2773 7.93298 20.2773Z"
fill="white"
/>
<path
d="M12.933 4.27734C12.6678 4.27734 12.4134 4.3827 12.2259 4.57024C12.0383 4.75777 11.933 5.01213 11.933 5.27734L11.933 12.8673L9.64298 10.5773C9.55333 10.4727 9.44301 10.3876 9.31895 10.3276C9.19488 10.2676 9.05975 10.2339 8.92203 10.2285C8.78431 10.2232 8.64698 10.2464 8.51865 10.2967C8.39033 10.347 8.27378 10.4232 8.17632 10.5207C8.07887 10.6181 8.00261 10.7347 7.95234 10.863C7.90206 10.9913 7.87886 11.1287 7.88418 11.2664C7.8895 11.4041 7.92323 11.5392 7.98325 11.6633C8.04327 11.7874 8.12829 11.8977 8.23297 11.9873L12.233 15.9873C12.3259 16.0811 12.4365 16.1555 12.5584 16.2062C12.6803 16.257 12.811 16.2831 12.943 16.2831C13.075 16.2831 13.2057 16.257 13.3276 16.2062C13.4494 16.1555 13.56 16.0811 13.653 15.9873L17.653 11.9873C17.8168 11.796 17.9024 11.55 17.8927 11.2983C17.883 11.0466 17.7786 10.8079 17.6005 10.6298C17.4224 10.4517 17.1837 10.3474 16.932 10.3376C16.6804 10.3279 16.4343 10.4135 16.243 10.5773L13.933 12.8673L13.933 5.27734C13.933 5.01213 13.8276 4.75777 13.6401 4.57024C13.4525 4.3827 13.1982 4.27734 12.933 4.27734Z"
fill="white"
/>
</SVG>
)
export default download2

View File

@ -0,0 +1,21 @@
/**
* WordPress dependencies
*/
import { Path, SVG, G } from '@wordpress/primitives'
const featured = (
<SVG fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<Path
d="m11.2721 16.9866.6041 2.2795.6042-2.2795.6213-2.3445c.0001-.0002.0001-.0004.0002-.0006.2404-.9015.8073-1.5543 1.4638-1.8165.0005-.0002.0009-.0004.0013-.0006l1.9237-.7555 1.4811-.5818-1.4811-.5817-1.9264-.7566c0-.0001-.0001-.0001-.0001-.0001-.0001 0-.0001 0-.0001 0-.654-.25727-1.2213-.90816-1.4621-1.81563-.0001-.00006-.0001-.00011-.0001-.00017l-.6215-2.34519-.6042-2.27947-.6041 2.27947-.6216 2.34519v.00017c-.2409.90747-.80819 1.55836-1.46216 1.81563-.00002 0-.00003 0-.00005 0-.00006 0-.00011 0-.00017.0001l-1.92637.7566-1.48108.5817 1.48108.5818 1.92637.7566c.00007 0 .00015.0001.00022.0001.65397.2572 1.22126.9082 1.46216 1.8156v.0002z"
stroke="currentColor"
strokeWidth="1.25"
fill="none"
/>
<G fill="currentColor">
<Path d="m18.1034 18.3982-.2787.8625-.2787-.8625c-.1314-.4077-.4511-.7275-.8589-.8589l-.8624-.2786.8624-.2787c.4078-.1314.7275-.4512.8589-.8589l.2787-.8624.2787.8624c.1314.4077.4511.7275.8589.8589l.8624.2787-.8624.2786c-.4078.1314-.7269.4512-.8589.8589z" />
<Path d="m6.33141 6.97291-.27868.86242-.27867-.86242c-.13142-.40775-.45116-.72749-.8589-.85891l-.86243-.27867.86243-.27868c.40774-.13141.72748-.45115.8589-.8589l.27867-.86242.27868.86242c.13142.40775.45116.72749.8589.8589l.86242.27868-.86242.27867c-.40774.13142-.7269.45116-.8589.85891z" />
</G>
</SVG>
)
export default featured

View File

@ -0,0 +1,17 @@
import { Path, SVG } from '@wordpress/primitives'
const growthArrow = (
<SVG
fill="none"
height="25"
viewBox="0 0 25 25"
width="25"
xmlns="http://www.w3.org/2000/svg">
<Path
d="m16.2382 9.17969.7499.00645.0066-.75988-.7599.00344zm-5.5442.77506 5.5475-.02507-.0067-1.49998-5.5476.02506zm4.7942-.78152-.0476 5.52507 1.5.0129.0475-5.52506zm.2196-.52387-7.68099 7.68104 1.06066 1.0606 7.68103-7.68098z"
fill="currentColor"
/>
</SVG>
)
export default growthArrow

View File

@ -0,0 +1,20 @@
/**
* WordPress dependencies
*/
import { Path, SVG, G } from '@wordpress/primitives'
const layouts = (
<SVG
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg">
<G stroke="currentColor" strokeWidth="1.5">
<Path d="m6 4.75h12c.6904 0 1.25.55964 1.25 1.25v12c0 .6904-.5596 1.25-1.25 1.25h-12c-.69036 0-1.25-.5596-1.25-1.25v-12c0-.69036.55964-1.25 1.25-1.25z" />
<Path d="m9.25 19v-14" />
</G>
</SVG>
)
export default layouts

View File

@ -0,0 +1,54 @@
/**
* WordPress dependencies
*/
import { Path, SVG } from '@wordpress/primitives'
const patterns = (
<SVG
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<Path
fillRule="evenodd"
clipRule="evenodd"
d="M7.49271 18.0092C6.97815 17.1176 7.28413 15.9755 8.17569 15.4609C9.06724 14.946 10.2094 15.252 10.7243 16.1435C11.2389 17.0355 10.9329 18.1772 10.0413 18.6922C9.14978 19.2071 8.00764 18.9011 7.49271 18.0092V18.0092Z"
fill="currentColor"
/>
<Path
fillRule="evenodd"
clipRule="evenodd"
d="M16.5073 6.12747C17.0218 7.01903 16.7158 8.16117 15.8243 8.67573C14.9327 9.19066 13.7906 8.88467 13.2757 7.99312C12.7611 7.10119 13.0671 5.95942 13.9586 5.44449C14.8502 4.92956 15.9923 5.23555 16.5073 6.12747V6.12747Z"
fill="currentColor"
/>
<Path
fillRule="evenodd"
clipRule="evenodd"
d="M4.60135 11.1355C5.11628 10.2439 6.25805 9.93793 7.14998 10.4525C8.04153 10.9674 8.34752 12.1096 7.83296 13.0011C7.31803 13.8927 6.17588 14.1987 5.28433 13.6841C4.39278 13.1692 4.08679 12.0274 4.60135 11.1355V11.1355Z"
fill="currentColor"
/>
<Path
fillRule="evenodd"
clipRule="evenodd"
d="M19.3986 13.0011C18.8837 13.8927 17.7419 14.1987 16.85 13.6841C15.9584 13.1692 15.6525 12.027 16.167 11.1355C16.682 10.2439 17.8241 9.93793 18.7157 10.4525C19.6072 10.9674 19.9132 12.1092 19.3986 13.0011V13.0011Z"
fill="currentColor"
/>
<Path
d="M9.10857 8.92594C10.1389 8.92594 10.9742 8.09066 10.9742 7.06029C10.9742 6.02992 10.1389 5.19464 9.10857 5.19464C8.0782 5.19464 7.24292 6.02992 7.24292 7.06029C7.24292 8.09066 8.0782 8.92594 9.10857 8.92594Z"
fill="currentColor"
/>
<Path
d="M14.8913 18.942C15.9217 18.942 16.7569 18.1067 16.7569 17.0763C16.7569 16.046 15.9217 15.2107 14.8913 15.2107C13.8609 15.2107 13.0256 16.046 13.0256 17.0763C13.0256 18.1067 13.8609 18.942 14.8913 18.942Z"
fill="currentColor"
/>
<Path
fillRule="evenodd"
clipRule="evenodd"
d="M10.3841 13.0011C9.86951 12.1096 10.1755 10.9674 11.067 10.4525C11.9586 9.93793 13.1007 10.2439 13.6157 11.1355C14.1302 12.0274 13.8242 13.1692 12.9327 13.6841C12.0411 14.1987 10.899 13.8927 10.3841 13.0011V13.0011Z"
fill="currentColor"
/>
</SVG>
)
export default patterns

View File

@ -0,0 +1,20 @@
/**
* WordPress dependencies
*/
import { Path, SVG } from '@wordpress/primitives'
const star = (
<SVG
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg">
<Path
d="m11.7758 3.45425c.0917-.18582.3567-.18581.4484 0l2.3627 4.78731c.0364.07379.1068.12493.1882.13676l5.2831.76769c.2051.02979.287.28178.1386.42642l-3.8229 3.72637c-.0589.0575-.0858.1402-.0719.2213l.9024 5.2618c.0351.2042-.1793.36-.3627.2635l-4.7254-2.4842c-.0728-.0383-.1598-.0383-.2326 0l-4.7254 2.4842c-.18341.0965-.39776-.0593-.36274-.2635l.90247-5.2618c.01391-.0811-.01298-.1638-.0719-.2213l-3.8229-3.72637c-.14838-.14464-.0665-.39663.13855-.42642l5.28312-.76769c.08143-.01183.15182-.06297.18823-.13676z"
fill="currentColor"
/>
</SVG>
)
export default star

View File

@ -0,0 +1,70 @@
/**
* WordPress dependencies
*/
import { Path, SVG, G, Circle, Rect } from '@wordpress/primitives'
const download = (
<SVG
fill="none"
viewBox="0 0 151 148"
width="151"
xmlns="http://www.w3.org/2000/svg">
<Circle cx="65.6441" cy="66.6114" fill="#0b4a43" r="65.3897" />
<G fill="#cbc3f5" stroke="#0b4a43">
<Path
d="m61.73 11.3928 3.0825 8.3304.1197.3234.3234.1197 8.3304 3.0825-8.3304 3.0825-.3234.1197-.1197.3234-3.0825 8.3304-3.0825-8.3304-.1197-.3234-.3234-.1197-8.3304-3.0825 8.3304-3.0825.3234-.1197.1197-.3234z"
strokeWidth="1.5"
/>
<Path
d="m84.3065 31.2718c0 5.9939-12.4614 22.323-18.6978 22.323h-17.8958v56.1522c3.5249.9 11.6535 0 17.8958 0h6.2364c11.2074 3.33 36.0089 7.991 45.5529 0l-9.294-62.1623c-2.267-1.7171-5.949-6.6968-2.55-12.8786 3.4-6.1817 2.55-18.0406 0-24.5756-1.871-4.79616-8.3289-8.90882-14.4482-8.90882s-7.0825 4.00668-6.7993 6.01003z"
strokeWidth="1.75"
/>
<Rect
height="45.5077"
rx="9.13723"
strokeWidth="1.75"
transform="matrix(0 1 -1 0 191.5074 -96.0026)"
width="18.2745"
x="143.755"
y="47.7524"
/>
<Rect
height="42.3038"
rx="8.73674"
strokeWidth="1.75"
transform="matrix(0 1 -1 0 241.97 -50.348)"
width="17.4735"
x="146.159"
y="95.811"
/>
<Rect
height="55.9204"
rx="8.73674"
strokeWidth="1.75"
transform="matrix(0 1 -1 0 213.1347 -85.5913)"
width="17.4735"
x="149.363"
y="63.7717"
/>
<Rect
height="51.1145"
rx="8.73674"
strokeWidth="1.75"
transform="matrix(0 1 -1 0 229.1545 -69.5715)"
width="17.4735"
x="149.363"
y="79.7915"
/>
<Path
d="m75.7483 105.349c.9858-25.6313-19.2235-42.0514-32.8401-44.0538v12.0146c8.5438 1.068 24.8303 9.7642 24.8303 36.0442 0 23.228 19.4905 33.374 29.6362 33.641v-10.413s-22.6122-1.602-21.6264-27.233z"
strokeWidth="1.75"
/>
<Path
d="m68.5388 109.354c.9858-25.6312-19.2234-42.0513-32.8401-44.0537v12.0147c8.5438 1.0679 24.8303 9.7641 24.8303 36.044 0 23.228 19.4905 33.374 29.6362 33.641v-10.413s-22.6122-1.602-21.6264-27.233z"
strokeWidth="1.75"
/>
</G>
</SVG>
)
export default download

View File

@ -0,0 +1,39 @@
/**
* WordPress dependencies
*/
import { SVG, Circle } from '@wordpress/primitives'
const layouts = (
<SVG
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<Circle
cx="12"
cy="12"
r="7.25"
stroke="currentColor"
strokeWidth="1.5"
/>
<Circle
cx="12"
cy="12"
r="4.25"
stroke="currentColor"
strokeWidth="1.5"
/>
<Circle
cx="11.9999"
cy="12.2"
r="6"
transform="rotate(-45 11.9999 12.2)"
stroke="currentColor"
strokeWidth="3"
strokeDasharray="1.5 4"
/>
</SVG>
)
export default layouts

View File

@ -0,0 +1,17 @@
/**
* WordPress dependencies
*/
import { Path, SVG } from '@wordpress/primitives'
const user = (
<SVG fill="none" viewBox="0 0 25 25" xmlns="http://www.w3.org/2000/svg">
<Path
clipRule="evenodd"
d="m13 4c4.9545 0 9 4.04545 9 9 0 4.9545-4.0455 9-9 9-4.95455 0-9-4.0455-9-9 0-4.95455 4.04545-9 9-9zm5.0909 13.4545c-1.9545 3.8637-8.22726 3.8637-10.22726 0-.04546-.1818-.04546-.3636 0-.5454 2-3.8636 8.27276-3.8636 10.22726 0 .0909.1818.0909.3636 0 .5454zm-5.0909-8.90905c-1.2727 0-2.3182 1.04546-2.3182 2.31815 0 1.2728 1.0455 2.3182 2.3182 2.3182s2.3182-1.0454 2.3182-2.3182c0-1.27269-1.0455-2.31815-2.3182-2.31815z"
fill="currentColor"
fillRule="evenodd"
/>
</SVG>
)
export default user

View File

@ -0,0 +1,105 @@
import { Icon } from '@wordpress/components'
import { safeHTML } from '@wordpress/dom'
import { useState, useRef } from '@wordpress/element'
import { __, sprintf } from '@wordpress/i18n'
import { General } from '@extendify/api/General'
import { Plugins } from '@extendify/api/Plugins'
import { download2, brandLogo } from '@extendify/components/icons'
import { useGlobalStore } from '@extendify/state/GlobalState'
import { useUserStore } from '@extendify/state/User'
import { SplitModal } from './SplitModal'
export const InstallStandaloneModal = () => {
const [text, setText] = useState(__('Install Extendify', 'extendify'))
const [success, setSuccess] = useState(false)
const [disabled, setDisabled] = useState(false)
const initialFocus = useRef(null)
const markNoticeSeen = useUserStore((state) => state.markNoticeSeen)
const giveFreebieImports = useUserStore((state) => state.giveFreebieImports)
const removeAllModals = useGlobalStore((state) => state.removeAllModals)
const installAndActivate = () => {
setText(__('Installing...', 'extendify'))
setDisabled(true)
Promise.all([
General.ping('stln-modal-install'),
Plugins.installAndActivate(['extendify']),
new Promise((resolve) => setTimeout(resolve, 1000)),
])
.then(async () => {
setText(__('Success! Reloading...', 'extendify'))
setSuccess(true)
giveFreebieImports(10)
await General.ping('stln-modal-success')
window.location.reload()
})
.catch(async (error) => {
console.error(error)
setText(__('Error. See console.', 'extendify'))
await General.ping('stln-modal-fail')
})
}
const dismiss = async () => {
removeAllModals()
markNoticeSeen('standalone', 'modalNotices')
await General.ping('stln-modal-x')
}
return (
<SplitModal ref={initialFocus} onClose={dismiss}>
<div>
<div className="mb-10 flex items-center space-x-2 text-extendify-black">
{brandLogo}
</div>
<h3 className="text-xl">
{__(
'Get the brand new Extendify plugin today!',
'extendify',
)}
</h3>
<p
className="text-sm text-black"
dangerouslySetInnerHTML={{
__html: safeHTML(
sprintf(
__(
'Install the new Extendify Library plugin to get the latest we have to offer — right from WordPress.org. Plus, well send you %1$s10 more imports%2$s. Nice.',
'extendify',
),
'<strong>',
'</strong>',
),
),
}}
/>
<div>
<button
onClick={installAndActivate}
ref={initialFocus}
disabled={disabled}
className="button-extendify-main button-focus mt-2 inline-flex justify-center px-4 py-3"
style={{ minWidth: '225px' }}>
{text}
{success || (
<Icon
icon={download2}
size={24}
className="ml-2 w-6 flex-grow-0"
/>
)}
</button>
</div>
</div>
<div className="flex w-full justify-end rounded-tr-sm rounded-br-sm bg-extendify-secondary">
<img
alt={__('Upgrade Now', 'extendify')}
className="roudned-br-sm max-w-full rounded-tr-sm"
src={
window.extendifyData.asset_path +
'/modal-extendify-purple.png'
}
/>
</div>
</SplitModal>
)
}

View File

@ -0,0 +1,75 @@
import { Button } from '@wordpress/components'
import { Fragment, useRef, forwardRef } from '@wordpress/element'
import { __ } from '@wordpress/i18n'
import { Icon, close } from '@wordpress/icons'
import { Dialog, Transition } from '@headlessui/react'
import { useGlobalStore } from '@extendify/state/GlobalState'
export const Modal = forwardRef(
({ isOpen, heading, onClose, children }, initialFocus) => {
const focusBackup = useRef(null)
const defaultClose = useGlobalStore((state) => state.removeAllModals)
onClose = onClose ?? defaultClose
return (
<Transition
appear
show={isOpen}
as={Fragment}
className="extendify">
<Dialog
initialFocus={initialFocus ?? focusBackup}
onClose={onClose}>
<div className="fixed inset-0 z-high flex">
<Transition.Child
as={Fragment}
enter="ease-out duration-200 transition"
enterFrom="opacity-0"
enterTo="opacity-100">
<Dialog.Overlay className="fixed inset-0 bg-black bg-opacity-40" />
</Transition.Child>
<Transition.Child
as={Fragment}
enter="ease-out duration-300 translate transform"
enterFrom="opacity-0 translate-y-4 sm:translate-y-5"
enterTo="opacity-100 translate-y-0">
<div className="relative m-auto w-full">
<div className="relative m-auto w-full max-w-lg items-center justify-center rounded-sm bg-white shadow-modal">
{heading ? (
<div className="flex items-center justify-between border-b py-2 pl-6 pr-3 leading-none">
<span className="whitespace-nowrap text-base text-extendify-black">
{heading}
</span>
<CloseButton onClick={onClose} />
</div>
) : (
<div className="absolute top-0 right-0 block px-4 py-4 ">
<CloseButton
ref={focusBackup}
onClick={onClose}
/>
</div>
)}
<div>{children}</div>
</div>
</div>
</Transition.Child>
</div>
</Dialog>
</Transition>
)
},
)
const CloseButton = forwardRef((props, focusRef) => {
return (
<Button
{...props}
icon={<Icon icon={close} />}
ref={focusRef}
className="text-extendify-black opacity-75 hover:opacity-100"
showTooltip={false}
label={__('Close dialog', 'extendify')}
/>
)
})

View File

@ -0,0 +1,104 @@
import { Icon } from '@wordpress/components'
import { Button } from '@wordpress/components'
import { useRef } from '@wordpress/element'
import { __ } from '@wordpress/i18n'
import { General } from '@extendify/api/General'
import { useGlobalStore } from '@extendify/state/GlobalState'
import { useUserStore } from '@extendify/state/User'
import {
growthArrow,
patterns,
layouts,
support,
star,
brandLogo,
diamond,
} from '../icons'
import { SplitModal } from './SplitModal'
import { SettingsModal } from './settings/SettingsModal'
export const NoImportModal = () => {
const pushModal = useGlobalStore((state) => state.pushModal)
const initialFocus = useRef(null)
return (
<SplitModal
isOpen={true}
ref={initialFocus}
leftContainerBgColor="bg-white">
<div>
<div className="mb-5 flex items-center space-x-2 text-extendify-black">
{brandLogo}
</div>
<h3 className="mt-0 text-xl">
{__("You're out of imports", 'extendify')}
</h3>
<p className="text-sm text-black">
{__(
'Sign up today and get unlimited access to our entire collection of patterns and page layouts.',
'extendify',
)}
</p>
<div>
<a
target="_blank"
ref={initialFocus}
className="button-extendify-main button-focus mt-2 inline-flex justify-center px-4 py-3"
style={{ minWidth: '225px' }}
href={`https://extendify.com/pricing/?utm_source=${
window.extendifyData.sdk_partner
}&utm_medium=library&utm_campaign=no-imports-modal&utm_content=get-unlimited-imports&utm_group=${useUserStore
.getState()
.activeTestGroupsUtmValue()}`}
onClick={async () =>
await General.ping('no-imports-modal-click')
}
rel="noreferrer">
{__('Get Unlimited Imports', 'extendify')}
<Icon icon={growthArrow} size={24} className="-mr-1" />
</a>
<p className="mb-0 text-left text-sm text-extendify-gray">
{__('Have an account?', 'extendify')}
<Button
onClick={() => pushModal(<SettingsModal />)}
className="pl-2 text-sm text-extendify-gray underline hover:no-underline">
{__('Sign in', 'extendify')}
</Button>
</p>
</div>
</div>
<div className="flex h-full flex-col justify-center space-y-2 p-10 text-black">
<div className="flex items-center space-x-3">
<Icon icon={patterns} size={24} />
<span className="text-sm leading-none">
{__("Access to 100's of Patterns", 'extendify')}
</span>
</div>
<div className="flex items-center space-x-3">
<Icon icon={diamond} size={24} />
<span className="text-sm leading-none">
{__('Access to "Pro" catalog', 'extendify')}
</span>
</div>
<div className="flex items-center space-x-3">
<Icon icon={layouts} size={24} />
<span className="text-sm leading-none">
{__('Beautiful full page layouts', 'extendify')}
</span>
</div>
<div className="flex items-center space-x-3">
<Icon icon={support} size={24} />
<span className="text-sm leading-none">
{__('Fast and friendly support', 'extendify')}
</span>
</div>
<div className="flex items-center space-x-3">
<Icon icon={star} size={24} />
<span className="text-sm leading-none">
{__('14-Day guarantee', 'extendify')}
</span>
</div>
</div>
</SplitModal>
)
}

View File

@ -0,0 +1,61 @@
import { Icon } from '@wordpress/components'
import { useRef } from '@wordpress/element'
import { __ } from '@wordpress/i18n'
import { General } from '@extendify/api/General'
import { growthArrow, brandLogo } from '@extendify/components/icons'
import { useUserStore } from '@extendify/state/User'
import { SplitModal } from './SplitModal'
export const ProModal = () => {
const initialFocus = useRef(null)
return (
<SplitModal isOpen={true} invertedButtonColor={true} ref={initialFocus}>
<div>
<div className="mb-5 flex items-center space-x-2 text-extendify-black">
{brandLogo}
</div>
<h3 className="mt-0 text-xl">
{__(
'Get unlimited access to all our Pro patterns & layouts',
'extendify',
)}
</h3>
<p className="text-sm text-black">
{__(
"Upgrade to Extendify Pro and use all the patterns and layouts you'd like, including our exclusive Pro catalog.",
'extendify',
)}
</p>
<div>
<a
target="_blank"
ref={initialFocus}
className="button-extendify-main button-focus mt-2 inline-flex justify-center px-4 py-3"
style={{ minWidth: '225px' }}
href={`https://extendify.com/pricing/?utm_source=${
window.extendifyData.sdk_partner
}&utm_medium=library&utm_campaign=pro-modal&utm_content=upgrade-now&utm_group=${useUserStore
.getState()
.activeTestGroupsUtmValue()}`}
onClick={async () =>
await General.ping('pro-modal-click')
}
rel="noreferrer">
{__('Upgrade Now', 'extendify')}
<Icon icon={growthArrow} size={24} className="-mr-1" />
</a>
</div>
</div>
<div className="justify-endrounded-tr-sm flex w-full rounded-br-sm bg-black">
<img
alt={__('Upgrade Now', 'extendify')}
className="max-w-full rounded-tr-sm rounded-br-sm"
src={
window.extendifyData.asset_path +
'/modal-extendify-black.png'
}
/>
</div>
</SplitModal>
)
}

View File

@ -0,0 +1,77 @@
import { Fragment, forwardRef, useRef } from '@wordpress/element'
import { __ } from '@wordpress/i18n'
import { Icon, close } from '@wordpress/icons'
import { Dialog, Transition } from '@headlessui/react'
import { useGlobalStore } from '@extendify/state/GlobalState'
export const SplitModal = forwardRef(
(
{
onClose,
isOpen,
invertedButtonColor,
children,
leftContainerBgColor = 'bg-white',
rightContainerBgColor = 'bg-gray-100',
},
initialFocus,
) => {
const focusBackup = useRef(null)
const defaultClose = useGlobalStore((state) => state.removeAllModals)
onClose = onClose ?? defaultClose
return (
<Transition.Root appear show={true} as={Fragment}>
<Dialog
as="div"
static
open={isOpen}
className="extendify"
initialFocus={initialFocus ?? focusBackup}
onClose={onClose}>
<div className="fixed inset-0 z-high flex">
<Transition.Child
as={Fragment}
enter="ease-out duration-50 transition"
enterFrom="opacity-0"
enterTo="opacity-100">
<Dialog.Overlay className="fixed inset-0 bg-black bg-opacity-40 transition-opacity" />
</Transition.Child>
<Transition.Child
as={Fragment}
enter="ease-out duration-300 translate transform"
enterFrom="opacity-0 translate-y-4 sm:translate-y-5"
enterTo="opacity-100 translate-y-0">
<div className="m-auto">
<div className="relative m-8 max-w-md justify-between rounded-sm shadow-modal md:m-0 md:flex md:max-w-2xl">
<button
onClick={onClose}
ref={focusBackup}
className="absolute top-0 right-0 block cursor-pointer rounded-md bg-transparent p-4 text-gray-700 opacity-30 hover:opacity-100"
style={
invertedButtonColor && {
filter: 'invert(1)',
}
}>
<span className="sr-only">
{__('Close', 'extendify')}
</span>
<Icon icon={close} />
</button>
<div
className={`w-7/12 p-12 ${leftContainerBgColor}`}>
{children[0]}
</div>
<div
className={`hidden w-6/12 md:block ${rightContainerBgColor}`}>
{children[1]}
</div>
</div>
</div>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
)
},
)

View File

@ -0,0 +1,88 @@
import { Button } from '@wordpress/components'
import { useState } from '@wordpress/element'
import { useIsDevMode } from '@extendify/hooks/helpers'
import { useTaxonomyStore } from '@extendify/state/Taxonomies'
import { useTemplatesStore } from '@extendify/state/Templates'
import { useUserStore } from '@extendify/state/User'
export const DevSettings = () => {
const [processing, setProcessing] = useState(false)
const [canHydrate, setCanHydrate] = useState(false)
const devMode = useIsDevMode()
const handleReset = async () => {
if (processing) return
setProcessing(true)
if (canHydrate) {
setCanHydrate(false)
useUserStore.setState({
participatingTestsGroups: [],
})
await useUserStore.persist.rehydrate()
window.extendifyData._canRehydrate = false
setProcessing(false)
return
}
useUserStore.persist.clearStorage()
await new Promise((resolve) => setTimeout(resolve, 1000))
window.extendifyData._canRehydrate = true
setCanHydrate(true)
setProcessing(false)
}
const handleServerSwitch = async () => {
const params = new URLSearchParams(window.location.search)
params.delete('LOCALMODE', 1)
params[params.has('DEVMODE') || devMode ? 'delete' : 'append'](
'DEVMODE',
1,
)
window.history.replaceState(
null,
null,
window.location.pathname + '?' + params.toString(),
)
await new Promise((resolve) => setTimeout(resolve, 500))
window.dispatchEvent(new Event('popstate'))
useTemplatesStore.getState().resetTemplates()
useTemplatesStore.getState().updateSearchParams({})
useTaxonomyStore.persist.clearStorage()
useTaxonomyStore.persist.rehydrate()
useTemplatesStore.setState({
taxonomyDefaultState: {},
})
useTaxonomyStore
.getState()
.fetchTaxonomies()
.then(() => {
useTemplatesStore.getState().setupDefaultTaxonomies()
})
}
if (!window.extendifyData.devbuild) return null
return (
<section className="p-6 flex flex-col space-y-6 border-l-8 border-extendify-secondary">
<div>
<p className="text-base m-0 text-extendify-black">
Development Settings
</p>
<p className="text-sm italic m-0 text-gray-500">
Only available on dev builds
</p>
</div>
<div className="flex space-x-2">
<Button isSecondary onClick={handleServerSwitch}>
Switch to {devMode ? 'Live' : 'Dev'} Server
</Button>
<Button isSecondary onClick={handleReset}>
{processing
? 'Processing...'
: canHydrate
? 'OK! Press to rehydrate app'
: 'Reset User Data'}
</Button>
</div>
</section>
)
}

View File

@ -0,0 +1,240 @@
import { Spinner, Button } from '@wordpress/components'
import { useState, useEffect, useRef } from '@wordpress/element'
import { __ } from '@wordpress/i18n'
import { Icon } from '@wordpress/icons'
import classNames from 'classnames'
import { General } from '@extendify/api/General'
import { User as UserApi } from '@extendify/api/User'
import { useIsDevMode } from '@extendify/hooks/helpers'
import { useUserStore } from '@extendify/state/User'
import { user } from '../../icons'
import { success as successIcon } from '../../icons'
export default function LoginInterface({ actionCallback, initialFocus }) {
const loggedIn = useUserStore((state) => state.apiKey.length)
const [email, setEmail] = useState('')
const [apiKey, setApiKey] = useState('')
const [feedback, setFeedback] = useState('')
const [feedbackType, setFeedbackType] = useState('info')
const [isWorking, setIsWorking] = useState(false)
const [success, setSuccess] = useState(false)
const viewPatternsButtonRef = useRef(null)
const licenseKeyRef = useRef(null)
const devMode = useIsDevMode()
useEffect(() => {
setEmail(useUserStore.getState().email)
// This will reset the default error state to info
return () => setFeedbackType('info')
}, [])
useEffect(() => {
success && viewPatternsButtonRef?.current?.focus()
}, [success])
const logout = () => {
setApiKey('')
useUserStore.setState({ apiKey: '' })
setTimeout(() => {
licenseKeyRef?.current?.focus()
}, 0)
}
const confirmKey = async (event) => {
event.preventDefault()
setIsWorking(true)
setFeedback('')
const { token, error, exception, message } = await UserApi.authenticate(
email,
apiKey,
)
if (typeof message !== 'undefined') {
setFeedbackType('error')
setIsWorking(false)
setFeedback(
message?.length
? message
: 'Error: Are you interacting with the wrong server?',
)
return
}
if (error || exception) {
setFeedbackType('error')
setIsWorking(false)
setFeedback(error?.length ? error : exception)
return
}
if (!token || typeof token !== 'string') {
setFeedbackType('error')
setIsWorking(false)
setFeedback(__('Something went wrong', 'extendify'))
return
}
setFeedbackType('success')
setFeedback('Success!')
setSuccess(true)
setIsWorking(false)
useUserStore.setState({
email: email,
apiKey: token,
})
}
if (success) {
return (
<section className="space-y-6 p-6 text-center flex flex-col items-center">
<Icon icon={successIcon} size={148} />
<p className="text-center text-lg font-semibold m-0 text-extendify-black">
{__("You've signed in to Extendify", 'extendify')}
</p>
<Button
ref={viewPatternsButtonRef}
className="cursor-pointer rounded bg-extendify-main p-2 px-4 text-center text-white"
onClick={actionCallback}>
{__('View patterns', 'extendify')}
</Button>
</section>
)
}
if (loggedIn) {
return (
<section className="w-full space-y-6 p-6">
<p className="text-base m-0 text-extendify-black">
{__('Account', 'extendify')}
</p>
<div className="flex items-center justify-between">
<div className="-ml-2 flex items-center space-x-2">
<Icon icon={user} size={48} />
<p className="text-extendify-black">
{email?.length
? email
: __('Logged In', 'extendify')}
</p>
</div>
{devMode && (
<Button
className="cursor-pointer rounded bg-extendify-main px-4 py-3 text-center text-white hover:bg-extendify-main-dark"
onClick={logout}>
{__('Sign out', 'extendify')}
</Button>
)}
</div>
</section>
)
}
return (
<section className="space-y-6 p-6 text-left">
<div>
<p className="text-center text-lg font-semibold m-0 text-extendify-black">
{__('Sign in to Extendify', 'extendify')}
</p>
<p className="space-x-1 text-center text-sm m-0 text-extendify-gray">
<span>{__("Don't have an account?", 'extendify')}</span>
<a
href={`https://extendify.com/pricing?utm_source=${
window.extendifyData.sdk_partner
}&utm_medium=library&utm_campaign=sign-in-form&utm_content=sign-up&utm_group=${useUserStore
.getState()
.activeTestGroupsUtmValue()}`}
target="_blank"
onClick={async () =>
await General.ping(
'sign-up-link-from-login-modal-click',
)
}
className="underline hover:no-underline text-extendify-gray"
rel="noreferrer">
{__('Sign up', 'extendify')}
</a>
</p>
</div>
<form
onSubmit={confirmKey}
className="flex flex-col items-center justify-center space-y-2">
<div className="flex items-center">
<label className="sr-only" htmlFor="extendify-login-email">
{__('Email address', 'extendify')}
</label>
<input
ref={initialFocus}
id="extendify-login-email"
name="extendify-login-email"
style={{ minWidth: '320px' }}
type="email"
className="w-full rounded border-2 p-2"
placeholder={__('Email address', 'extendify')}
value={email.length ? email : ''}
onChange={(event) => setEmail(event.target.value)}
/>
</div>
<div className="flex items-center">
<label
className="sr-only"
htmlFor="extendify-login-license">
{__('License key', 'extendify')}
</label>
<input
ref={licenseKeyRef}
id="extendify-login-license"
name="extendify-login-license"
style={{ minWidth: '320px' }}
type="text"
className="w-full rounded border-2 p-2"
placeholder={__('License key', 'extendify')}
value={apiKey}
onChange={(event) => setApiKey(event.target.value)}
/>
</div>
<div className="flex justify-center pt-2">
<button
type="submit"
className="relative flex w-72 max-w-full cursor-pointer justify-center rounded bg-extendify-main p-2 py-3 text-center text-base text-white hover:bg-extendify-main-dark ">
<span>{__('Sign In', 'extendify')}</span>
{isWorking && (
<div className="absolute right-2.5">
<Spinner />
</div>
)}
</button>
</div>
{feedback && (
<div
className={classNames({
'border-gray-900 text-gray-900':
feedbackType === 'info',
'border-wp-alert-red text-wp-alert-red':
feedbackType === 'error',
'border-extendify-main text-extendify-main':
feedbackType === 'success',
})}>
{feedback}
</div>
)}
<div className="pt-4 text-center">
<a
target="_blank"
rel="noreferrer"
href={`https://extendify.com/guides/sign-in?utm_source=${
window.extendifyData.sdk_partner
}&utm_medium=library&utm_campaign=sign-in-form&utm_content=need-help&utm_group=${useUserStore
.getState()
.activeTestGroupsUtmValue()}`}
onClick={async () =>
await General.ping(
'need-help-link-from-login-modal-click',
)
}
className="underline hover:no-underline text-sm text-extendify-gray">
{__('Need Help?', 'extendify')}
</a>
</div>
</form>
</section>
)
}

View File

@ -0,0 +1,26 @@
import { useRef } from '@wordpress/element'
import { __ } from '@wordpress/i18n'
import { useGlobalStore } from '@extendify/state/GlobalState'
import { Modal } from '../Modal'
import { DevSettings } from './DevSettings'
import LoginInterface from './LoginInterface'
export const SettingsModal = () => {
const initialFocus = useRef(null)
const actionCallback = useGlobalStore((state) => state.removeAllModals)
return (
<Modal
heading={__('Settings', 'extendify')}
isOpen={true}
ref={initialFocus}>
<div className="flex justify-center flex-col divide-y">
<DevSettings />
<LoginInterface
initialFocus={initialFocus}
actionCallback={actionCallback}
/>
</div>
</Modal>
)
}

View File

@ -0,0 +1,36 @@
import { Button } from '@wordpress/components'
import { __ } from '@wordpress/i18n'
import { General } from '@extendify/api/General'
import { useUserStore } from '@extendify/state/User'
export default function FeedbackNotice() {
return (
<>
<span className="text-black">
{__(
'Tell us how to make the Extendify Library work better for you',
'extendify',
)}
</span>
<span className="px-2 opacity-50" aria-hidden="true">
&#124;
</span>
<div className="flex items-center justify-center space-x-2">
<Button
variant="link"
className="h-auto p-0 text-black underline hover:no-underline"
href={`https://extendify.com/feedback/?utm_source=${
window.extendifyData.sdk_partner
}&utm_medium=library&utm_campaign=feedback-notice&utm_content=give-feedback&utm_group=${useUserStore
.getState()
.activeTestGroupsUtmValue()}`}
onClick={async () =>
await General.ping('feedback-notice-click')
}
target="_blank">
{__('Give feedback', 'extendify')}
</Button>
</div>
</>
)
}

View File

@ -0,0 +1,109 @@
import { Button } from '@wordpress/components'
import { useState, useEffect, useRef } from '@wordpress/element'
import { __ } from '@wordpress/i18n'
import { Icon, closeSmall } from '@wordpress/icons'
import { General } from '@extendify/api/General'
import { useGlobalStore } from '@extendify/state/GlobalState'
import { useUserStore } from '@extendify/state/User'
import FeedbackNotice from './FeedbackNotice'
import { InstallStandaloneNotice } from './InstallStandaloneNotice'
import PromotionNotice from './PromotionNotice'
// import WelcomeNotice from './WelcomeNotice'
const NoticesByPriority = {
// welcome: WelcomeNotice,
promotion: PromotionNotice,
feedback: FeedbackNotice,
standalone: InstallStandaloneNotice,
}
export default function FooterNotice({ className = '' }) {
const [hasNotice, setHasNotice] = useState(null)
const once = useRef(false)
const promotionData = useGlobalStore(
(state) => state.metaData?.banners?.footer,
)
const showFeedback = () => {
const imports = useUserStore.getState().imports ?? 0
const firstLoadedOn =
useUserStore.getState()?.firstLoadedOn ?? new Date()
const timeDifference =
new Date().getTime() - new Date(firstLoadedOn).getTime()
const daysSinceActivated = timeDifference / 86_400_000 // 24 hours
return imports >= 3 && daysSinceActivated > 3
}
// Find the first notice key to use
// TODO: extract this logic into the individual component instead of controlling it here
const componentKey =
Object.keys(NoticesByPriority).find((key) => {
if (key === 'promotion') {
return (
// When checking promotions, use the key sent from the server
// to check whether it's been dismissed
promotionData?.key &&
!useUserStore.getState().noticesDismissedAt[
promotionData.key
]
)
}
if (key === 'feedback') {
return (
showFeedback() &&
!useUserStore.getState().noticesDismissedAt[key]
)
}
if (key === 'standalone') {
return (
!window.extendifyData.standalone &&
!useUserStore.getState().noticesDismissedAt[key]
)
}
return !useUserStore.getState().noticesDismissedAt[key]
}) ?? null
const Notice = NoticesByPriority[componentKey]
const dismiss = async () => {
setHasNotice(false)
// The noticesDismissedAt key will either be the key from NoticesByPriority,
// or a key passed in from the server, such as 'holiday-sale2077'
const key =
componentKey === 'promotion' ? promotionData.key : componentKey
useUserStore.getState().markNoticeSeen(key, 'notices')
await General.ping(`footer-notice-x-${key}`)
}
useEffect(() => {
// Only show the notice once on main render and only if a notice exists.
if (NoticesByPriority[componentKey] && !once.current) {
setHasNotice(true)
once.current = true
}
}, [componentKey])
if (!hasNotice) {
return null
}
return (
<div
className={`${className} relative mx-auto hidden max-w-screen-4xl items-center justify-center space-x-4 bg-extendify-secondary py-3 px-5 lg:flex`}>
{/* Pass all data to all components and let them decide what they use */}
<Notice promotionData={promotionData} />
<div className="absolute right-1">
<Button
className="text-extendify-black opacity-50 hover:opacity-100 focus:opacity-100"
icon={<Icon icon={closeSmall} />}
label={__('Dismiss this notice', 'extendify')}
onClick={dismiss}
showTooltip={false}
/>
</div>
</div>
)
}

View File

@ -0,0 +1,66 @@
import { Button } from '@wordpress/components'
import { useState } from '@wordpress/element'
import { __ } from '@wordpress/i18n'
import classNames from 'classnames'
import { General } from '@extendify/api/General'
import { Plugins } from '@extendify/api/Plugins'
import { useUserStore } from '@extendify/state/User'
export const InstallStandaloneNotice = () => {
const [text, setText] = useState('')
const giveFreebieImports = useUserStore((state) => state.giveFreebieImports)
const installAndActivate = () => {
setText(__('Installing...', 'extendify'))
Promise.all([
General.ping('stln-footer-install'),
Plugins.installAndActivate(['extendify']),
new Promise((resolve) => setTimeout(resolve, 1000)),
])
.then(async () => {
giveFreebieImports(10)
setText(__('Success! Reloading...', 'extendify'))
await General.ping('stln-footer-success')
window.location.reload()
})
.catch(async (error) => {
console.error(error)
setText(__('Error. See console.', 'extendify'))
await General.ping('stln-footer-fail')
})
}
return (
<div>
<span className="text-black">
{__(
'Install the new Extendify Library plugin to get the latest we have to offer',
'extendify',
)}
</span>
<span className="px-2 opacity-50" aria-hidden="true">
&#124;
</span>
<div className="relative inline-flex items-center space-x-2">
<Button
variant="link"
className={classNames(
'h-auto p-0 text-black underline hover:no-underline',
{ 'opacity-0': text },
)}
onClick={installAndActivate}>
{__('Install Extendify standalone plugin', 'extendify')}
</Button>
{/* Little hacky to keep the text in place. Might need to tweak this */}
{text ? (
<Button
variant="link"
disabled={true}
className="absolute left-0 h-auto p-0 text-black underline opacity-100 hover:no-underline"
onClick={() => {}}>
{text}
</Button>
) : null}
</div>
</div>
)
}

View File

@ -0,0 +1,32 @@
import { Button } from '@wordpress/components'
import { useUserStore } from '@extendify/state/User'
import { General } from '../../api/General'
export default function PromotionNotice({ promotionData }) {
return (
<>
<span className="text-black">{promotionData?.text ?? ''}</span>
<span className="px-2 opacity-50" aria-hidden="true">
&#124;
</span>
<div className="flex items-center justify-center space-x-2">
{promotionData?.url && (
<Button
variant="link"
className="h-auto p-0 text-black underline hover:no-underline"
href={`${promotionData.url}&utm_source=${
window.extendifyData.sdk_partner
}&utm_group=${useUserStore
.getState()
.activeTestGroupsUtmValue()}`}
onClick={async () =>
await General.ping('promotion-notice-click')
}
target="_blank">
{promotionData?.button_text}
</Button>
)}
</div>
</>
)
}

View File

@ -0,0 +1,58 @@
import { Button } from '@wordpress/components'
import { __ } from '@wordpress/i18n'
import { General } from '@extendify/api/General'
import { useGlobalStore } from '@extendify/state/GlobalState'
import { useUserStore } from '@extendify/state/User'
export default function WelcomeNotice() {
const setOpen = useGlobalStore((state) => state.setOpen)
const disableLibrary = () => {
const button = document.getElementById(
'extendify-templates-inserter-btn',
)
button.classList.add('invisible')
useUserStore.setState({ enabled: false })
setOpen(false)
}
return (
<>
<span className="text-black">
{__('Welcome to the Extendify Library', 'extendify')}
</span>
<span className="px-2 opacity-50" aria-hidden="true">
&#124;
</span>
<div className="flex items-center justify-center space-x-2">
<Button
variant="link"
className="h-auto p-0 text-black underline hover:no-underline"
href={`https://extendify.com/welcome/?utm_source=${
window.extendifyData.sdk_partner
}&utm_medium=library&utm_campaign=welcome-notice&utm_content=tell-me-more&utm_group=${useUserStore
.getState()
.activeTestGroupsUtmValue()}`}
onClick={async () =>
await General.ping('welcome-notice-tell-me-more-click')
}
target="_blank">
{__('Tell me more', 'extendify')}
</Button>
{window.extendifyData.standalone ? null : (
<>
<span className="font-bold" aria-hidden="true">
&bull;
</span>
<Button
variant="link"
className="h-auto p-0 text-black underline hover:no-underline"
onClick={disableLibrary}>
{__('Turn off the library', 'extendify')}
</Button>
</>
)}
</div>
</>
)
}

View File

@ -0,0 +1,74 @@
import { Button, Popover } from '@wordpress/components'
import { safeHTML } from '@wordpress/dom'
import { __, sprintf } from '@wordpress/i18n'
import { Icon, close } from '@wordpress/icons'
export const NewImportsPopover = ({
anchorRef,
onPressX,
onClick,
onClickOutside,
}) => {
if (!anchorRef.current) return null
return (
<Popover
anchorRef={anchorRef.current}
shouldAnchorIncludePadding={true}
className="extendify-tooltip-default"
focusOnMount={false}
onFocusOutside={onClickOutside}
onClick={onClick}
position="bottom center"
noArrow={false}>
<>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '0.5rem',
}}>
<span
style={{
textTransform: 'uppercase',
color: '#8b8b8b',
}}>
{__('Monthly Imports', 'extendify')}
</span>
<Button
style={{
color: 'white',
position: 'relative',
right: '-5px',
padding: '0',
minWidth: '0',
height: '20px',
width: '20px',
}}
onClick={(event) => {
event.stopPropagation()
onPressX()
}}
icon={<Icon icon={close} size={12} />}
showTooltip={false}
label={__('Close callout', 'extendify')}
/>
</div>
<div
dangerouslySetInnerHTML={{
__html: safeHTML(
sprintf(
__(
"%1$sGood news!%2$s We've added more imports to your library. Enjoy!",
'extendify',
),
'<strong>',
'</strong>',
),
),
}}
/>
</>
</Popover>
)
}

View File

@ -0,0 +1,85 @@
import { useRef, useEffect, useState } from '@wordpress/element'
export function useIsMounted() {
const isMounted = useRef(false)
useEffect(() => {
isMounted.current = true
return () => (isMounted.current = false)
})
return isMounted
}
export const useIsDevMode = () => {
const [devMode, setDevMode] = useState(false)
const check = () => {
return (
window.location.search.indexOf('DEVMODE') > -1 ||
window.location.search.indexOf('LOCALMODE') > -1
)
}
useEffect(() => {
const handle = () => setDevMode(check())
handle()
window.addEventListener('popstate', handle)
return () => {
window.removeEventListener('popstate', handle)
}
}, [])
return devMode
}
export const useWhenIdle = (time) => {
const [userInteracted, setUserInteracted] = useState(true)
const [idle, setIdle] = useState(false)
const isMounted = useIsMounted()
const timerId = useRef()
useEffect(() => {
const handleMovement = () => setUserInteracted(true)
const passive = { passive: true }
window.addEventListener('keydown', handleMovement, passive)
window.addEventListener('mousemove', handleMovement, passive)
window.addEventListener('touchmove', handleMovement, passive)
return () => {
window.removeEventListener('keydown', handleMovement)
window.removeEventListener('mousemove', handleMovement)
window.removeEventListener('touchmove', handleMovement)
}
}, [])
useEffect(() => {
if (!userInteracted) return
setUserInteracted(false)
setIdle(false)
window.clearTimeout(timerId.current)
timerId.current = window.setTimeout(() => {
isMounted.current && setIdle(true)
}, time)
}, [userInteracted, time, isMounted])
return idle
}
/** Dev debugging tool to identify leaky renders: https://usehooks.com/useWhyDidYouUpdate/ */
export const useWhyDidYouUpdate = (name, props) => {
const previousProps = useRef()
useEffect(() => {
if (previousProps.current) {
const allKeys = Object.keys({ ...previousProps.current, ...props })
const changesObj = {}
allKeys.forEach((key) => {
if (previousProps.current[key] !== props[key]) {
changesObj[key] = {
from: previousProps.current[key],
to: props[key],
}
}
})
if (Object.keys(changesObj).length) {
console.log('[why-did-you-update]', name, changesObj)
}
}
previousProps.current = props
})
}

View File

@ -0,0 +1,48 @@
import { useEffect, useState } from '@wordpress/element'
import { InstallStandaloneModal } from '@extendify/components/modals/InstallStandaloneModal'
import { useGlobalStore } from '@extendify/state/GlobalState'
import { useUserStore } from '@extendify/state/User'
/** Return any pending modals and check if any need to show */
export const useModal = () => {
const [modal, setModal] = useState(null)
const open = useGlobalStore((state) => state.open)
const pushModal = useGlobalStore((state) => state.pushModal)
const removeAllModals = useGlobalStore((state) => state.removeAllModals)
// Watches modals added anywhere
useEffect(
() =>
useGlobalStore.subscribe(
(state) => state.modals,
(value) => setModal(value?.length > 0 ? value[0] : null),
),
[],
)
// Checks for modals that need to be shown on load
useEffect(() => {
if (!open) {
removeAllModals()
return
}
const ModalNoticesByPriority = {
standalone: InstallStandaloneModal,
}
const componentKey =
Object.keys(ModalNoticesByPriority).find((key) => {
if (key === 'standalone') {
return (
!window.extendifyData.standalone &&
!useUserStore.getState().modalNoticesDismissedAt[key]
)
}
return !useUserStore.getState().modalNoticesDismissedAt[key]
}) ?? null
const Modal = ModalNoticesByPriority[componentKey]
if (Modal) pushModal(<Modal />)
}, [open, pushModal, removeAllModals])
return modal
}

View File

@ -0,0 +1,22 @@
import { useState, useLayoutEffect } from '@wordpress/element'
import { useGlobalStore } from '@extendify/state/GlobalState'
import { useUserStore as user } from '@extendify/state/User'
export const useTestGroup = (key, options, override) => {
const [group, setGroup] = useState()
const ready = useGlobalStore((state) => state.ready)
useLayoutEffect(() => {
if (
override ||
(ready && !group) ||
// Let the devbuild reset this
window.extendifyData._canRehydrate
) {
const testGroup = user.getState().testGroup(key, options)
setGroup(testGroup)
}
}, [key, options, group, ready, override])
return group
}

View File

@ -0,0 +1,4 @@
import { softErrorHandler } from './softerror-encountered'
import { templateHandler } from './template-inserted'
;[templateHandler, softErrorHandler].forEach((listener) => listener.register())

View File

@ -0,0 +1,24 @@
import { render } from '@wordpress/element'
import { camelCase } from 'lodash'
import RequiredPluginsModal from '@extendify/middleware/hasRequiredPlugins/RequiredPluginsModal'
// use this to trigger an error from outside the application
export const softErrorHandler = {
register() {
window.addEventListener('extendify::softerror-encountered', (event) => {
this[camelCase(event.detail.type)](event.detail)
})
},
versionOutdated(error) {
render(
<RequiredPluginsModal
title={error.data.title}
requiredPlugins={['extendify']}
message={error.data.message}
buttonLabel={error.data.buttonLabel}
forceOpen={true}
/>,
document.getElementById('extendify-root'),
)
},
}

View File

@ -0,0 +1,24 @@
import { dispatch } from '@wordpress/data'
import { __ } from '@wordpress/i18n'
import { Templates } from '@extendify/api/Templates'
import { useUserStore } from '@extendify/state/User'
// This fires after a template is inserted
export const templateHandler = {
register() {
const { createNotice } = dispatch('core/notices')
const increaseImports = useUserStore.getState().incrementImports
window.addEventListener('extendify::template-inserted', (event) => {
createNotice('info', __('Page layout added'), {
isDismissible: true,
type: 'snackbar',
})
// This is put off to the stack in attempt to fix a bug where
// some users are having their imports go from 3->0 in an instant
setTimeout(() => {
increaseImports()
Templates.import(event.detail?.template)
}, 0)
})
},
}

View File

@ -0,0 +1,65 @@
import { Modal, Button } from '@wordpress/components'
import { render } from '@wordpress/element'
import { __, sprintf } from '@wordpress/i18n'
import ExtendifyLibrary from '@extendify/ExtendifyLibrary'
import { useWantedTemplateStore } from '@extendify/state/Importing'
import { getPluginDescription } from '@extendify/util/general'
export default function NeedsPermissionModal() {
const wantedTemplate = useWantedTemplateStore(
(store) => store.wantedTemplate,
)
const closeModal = () =>
render(
<ExtendifyLibrary show={true} />,
document.getElementById('extendify-root'),
)
const requiredPlugins = wantedTemplate?.fields?.required_plugins || []
return (
<Modal
title={__('Plugins required', 'extendify')}
isDismissible={false}>
<p
style={{
maxWidth: '400px',
}}>
{sprintf(
__(
'In order to add this %s to your site, the following plugins are required to be installed and activated.',
'extendify',
),
wantedTemplate?.fields?.type ?? 'template',
)}
</p>
<ul>
{
// Hardcoded temporarily to not force EP install
// requiredPlugins.map((plugin) =>
requiredPlugins
.filter((p) => p !== 'editorplus')
.map((plugin) => (
<li key={plugin}>{getPluginDescription(plugin)}</li>
))
}
</ul>
<p
style={{
maxWidth: '400px',
fontWeight: 'bold',
}}>
{__(
'Please contact a site admin for assistance in adding these plugins to your site.',
'extendify',
)}
</p>
<Button
isPrimary
onClick={closeModal}
style={{
boxShadow: 'none',
}}>
{__('Return to library', 'extendify')}
</Button>
</Modal>
)
}

View File

@ -0,0 +1,49 @@
import { Modal, Button, ButtonGroup } from '@wordpress/components'
import { dispatch, select } from '@wordpress/data'
import { useState } from '@wordpress/element'
import { __ } from '@wordpress/i18n'
export default function ReloadRequiredModal() {
const [isSaving, setSaving] = useState(false)
const { isEditedPostDirty } = select('core/editor')
const hasUnsavedChanges = isEditedPostDirty()
const saveChanges = () => {
setSaving(true)
dispatch('core/editor').savePost()
setSaving(false)
}
const reload = () => {
// location.reload()
}
if (!hasUnsavedChanges) {
reload()
return null
}
return (
<Modal title={__('Reload required', 'extendify')} isDismissible={false}>
<p
style={{
maxWidth: '400px',
}}>
{__(
'Just one more thing! We need to reload the page to continue.',
'extendify',
)}
</p>
<ButtonGroup>
<Button isPrimary onClick={reload} disabled={isSaving}>
{__('Reload page', 'extendify')}
</Button>
<Button
isSecondary
onClick={saveChanges}
isBusy={isSaving}
style={{
margin: '0 4px',
}}>
{__('Save changes', 'extendify')}
</Button>
</ButtonGroup>
</Modal>
)
}

View File

@ -0,0 +1,78 @@
import { Modal, Button, ButtonGroup } from '@wordpress/components'
import { render } from '@wordpress/element'
import { __, sprintf } from '@wordpress/i18n'
import ExtendifyLibrary from '@extendify/ExtendifyLibrary'
import { useWantedTemplateStore } from '@extendify/state/Importing'
import { useUserStore } from '@extendify/state/User'
import { getPluginDescription } from '@extendify/util/general'
import NeedsPermissionModal from '../NeedsPermissionModal'
import ActivatingModal from './ActivatingModal'
export default function ActivatePluginsModal(props) {
const wantedTemplate = useWantedTemplateStore(
(store) => store.wantedTemplate,
)
const closeModal = () =>
render(
<ExtendifyLibrary show={true} />,
document.getElementById('extendify-root'),
)
const installPlugins = () =>
render(<ActivatingModal />, document.getElementById('extendify-root'))
const requiredPlugins = wantedTemplate?.fields?.required_plugins || []
if (!useUserStore.getState()?.canActivatePlugins) {
return <NeedsPermissionModal />
}
return (
<Modal
title={__('Activate required plugins', 'extendify')}
isDismissible={false}>
<div>
<p
style={{
maxWidth: '400px',
}}>
{props.message ??
__(
sprintf(
'There is just one more step. This %s requires the following plugins to be installed and activated:',
wantedTemplate?.fields?.type ?? 'template',
),
'extendify',
)}
</p>
<ul>
{
// Hardcoded temporarily to not force EP install
// requiredPlugins.map((plugin) =>
requiredPlugins
.filter((p) => p !== 'editorplus')
.map((plugin) => (
<li key={plugin}>
{getPluginDescription(plugin)}
</li>
))
}
</ul>
<ButtonGroup>
<Button isPrimary onClick={installPlugins}>
{__('Activate Plugins', 'extendify')}
</Button>
{props.showClose && (
<Button
isTertiary
onClick={closeModal}
style={{
boxShadow: 'none',
margin: '0 4px',
}}>
{__('No thanks, return to library', 'extendify')}
</Button>
)}
</ButtonGroup>
</div>
</Modal>
)
}

View File

@ -0,0 +1,58 @@
import { Modal, Button } from '@wordpress/components'
import { useState, render } from '@wordpress/element'
import { __ } from '@wordpress/i18n'
import { Plugins } from '@extendify/api/Plugins'
import { useWantedTemplateStore } from '@extendify/state/Importing'
import ReloadRequiredModal from '../ReloadRequiredModal'
import ErrorActivating from './ErrorActivating'
export default function ActivatingModal() {
const [errorMessage, setErrorMessage] = useState('')
const wantedTemplate = useWantedTemplateStore(
(store) => store.wantedTemplate,
)
// Hardcoded temporarily to not force EP install
// const required = wantedTemplate?.fields?.required_plugins
const required = wantedTemplate?.fields?.required_plugins.filter(
(p) => p !== 'editorplus',
)
Plugins.installAndActivate(required)
.then(() => {
useWantedTemplateStore.setState({
importOnLoad: true,
})
})
.then(async () => {
await new Promise((resolve) => setTimeout(resolve, 1000))
render(
<ReloadRequiredModal />,
document.getElementById('extendify-root'),
)
})
.catch(({ response }) => {
setErrorMessage(response.data.message)
})
if (errorMessage) {
return <ErrorActivating msg={errorMessage} />
}
return (
<Modal
title={__('Activating plugins', 'extendify')}
isDismissible={false}>
<Button
style={{
width: '100%',
}}
disabled
isPrimary
isBusy
onClick={() => {}}>
{__('Activating...', 'extendify')}
</Button>
</Modal>
)
}

View File

@ -0,0 +1,34 @@
import { Modal, Button, Notice } from '@wordpress/components'
import { render } from '@wordpress/element'
import { __ } from '@wordpress/i18n'
import ActivatePluginsModal from './ActivatePluginsModal'
export default function ErrorActivating({ msg }) {
const goBack = () => {
render(
<ActivatePluginsModal />,
document.getElementById('extendify-root'),
)
}
return (
<Modal
style={{
maxWidth: '500px',
}}
title={__('Error Activating plugins', 'extendify')}
isDismissible={false}>
{__(
'You have encountered an error that we cannot recover from. Please try again.',
'extendify',
)}
<br />
<Notice isDismissible={false} status="error">
{msg}
</Notice>
<Button isPrimary onClick={goBack}>
{__('Go back', 'extendify')}
</Button>
</Modal>
)
}

View File

@ -0,0 +1,19 @@
import { render } from '@wordpress/element'
import { checkIfUserNeedsToActivatePlugins } from '../helpers'
import ActivatePluginsModal from './ActivatePluginsModal'
export const hasPluginsActivated = async (template) => {
return {
id: 'hasPluginsActivated',
pass: !(await checkIfUserNeedsToActivatePlugins(template)),
allow() {},
deny() {
return new Promise(() => {
render(
<ActivatePluginsModal showClose={true} />,
document.getElementById('extendify-root'),
)
})
},
}
}

View File

@ -0,0 +1,33 @@
import { Modal, Button, Notice } from '@wordpress/components'
import { render } from '@wordpress/element'
import { __ } from '@wordpress/i18n'
import RequiredPluginsModal from './RequiredPluginsModal'
export default function ErrorInstalling({ msg }) {
const goBack = () =>
render(
<RequiredPluginsModal />,
document.getElementById('extendify-root'),
)
return (
<Modal
style={{
maxWidth: '500px',
}}
title={__('Error installing plugins', 'extendify')}
isDismissible={false}>
{__(
'You have encountered an error that we cannot recover from. Please try again.',
'extendify',
)}
<br />
<Notice isDismissible={false} status="error">
{msg}
</Notice>
<Button isPrimary onClick={goBack}>
{__('Go back', 'extendify')}
</Button>
</Modal>
)
}

View File

@ -0,0 +1,57 @@
import { Modal, Button } from '@wordpress/components'
import { useState, render } from '@wordpress/element'
import { __ } from '@wordpress/i18n'
import { Plugins } from '@extendify/api/Plugins'
import { useWantedTemplateStore } from '@extendify/state/Importing'
import ReloadRequiredModal from '../ReloadRequiredModal'
import ErrorInstalling from './ErrorInstalling'
export default function InstallingModal({ requiredPlugins }) {
const [errorMessage, setErrorMessage] = useState('')
const wantedTemplate = useWantedTemplateStore(
(store) => store.wantedTemplate,
)
// Hardcoded temporarily to not force EP install
// const required = wantedTemplate?.fields?.required_plugins
const required =
requiredPlugins ??
wantedTemplate?.fields?.required_plugins.filter(
(p) => p !== 'editorplus',
)
Plugins.installAndActivate(required)
.then(() => {
useWantedTemplateStore.setState({
importOnLoad: true,
})
render(
<ReloadRequiredModal />,
document.getElementById('extendify-root'),
)
})
.catch(({ message }) => {
setErrorMessage(message)
})
if (errorMessage) {
return <ErrorInstalling msg={errorMessage} />
}
return (
<Modal
title={__('Installing plugins', 'extendify')}
isDismissible={false}>
<Button
style={{
width: '100%',
}}
disabled
isPrimary
isBusy
onClick={() => {}}>
{__('Installing...', 'extendify')}
</Button>
</Modal>
)
}

View File

@ -0,0 +1,95 @@
import { Modal, Button, ButtonGroup } from '@wordpress/components'
import { render } from '@wordpress/element'
import { __, sprintf } from '@wordpress/i18n'
import ExtendifyLibrary from '@extendify/ExtendifyLibrary'
import { useWantedTemplateStore } from '@extendify/state/Importing'
import { useUserStore } from '@extendify/state/User'
import { getPluginDescription } from '@extendify/util/general'
import NeedsPermissionModal from '../NeedsPermissionModal'
import InstallingModal from './InstallingModal'
export default function RequiredPluginsModal({
forceOpen,
buttonLabel,
title,
message,
requiredPlugins,
}) {
// If there's a template in cache ready to be installed.
// TODO: this could probably be refactored out when overhauling required plugins
const wantedTemplate = useWantedTemplateStore(
(store) => store.wantedTemplate,
)
requiredPlugins =
requiredPlugins ?? wantedTemplate?.fields?.required_plugins
const closeModal = () => {
if (forceOpen) {
return
}
render(
<ExtendifyLibrary show={true} />,
document.getElementById('extendify-root'),
)
}
const installPlugins = () =>
render(
<InstallingModal requiredPlugins={requiredPlugins} />,
document.getElementById('extendify-root'),
)
if (!useUserStore.getState()?.canInstallPlugins) {
return <NeedsPermissionModal />
}
return (
<Modal
title={title ?? __('Install required plugins', 'extendify')}
isDismissible={false}>
<p
style={{
maxWidth: '400px',
}}>
{message ??
__(
sprintf(
'There is just one more step. This %s requires the following to be automatically installed and activated:',
wantedTemplate?.fields?.type ?? 'template',
),
'extendify',
)}
</p>
{message?.length > 0 || (
<ul>
{
// Hardcoded temporarily to not force EP install
// requiredPlugins.map((plugin) =>
requiredPlugins
.filter((p) => p !== 'editorplus')
.map((plugin) => (
<li key={plugin}>
{getPluginDescription(plugin)}
</li>
))
}
</ul>
)}
<ButtonGroup>
<Button isPrimary onClick={installPlugins}>
{buttonLabel ?? __('Install Plugins', 'extendify')}
</Button>
{forceOpen || (
<Button
isTertiary
onClick={closeModal}
style={{
boxShadow: 'none',
margin: '0 4px',
}}>
{__('No thanks, take me back', 'extendify')}
</Button>
)}
</ButtonGroup>
</Modal>
)
}

View File

@ -0,0 +1,19 @@
import { render } from '@wordpress/element'
import { checkIfUserNeedsToInstallPlugins } from '../helpers'
import RequiredPluginsModal from './RequiredPluginsModal'
export const hasRequiredPlugins = async (template) => {
return {
id: 'hasRequiredPlugins',
pass: !(await checkIfUserNeedsToInstallPlugins(template)),
allow() {},
deny() {
return new Promise(() => {
render(
<RequiredPluginsModal />,
document.getElementById('extendify-root'),
)
})
},
}
}

View File

@ -0,0 +1,61 @@
import { Plugins } from '@extendify/api/Plugins'
let installedPlugins = []
let activatedPlugins = []
export async function checkIfUserNeedsToInstallPlugins(template) {
let required = template?.fields?.required_plugins ?? []
// Hardcoded temporarily to not force EP install
required = required.filter((p) => p !== 'editorplus')
if (!required?.length) {
return false
}
if (!installedPlugins?.length) {
installedPlugins = Object.keys(await Plugins.getInstalled())
}
// if no dependencies are required, then this will be false automatically
const weNeedInstalls = required?.length
? required.filter((plugin) => {
// TODO: if we have better data to work with this can be more literal
return !installedPlugins.some((k) => {
return k.includes(plugin)
})
})
: false
return weNeedInstalls.length
}
export async function checkIfUserNeedsToActivatePlugins(template) {
let required = template?.fields?.required_plugins ?? []
// Hardcoded temporarily to not force EP install
required = required.filter((p) => p !== 'editorplus')
if (!required?.length) {
return false
}
if (!activatedPlugins?.length) {
activatedPlugins = Object.values(await Plugins.getActivated())
}
// if no dependencies are required, then this will be false automatically
const weNeedActivations = required?.length
? required.filter((plugin) => {
// TODO: if we have better data to work with this can be more literal
return !activatedPlugins.some((k) => {
return k.includes(plugin)
})
})
: false
// if the plugins we need to have activated are not even installed, handle them elsewhere
if (weNeedActivations) {
// This call is a bit more expensive so only run it if needed
if (await checkIfUserNeedsToInstallPlugins(template)) {
return false
}
}
return weNeedActivations?.length
}

View File

@ -0,0 +1,44 @@
import { hasPluginsActivated } from './hasPluginsActivated'
import { hasRequiredPlugins } from './hasRequiredPlugins'
export const Middleware = (middleware = []) => {
return {
hasRequiredPlugins: hasRequiredPlugins,
hasPluginsActivated: hasPluginsActivated,
stack: [],
async check(template) {
for (const m of middleware) {
const cb = await this[`${m}`](template)
this.stack.push(cb.pass ? cb.allow : cb.deny)
}
},
reset() {
this.stack = []
},
}
}
export async function AuthorizationCheck(middleware) {
const middlewareGenerator = MiddlewareGenerator(middleware.stack)
while (true) {
let result
try {
result = await middlewareGenerator.next()
} catch {
// Reset the stack and exit the middleware
// This is used if you want to have the user cancel
middleware.reset()
throw 'Middleware exited'
}
// TODO: Could probably have a check for errors here
if (result.done) {
break
}
}
}
export async function* MiddlewareGenerator(middleware) {
for (const m of middleware) {
yield await m()
}
}

View File

@ -0,0 +1,274 @@
import { Spinner, Button } from '@wordpress/components'
import {
useEffect,
useState,
useCallback,
useRef,
memo,
} from '@wordpress/element'
import { __, sprintf } from '@wordpress/i18n'
import { cloneDeep } from 'lodash'
import { useInView } from 'react-intersection-observer'
import Masonry from 'react-masonry-css'
import { Templates as TemplatesApi } from '@extendify/api/Templates'
import { ImportTemplateBlock } from '@extendify/components/ImportTemplateBlock'
import { useIsMounted } from '@extendify/hooks/helpers'
import { useTestGroup } from '@extendify/hooks/useTestGroup'
import { useGlobalStore } from '@extendify/state/GlobalState'
import { useTaxonomyStore } from '@extendify/state/Taxonomies'
import { useTemplatesStore } from '@extendify/state/Templates'
export const GridView = memo(function GridView() {
const isMounted = useIsMounted()
const templates = useTemplatesStore((state) => state.templates)
const appendTemplates = useTemplatesStore((state) => state.appendTemplates)
const [serverError, setServerError] = useState('')
const [nothingFound, setNothingFound] = useState(false)
const [loading, setLoading] = useState(false)
const [loadMoreRef, inView] = useInView()
const searchParamsRaw = useTemplatesStore((state) => state.searchParams)
const currentType = useGlobalStore((state) => state.currentType)
const resetTemplates = useTemplatesStore((state) => state.resetTemplates)
const open = useGlobalStore((state) => state.open)
const taxonomies = useTaxonomyStore((state) => state.taxonomies)
const updateType = useTemplatesStore((state) => state.updateType)
const updateTaxonomies = useTemplatesStore(
(state) => state.updateTaxonomies,
)
// Store the next page in case we have pagination
const nextPage = useRef(useTemplatesStore.getState().nextPage)
const searchParams = useRef(useTemplatesStore.getState().searchParams)
const taxonomyType =
searchParams.current.type === 'pattern' ? 'patternType' : 'layoutType'
const currentTax = searchParams.current.taxonomies[taxonomyType]
const defaultOrAlt = useTestGroup('default-or-alt-sitetype', ['A', 'B'])
// Subscribing to the store will keep these values updates synchronously
useEffect(() => {
return useTemplatesStore.subscribe(
(state) => state.nextPage,
(n) => (nextPage.current = n),
)
}, [])
useEffect(() => {
return useTemplatesStore.subscribe(
(state) => state.searchParams,
(s) => (searchParams.current = s),
)
}, [])
// Fetch the templates then add them to the current state
const fetchTemplates = useCallback(() => {
if (!defaultOrAlt) {
return
}
setServerError('')
setNothingFound(false)
const defaultError = __(
'Unknown error occured. Check browser console or contact support.',
'extendify',
)
const args = { offset: nextPage.current }
// AB test the default or defaultAlt site type
const defaultSiteType =
defaultOrAlt === 'A' ? { slug: 'default' } : { slug: 'defaultAlt' }
const siteType = searchParams.current.taxonomies?.siteType?.slug?.length
? searchParams.current.taxonomies.siteType
: defaultSiteType
// End AB test - otherwise use { slug: 'default' } when empty
const params = cloneDeep(searchParams.current)
params.taxonomies.siteType = siteType
TemplatesApi.get(params, args)
.then((response) => {
if (!isMounted.current) return
if (response?.error?.length) {
setServerError(response?.error)
return
}
if (response?.records?.length <= 0) {
setNothingFound(true)
return
}
if (
searchParamsRaw === searchParams.current &&
response?.records?.length
) {
useTemplatesStore.setState({
nextPage: response?.offset ?? '',
})
appendTemplates(response.records)
setLoading(false)
}
})
.catch((error) => {
if (!isMounted.current) return
console.error(error)
setServerError(defaultError)
})
}, [appendTemplates, isMounted, searchParamsRaw, defaultOrAlt])
useEffect(() => {
if (templates?.length === 0) {
setLoading(true)
return
}
}, [templates?.length, searchParamsRaw])
useEffect(() => {
// This will check the URL for a pattern type and set that and remove it
// TODO: possibly refactor this if we exapnd it to support layouts
if (!open || !taxonomies?.patternType?.length) return
const search = new URLSearchParams(window.location.search)
if (!search.has('ext-patternType')) return
const term = search.get('ext-patternType')
// Delete it right away
search.delete('ext-patternType')
window.history.replaceState(
null,
null,
window.location.pathname + '?' + search.toString(),
)
// Search the slug in patternTypes
const tax = taxonomies.patternType.find((t) => t.slug === term)
if (!tax) return
updateTaxonomies({ patternType: tax })
updateType('pattern')
}, [open, taxonomies, updateType, updateTaxonomies])
// This is the main driver for loading templates
// This loads the initial batch of templates. But if we don't yet have taxonomies.
// There's also an option to skip loading on first mount
useEffect(() => {
if (!Object.keys(searchParams.current?.taxonomies)?.length) {
return
}
if (useTemplatesStore.getState().skipNextFetch) {
// This is useful if the templates are fetched already and
// the library moves to/from another state that re-renders
// The point is to keep the logic close to the list. That may change someday
useTemplatesStore.setState({
skipNextFetch: false,
})
return
}
fetchTemplates()
return () => resetTemplates()
}, [fetchTemplates, searchParams, resetTemplates])
// Fetches when the load more is in view
useEffect(() => {
nextPage.current && inView && fetchTemplates()
}, [inView, fetchTemplates, templates])
if (serverError.length) {
return (
<div className="text-left">
<h2 className="text-left">{__('Server error', 'extendify')}</h2>
<code
className="mb-4 block max-w-xl p-4"
style={{ minHeight: '10rem' }}>
{serverError}
</code>
<Button
isTertiary
onClick={() => resetTemplates() && fetchTemplates()}>
{__('Press here to reload')}
</Button>
</div>
)
}
if (nothingFound) {
return (
<div className="-mt-2 flex h-full w-full items-center justify-center sm:mt-0">
<h2 className="text-sm font-normal text-extendify-gray">
{sprintf(
searchParams.current.type === 'template'
? __(
'We couldn\'t find any layouts in the "%s" category.',
'extendify',
)
: __(
'We couldn\'t find any patterns in the "%s" category.',
'extendify',
),
currentTax?.title ?? currentTax.slug,
)}
</h2>
</div>
)
}
return (
<>
{loading && (
<div className="-mt-2 flex h-full w-full items-center justify-center sm:mt-0">
<Spinner />
</div>
)}
<Grid type={currentType} templates={templates}>
{templates.map((template) => {
return (
<ImportTemplateBlock
maxHeight={
currentType === 'template' ? 520 : 'none'
}
key={template.id}
template={template}
/>
)
})}
</Grid>
{nextPage.current && (
<>
<div className="my-20">
<Spinner />
</div>
{/* This is a large div that, when in view, will trigger more patterns to load */}
<div
className="relative flex -translate-y-full transform flex-col items-end justify-end"
ref={loadMoreRef}
style={{
zIndex: -1,
marginBottom: '-100%',
height:
currentType === 'template' ? '150vh' : '75vh',
}}
/>
</>
)}
</>
)
})
const Grid = ({ type, children }) => {
const sharedClasses = 'relative min-h-screen z-10 pb-40 pt-0.5'
switch (type) {
case 'template':
return (
<div
className={`grid gap-6 md:gap-8 lg:grid-cols-2 ${sharedClasses}`}>
{children}
</div>
)
}
const breakpointColumnsObj = {
default: 3,
1600: 2,
860: 1,
599: 2,
400: 1,
}
return (
<Masonry
breakpointCols={breakpointColumnsObj}
className={`-ml-6 flex w-auto px-0.5 md:-ml-8 ${sharedClasses}`}
columnClassName="pl-6 md:pl-8 bg-clip-padding space-y-6 md:space-y-8">
{children}
</Masonry>
)
}

View File

@ -0,0 +1,66 @@
import { Fragment, useRef } from '@wordpress/element'
import { Dialog, Transition } from '@headlessui/react'
import FooterNotice from '@extendify/components/notices/FooterNotice'
import { useModal } from '@extendify/hooks/useModal'
import { useTestGroup } from '@extendify/hooks/useTestGroup'
import { useGlobalStore } from '@extendify/state/GlobalState'
import { Layout } from './layout/Layout'
export default function MainWindow() {
const containerRef = useRef(null)
const open = useGlobalStore((state) => state.open)
const setOpen = useGlobalStore((state) => state.setOpen)
const modal = useModal(open)
const ready = useGlobalStore((state) => state.ready)
const footerNoticePosition = useTestGroup('notice-position', ['A', 'B'])
return (
<Transition appear show={open} as={Fragment}>
<Dialog
as="div"
static
className="extendify"
initialFocus={containerRef}
onClose={() => setOpen(false)}>
<div className="fixed inset-0 z-high m-auto h-screen w-screen overflow-y-auto sm:h-auto sm:w-auto">
<div className="flex min-h-screen items-end justify-center px-4 pt-4 pb-20 text-center sm:block sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100">
<Dialog.Overlay className="fixed inset-0 bg-black bg-opacity-40 transition-opacity" />
</Transition.Child>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-5"
enterTo="opacity-100 translate-y-0">
<div
ref={containerRef}
tabIndex="0"
onClick={(e) =>
e.target === e.currentTarget &&
setOpen(false)
}
className="fixed inset-0 transform p-2 transition-all lg:absolute lg:overflow-hidden lg:p-16">
{footerNoticePosition === 'B' && (
<FooterNotice className="-mt-6" />
)}
<Layout />
{ready ? (
<>
{footerNoticePosition === 'A' && (
<FooterNotice />
)}
{modal}
</>
) : null}
</div>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
)
}

View File

@ -0,0 +1,103 @@
import { Panel } from '@wordpress/components'
import { Button } from '@wordpress/components'
import { memo } from '@wordpress/element'
import { __ } from '@wordpress/i18n'
import { Icon, close } from '@wordpress/icons'
import classNames from 'classnames'
import { ImportCounter } from '@extendify/components/ImportCounter'
import { SidebarNotice } from '@extendify/components/SidebarNotice'
import { SiteTypeSelector } from '@extendify/components/SiteTypeSelector'
import TaxonomySection from '@extendify/components/TaxonomySection'
import { featured } from '@extendify/components/icons'
import { brandMark } from '@extendify/components/icons/'
import { useTestGroup } from '@extendify/hooks/useTestGroup'
import { useGlobalStore } from '@extendify/state/GlobalState'
import { useTaxonomyStore } from '@extendify/state/Taxonomies'
import { useTemplatesStore } from '@extendify/state/Templates'
import { useUserStore } from '@extendify/state/User'
export const Sidebar = memo(function Sidebar() {
const taxonomies = useTaxonomyStore((state) => state.taxonomies)
const searchParams = useTemplatesStore((state) => state.searchParams)
const updatePreferredSiteType = useUserStore(
(state) => state.updatePreferredSiteType,
)
const updateTaxonomies = useTemplatesStore(
(state) => state.updateTaxonomies,
)
const apiKey = useUserStore((state) => state.apiKey)
const taxonomyType =
searchParams.type === 'pattern' ? 'patternType' : 'layoutType'
const isFeatured = !searchParams?.taxonomies[taxonomyType]?.slug?.length
const setOpen = useGlobalStore((state) => state.setOpen)
const noticeVariety = useTestGroup('import-counter-type', ['A', 'B'])
return (
<>
<div className="-ml-1.5 hidden px-5 text-extendify-black sm:flex">
<Icon icon={brandMark} size={40} />
</div>
<div className="flex md:hidden items-center justify-end -mt-5 mx-1">
<Button
onClick={() => setOpen(false)}
icon={<Icon icon={close} size={24} />}
label={__('Close library', 'extendify')}
/>
</div>
<div className="px-5 hidden md:block">
<button
onClick={() =>
updateTaxonomies({
[taxonomyType]: { slug: '', title: 'Featured' },
})
}
className={classNames(
'button-focus m-0 flex w-full cursor-pointer items-center space-x-1 bg-transparent px-0 py-2 text-left text-sm leading-none transition duration-200 hover:text-wp-theme-500',
{ 'text-wp-theme-500': isFeatured },
)}>
<Icon icon={featured} size={24} />
<span className="text-sm">
{__('Featured', 'extendify')}
</span>
</button>
</div>
<div className="mx-6 px-5 pt-0.5 sm:mx-0 sm:mb-8 sm:mt-0">
{Object.keys(taxonomies?.siteType ?? {}).length > 0 && (
<SiteTypeSelector
value={searchParams?.taxonomies?.siteType ?? ''}
setValue={(termData) => {
updatePreferredSiteType(termData)
updateTaxonomies({ siteType: termData })
}}
terms={taxonomies.siteType}
/>
)}
</div>
<div className="mt-px hidden flex-grow overflow-y-auto pb-36 pt-px sm:block space-y-6">
<Panel className="bg-transparent">
<TaxonomySection
taxType={taxonomyType}
taxonomies={taxonomies[taxonomyType]?.filter(
(term) => !term?.designType,
)}
/>
</Panel>
<Panel className="bg-transparent">
<TaxonomySection
taxLabel={__('Design', 'extendify')}
taxType={taxonomyType}
taxonomies={taxonomies[taxonomyType]?.filter((term) =>
Boolean(term?.designType),
)}
/>
</Panel>
</div>
{!apiKey.length && (
<div className="px-5">
{noticeVariety === 'A' ? <ImportCounter /> : null}
{noticeVariety === 'B' ? <SidebarNotice /> : null}
</div>
)}
</>
)
})

View File

@ -0,0 +1,19 @@
import { useGlobalStore } from '@extendify/state/GlobalState'
export default function HasSidebar({ children }) {
const ready = useGlobalStore((state) => state.ready)
return (
<>
<aside className="relative flex-shrink-0 border-r border-extendify-transparent-black-100 bg-extendify-transparent-white py-0 backdrop-blur-xl backdrop-saturate-200 backdrop-filter sm:py-5">
<div className="flex h-full flex-col py-6 sm:w-72 sm:space-y-6 sm:py-0">
{ready ? children[0] : null}
</div>
</aside>
<main
id="extendify-templates"
className="h-full w-full overflow-hidden bg-gray-50 pt-6 sm:pt-0">
{ready ? children[1] : null}
</main>
</>
)
}

View File

@ -0,0 +1,82 @@
import { Button } from '@wordpress/components'
import { useRef, useEffect, useState, useCallback } from '@wordpress/element'
import { __ } from '@wordpress/i18n'
import { useWhenIdle } from '@extendify/hooks/helpers'
import { GridView } from '@extendify/pages/GridView'
import { Sidebar } from '@extendify/pages/Sidebar'
import { useTemplatesStore } from '@extendify/state/Templates'
import HasSidebar from './HasSidebar'
import { Toolbar } from './Toolbar'
export const Layout = ({ setOpen }) => {
const gridContainer = useRef()
const searchParams = useTemplatesStore((state) => state.searchParams)
const [showIdleScreen, setShowIdleScreen] = useState(false)
const resetTemplates = useTemplatesStore((state) => state.resetTemplates)
const idle = useWhenIdle(300_000) // 5 minutes
const removeIdleScreen = useCallback(() => {
setShowIdleScreen(false)
resetTemplates()
}, [resetTemplates])
useEffect(() => {
if (idle) setShowIdleScreen(true)
}, [idle])
useEffect(() => {
setShowIdleScreen(false)
}, [searchParams])
useEffect(() => {
if (!gridContainer.current) return
gridContainer.current.scrollTop = 0
}, [searchParams])
return (
<div className="relative mx-auto flex h-full max-w-screen-4xl flex-col items-center">
<div className="w-full flex-grow overflow-hidden">
<button
onClick={() =>
document
.getElementById('extendify-templates')
.querySelector('button')
.focus()
}
className="extendify-skip-to-sr-link sr-only focus:not-sr-only focus:text-blue-500">
{__('Skip to templates', 'extendify')}
</button>
<div className="relative mx-auto h-full sm:flex">
<HasSidebar>
<Sidebar />
<div className="relative z-30 flex h-full flex-col">
<Toolbar
className="hidden h-20 w-full flex-shrink-0 px-6 sm:block md:px-8"
hideLibrary={() => setOpen(false)}
/>
<div
ref={gridContainer}
className="z-20 flex-grow overflow-y-auto px-6 md:px-8">
{showIdleScreen ? (
<IdleScreen callback={removeIdleScreen} />
) : (
<GridView />
)}
</div>
</div>
</HasSidebar>
</div>
</div>
</div>
)
}
const IdleScreen = ({ callback }) => (
<div className="flex h-full flex-col items-center justify-center">
<p className="mb-6 text-sm font-normal text-extendify-gray">
{__("We've added new stuff while you were away.", 'extendify')}
</p>
<Button
className="components-button border-color-wp-theme-500 bg-wp-theme-500 text-white hover:bg-wp-theme-600"
onClick={callback}>
{__('Reload')}
</Button>
</div>
)

View File

@ -0,0 +1,37 @@
import { Button } from '@wordpress/components'
import { memo } from '@wordpress/element'
import { __ } from '@wordpress/i18n'
import { Icon, close } from '@wordpress/icons'
import { TypeSelect } from '@extendify/components/TypeSelect'
import { user } from '@extendify/components/icons/'
import { SettingsModal } from '@extendify/components/modals/settings/SettingsModal'
import { useGlobalStore } from '@extendify/state/GlobalState'
import { useUserStore } from '@extendify/state/User'
export const Toolbar = memo(function Toolbar({ className }) {
const setOpen = useGlobalStore((state) => state.setOpen)
const pushModal = useGlobalStore((state) => state.pushModal)
const loggedIn = useUserStore((state) => state.apiKey.length)
return (
<div className={className}>
<div className="flex h-full items-center justify-between">
<div className="flex-1"></div>
<TypeSelect className="flex flex-1 items-center justify-center" />
<div className="flex flex-1 items-center justify-end">
<Button
onClick={() => pushModal(<SettingsModal />)}
icon={<Icon icon={user} size={24} />}
label={__('Login and settings area', 'extendify')}>
{loggedIn ? '' : __('Sign in', 'extendify')}
</Button>
<Button
onClick={() => setOpen(false)}
icon={<Icon icon={close} size={24} />}
label={__('Close library', 'extendify')}
/>
</div>
</div>
</div>
)
})

View File

@ -0,0 +1,38 @@
import create from 'zustand'
import { persist, subscribeWithSelector } from 'zustand/middleware'
export const useGlobalStore = create(
subscribeWithSelector(
persist(
(set, get) => ({
open: false,
ready: false,
metaData: {},
// These two are here just to persist their previous values,
// but could be refactored to be the source instead.
// It would require a refactor to state/Templates.js
currentTaxonomies: {},
currentType: 'pattern',
modals: [],
pushModal: (modal) => set({ modals: [modal, ...get().modals] }),
popModal: () => set({ modals: get().modals.slice(1) }),
removeAllModals: () => set({ modals: [] }),
updateCurrentTaxonomies: (data) =>
set({
currentTaxonomies: Object.assign({}, data),
}),
updateCurrentType: (data) => set({ currentType: data }),
setOpen: (value) => set({ open: value }),
setReady: (value) => set({ ready: value }),
}),
{
name: 'extendify-global-state',
partialize: (state) => {
delete state.modals
delete state.ready
return state
},
},
),
),
)

View File

@ -0,0 +1,22 @@
import create from 'zustand'
import { persist } from 'zustand/middleware'
export const useWantedTemplateStore = create(
persist(
(set) => ({
wantedTemplate: {},
importOnLoad: false,
setWanted: (template) =>
set({
wantedTemplate: template,
}),
removeWanted: () =>
set({
wantedTemplate: {},
}),
}),
{
name: 'extendify-wanted-template',
},
),
)

View File

@ -0,0 +1,21 @@
import create from 'zustand'
import { persist } from 'zustand/middleware'
import { SiteSettings } from '@extendify/api/SiteSettings'
const storage = {
getItem: async () => await SiteSettings.getData(),
setItem: async (_name, value) => await SiteSettings.setData(value),
removeItem: () => {},
}
export const useSiteSettingsStore = create(
persist(
() => ({
enabled: true,
}),
{
name: 'extendify-sitesettings',
getStorage: () => storage,
},
),
)

View File

@ -0,0 +1,26 @@
import create from 'zustand'
import { persist } from 'zustand/middleware'
import { Taxonomies as TaxonomiesApi } from '@extendify/api/Taxonomies'
export const useTaxonomyStore = create(
persist(
(set, get) => ({
taxonomies: {},
setTaxonomies: (taxonomies) => set({ taxonomies }),
fetchTaxonomies: async () => {
let tax = await TaxonomiesApi.get()
tax = Object.keys(tax).reduce((taxFiltered, key) => {
taxFiltered[key] = tax[key]
return taxFiltered
}, {})
if (!Object.keys(tax)?.length) {
return
}
get().setTaxonomies(tax)
},
}),
{
name: 'extendify-taxonomies',
},
),
)

View File

@ -0,0 +1,130 @@
import create from 'zustand'
import { subscribeWithSelector } from 'zustand/middleware'
import { useGlobalStore } from './GlobalState'
import { useTaxonomyStore } from './Taxonomies'
import { useUserStore } from './User'
const defaultCategoryForType = (tax) =>
tax === 'siteType'
? { slug: '', title: 'Unknown' }
: { slug: '', title: 'Featured' }
export const useTemplatesStore = create(
subscribeWithSelector((set, get) => ({
templates: [],
skipNextFetch: false,
fetchToken: null,
taxonomyDefaultState: {},
nextPage: '',
searchParams: {
taxonomies: {},
type: 'pattern',
},
initTemplateData() {
set({ activeTemplate: {} })
get().setupDefaultTaxonomies()
get().updateType(useGlobalStore.getState().currentType)
},
appendTemplates: (templates) =>
set({
templates: [
...new Map(
[...get().templates, ...templates].map((item) => [
item.id,
item,
]),
).values(),
],
}),
setupDefaultTaxonomies: () => {
const taxonomies = useTaxonomyStore.getState().taxonomies
let taxonomyDefaultState = Object.entries(taxonomies).reduce(
(state, current) => (
(state[current[0]] = defaultCategoryForType(current[0])),
state
),
{},
)
const tax = {}
let preferredTax =
useUserStore.getState().preferredOptions?.taxonomies ?? {}
// Check for old site type and set it if it exists
if (preferredTax.tax_categories) {
preferredTax = get().getLegacySiteType(preferredTax, taxonomies)
}
taxonomyDefaultState = Object.assign(
{},
taxonomyDefaultState,
// Override with the user's preferred taxonomies
preferredTax,
// Override with the global state
useGlobalStore.getState()?.currentTaxonomies ?? {},
)
tax.taxonomies = Object.assign({}, taxonomyDefaultState)
set({
taxonomyDefaultState: taxonomyDefaultState,
searchParams: {
...Object.assign(get().searchParams, tax),
},
})
},
updateTaxonomies: (params) => {
const data = {}
data.taxonomies = Object.assign(
{},
get().searchParams.taxonomies,
params,
)
if (data?.taxonomies?.siteType) {
// This is what the user "prefers", which may be used outside the library
// which is persisted to the database, where as the global library state is in local storage
useUserStore
.getState()
.updatePreferredOption(
'siteType',
data?.taxonomies?.siteType,
)
}
useGlobalStore.getState().updateCurrentTaxonomies(data?.taxonomies)
get().updateSearchParams(data)
},
updateType(type) {
useGlobalStore.getState().updateCurrentType(type)
get().updateSearchParams({ type })
},
updateSearchParams: (params) => {
// If taxonomies are set to {}, lets use the default
if (params?.taxonomies && !Object.keys(params.taxonomies).length) {
params.taxonomies = get().taxonomyDefaultState
}
const searchParams = Object.assign({}, get().searchParams, params)
// If the params are not the same, then update
if (
JSON.stringify(searchParams) !==
JSON.stringify(get().searchParams)
) {
set({ templates: [], nextPage: '', searchParams })
}
},
resetTemplates: () => set({ templates: [], nextPage: '' }),
getLegacySiteType: (preferredTax, taxonomies) => {
const oldSiteType = taxonomies.siteType.find((t) =>
[t.slug, t?.title].includes(preferredTax.tax_categories),
)
// TODO: This is kind of wonky, as we keep track of the state in two places.
useUserStore.getState().updatePreferredSiteType(oldSiteType)
get().updateTaxonomies({ siteType: oldSiteType })
// Remove the legacy term so this only runs once
useUserStore
.getState()
.updatePreferredOption('tax_categories', null)
return useUserStore.getState().preferredOptions.taxonomies
},
})),
)

View File

@ -0,0 +1,186 @@
import { sample } from 'lodash'
import create from 'zustand'
import { persist } from 'zustand/middleware'
import { User } from '@extendify/api/User'
const storage = {
getItem: async () => await User.getData(),
setItem: async (_name, value) => await User.setData(value),
removeItem: async () => await User.deleteData(),
}
const isGlobalLibraryEnabled = () =>
window.extendifyData.sitesettings === null ||
window.extendifyData?.sitesettings?.state?.enabled
// Keep track of active tests as some might be active
// but never rendered.
const activeTests = {
['notice-position']: '0001',
['main-button-text']: '0002',
['default-or-alt-sitetype']: '0004',
['import-counter-type']: '0005',
}
export const useUserStore = create(
persist(
(set, get) => ({
_hasHydrated: false,
firstLoadedOn: new Date().toISOString(),
email: '',
apiKey: '',
uuid: '',
sdkPartner: '',
noticesDismissedAt: {},
modalNoticesDismissedAt: {},
imports: 0, // total imports over time
runningImports: 0, // timed imports, resets to 0 every month
allowedImports: 0, // Max imports the Extendify service allows
freebieImports: 0, // Various free imports from actions (rewards)
entryPoint: 'not-set',
enabled: isGlobalLibraryEnabled(),
canInstallPlugins: false,
canActivatePlugins: false,
participatingTestsGroups: {},
preferredOptions: {
taxonomies: {},
type: '',
search: '',
},
preferredOptionsHistory: {
siteType: [],
},
incrementImports: () => {
// If the user has freebie imports, use those first
const freebieImports =
Number(get().freebieImports) > 0
? Number(get().freebieImports) - 1
: Number(get().freebieImports)
// If they don't, then increment the running imports
const runningImports =
Number(get().runningImports) + +(freebieImports < 1)
set({
imports: Number(get().imports) + 1,
runningImports,
freebieImports,
})
},
giveFreebieImports: (amount) => {
set({ freebieImports: get().freebieImports + amount })
},
totalAvailableImports: () => {
return (
Number(get().allowedImports) + Number(get().freebieImports)
)
},
testGroup(testKey, groupOptions) {
if (!Object.keys(activeTests).includes(testKey)) return
let groups = get().participatingTestsGroups
// If the test is already in the group, don't add it again
if (!groups[testKey]) {
set({
participatingTestsGroups: Object.assign({}, groups, {
[testKey]: sample(groupOptions),
}),
})
}
groups = get().participatingTestsGroups
return groups[testKey]
},
activeTestGroups() {
return Object.entries(get().participatingTestsGroups)
.filter(([key]) => Object.keys(activeTests).includes(key))
.reduce((obj, [key, value]) => {
obj[key] = value
return obj
}, {})
},
activeTestGroupsUtmValue() {
const active = Object.entries(get().activeTestGroups())
.map(([key, value]) => {
return `${activeTests[key]}=${value}`
}, '')
.join(':')
return encodeURIComponent(active)
},
hasAvailableImports: () => {
return get().apiKey
? true
: Number(get().runningImports) <
Number(get().totalAvailableImports())
},
remainingImports: () => {
const remaining =
Number(get().totalAvailableImports()) -
Number(get().runningImports)
// If they have no allowed imports, this might be a first load
// where it's just fetching templates (and/or their max alllowed)
if (!get().allowedImports) {
return null
}
return remaining > 0 ? remaining : 0
},
updatePreferredSiteType: (value) => {
get().updatePreferredOption('siteType', value)
if (!value?.slug || value.slug === 'unknown') return
const current = get().preferredOptionsHistory?.siteType ?? []
// If the site type isn't already included, prepend it
if (!current.find((t) => t.slug === value.slug)) {
const siteType = [value, ...current]
set({
preferredOptionsHistory: Object.assign(
{},
get().preferredOptionsHistory,
{ siteType: siteType.slice(0, 3) },
),
})
}
},
updatePreferredOption: (option, value) => {
// If the option doesn't exist, assume it's a taxonomy
if (
!Object.prototype.hasOwnProperty.call(
get().preferredOptions,
option,
)
) {
value = Object.assign(
{},
get().preferredOptions?.taxonomies ?? {},
{ [option]: value },
)
option = 'taxonomies'
}
set({
preferredOptions: {
...Object.assign({}, get().preferredOptions, {
[option]: value,
}),
},
})
},
// Will mark a modal or footer notice
markNoticeSeen: (key, type) => {
set({
[`${type}DismissedAt`]: {
...get()[`${type}DismissedAt`],
[key]: new Date().toISOString(),
},
})
},
}),
{
name: 'extendify-user',
getStorage: () => storage,
onRehydrateStorage: () => () => {
useUserStore.setState({ _hasHydrated: true })
},
partialize: (state) => {
delete state._hasHydrated
return state
},
},
),
)

View File

@ -0,0 +1,59 @@
import { isString, toLower } from 'lodash'
import { useUserStore } from '@extendify/state/User'
/**
* Will check if the given string contains the search string
*
* @param {string} string
* @param {string} searchString
*/
export function search(string, searchString) {
// type validation
if (!isString(string) || !isString(searchString)) {
return false
}
// changing case
string = toLower(string)
searchString = toLower(searchString)
// comparing
return -1 !== searchString.indexOf(string) ? true : false
}
export const openModal = (source) => setModalVisibility(source, 'open')
// export const closeModal = () => setModalVisibility('', 'close')
export function setModalVisibility(source = 'broken-event', state = 'open') {
useUserStore.setState({
entryPoint: source,
})
window.dispatchEvent(
new CustomEvent(`extendify::${state}-library`, {
detail: source,
bubbles: true,
}),
)
}
export function getPluginDescription(plugin) {
switch (plugin) {
case 'editorplus':
return 'Editor Plus'
case 'ml-slider':
return 'MetaSlider'
}
return plugin
}
export function getTaxonomyName(key) {
switch (key) {
case 'siteType':
return 'Site Type'
case 'patternType':
return 'Content'
case 'layoutType':
return 'Page Types'
}
return key
}

View File

@ -0,0 +1,32 @@
import { dispatch, select } from '@wordpress/data'
export function injectTemplateBlocks(blocks, templateRaw) {
const { insertBlocks, replaceBlock } = dispatch('core/block-editor')
const {
getSelectedBlock,
getBlockHierarchyRootClientId,
getBlockIndex,
getGlobalBlockCount,
} = select('core/block-editor')
const { clientId, name, attributes } = getSelectedBlock() || {}
const rootClientId = clientId ? getBlockHierarchyRootClientId(clientId) : ''
const insertPointIndex =
(rootClientId ? getBlockIndex(rootClientId) : getGlobalBlockCount()) + 1
const injectblock = () =>
name === 'core/paragraph' && attributes?.content === ''
? replaceBlock(clientId, blocks)
: insertBlocks(blocks, insertPointIndex)
return injectblock().then(() =>
window.dispatchEvent(
new CustomEvent('extendify::template-inserted', {
detail: {
template: templateRaw,
},
bubbles: true,
}),
),
)
}

View File

@ -0,0 +1,122 @@
import { InspectorAdvancedControls } from '@wordpress/block-editor'
import { FormTokenField } from '@wordpress/components'
import { createHigherOrderComponent } from '@wordpress/compose'
import { addFilter } from '@wordpress/hooks'
import { __ } from '@wordpress/i18n'
import suggestions from '../../utility-framework/suggestions.json'
function addAttributes(settings) {
// Add new extUtilities attribute to block settings.
return {
...settings,
attributes: {
...settings.attributes,
extUtilities: {
type: 'array',
default: [],
},
},
}
}
function addEditProps(settings) {
const existingGetEditWrapperProps = settings.getEditWrapperProps
settings.getEditWrapperProps = (attributes) => {
let props = {}
if (existingGetEditWrapperProps) {
props = existingGetEditWrapperProps(attributes)
}
return addSaveProps(props, settings, attributes)
}
return settings
}
// Create HOC to add Extendify Utility to Advanced Panel of block.
const utilityClassEdit = createHigherOrderComponent((BlockEdit) => {
return function editPanel(props) {
const classes = props?.attributes?.extUtilities ?? []
const suggestionList = suggestions.suggestions.map((s) => {
// Remove all extra // and . from classnames
return s.replace('.', '').replace(new RegExp('\\\\', 'g'), '')
})
return (
<>
<BlockEdit {...props} />
{classes && (
<InspectorAdvancedControls>
<FormTokenField
label={__('Extendify Utilities', 'extendify')}
tokenizeOnSpace={true}
value={classes}
suggestions={suggestionList}
onChange={(value) => {
props.setAttributes({
extUtilities: value,
})
}}
/>
</InspectorAdvancedControls>
)}
</>
)
}
}, 'utilityClassEdit')
function addSaveProps(saveElementProps, blockType, attributes) {
const generatedClasses = saveElementProps?.className ?? []
const classes = attributes?.extUtilities ?? []
const additionalClasses = attributes?.className ?? []
if (!classes || !Object.keys(classes).length) {
return saveElementProps
}
// EK seems to be converting string values to objects in some situations
const normalizeAsArray = (item) => {
switch (Object.prototype.toString.call(item)) {
case '[object String]':
return item.split(' ')
case '[object Array]':
return item
default:
return []
}
}
const classesCombined = new Set([
...normalizeAsArray(additionalClasses),
...normalizeAsArray(generatedClasses),
...normalizeAsArray(classes),
])
return Object.assign({}, saveElementProps, {
className: [...classesCombined].join(' '),
})
}
addFilter(
'blocks.registerBlockType',
'extendify/utilities/attributes',
addAttributes,
)
addFilter(
'blocks.registerBlockType',
'extendify/utilities/addEditProps',
addEditProps,
)
addFilter(
'editor.BlockEdit',
'extendify/utilities/advancedClassControls',
utilityClassEdit,
)
addFilter(
'blocks.getSaveContent.extraProps',
'extendify/utilities/extra-props',
addSaveProps,
)