initial commit
This commit is contained in:
@ -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 />
|
||||
}
|
@ -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 ?? '',
|
||||
})
|
||||
},
|
||||
}
|
@ -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')
|
||||
},
|
||||
}
|
@ -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',
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
import { Axios as api } from './axios'
|
||||
|
||||
export const Taxonomies = {
|
||||
async get() {
|
||||
return await api.get('taxonomies')
|
||||
},
|
||||
}
|
@ -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, '')
|
||||
}
|
@ -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')
|
||||
},
|
||||
}
|
@ -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 }
|
@ -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;
|
||||
}
|
@ -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: {},
|
||||
})
|
||||
})
|
@ -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} />,
|
||||
})
|
@ -0,0 +1,2 @@
|
||||
import './block-category.js'
|
||||
import './library/block.js'
|
@ -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(),
|
||||
)
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
})
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
})
|
@ -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>
|
||||
)
|
||||
}
|
@ -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
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
})
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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'
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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>
|
||||
)
|
||||
}
|
@ -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')}
|
||||
/>
|
||||
)
|
||||
})
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
},
|
||||
)
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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">
|
||||
|
|
||||
</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>
|
||||
</>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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">
|
||||
|
|
||||
</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>
|
||||
)
|
||||
}
|
@ -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">
|
||||
|
|
||||
</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>
|
||||
</>
|
||||
)
|
||||
}
|
@ -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">
|
||||
|
|
||||
</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">
|
||||
•
|
||||
</span>
|
||||
<Button
|
||||
variant="link"
|
||||
className="h-auto p-0 text-black underline hover:no-underline"
|
||||
onClick={disableLibrary}>
|
||||
{__('Turn off the library', 'extendify')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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
|
||||
})
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
import { softErrorHandler } from './softerror-encountered'
|
||||
import { templateHandler } from './template-inserted'
|
||||
|
||||
;[templateHandler, softErrorHandler].forEach((listener) => listener.register())
|
@ -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'),
|
||||
)
|
||||
},
|
||||
}
|
@ -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)
|
||||
})
|
||||
},
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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'),
|
||||
)
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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'),
|
||||
)
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
})
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
@ -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>
|
||||
)
|
||||
})
|
@ -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
|
||||
},
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
@ -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',
|
||||
},
|
||||
),
|
||||
)
|
@ -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,
|
||||
},
|
||||
),
|
||||
)
|
@ -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',
|
||||
},
|
||||
),
|
||||
)
|
@ -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
|
||||
},
|
||||
})),
|
||||
)
|
@ -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
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
@ -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
|
||||
}
|
@ -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,
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
@ -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,
|
||||
)
|
Reference in New Issue
Block a user