initial commit

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

View File

@@ -0,0 +1,10 @@
<?php
/**
* Silence is golden.
*
* @package Redux Framework
*/
// Shim file for odd theme integrations.
echo null;

View File

@@ -0,0 +1,10 @@
<?php
/**
* Silence is golden.
*
* @package Redux Framework
*/
// Shim file for odd theme integrations.
echo null;

View File

@@ -0,0 +1,8 @@
<?php
/**
* Silence is golden.
*
* @package Redux Framework
*/
echo null;

View File

@@ -0,0 +1,10 @@
<?php
/**
* Silence is golden.
*
* @package Redux Framework
*/
// Shim file for odd theme integrations.
echo null;

View File

@@ -0,0 +1,10 @@
<?php
/**
* Silence is golden.
*
* @package Redux Framework
*/
// Shim file for odd theme integrations.
echo null;

View File

@@ -0,0 +1,10 @@
<?php
/**
* Silence is golden.
*
* @package Redux Framework
*/
// Shim file for odd theme integrations.
echo null;

View File

@@ -0,0 +1,8 @@
<?php
/**
* Silence is golden.
*
* @package Redux Framework
*/
echo null;

View File

@@ -0,0 +1,10 @@
<?php
/**
* Silence is golden.
*
* @package Redux Framework
*/
// Shim file for bad theme developers.
echo null;

View File

@@ -0,0 +1,8 @@
<?php
/**
* Silence is golden.
*
* @package Redux Framework
*/
echo null;

View File

@@ -0,0 +1,8 @@
<?php
/**
* Silence is golden.
*
* @package Redux Framework
*/
echo null;

View File

@@ -0,0 +1,8 @@
<?php
/**
* Silence is golden.
*
* @package Redux Framework
*/
echo null;

View File

@@ -0,0 +1,546 @@
<?php
/**
* Redux_Framework_Plugin main class
*
* @package Redux Framework
* @since 3.0.0
*/
// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;
if ( ! class_exists( 'Redux_Framework_Plugin', false ) ) {
/**
* Main Redux_Framework_Plugin class
*
* @since 3.0.0
*/
class Redux_Framework_Plugin {
/**
* Option array for demo mode.
*
* @access protected
* @var array $options Array of config options, used to check for demo mode
* @since 3.0.0
*/
protected $options = array();
/**
* Use this value as the text domain when translating strings from this plugin. It should match
* the Text Domain field set in the plugin header, as well as the directory name of the plugin.
* Additionally, text domains should only contain letters, number and hyphens, not underscores
* or spaces.
*
* @access protected
* @var string $plugin_slug The unique ID (slug) of this plugin
* @since 3.0.0
*/
protected $plugin_slug = 'redux-framework';
/**
* Set on network activate.
*
* @access protected
* @var string $plugin_network_activated Check for plugin network activation
* @since 3.0.0
*/
protected $plugin_network_activated = null;
/**
* Class instance.
*
* @access private
* @var Redux_Framework_Plugin $instance The one true Redux_Framework_Plugin
* @since 3.0.0
*/
private static $instance;
/**
* Crash flag.
*
* @access private
* @var Redux_Framework_Plugin $crash Crash flag if inside a crash.
* @since 4.1.15
*/
public static $crash = false;
/**
* Get active instance
*
* @access public
* @since 3.1.3
* @return self::$instance The one true Redux_Framework_Plugin
*/
public static function instance(): ?Redux_Framework_Plugin {
$path = REDUX_PLUGIN_FILE;
$res = false;
if ( function_exists( 'get_plugin_data' ) && file_exists( $path ) ) {
$data = get_plugin_data( $path );
if ( isset( $data ) && isset( $data['Version'] ) && '' !== $data['Version'] ) {
$res = version_compare( $data['Version'], '4', '<' );
}
if ( is_plugin_active( 'redux-framework/redux-framework.php' ) && true === $res ) {
echo '<div class="error"><p>' . esc_html__( 'Redux Framework version 4 is activated but not loaded. Redux Framework version 3 is still installed and activated. Please deactivate Redux Framework version 3.', 'redux-framework' ) . '</p></div>'; // phpcs:ignore WordPress.Security.EscapeOutput
return null;
}
}
if ( ! self::$instance ) {
self::$instance = new self();
if ( class_exists( 'ReduxFramework' ) ) {
self::$instance->load_first();
} else {
self::$instance->get_redux_options();
self::$instance->includes();
self::$instance->hooks();
}
}
return self::$instance;
}
/**
* Shim for getting instance
*
* @access public
* @since 4.0.1
* @return self::$instance The one true Redux_Framework_Plugin
*/
public static function get_instance(): ?Redux_Framework_Plugin {
return self::instance();
}
/**
* Get Redux options
*
* @access public
* @since 3.1.3
* @return void
*/
public function get_redux_options() {
// Setup defaults.
$defaults = array(
'demo' => false,
);
// If multisite is enabled.
if ( is_multisite() ) {
// Get network activated plugins.
$plugins = get_site_option( 'active_sitewide_plugins' );
foreach ( $plugins as $file => $plugin ) {
if ( strpos( $file, 'redux-framework.php' ) !== false ) {
$this->plugin_network_activated = true;
$this->options = get_site_option( 'ReduxFrameworkPlugin', $defaults );
}
}
}
// If options aren't set, grab them now!
if ( empty( $this->options ) ) {
$this->options = get_option( 'ReduxFrameworkPlugin', $defaults );
}
}
/**
* Include necessary files
*
* @access public
* @since 3.1.3
* @return void
*/
public function includes() {
// Include Redux_Core.
if ( file_exists( dirname( __FILE__ ) . '/redux-core/framework.php' ) ) {
require_once dirname( __FILE__ ) . '/redux-core/framework.php';
}
Redux_Core::$redux_templates_enabled = (bool) get_option( 'use_redux_templates' );
Redux_Core::$extendify_templates_enabled = (bool) get_option( 'use_extendify_templates', true );
// Including extendify sdk.
if ( true === (bool) get_option( 'use_extendify_templates', true ) ) {
if ( file_exists( dirname( __FILE__ ) . '/extendify-sdk/loader.php' ) ) {
$GLOBALS['extendify_sdk_partner'] = 'Redux';
require_once dirname( __FILE__ ) . '/extendify-sdk/loader.php';
}
}
if ( file_exists( dirname( __FILE__ ) . '/redux-templates/redux-templates.php' ) ) {
require_once dirname( __FILE__ ) . '/redux-templates/redux-templates.php';
}
if ( isset( Redux_Core::$as_plugin ) ) {
Redux_Core::$as_plugin = true;
}
add_action( 'setup_theme', array( $this, 'load_sample_config' ) );
}
/**
* Loads the sample config after everything is loaded.
*
* @access public
* @since 4.0.2
* @return void
*/
public function load_sample_config() {
// Include demo config, if demo mode is active.
if ( $this->options['demo'] && file_exists( dirname( __FILE__ ) . '/sample/sample-config.php' ) ) {
require_once dirname( __FILE__ ) . '/sample/sample-config.php';
}
}
/**
* Run action and filter hooks
*
* @access private
* @since 3.1.3
* @return void
*/
private function hooks() {
add_action( 'activated_plugin', array( $this, 'load_first' ) );
add_action( 'wp_loaded', array( $this, 'options_toggle_check' ) );
// Activate plugin when new blog is added.
add_action( 'wpmu_new_blog', array( $this, 'activate_new_site' ) );
// Display admin notices.
add_action( 'admin_notices', array( $this, 'admin_notices' ) );
// Edit plugin metalinks.
add_filter( 'plugin_row_meta', array( $this, 'plugin_metalinks' ), null, 2 );
add_filter( 'network_admin_plugin_action_links', array( $this, 'add_settings_link' ), 1, 2 );
add_filter( 'plugin_action_links', array( $this, 'add_settings_link' ), 1, 2 );
// phpcs:ignore WordPress.NamingConventions.ValidHookName
do_action( 'redux/plugin/hooks', $this );
}
/**
* Pushes Redux to top of plugin load list, so it initializes before any plugin that may use it.
*/
public function load_first() {
if ( ! class_exists( 'Redux_Functions_Ex' ) ) {
require_once dirname( __FILE__ ) . '/redux-core/inc/classes/class-redux-functions-ex.php';
}
$plugin_dir = Redux_Functions_Ex::wp_normalize_path( WP_PLUGIN_DIR ) . '/';
$self_file = Redux_Functions_Ex::wp_normalize_path( __FILE__ );
$path = str_replace( $plugin_dir, '', $self_file );
$path = str_replace( 'class-redux-framework-plugin.php', 'redux-framework.php', $path );
$plugins = get_option( 'active_plugins' );
if ( $plugins ) {
$key = array_search( $path, $plugins, true );
if ( false !== $key ) {
array_splice( $plugins, $key, 1 );
array_unshift( $plugins, $path );
update_option( 'active_plugins', $plugins );
}
if ( class_exists( 'Redux_Pro' ) ) {
$self_file = Redux_Functions_Ex::wp_normalize_path( Redux_Pro::$dir );
$path = str_replace( $plugin_dir, '', $self_file );
// phpcs:ignore WordPress.NamingConventions.ValidHookName
$basename = apply_filters( 'redux/pro/basename', 'redux-pro.php' );
$key = array_search( $path . '/' . $basename, $plugins, true );
if ( false !== $key ) {
array_splice( $plugins, $key, 1 );
array_unshift( $plugins, $path . '/' . $basename );
update_option( 'active_plugins', $plugins );
}
}
}
}
/**
* Fired on plugin activation
*
* @access public
* @return void
* @since 3.0.0
*/
public static function activate() {
delete_site_transient( 'update_plugins' );
}
/**
* Fired when plugin is deactivated
*
* @access public
* @since 3.0.0
*
* @param boolean $network_wide True if plugin is network activated, false otherwise.
*
* @return void
*/
public static function deactivate( ?bool $network_wide ) {
if ( function_exists( 'is_multisite' ) && is_multisite() ) {
if ( $network_wide ) {
// Get all blog IDs.
$blog_ids = self::get_blog_ids();
foreach ( $blog_ids as $blog_id ) {
switch_to_blog( $blog_id );
self::single_deactivate();
}
restore_current_blog();
} else {
self::single_deactivate();
}
} else {
self::single_deactivate();
}
delete_option( 'ReduxFrameworkPlugin' );
Redux_Enable_Gutenberg::cleanup_options( 'redux-framework' ); // Auto disable Gutenberg and all that.
}
/**
* Fired when a new WPMU site is activated
*
* @access public
*
* @param int $blog_id The ID of the new blog.
*
* @return void
* @since 3.0.0
*/
public function activate_new_site( int $blog_id ) {
if ( 1 !== did_action( 'wpmu_new_blog' ) ) {
return;
}
switch_to_blog( $blog_id );
self::single_activate();
restore_current_blog();
}
/**
* Get all IDs of blogs that are not activated, not spam, and not deleted
*
* @access private
* @since 3.0.0
* @global object $wpdb
* @return array|false Array of IDs or false if none are found
*/
private static function get_blog_ids() {
global $wpdb;
$var = '0';
// Get an array of IDs (We have to do it this way because WordPress says so, however redundant).
$result = wp_cache_get( 'redux-blog-ids' );
if ( false === $result ) {
// WordPress says get_col is discouraged? I found no alternative. So...ignore! - kp.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
$result = $wpdb->get_col( $wpdb->prepare( "SELECT blog_id FROM $wpdb->blogs WHERE archived = %s AND spam = %s AND deleted = %s", $var, $var, $var ) );
wp_cache_set( 'redux-blog-ids', $result );
}
return $result;
}
/**
* Fired for each WPMS blog on plugin activation
*
* @access private
* @since 3.0.0
* @return void
*/
private static function single_activate() {
$nonce = wp_create_nonce( 'redux_framework_demo' );
$notices = get_option( 'ReduxFrameworkPlugin_ACTIVATED_NOTICES', array() );
$notices[] = esc_html__( 'Redux Framework has an embedded demo.', 'redux-framework' ) . ' <a href="./plugins.php?redux-framework-plugin=demo&nonce=' . $nonce . '">' . esc_html__( 'Click here to activate the sample config file.', 'redux-framework' ) . '</a>';
update_option( 'ReduxFrameworkPlugin_ACTIVATED_NOTICES', $notices );
}
/**
* Display admin notices
*
* @access public
* @since 3.0.0
* @return void
*/
public function admin_notices() {
do_action( 'redux_framework_plugin_admin_notice' );
$notices = get_option( 'ReduxFrameworkPlugin_ACTIVATED_NOTICES', '' );
if ( ! empty( $notices ) ) {
foreach ( $notices as $notice ) {
echo '<div class="updated notice is-dismissible"><p>' . $notice . '</p></div>'; // phpcs:ignore WordPress.Security.EscapeOutput
}
delete_option( 'ReduxFrameworkPlugin_ACTIVATED_NOTICES' );
}
}
/**
* Fired for each blog when the plugin is deactivated
*
* @access private
* @since 3.0.0
* @return void
*/
private static function single_deactivate() {
delete_option( 'ReduxFrameworkPlugin_ACTIVATED_NOTICES' );
}
/**
* Turn on or off
*
* @access public
* @since 3.0.0
* @return void
*/
public function options_toggle_check() {
if ( isset( $_GET['nonce'] ) && wp_verify_nonce( sanitize_key( $_GET['nonce'] ), 'redux_framework_demo' ) ) {
if ( isset( $_GET['redux-framework-plugin'] ) && 'demo' === $_GET['redux-framework-plugin'] ) {
$url = admin_url( add_query_arg( array( 'page' => 'redux-framework' ), 'options-general.php' ) );
if ( false === $this->options['demo'] ) {
$this->options['demo'] = true;
$url = admin_url( add_query_arg( array( 'page' => 'redux_demo' ), 'admin.php' ) );
} else {
$this->options['demo'] = false;
}
if ( is_multisite() && $this->plugin_network_activated ) {
update_site_option( 'ReduxFrameworkPlugin', $this->options );
} else {
update_option( 'ReduxFrameworkPlugin', $this->options );
}
wp_safe_redirect( esc_url( $url ) );
exit();
}
}
}
/**
* Add a settings link to the Redux entry in the plugin overview screen
*
* @param array $links Links array.
* @param string $file Plugin filename/slug.
*
* @return array
* @see filter:plugin_action_links
* @since 1.0
*/
public function add_settings_link( array $links, string $file ): array {
if ( basename( plugin_dir_path( __FILE__ ) ) . '/redux-framework.php' !== $file ) {
return $links;
}
$links[] = sprintf(
'<a href="%1$s" target="_blank">%2$s</a>',
esc_url( $this->get_site_utm_url( '', 'plugins-page','', 'go-pro' ) ),
sprintf(
'<span style="font-weight: bold;">%s</span>',
__( 'Go Pro', 'redux-framework' )
)
);
return $links;
}
/**
* Get the url where the Admin Columns website is hosted
*
* @param string $path Path to add to url.
*
* @return string
*/
private function get_site_url( string $path = '' ): string {
$url = 'https://extendify.com/pricing/';
if ( ! empty( $path ) ) {
$url .= '/' . trim( $path, '/' ) . '/';
}
return $url;
}
/**
* Url with utm tags
*
* @param string $path Path on site.
* @param string $utm_medium Medium var.
* @param string|null $utm_content Content var.
* @param string|bool $utm_campaign Campaign var.
*
* @return string
*/
public function get_site_utm_url( string $path, string $utm_medium, string $utm_content = null, $utm_campaign = false ): string {
$url = self::get_site_url( $path );
if ( ! $utm_campaign ) {
$utm_campaign = 'plugin-installation';
}
$args = array(
// Referrer: plugin.
'utm_source' => 'redux',
// Specific promotions or sales.
'utm_campaign' => $utm_campaign,
// Marketing medium: banner, documentation or email.
'utm_medium' => $utm_medium,
// Used for differentiation of medium.
'utm_content' => $utm_content,
);
$args = array_map( 'sanitize_key', array_filter( $args ) );
return add_query_arg( $args, $url );
}
/**
* Edit plugin metalinks
*
* @access public
*
* @param array $links The current array of links.
* @param string $file A specific plugin row.
*
* @return array The modified array of links
* @since 3.0.0
*/
public function plugin_metalinks( array $links, string $file ): array {
/* if ( strpos( $file, 'redux-framework.php' ) !== false && is_plugin_active( $file ) ) {
$links[] = '<a href="' . esc_url( admin_url( add_query_arg( array( 'page' => 'redux-framework' ), 'options-general.php' ) ) ) . '">' . esc_html__( 'What is this?', 'redux-framework' ) . '</a>';
if ( true === Redux_Core::$redux_templates_enabled ) {
$links[] = '<a href="' . esc_url( admin_url( add_query_arg( array( 'post_type' => 'page' ), 'post-new.php' ) ) ) . '#redux_templates=1">' . esc_html__( 'Template Library', 'redux-framework' ) . '</a>';
}
}*/
return $links;
}
}
if ( ! class_exists( 'ReduxFrameworkPlugin' ) ) {
class_alias( 'Redux_Framework_Plugin', 'ReduxFrameworkPlugin' );
}
}

View File

@@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/>
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
{description}
Copyright (C) {year} {fullname}
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
{signature of Ty Coon}, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

View File

@@ -0,0 +1,177 @@
<?php
/**
* Admin.
*/
namespace Extendify\Library;
use Extendify\Library\App;
use Extendify\Library\User;
use Extendify\Library\SiteSettings;
/**
* This class handles any file loading for the admin area.
*/
class Admin
{
/**
* The instance
*
* @var $instance
*/
public static $instance = null;
/**
* Adds various actions to set up the page
*
* @return self|void
*/
public function __construct()
{
if (self::$instance) {
return self::$instance;
}
self::$instance = $this;
$this->loadScripts();
\add_filter('plugin_action_links_' . EXTENDIFY_PLUGIN_BASENAME, [ $this, 'pluginActionLinks' ]);
}
/**
* Adds action links to the plugin list table
*
* @param array $links An array of plugin action links.
* @return array An array of plugin action links.
*/
public function pluginActionLinks($links)
{
$theme = get_option('template');
$label = esc_html__('Upgrade', 'extendify');
$links['upgrade'] = sprintf('<a href="%1$s" target="_blank"><b>%2$s</b></a>', "https://extendify.com/pricing?utm_source=extendify-plugin&utm_medium=wp-dash&utm_campaign=action-link&utm_content=$label&utm_term=$theme", $label);
return $links;
}
/**
* Adds scripts to the admin
*
* @return void
*/
public function loadScripts()
{
\add_action(
'admin_enqueue_scripts',
function ($hook) {
if (!current_user_can(App::$requiredCapability)) {
return;
}
if (!$this->checkItsGutenbergPost($hook)) {
return;
}
if (!$this->isLibraryEnabled()) {
return;
}
$this->addScopedScriptsAndStyles();
}
);
}
/**
* Makes sure we are on the correct page
*
* @param string $hook - An optional hook provided by WP to identify the page.
* @return boolean
*/
public function checkItsGutenbergPost($hook = '')
{
if (isset($GLOBALS['typenow']) && \use_block_editor_for_post_type($GLOBALS['typenow'])) {
return $hook && in_array($hook, ['post.php', 'post-new.php'], true);
}
return false;
}
/**
* Adds various JS scripts
*
* @return void
*/
public function addScopedScriptsAndStyles()
{
$version = App::$environment === 'PRODUCTION' ? App::$version : uniqid();
\wp_register_script(
App::$slug . '-scripts',
EXTENDIFY_BASE_URL . 'public/build/extendify.js',
[
'wp-i18n',
'wp-components',
'wp-element',
'wp-editor',
],
$version,
true
);
\wp_localize_script(
App::$slug . '-scripts',
'extendifyData',
[
'root' => \esc_url_raw(rest_url(APP::$slug . '/' . APP::$apiVersion)),
'nonce' => \wp_create_nonce('wp_rest'),
'user' => json_decode(User::data('extendifysdk_user_data'), true),
'sitesettings' => json_decode(SiteSettings::data()),
'sdk_partner' => \esc_attr(APP::$sdkPartner),
'asset_path' => \esc_url(EXTENDIFY_URL . 'public/assets'),
'standalone' => \esc_attr(APP::$standalone),
'devbuild' => \esc_attr(APP::$environment === 'DEVELOPMENT'),
]
);
\wp_enqueue_script(App::$slug . '-scripts');
\wp_set_script_translations(App::$slug . '-scripts', 'extendify');
// Inline the library styles to keep them out of the iframe live preview.
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
$css = file_get_contents(EXTENDIFY_PATH . 'public/build/extendify.css');
\wp_register_style(App::$slug, false, [], $version);
\wp_enqueue_style(App::$slug);
\wp_add_inline_style(App::$slug, $css);
}
/**
* Check if current user is Admin
*
* @return Boolean
*/
private function isAdmin()
{
if (\is_multisite()) {
return \is_super_admin();
}
return in_array('administrator', \wp_get_current_user()->roles, true);
}
/**
* Check if scripts should add
*
* @return Boolean
*/
public function isLibraryEnabled()
{
$settings = json_decode(SiteSettings::data());
// If it's disabled, only show it for admins.
if (isset($settings->state) && (isset($settings->state->enabled)) && !$settings->state->enabled) {
return $this->isAdmin();
}
return true;
}
}

View File

@@ -0,0 +1,137 @@
<?php
/**
* API router
*/
namespace Extendify\Library;
use Extendify\Library\App;
use Extendify\Library\Http;
/**
* Simple router for the REST Endpoints
*/
class ApiRouter extends \WP_REST_Controller
{
/**
* The class instance.
*
* @var $instance
*/
protected static $instance = null;
/**
* The capablity required for access.
*
* @var $capability
*/
protected $capability;
/**
* The constructor
*/
public function __construct()
{
$this->capability = App::$requiredCapability;
add_filter(
'rest_request_before_callbacks',
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed
function ($response, $handler, $request) {
// Add the request to our helper class.
if ($request->get_header('x_extendify')) {
Http::init($request);
}
return $response;
},
10,
3
);
}
/**
* Check the authorization of the request
*
* @return boolean
*/
public function checkPermission()
{
// Check for the nonce on the server (used by WP REST).
if (isset($_SERVER['HTTP_X_WP_NONCE']) && \wp_verify_nonce(sanitize_text_field(wp_unslash($_SERVER['HTTP_X_WP_NONCE'])), 'wp_rest')) {
return \current_user_can($this->capability);
}
return false;
}
/**
* Register dynamic routes
*
* @param string $namespace - The api name space.
* @param string $endpoint - The endpoint.
* @param function $callback - The callback to run.
*
* @return void
*/
public function getHandler($namespace, $endpoint, $callback)
{
\register_rest_route(
$namespace,
$endpoint,
[
'methods' => 'GET',
'callback' => $callback,
'permission_callback' => [
$this,
'checkPermission',
],
]
);
}
/**
* The post handler
*
* @param string $namespace - The api name space.
* @param string $endpoint - The endpoint.
* @param string $callback - The callback to run.
*
* @return void
*/
public function postHandler($namespace, $endpoint, $callback)
{
\register_rest_route(
$namespace,
$endpoint,
[
'methods' => 'POST',
'callback' => $callback,
'permission_callback' => [
$this,
'checkPermission',
],
]
);
}
/**
* The caller
*
* @param string $name - The name of the method to call.
* @param array $arguments - The arguments to pass in.
*
* @return mixed
*/
public static function __callStatic($name, array $arguments)
{
$name = "{$name}Handler";
if (is_null(self::$instance)) {
self::$instance = new static();
}
$r = self::$instance;
return $r->$name(APP::$slug . '/' . APP::$apiVersion, ...$arguments);
}
}

View File

@@ -0,0 +1,111 @@
<?php
/**
* The App details file
*/
namespace Extendify\Library;
use Extendify\Library\Plugin;
/**
* Controller for handling various app data
*/
class App
{
/**
* Plugin name
*
* @var string
*/
public static $name = '';
/**
* Plugin slug
*
* @var string
*/
public static $slug = '';
/**
* Plugin version
*
* @var string
*/
public static $version = '';
/**
* Plugin API REST version
*
* @var string
*/
public static $apiVersion = 'v1';
/**
* Whether this is the standalone plugin
*
* @var boolean
*/
public static $standalone;
/**
* Plugin environment
*
* @var string
*/
public static $environment = '';
/**
* The partner plugin/theme
*
* @var string
*/
public static $sdkPartner = 'standalone';
/**
* Host plugin
*
* @var string
*/
public static $requiredCapability = 'upload_files';
/**
* Plugin config
*
* @var array
*/
public static $config = [];
/**
* Process the readme file to get version and name
*
* @return void
*/
public function __construct()
{
if (isset($GLOBALS['extendify_sdk_partner']) && $GLOBALS['extendify_sdk_partner']) {
self::$sdkPartner = $GLOBALS['extendify_sdk_partner'];
}
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
$readme = file_get_contents(dirname(__DIR__) . '/readme.txt');
preg_match('/=== (.+) ===/', $readme, $matches);
self::$name = $matches[1];
self::$slug = strtolower(trim(preg_replace('/[^A-Za-z0-9-]+/', '-', self::$name), '-'));
preg_match('/Stable tag: ([0-9.:]+)/', $readme, $matches);
self::$version = $matches[1];
// An easy way to check if we are in dev mode is to look for a dev specific file.
$isDev = is_readable(EXTENDIFY_PATH . 'node_modules') || is_readable(EXTENDIFY_PATH . '.devbuild');
self::$environment = $isDev ? 'DEVELOPMENT' : 'PRODUCTION';
self::$standalone = self::$sdkPartner === 'standalone';
// Add the config.
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
$config = file_get_contents(dirname(__DIR__) . '/config.json');
self::$config = json_decode($config, true);
}
}

View File

@@ -0,0 +1,43 @@
<?php
/**
* Controls Auth
*/
namespace Extendify\Library\Controllers;
use Extendify\Library\Http;
if (!defined('ABSPATH')) {
die('No direct access.');
}
/**
* The controller for dealing registration and authentication
*/
class AuthController
{
/**
* Login a user to extendify - it will return the API key
*
* @param \WP_REST_Request $request - The request.
* @return WP_REST_Response|WP_Error
*/
public static function login($request)
{
$response = Http::post('/login', $request->get_params());
return new \WP_REST_Response($response);
}
/**
* Handle registration - It will return the API key.
*
* @param \WP_REST_Request $request - The request.
* @return WP_REST_Response|WP_Error
*/
public static function register($request)
{
$response = Http::post('/register', $request->get_params());
return new \WP_REST_Response($response);
}
}

View File

@@ -0,0 +1,30 @@
<?php
/**
* Controls Http requests
*/
namespace Extendify\Library\Controllers;
use Extendify\Library\Http;
if (!defined('ABSPATH')) {
die('No direct access.');
}
/**
* The controller for sending little bits of info
*/
class MetaController
{
/**
* Send data about a specific topic
*
* @param \WP_REST_Request $request - The request.
* @return WP_REST_Response|WP_Error
*/
public static function getAll($request)
{
$response = Http::get('/meta-data', $request->get_params());
return new \WP_REST_Response($response);
}
}

View File

@@ -0,0 +1,30 @@
<?php
/**
* Controls Http requests
*/
namespace Extendify\Library\Controllers;
use Extendify\Library\Http;
if (!defined('ABSPATH')) {
die('No direct access.');
}
/**
* The controller for sending little bits of info
*/
class PingController
{
/**
* Send data about a specific topic
*
* @param \WP_REST_Request $request - The request.
* @return WP_REST_Response|WP_Error
*/
public static function ping($request)
{
$response = Http::post('/ping', $request->get_params());
return new \WP_REST_Response($response);
}
}

View File

@@ -0,0 +1,67 @@
<?php
/**
* Controls Plugins
*/
namespace Extendify\Library\Controllers;
use Extendify\Library\Plugin;
if (!defined('ABSPATH')) {
die('No direct access.');
}
/**
* The controller for plugin dependency checking, etc
*/
class PluginController
{
/**
* Return all plugins
*
* @return array
*/
public static function index()
{
if (! function_exists('get_plugins')) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
return \get_plugins();
}
/**
* List active plugins
*
* @return array
*/
public static function active()
{
return \get_option('active_plugins');
}
/**
* Install plugins
*
* @param \WP_REST_Request $request - The request.
* @return bool|WP_Error
*/
public static function install($request)
{
if (!\current_user_can('activate_plugins')) {
return new \WP_Error('not_allowed', \__('You are not allowed to activate plugins on this site.', 'extendify'));
}
$requiredPlugins = json_decode($request->get_param('plugins'), true);
foreach ($requiredPlugins as $plugin) {
$status = Plugin::install_and_activate_plugin($plugin);
if (\is_wp_error($status)) {
// Return first error encountered.
return $status;
}
}
return true;
}
}

View File

@@ -0,0 +1,42 @@
<?php
/**
* Controls User info
*/
namespace Extendify\Library\Controllers;
use Extendify\Library\SiteSettings;
if (!defined('ABSPATH')) {
die('No direct access.');
}
/**
* The controller for managing Extendify SiteSettings.
*/
class SiteSettingsController
{
/**
* Return Current SiteSettings meta data
*
* @return array
*/
public static function show()
{
return new \WP_REST_Response(SiteSettings::data());
}
/**
* Persist the data
*
* @param \WP_REST_Request $request - The request.
* @return array
*/
public static function store($request)
{
$settingsData = json_decode($request->get_param('data'), true);
\update_option(SiteSettings::key(), $settingsData, true);
return new \WP_REST_Response(SiteSettings::data());
}
}

View File

@@ -0,0 +1,29 @@
<?php
/**
* Controls Taxonomies
*/
namespace Extendify\Library\Controllers;
use Extendify\Library\Http;
if (!defined('ABSPATH')) {
die('No direct access.');
}
/**
* The controller for dealing with taxonomies
*/
class TaxonomyController
{
/**
* Return all taxonomies
*
* @return WP_REST_Response|WP_Error
*/
public static function index()
{
$response = Http::get('/taxonomies', []);
return new \WP_REST_Response($response);
}
}

View File

@@ -0,0 +1,43 @@
<?php
/**
* Controls Http requests
*/
namespace Extendify\Library\Controllers;
use Extendify\Library\Http;
if (!defined('ABSPATH')) {
die('No direct access.');
}
/**
* The controller for dealing with templates
*/
class TemplateController
{
/**
* Return info about a template
*
* @param \WP_REST_Request $request - The request.
* @return WP_REST_Response|WP_Error
*/
public static function index($request)
{
$response = Http::post('/templates', $request->get_params());
return new \WP_REST_Response($response);
}
/**
* Send data about a specific template
*
* @param \WP_REST_Request $request - The request.
* @return WP_REST_Response|WP_Error
*/
public static function ping($request)
{
$response = Http::post('/templates', $request->get_params());
return new \WP_REST_Response($response);
}
}

View File

@@ -0,0 +1,91 @@
<?php
/**
* Controls User info
*/
namespace Extendify\Library\Controllers;
use Extendify\Library\Http;
use Extendify\Library\User;
if (!defined('ABSPATH')) {
die('No direct access.');
}
/**
* The controller for managing user data like API keys, etc
*/
class UserController
{
/**
* Return the current user state
*
* @return array
*/
public static function show()
{
return new \WP_REST_Response(User::state());
}
/**
* Return meta info about the current user
*
* @param \WP_REST_Request $request - The request.
* @return array
*/
public static function meta($request)
{
$key = \sanitize_text_field(\wp_unslash($request->get_param('key')));
return new \WP_REST_Response(User::data($key));
}
/**
* Persist the data
*
* @param \WP_REST_Request $request - The request.
* @return array
*/
public static function store($request)
{
$userData = json_decode($request->get_param('data'), true);
// Keep this key for historical reasons.
\update_user_meta(\get_current_user_id(), 'extendifysdk_user_data', $userData);
return new \WP_REST_Response(User::state());
}
/**
* Delete the data
*
* @return array
*/
public static function delete()
{
\delete_user_meta(\get_current_user_id(), 'extendifysdk_user_data');
return new \WP_REST_Response(User::state());
}
/**
* Sign up the user to the mailing list.
*
* @param \WP_REST_Request $request - The request.
* @return WP_REST_Response|WP_Error
*/
public static function mailingList($request)
{
$response = Http::post('/register-mailing-list', $request->get_params());
return new \WP_REST_Response($response);
}
/**
* Get the max imports
*
* @return WP_REST_Response|WP_Error
*/
public static function maxImports()
{
$response = Http::get('/max-free-imports');
return new \WP_REST_Response($response);
}
}

View File

@@ -0,0 +1,62 @@
<?php
/**
* Manage any frontend related tasks here.
*/
namespace Extendify\Library;
use Extendify\Library\App;
/**
* This class handles any file loading for the frontend of the site.
*/
class Frontend
{
/**
* The instance
*
* @var $instance
*/
public static $instance = null;
/**
* Adds various actions to set up the page
*
* @return self|void
*/
public function __construct()
{
if (self::$instance) {
return self::$instance;
}
self::$instance = $this;
$this->loadScripts();
}
/**
* Adds scripts and styles to every page is enabled
*
* @return void
*/
public function loadScripts()
{
\add_action(
'wp_enqueue_scripts',
function () {
// TODO: Determine a way to conditionally load assets (https://github.com/extendify/company-product/issues/72).
$this->addStylesheets();
}
);
}
/**
* Adds stylesheets as needed
*
* @return void
*/
public function addStylesheets()
{
}
}

View File

@@ -0,0 +1,151 @@
<?php
/**
* Helper class for making http requests
*/
namespace Extendify\Library;
use Extendify\Library\App;
use Extendify\Library\User;
/**
* Controller for http communication
*/
class Http
{
/**
* The api endpoint
*
* @var string
*/
public $baseUrl = '';
/**
* Request data sent to the server
*
* @var array
*/
public $data = [];
/**
* Any headers required
*
* @var array
*/
public $headers = [];
/**
* The class instance.
*
* @var $instance
*/
protected static $instance = null;
/**
* Set up the base object to send with every request
*
* @param \WP_REST_Request $request - The request.
* @return void
*/
public function __construct($request)
{
// Redundant, but extra prodection!
if (!\wp_verify_nonce(sanitize_text_field(wp_unslash($request->get_header('x_wp_nonce'))), 'wp_rest')) {
return;
}
// Some special cases for development.
$this->baseUrl = $request->get_header('x_extendify_dev_mode') !== 'false' ? App::$config['api']['dev'] : App::$config['api']['live'];
$this->baseUrl = $request->get_header('x_extendify_local_mode') !== 'false' ? App::$config['api']['local'] : $this->baseUrl;
$this->data = [
'wp_language' => \get_locale(),
'wp_theme' => \get_option('template'),
'mode' => App::$environment,
'uuid' => User::data('uuid'),
'library_version' => App::$version,
'wp_active_plugins' => $request->get_method() === 'POST' ? \get_option('active_plugins') : [],
'sdk_partner' => App::$sdkPartner,
];
$this->headers = [
'Accept' => 'application/json',
'referer' => $request->get_header('referer'),
'user_agent' => $request->get_header('user_agent'),
];
}
/**
* Register dynamic routes
*
* @param string $endpoint - The endpoint.
* @param array $data - The data to include.
* @param array $headers - The headers to include.
*
* @return array
*/
public function getHandler($endpoint, $data = [], $headers = [])
{
$url = \esc_url_raw(
\add_query_arg(
\urlencode_deep(\urldecode_deep(array_merge($this->data, $data))),
$this->baseUrl . $endpoint
)
);
$response = \wp_remote_get(
$url,
[
'headers' => array_merge($this->headers, $headers),
]
);
$responseBody = \wp_remote_retrieve_body($response);
return json_decode($responseBody, true);
}
/**
* Register dynamic routes
*
* @param string $endpoint - The endpoint.
* @param array $data - The arguments to include.
* @param array $headers - The headers to include.
*
* @return array
*/
public function postHandler($endpoint, $data = [], $headers = [])
{
$response = \wp_remote_post(
$this->baseUrl . $endpoint,
[
'headers' => array_merge($this->headers, $headers),
'body' => array_merge($this->data, $data),
]
);
$responseBody = \wp_remote_retrieve_body($response);
return json_decode($responseBody, true);
}
/**
* The caller
*
* @param string $name - The name of the method to call.
* @param array $arguments - The arguments to pass in.
*
* @return mixed
*/
public static function __callStatic($name, array $arguments)
{
if ($name === 'init') {
self::$instance = new static($arguments[0]);
return;
}
$name = "{$name}Handler";
$r = self::$instance;
return $r->$name(...$arguments);
}
}

View File

@@ -0,0 +1,318 @@
<?php
// phpcs:ignoreFile
// This class was copied from JetPack (mostly)
// so will be a bit of work to refactor
/**
* Manage plugin dependencies
*/
namespace Extendify\Library;
class Plugin
{
/**
* Install and activate a plugin.
*
* @since 5.8.0
*
* @param string $slug Plugin slug.
*
* @return bool|WP_Error True if installation succeeded, error object otherwise.
*/
public static function install_and_activate_plugin($slug)
{
$plugin_id = self::get_plugin_id_by_slug($slug);
if (! $plugin_id) {
$installed = self::install_plugin($slug);
if (is_wp_error($installed)) {
return $installed;
}
$plugin_id = self::get_plugin_id_by_slug($slug);
} elseif (is_plugin_active($plugin_id)) {
return true; // Already installed and active.
}
if (! current_user_can('activate_plugins')) {
return new \WP_Error('not_allowed', __('You are not allowed to activate plugins on this site.', 'jetpack'));
}
$activated = activate_plugin($plugin_id);
if (is_wp_error($activated)) {
return $activated;
}
return true;
}
/**
* Install a plugin.
*
* @since 5.8.0
*
* @param string $slug Plugin slug.
*
* @return bool|WP_Error True if installation succeeded, error object otherwise.
*/
public static function install_plugin($slug)
{
if (is_multisite() && ! current_user_can('manage_network')) {
return new \WP_Error('not_allowed', __('You are not allowed to install plugins on this site.', 'jetpack'));
}
$skin = new PluginUpgraderSkin();
$upgrader = new \Plugin_Upgrader($skin);
$zip_url = self::generate_wordpress_org_plugin_download_link($slug);
$result = $upgrader->install($zip_url);
if (is_wp_error($result)) {
return $result;
}
$plugin = self::get_plugin_id_by_slug($slug);
$error_code = 'install_error';
if (! $plugin) {
$error = __('There was an error installing your plugin', 'jetpack');
}
if (! $result) {
$error_code = $upgrader->skin->get_main_error_code();
$message = $upgrader->skin->get_main_error_message();
$error = $message ? $message : __('An unknown error occurred during installation', 'jetpack');
}
if (! empty($error)) {
if ('download_failed' === $error_code) {
// For backwards compatibility: versions prior to 3.9 would return no_package instead of download_failed.
$error_code = 'no_package';
}
return new \WP_Error($error_code, $error, 400);
}
return (array) $upgrader->skin->get_upgrade_messages();
}
/**
* Get WordPress.org zip download link from a plugin slug
*
* @param string $plugin_slug Plugin slug.
*/
protected static function generate_wordpress_org_plugin_download_link($plugin_slug)
{
return "https://downloads.wordpress.org/plugin/$plugin_slug.latest-stable.zip";
}
/**
* Get the plugin ID (composed of the plugin slug and the name of the main plugin file) from a plugin slug.
*
* @param string $slug Plugin slug.
*/
public static function get_plugin_id_by_slug($slug)
{
// Check if get_plugins() function exists. This is required on the front end of the
// site, since it is in a file that is normally only loaded in the admin.
if (! function_exists('get_plugins')) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
/** This filter is documented in wp-admin/includes/class-wp-plugins-list-table.php */
$plugins = apply_filters('all_plugins', get_plugins());
if (! is_array($plugins)) {
return false;
}
foreach ($plugins as $plugin_file => $plugin_data) {
if (self::get_slug_from_file_path($plugin_file) === $slug) {
return $plugin_file;
}
}
return false;
}
/**
* Get the plugin slug from the plugin ID (composed of the plugin slug and the name of the main plugin file)
*
* @param string $plugin_file Plugin file (ID -- e.g. hello-dolly/hello.php).
*/
protected static function get_slug_from_file_path($plugin_file)
{
// Similar to get_plugin_slug() method.
$slug = dirname($plugin_file);
if ('.' === $slug) {
$slug = preg_replace('/(.+)\.php$/', '$1', $plugin_file);
}
return $slug;
}
/**
* Get the activation status for a plugin.
*
* @since 8.9.0
*
* @param string $plugin_file The plugin file to check.
* @return string Either 'network-active', 'active' or 'inactive'.
*/
public static function get_plugin_status($plugin_file)
{
if (is_plugin_active_for_network($plugin_file)) {
return 'network-active';
}
if (is_plugin_active($plugin_file)) {
return 'active';
}
return 'inactive';
}
/**
* Returns a list of all plugins in the site.
*
* @since 8.9.0
* @uses get_plugins()
*
* @return array
*/
public static function get_plugins()
{
if (! function_exists('get_plugins')) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
/** This filter is documented in wp-admin/includes/class-wp-plugins-list-table.php */
$plugins = apply_filters('all_plugins', get_plugins());
if (is_array($plugins) && ! empty($plugins)) {
foreach ($plugins as $plugin_slug => $plugin_data) {
$plugins[ $plugin_slug ]['active'] = in_array(
self::get_plugin_status($plugin_slug),
array( 'active', 'network-active' ),
true
);
}
return $plugins;
}
return array();
}
}
include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
include_once ABSPATH . 'wp-admin/includes/file.php';
/**
* Allows us to capture that the site doesn't have proper file system access.
* In order to update the plugin.
*/
class PluginUpgraderSkin extends \Automatic_Upgrader_Skin
{
/**
* Stores the last error key;
**/
protected $main_error_code = 'install_error';
/**
* Stores the last error message.
**/
protected $main_error_message = 'An unknown error occurred during installation';
/**
* Overwrites the set_upgrader to be able to tell if we e ven have the ability to write to the files.
*
* @param WP_Upgrader $upgrader
*
*/
public function set_upgrader(&$upgrader)
{
parent::set_upgrader($upgrader);
// Check if we even have permission to.
$result = $upgrader->fs_connect(array( WP_CONTENT_DIR, WP_PLUGIN_DIR ));
if (! $result) {
// set the string here since they are not available just yet
$upgrader->generic_strings();
$this->feedback('fs_unavailable');
}
}
/**
* Overwrites the error function
*/
public function error($error)
{
if (is_wp_error($error)) {
$this->feedback($error);
}
}
private function set_main_error_code($code)
{
// Don't set the process_failed as code since it is not that helpful unless we don't have one already set.
$this->main_error_code = ($code === 'process_failed' && $this->main_error_code ? $this->main_error_code : $code);
}
private function set_main_error_message($message, $code)
{
// Don't set the process_failed as message since it is not that helpful unless we don't have one already set.
$this->main_error_message = ($code === 'process_failed' && $this->main_error_code ? $this->main_error_code : $message);
}
public function get_main_error_code()
{
return $this->main_error_code;
}
public function get_main_error_message()
{
return $this->main_error_message;
}
/**
* Overwrites the feedback function
*
* @param string|array|WP_Error $data Data.
* @param mixed ...$args Optional text replacements.
*/
public function feedback($data, ...$args)
{
$current_error = null;
if (is_wp_error($data)) {
$this->set_main_error_code($data->get_error_code());
$string = $data->get_error_message();
} elseif (is_array($data)) {
return;
} else {
$string = $data;
}
if (! empty($this->upgrader->strings[$string])) {
$this->set_main_error_code($string);
$current_error = $string;
$string = $this->upgrader->strings[$string];
}
if (strpos($string, '%') !== false) {
if (! empty($args)) {
$string = vsprintf($string, $args);
}
}
$string = trim($string);
$string = wp_kses(
$string,
array(
'a' => array(
'href' => true
),
'br' => true,
'em' => true,
'strong' => true,
)
);
$this->set_main_error_message($string, $current_error);
$this->messages[] = $string;
}
}

View File

@@ -0,0 +1,247 @@
<?php
/**
* Manage any shared assets that load within the editor and the front-end.
*/
namespace Extendify\Library;
use Extendify\Library\App;
/**
* This class handles assets that load within the editor and the front-end.
*/
class Shared
{
/**
* Adds various actions to set up the page
*
* @return self|void
*/
public function __construct()
{
\add_action(
'wp_enqueue_scripts',
function () {
$this->themeCompatInlineStyles();
}
);
\add_action(
'admin_init',
function () {
$this->themeCompatInlineStyles();
}
);
}
/**
* Inline styles to be applied for compatible themes
*
* @return void
*/
// phpcs:ignore
public function themeCompatInlineStyles()
{
$css = '';
$theme = get_option('template');
if ($theme === 'kadence') {
$css = 'body, .editor-styles-wrapper {
--wp--preset--color--background: var(--global-palette8);
--wp--preset--color--foreground: var(--global-palette4);
--wp--preset--color--primary: var(--global-palette1);
--wp--preset--color--secondary: var(--global-palette2);
--wp--preset--color--tertiary: var(--global-palette7);
--wp--custom--spacing--large: clamp(var(--global-sm-spacing), 5vw, var(--global-xxl-spacing));
--wp--preset--font-size--large: var(--h2FontSize);
--wp--preset--font-size--huge: var(--h1FontSize);
}';
}
if ($theme === 'neve') {
$css = 'body, .editor-styles-wrapper {
--wp--preset--color--background: var(--nv-site-bg);
--wp--preset--color--foreground: var(--nv-text-color);
--wp--preset--color--primary: var(--nv-primary-accent);
--wp--preset--color--secondary: var(--nv-secondary-accent);
--wp--preset--color--tertiary: var(--nv-light-bg);
--wp--custom--spacing--large: clamp(15px, 5vw, 80px);
--wp--preset--font-size--large: var(--h2FontSize);
--wp--preset--font-size--huge: var(--h1FontSize);
}';
}
if ($theme === 'blocksy') {
$css = 'body, .editor-styles-wrapper {
--wp--preset--color--background: var(--paletteColor7);
--wp--preset--color--foreground: var(--color);
--wp--preset--color--primary: var(--paletteColor1);
--wp--preset--color--secondary: var(--paletteColor4);
}';
}
if ($theme === 'go') {
$css = 'body, .editor-styles-wrapper {
--wp--preset--color--background: var(--go--color--background);
--wp--preset--color--foreground: var(--go--color--text);
}';
}
if ($theme === 'astra') {
$css = 'body, .editor-styles-wrapper {
--wp--preset--color--background: #ffffff;
--wp--preset--color--foreground: var(--ast-global-color-2);
--wp--preset--color--primary: var(--ast-global-color-0);
--wp--preset--color--secondary: var(--ast-global-color-2);
}';
}
if ($theme === 'oceanwp') {
$background = get_theme_mod('ocean_background_color', '#ffffff');
$primary = get_theme_mod('ocean_primary_color', '#13aff0');
$secondary = get_theme_mod('ocean_hover_primary_color', '#0b7cac');
$gap = get_theme_mod('ocean_separate_content_padding', '30px');
$css = 'body, .editor-styles-wrapper {
--wp--preset--color--background: ' . $background . ';
--wp--preset--color--foreground: #1B1B1B;
--wp--preset--color--primary: ' . $primary . ';
--wp--preset--color--secondary: ' . $secondary . ';
--wp--style--block-gap: ' . $gap . ';
--wp--custom--spacing--large: clamp(2rem, 7vw, 8rem);
}';
}
if ($theme === 'generatepress') {
$settings = (array) get_option('generate_settings', []);
if (! array_key_exists('background_color', $settings)) {
$background = '#f7f8f9';
} else {
$background = $settings['background_color'];
}
if (! array_key_exists('text_color', $settings)) {
$foreground = '#222222';
} else {
$foreground = $settings['text_color'];
}
if (! array_key_exists('link_color', $settings)) {
$primary = '#1e73be';
} else {
$primary = $settings['link_color'];
}
if (! array_key_exists('link_color', $settings)) {
$primary = '#1e73be';
} else {
$primary = $settings['link_color'];
}
$css = 'body, .editor-styles-wrapper {
--wp--preset--color--background: ' . $background . ';
--wp--preset--color--foreground: ' . $foreground . ';
--wp--preset--color--primary: ' . $primary . ';
--wp--preset--color--secondary: #636363;
--wp--style--block-gap: 3rem;
--wp--custom--spacing--large: clamp(2rem, 7vw, 8rem);
--responsive--alignwide-width: 1120px;
}';
}//end if
if ($theme === 'twentytwentytwo') {
$css = 'body, .editor-styles-wrapper {
--extendify--spacing--large: clamp(2rem,8vw,8rem);
}';
}
if ($theme === 'twentytwentyone') {
$css = 'body, .editor-styles-wrapper {
--wp--preset--color--background: var(--global--color-background);
--wp--preset--color--foreground: var(--global--color-primary);
--wp--preset--color--primary: var(--global--color-gray);
--wp--preset--color--secondary: #464b56;
--wp--preset--color--tertiary: var(--global--color-light-gray);
--wp--style--block-gap: var(--global--spacing-unit);
--wp--preset--font-size--large: 2.5rem;
--wp--preset--font-size--huge: var(--global--font-size-xxl);
}
.has-foreground-background-color,
.has-primary-background-color,
.has-secondary-background-color {
--local--color-primary: var(--wp--preset--color--background);
--local--color-background: var(--wp--preset--color--primary);
}';
}
if ($theme === 'twentytwenty') {
$background = sanitize_hex_color_no_hash(get_theme_mod('background_color', 'f5efe0'));
$primary = get_theme_mod(
'accent_accessible_colors',
[
'content' => [ 'accent' => '#cd2653' ],
]
);
$primary = $primary['content']['accent'];
$css = 'body, .editor-styles-wrapper {
--wp--preset--color--background: #' . $background . ';
--wp--preset--color--foreground: #000;
--wp--preset--color--primary: ' . $primary . ';
--wp--preset--color--secondary: #69603e;
--wp--style--block-gap: 3rem;
--wp--custom--spacing--large: clamp(2rem, 7vw, 8rem);
--responsive--alignwide-width: 120rem;
}';
}//end if
if ($theme === 'twentynineteen') {
/**
* Use the color from Twenty Nineteen's customizer value.
*/
$primary = 199;
if (get_theme_mod('primary_color', 'default') !== 'default') {
$primary = absint(get_theme_mod('primary_color_hue', 199));
}
/**
* Filters Twenty Nineteen default saturation level.
*
* @since Twenty Nineteen 1.0
*
* @param int $saturation Color saturation level.
*/
// phpcs:ignore
$saturation = apply_filters('twentynineteen_custom_colors_saturation', 100);
$saturation = absint($saturation) . '%';
/**
* Filters Twenty Nineteen default lightness level.
*
* @since Twenty Nineteen 1.0
*
* @param int $lightness Color lightness level.
*/
// phpcs:ignore
$lightness = apply_filters('twentynineteen_custom_colors_lightness', 33);
$lightness = absint($lightness) . '%';
$css = 'body, .editor-styles-wrapper {
--wp--preset--color--foreground: #111;
--wp--preset--color--primary: hsl( ' . $primary . ', ' . $saturation . ', ' . $lightness . ' );
--wp--preset--color--secondary: #767676;
--wp--preset--color--tertiary: #f7f7f7;
}';
}//end if
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
$content = file_get_contents(EXTENDIFY_PATH . 'public/build/extendify-utilities.css');
$version = App::$environment === 'PRODUCTION' ? App::$version : uniqid();
\wp_register_style(App::$slug . '-utilities', false, [], $version);
\wp_enqueue_style(App::$slug . '-utilities');
\wp_add_inline_style(App::$slug . '-utilities', $content . $css);
// Adds inline to the live preview.
\wp_add_inline_style('wp-components', $content . $css);
}
}

View File

@@ -0,0 +1,72 @@
<?php
/**
* Helper class for interacting with the user
*/
namespace Extendify\Library;
/**
* Helper class for interacting with the user
*/
class SiteSettings
{
/**
* SiteSettings option_name - For historical reasons do not change.
*
* @var string
*/
protected $key = 'extendifysdk_sitesettings';
/**
* SiteSettings default value
*
* @var Json
*/
protected $default = '{"state":{"enabled":true}}';
/**
* The class instance.
*
* @var $instance
*/
protected static $instance = null;
/**
* Returns Setting
* Use it like Setting::data()
*
* @return mixed - Setting Data
*/
private function dataHandler()
{
return \get_option($this->key, $this->default);
}
/**
* Returns Setting Key
* Use it like Setting::key()
*
* @return string - Setting key
*/
private function keyHandler()
{
return $this->key;
}
/**
* Use it like Setting::method() e.g. Setting::data()
*
* @param string $name - The name of the method to call.
* @param array $arguments - The arguments to pass in.
*
* @return mixed
*/
public static function __callStatic($name, array $arguments)
{
$name = "{$name}Handler";
self::$instance = new static();
$r = self::$instance;
return $r->$name(...$arguments);
}
}

View File

@@ -0,0 +1,154 @@
<?php
/**
* Helper class for interacting with the user
*/
namespace Extendify\Library;
use Extendify\Library\App;
/**
* Helper class for interacting with the user
*/
class User
{
/**
* User unique, anonymous identifier
*
* @var string
*/
public $uuid = '';
/**
* A WP user
*
* @var \WP_User
*/
protected $user = null;
/**
* The DB key for scoping. For historical reasons do not change
*
* @var string
*/
protected $key = 'extendifysdk_';
/**
* The class instance.
*
* @var $instance
*/
protected static $instance = null;
/**
* Set up the user
*
* @param WP_User $user - A WP User object.
* @return void
*/
public function __construct($user)
{
$this->user = $user;
}
/**
* Return the user ID
*
* @return void
*/
private function setupUuid()
{
$uuid = \get_user_meta($this->user->ID, $this->key . 'uuid', true);
if (!$uuid) {
$id = \wp_hash(\wp_json_encode($this->user));
\update_user_meta($this->user->ID, $this->key . 'uuid', $id);
}
$this->uuid = $uuid;
}
/**
* Returns data about the user
* Use it like User::data('ID') to get the user id
*
* @param string $arguments - Right now a string of arguments, like ID.
* @return mixed - Data about the user.
*/
private function dataHandler($arguments)
{
// Right now assume a single argument, but could expand to multiple.
if (isset($this->user->$arguments)) {
return $this->user->$arguments;
}
return \get_user_meta($this->user->ID, $this->key . $arguments, true);
}
/**
* Returns the application state for he current user
* Use it like User::data('ID') to get the user id
*
* @return string - JSON representation of the current state
*/
private function stateHandler()
{
$state = \get_user_meta($this->user->ID, $this->key . 'user_data');
// Add some state boilerplate code for the first load.
if (!isset($state[0])) {
$state[0] = '{}';
}
$userData = json_decode($state[0], true);
if (!isset($userData['version'])) {
$userData['version'] = 0;
}
// This will reset the allowed max imports to 0 once a week which will force the library to re-check.
if (!get_transient('extendify_import_max_check_' . $this->user->ID)) {
set_transient('extendify_import_max_check_' . $this->user->ID, time(), strtotime('1 week', 0));
$userData['state']['allowedImports'] = 0;
}
// Similiar to above, this will give the user free imports once a month just for logging in.
if (!get_transient('extendify_free_extra_imports_check_' . $this->user->ID)) {
set_transient('extendify_free_extra_imports_check_' . $this->user->ID, time(), strtotime('first day of next month', 0));
$userData['state']['runningImports'] = 0;
}
if (!isset($userData['state']['sdkPartner']) || !$userData['state']['sdkPartner']) {
$userData['state']['sdkPartner'] = App::$sdkPartner;
}
$userData['state']['uuid'] = self::data('uuid');
$userData['state']['canInstallPlugins'] = \current_user_can('install_plugins');
$userData['state']['canActivatePlugins'] = \current_user_can('activate_plugins');
$userData['state']['isAdmin'] = \current_user_can('create_users');
return \wp_json_encode($userData);
}
/**
* Allows to dynamically setup the user with uuid
* Use it like User::data('ID') to get the user id
*
* @param string $name - The name of the method to call.
* @param array $arguments - The arguments to pass in.
*
* @return mixed
*/
public static function __callStatic($name, array $arguments)
{
$name = "{$name}Handler";
if (is_null(self::$instance)) {
require_once ABSPATH . 'wp-includes/pluggable.php';
self::$instance = new static(\wp_get_current_user());
$r = self::$instance;
$r->setupUuid();
}
$r = self::$instance;
return $r->$name(...$arguments);
}
}

View File

@@ -0,0 +1,48 @@
<?php
/**
* Bootstrap the application
*/
use Extendify\Library\Admin;
use Extendify\Library\Frontend;
use Extendify\Library\Shared;
if (!defined('ABSPATH')) {
die('No direct access.');
}
if (!defined('EXTENDIFY_PATH')) {
define('EXTENDIFY_PATH', \plugin_dir_path(__FILE__));
}
if (!defined('EXTENDIFY_URL')) {
define('EXTENDIFY_URL', \plugin_dir_url(__FILE__));
}
if (!defined('EXTENDIFY_PLUGIN_BASENAME')) {
define('EXTENDIFY_PLUGIN_BASENAME', \plugin_basename(__DIR__ . '/extendify.php'));
}
if (is_readable(EXTENDIFY_PATH . 'vendor/autoload.php')) {
require EXTENDIFY_PATH . 'vendor/autoload.php';
}
$extendifyAdmin = new Admin();
$extendifyFrontend = new Frontend();
$extendifyShared = new Shared();
require EXTENDIFY_PATH . 'routes/api.php';
require EXTENDIFY_PATH . 'editorplus/EditorPlus.php';
\add_action(
'init',
function () {
\load_plugin_textdomain('extendify', false, EXTENDIFY_PATH . 'languages');
}
);
// To cover legacy conflicts.
// phpcs:ignore
class ExtendifySdk
{
}

View File

@@ -0,0 +1,7 @@
{
"api": {
"live": "https://dashboard.extendify.com/api",
"dev": "https://testing.extendify.com/api",
"local": "http://templates.test/api"
}
}

View File

@@ -0,0 +1,221 @@
<?php
/**
* Handles editor related changes.
* Loaded (or not) in /bootstrap.php
*/
if (!class_exists('edpl__EditorPlus')) {
// phpcs:ignore Squiz.Classes.ClassFileName.NoMatch,Squiz.Commenting.ClassComment.Missing,PEAR.Commenting.ClassComment.Missing
final class ExtendifyEditorPlus
{
/**
* A reference to an instance of this class.
*
* @var $instance
*/
public static $instance;
/**
* The array of templates that this plugin tracks.
*
* @var array $templates
*/
protected $templates = ['editorplus-template.php' => 'Extendify Template'];
/**
* Returns an instance of this class.
*
* @return self
*/
public static function getInstance()
{
if (!current_user_can('install_plugins')) {
return;
}
if (is_null(self::$instance)) {
self::$instance = new ExtendifyEditorPlus();
}
return self::$instance;
}
/**
* Check whether we need to use the Extendify/EP template.
*/
public function __construct()
{
// Maybe show the styles on the frontend.
add_action('wp_head', function () {
if ($this->useDeprecatedTemplate()) {
$this->showStylesheet();
}
});
// Maybe show the styles in admin.
add_action('admin_head', function () {
if ($this->useDeprecatedTemplate()) {
$this->showStylesheet();
}
});
// Maybe load the JS to inject the admin styles.
add_action(
'admin_enqueue_scripts',
function () {
wp_enqueue_script(
'extendify-editorplus-scripts',
EXTENDIFY_BASE_URL . 'public/editorplus/editorplus.min.js',
[],
'1.0',
true
);
}
);
// Maybe add the body class name to the front end.
add_filter(
'body_class',
function ($classes) {
if ($this->useDeprecatedTemplate()) {
$classes[] = 'eplus_styles';
}
return $classes;
}
);
// Maybe add the body class name to the admin.
add_filter(
'admin_body_class',
function ($classes) {
if ($this->useDeprecatedTemplate()) {
$classes .= ' eplus_styles';
}
return $classes;
}
);
// Maybe register the template into WP.
add_filter('theme_page_templates', function ($templates) {
if (!$this->useDeprecatedTemplate()) {
return $templates;
}
return array_merge($templates, $this->templates);
});
// Maybe add template to the dropdown list.
add_filter('wp_insert_post_data', [$this, 'registerProjectTemplates']);
// Maybe add template file path.
add_filter('template_include', [$this, 'viewProjectTemplate']);
}
/**
* Checks whether the page needs the EP template
*
* @return boolean
*/
public function useDeprecatedTemplate()
{
$post = get_post();
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if (is_admin() && isset($_GET['post'])) {
// This will populate on the admin.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$post = get_post(sanitize_text_field(wp_unslash($_GET['post'])));
}
return isset($post->ID) && get_post_meta($post->ID, '_wp_page_template', true) === 'editorplus-template.php';
}
/**
* Used to echo out page template stylesheet if the page template is not active.
*
* @return void
*/
public function showStylesheet()
{
$post = get_post();
$cssContent = apply_filters(
// For historical reasons do not change this key.
'extendifysdk_template_css',
get_post_meta($post->ID, 'extendify_custom_stylesheet', true),
$post
);
// Note that esc_html() cannot be used because `div &gt; span` is not interpreted properly.
// See: https://github.com/WordPress/WordPress/blob/ccdb1766aead26d4cef79badb015bb2727fefd59/wp-includes/theme.php#L1824-L1833 for reference.
if ($cssContent) {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo "<style id='extendify-custom-stylesheet' type='text/css'>" . wp_strip_all_tags($cssContent) . '</style>';
}
}
/**
* Adds our template to the pages cache in order to trick WordPress,
* into thinking the template file exists where it doens't really exist.
*
* @param array $attributes - The attributes.
* @return array
*/
public function registerProjectTemplates($attributes)
{
if (!$this->useDeprecatedTemplate()) {
return $attributes;
}
// Create the key used for the themes cache.
$cacheKey = 'page_templates-' . wp_hash(get_theme_root() . '/' . get_stylesheet());
// Retrieve the cache list.
// If it doesn't exist, or it's empty prepare an array.
$templates = wp_get_theme()->get_page_templates();
if (empty($templates)) {
$templates = [];
}
// New cache, therefore remove the old one.
wp_cache_delete($cacheKey, 'themes');
// Now add our template to the list of templates by merging our templates.
// with the existing templates array from the cache.
$templates = array_merge($templates, $this->templates);
// Add the modified cache to allow WordPress to pick it up for listing available templates.
wp_cache_add($cacheKey, $templates, 'themes', 1800);
return $attributes;
}
/**
* Checks if the template is assigned to the page.
*
* @param string $template - The template.
* @return string
*/
public function viewProjectTemplate($template)
{
$post = get_post();
if (!$post || !$this->useDeprecatedTemplate()) {
return $template;
}
$currentTemplate = get_post_meta($post->ID, '_wp_page_template', true);
// Check that the set template is one we have defined.
if (!is_string($currentTemplate) || !array_key_exists($currentTemplate, $this->templates)) {
return $template;
}
$file = plugin_dir_path(__FILE__) . $currentTemplate;
if (!file_exists($file)) {
return $template;
}
return $file;
}
// phpcs:ignore Squiz.Classes.ClassDeclaration.SpaceBeforeCloseBrace
}
add_action('after_setup_theme', ['ExtendifyEditorPlus', 'getInstance']);
}//end if

View File

@@ -0,0 +1,50 @@
<?php
/**
* Template Name: Extendify Template
* Template Post Type: post, page
*/
?>
<?php wp_head(); ?>
<body <?php body_class(); ?>>
<div class="ep-temp-container ep-container">
<div class="ep-temp-entry-content">
<?php
if (have_posts()) {
while (have_posts()) {
the_post();
the_content();
}
}
?>
</div>
</div><!-- #site-content -->
<style>
.ep-temp-container {
margin-left: auto;
margin-right: auto;
}
@media(min-width: 700px) {
.ep-temp-container [class*=extendify-] [class*=wp-block] > * {
margin-top: 0px;
}
.ep-temp-container [class*=wp-block] > * .wp-block-button__link {
border-radius: 0px !important;
}
.ep-temp-container .wp-block-image:not(.alignwide):not(.alignfull):not(.alignleft):not(.alignright):not(.aligncenter) {
margin-top:0px;
}
body {background-color: #fff;}
html, body {
font-size: 16px !important;
}
}
</style>
</body>
<?php
wp_footer();

View File

@@ -0,0 +1,29 @@
// Quick method to hide the title if the template is active
if (window._wpLoadBlockEditor) {
const finished = window.wp.data.subscribe(() => {
const epTemplateSelected =
window.wp.data
.select('core/editor')
.getEditedPostAttribute('template') ===
'editorplus-template.php'
const title = document.querySelector(
'.edit-post-visual-editor__post-title-wrapper',
)
const wrapper = document.querySelector('.editor-styles-wrapper')
// Too early
if (!title || !wrapper) return
if (epTemplateSelected) {
// GB needs to compute the height first
Promise.resolve().then(() => (title.style.display = 'none'))
wrapper.style.paddingTop = '0'
wrapper.style.backgroundColor = '#ffffff'
} else {
title.style.removeProperty('display')
wrapper.style.removeProperty('padding-top')
wrapper.style.removeProperty('background-color')
}
finished()
})
}

View File

@@ -0,0 +1,2 @@
<?php
if (!defined('ABSPATH')) { exit; } if (!class_exists('ExtendifySdk') && !class_exists('Extendify')) : final class Extendify { public static $loaded = false; public function __invoke() { if (!apply_filters('extendify_load_library', true) || !apply_filters('extendifysdk_load_library', true)) { return; } if (version_compare(PHP_VERSION, '5.6', '<') || version_compare($GLOBALS['wp_version'], '5.5', '<')) { return; } if (!self::$loaded) { self::$loaded = true; require dirname(__FILE__) . '/bootstrap.php'; $app = new Extendify\Library\App(); if (!defined('EXTENDIFY_BASE_URL')) { define('EXTENDIFY_BASE_URL', plugin_dir_url(__FILE__)); } } } } $extendify = new Extendify(); $extendify(); endif;

View File

@@ -0,0 +1,53 @@
<?php
/**
* This file is used to help side load the library.
* Be sure to remove the front matter from extendify.php
*/
if (!defined('ABSPATH')) {
exit;
}
if (!function_exists('extendifyCheckPluginInstalled')) {
/**
* Will be truthy if the plugin is installed.
*
* @param string $name name of the plugin 'extendify'.
* @return bool|string - will return path, ex. 'extendify/extendify.php'.
*/
function extendifyCheckPluginInstalled($name)
{
if (!function_exists('get_plugins')) {
include_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$plugins = get_plugins();
// Don't cache plugins this early.
wp_cache_delete('plugins', 'plugins');
foreach ($plugins as $plugin => $data) {
if ($data['TextDomain'] === $name) {
return $plugin;
}
}
return false;
}
}//end if
$extendifyPluginName = extendifyCheckPluginInstalled('extendify');
if ($extendifyPluginName) {
// Exit if the library is installed and active.
// Remember, this file is only loaded by partner plugins.
if (is_plugin_active($extendifyPluginName)) {
// If the SDK is active then ignore the partner plugins.
$GLOBALS['extendify_sdk_partner'] = 'standalone';
return false;
}
}
// Next is first come, first serve. The later class is left in for historical reasons.
if (class_exists('Extendify') || class_exists('ExtendifySdk')) {
return false;
}
require_once plugin_dir_path(__FILE__) . 'extendify.php';

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,20 @@
/*
object-assign
(c) Sindre Sorhus
@license MIT
*/
/*!
Copyright (c) 2018 Jed Watson.
Licensed under the MIT License (MIT), see
http://jedwatson.github.io/classnames
*/
/** @license React v17.0.2
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

View File

@@ -0,0 +1 @@
(()=>{if(window._wpLoadBlockEditor)var e=window.wp.data.subscribe((function(){var t="editorplus-template.php"===window.wp.data.select("core/editor").getEditedPostAttribute("template"),o=document.querySelector(".edit-post-visual-editor__post-title-wrapper"),r=document.querySelector(".editor-styles-wrapper");o&&r&&(t?(Promise.resolve().then((function(){return o.style.display="none"})),r.style.paddingTop="0",r.style.backgroundColor="#ffffff"):(o.style.removeProperty("display"),r.style.removeProperty("padding-top"),r.style.removeProperty("background-color")),e())}))})();

View File

@@ -0,0 +1,169 @@
=== Extendify — Gutenberg Patterns and Templates ===
Contributors: extendify, richtabor, kbat82, clubkert, arturgrabo
Tags: templates, patterns, layouts, blocks, gutenberg
Requires at least: 5.4
Tested up to: 5.9.0
Stable tag: 0.6.0
Requires PHP: 5.6
License: GPLv2
License URI: https://www.gnu.org/licenses/gpl-2.0.html
The best WordPress templates, pattern, and layout library with 1,000+ designs built for the Gutenberg block editor.
== Description ==
Extendify is the platform of site design and creation tools for people that want to build a beautiful WordPress website with a library of patterns and full page layouts for the Gutenberg block editor.
Make a beautiful WordPress website easier and faster than ever before with Extendify's library of Gutenberg block patterns, templates, and page layouts for Gutenberg.
= A library of Gutenberg block patterns, templates, and layouts =
Our library of reusable website block patterns, templates, and full page layouts can be assembled to rapidly build beautiful websites or add to existing ones. These well-designed templates enable you to drag, drop and publish in WordPress, without a single line of code.
= Core Gutenberg blocks =
Get beautiful patterns and layouts without the added maintenance of third-party blocks or block collections, as our Gutenberg block patterns are made with core WordPress blocks whenever possible.
= Built for your theme =
Extendify is a theme-agnostic design experience platform that works with your Gutenberg-friendly WordPress theme or Block Theme — instantly leveling-up your editing and publishing flow today. If you change your theme, colors, typography, or any other element, your Extendify Gutenberg block patterns and full page layouts will adapt to the new design.
Below is a partial list of themes that are fully supported and work great with Extendify:
* GeneratePress
* Neve
* Genesis
* Kadence
* Storefront
* Blocksy
* Blockbase
* Tove
* Armando
* Hansen
* Ono
* Bricksy
* Naledi
* Clove
* Twenty Twenty-Two
* Twenty Twenty-One
* Twenty Twenty
* Twenty Nineteen
= Site Types =
Extendify has tons of Gutenberg block patterns for all sorts of site types. Whether youre publishing a site for a yoga studio, restaurant, dentist, or lawyer, select your site type in the library and see a curated selection of Gutenberg block patterns and full page layouts — with industry-specific images and copy.
= Block Pattern Types =
Choose from Gutenberg block pattern types for every part of your website, including:
* About page patterns
* Call to action patterns
* Feature patterns
* Gallery patterns
* Headline patterns
* Hero patterns
* Team patterns
* Text patterns
= Layouts =
Layouts are a full collection of Gutenberg block patterns that are put together in effective and compelling page templates. By selecting the “Layouts” tab at the top of the library, youll see the beautiful full page templates that you can then add to your site.
= Free Imports =
Users gets 5 free imports per site each calendar month. Free imports can be used with base Gutenberg block patterns, templates, and layouts. You can publish and use these elements on your site without a subscription to Extendify.
= Extendify Pro =
Upgrade to [Extendify Pro](https://extendify.com/pricing/) for unlimited access to the full library of beautiful Gutenberg block patterns, templates, and layouts. Extendify Pro includes access to our "Pro" patterns and layouts. Users can import any Gutenberg block patterns, templates, and layouts they wish and continue using them on their site forever. A subscription is not required for your Gutenberg block patterns, templates, and layouts to continue to work perfectly on your site.
= Like Extendify? =
- Follow us on [Twitter](https://www.twitter.com/extendifyinc).
- Rate us on [WordPress](https://wordpress.org/support/plugin/extendify/reviews/?filter=5/#new-post) :)
= Privacy =
Extendify is a SaaS (software as a service) connector plugin that uses a custom API to fetch block patterns and page layouts from our servers. API requests are only made when a user clicks on the Library button. In order to provide and improve this service, Extendify passes site data along with an API request, including:
* Browser
* Referring site
* Category selection
* WP language
* Active theme
* Active plugins
* Anonymized UUID
* Anonymized IP address
By activating the Extendify plugin and accessing the library, you agree to our [privacy policy](https://extendify.com/privacy-policy) and [terms of service](https://extendify.com/terms-of-service).
== Installation ==
1. Install using the WordPress plugin installer, or extract the zip file and drop the contents into the `wp-content/plugins/` directory of your WordPress site.
2. Activate the plugin through the 'Plugins' menu in WordPress.
3. Edit or create a new page on your site
4. Press the 'Library' button at the top left of the editor header
5. Browse and import beautiful patterns and full page layouts; click one to add it directly to the page.
For documentation and tutorials, read our [guides](https://extendify.com/guides/?utm_source=wp-repo&utm_medium=link&utm_campaign=readme).
== Frequently Asked Questions ==
**Can I use Extendify with any theme?**
Yes! You can create a beautiful site in just a few clicks with just about any Gutenberg-friendly WordPress theme.
**Can I use Extendify with WooCommerce?**
Yes! Extendify is compatible with WooCommerce. Youll need to install and configure WooCommerce separately to set up eCommerce functionality.
**Is Extendify free?**
Extendify is a free plugin available through the WordPress.org directory that allows users to extend the power of the Gutenberg Block Editor. Each user receives a limited number of imports completely free. We offer a paid subscription for folks who want unlimited access to all of our beautiful patterns and page layouts.
**What is Extendify Pro?**
Extendify Pro gives you unlimited access to the entire library of our patterns and page layouts, without restrictions.
**Will Extendify slow down my website?**
Nope! Extendify imports lightweight block-based content that is served directly from your WordPress site. Our templates use the latest WordPress technologies, leveraging the best that Gutenberg has to offer, without any of the bloat of traditional page builders.
== Screenshots ==
1. Select a site type/industry to use patterns with images and copy relevent to you
2. Quickly open the Extendify library with the Pattern Library block
3. The Extendify library, as seen with the Twenty Twenty Two block theme
== Changelog ==
= 0.6.0 - 2022-03-08 =
- Add new design categories
- Add ability to open library via search param
- Tweak get_plugins call to not cache results
- Improve toolbar on mobile
= 0.5.0 - 2022-02-22 =
- Add support for inverted style patterns
- Improve library loading time
- Add pattern library variants to open the library
= 0.4.0 - 2022-02-08 =
- Enhance layout view with autoscroll
- Improve modal layout design consistency
- Remove NeedsRegistrationModal
- Add various performance improvements
= 0.3.1 - 2022-01-26 =
- Add singular value when import count is 1
- Remove destructuring within block filters
- Fix typo in setTimeout function name
= 0.3.0 - 2022-01-25 =
- Improve layout rendering performance
- Improve utility styles to better support WordPress 5.9
- Add fallback for blurred background in Firefox
- Add refresh screen when user is idle after 10 minutes
- Fix missing property check
= 0.2.0 - 2022-01-13 =
- Add initial support for pro patterns
- Improve pattern loading speed
- Improve library header toolbar button
- Improve multi-site support
- Fix excess blockGap on full/wide blocks with WP 5.9
= 0.1.0 - 2022-01-06 =
* Add null check on import position
* Add support for importing patterns to a specific location
* Add `/extendify` slash command to open the library
* Add preview optimizations
* Add check for live preview visibility
* Fix pattern display bug with TT1 CSS Grid galleries

View File

@@ -0,0 +1,49 @@
<?php
/**
* Api routes
*/
if (!defined('ABSPATH')) {
die('No direct access.');
}
use Extendify\Library\ApiRouter;
use Extendify\Library\Controllers\AuthController;
use Extendify\Library\Controllers\MetaController;
use Extendify\Library\Controllers\PingController;
use Extendify\Library\Controllers\UserController;
use Extendify\Library\Controllers\PluginController;
use Extendify\Library\Controllers\SiteSettingsController;
use Extendify\Library\Controllers\TaxonomyController;
use Extendify\Library\Controllers\TemplateController;
\add_action(
'rest_api_init',
function () {
ApiRouter::get('/active-plugins', [PluginController::class, 'active']);
ApiRouter::get('/plugins', [PluginController::class, 'index']);
ApiRouter::post('/plugins', [PluginController::class, 'install']);
ApiRouter::get('/taxonomies', [TaxonomyController::class, 'index']);
ApiRouter::post('/templates', [TemplateController::class, 'index']);
ApiRouter::post('/templates/(?P<template_id>[a-zA-Z0-9-]+)', [TemplateController::class, 'ping']);
ApiRouter::get('/user', [UserController::class, 'show']);
ApiRouter::post('/user', [UserController::class, 'store']);
ApiRouter::post('/clear-user', [UserController::class, 'delete']);
ApiRouter::get('/user-meta', [UserController::class, 'meta']);
ApiRouter::get('/max-free-imports', [UserController::class, 'maxImports']);
ApiRouter::post('/register-mailing-list', [UserController::class, 'mailingList']);
ApiRouter::post('/register', [AuthController::class, 'register']);
ApiRouter::post('/login', [AuthController::class, 'login']);
ApiRouter::get('/meta-data', [MetaController::class, 'getAll']);
ApiRouter::post('/simple-ping', [PingController::class, 'ping']);
ApiRouter::get('/site-settings', [SiteSettingsController::class, 'show']);
ApiRouter::post('/site-settings', [SiteSettingsController::class, 'store']);
}
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More