initial commit
This commit is contained in:
@@ -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.
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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());
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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()
|
||||
{
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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
|
||||
{
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"api": {
|
||||
"live": "https://dashboard.extendify.com/api",
|
||||
"dev": "https://testing.extendify.com/api",
|
||||
"local": "http://templates.test/api"
|
||||
}
|
||||
}
|
@@ -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 > 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
|
@@ -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();
|
@@ -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()
|
||||
})
|
||||
}
|
@@ -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;
|
@@ -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
@@ -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.
|
||||
*/
|
@@ -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())}))})();
|
@@ -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 you’re 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, you’ll 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. You’ll 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
|
@@ -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']);
|
||||
}
|
||||
);
|
@@ -0,0 +1,69 @@
|
||||
import { useEffect, useCallback } from '@wordpress/element'
|
||||
import { General as GeneralApi } from '@extendify/api/General'
|
||||
import MainWindow from '@extendify/pages/MainWindow'
|
||||
import { useGlobalStore } from '@extendify/state/GlobalState'
|
||||
import { useTemplatesStore } from '@extendify/state/Templates'
|
||||
import { useUserStore } from '@extendify/state/User'
|
||||
import '@extendify/utility-control'
|
||||
import { useTaxonomyStore } from './state/Taxonomies'
|
||||
|
||||
export default function ExtendifyLibrary({ show = false }) {
|
||||
const open = useGlobalStore((state) => state.open)
|
||||
const setReady = useGlobalStore((state) => state.setReady)
|
||||
const setOpen = useGlobalStore((state) => state.setOpen)
|
||||
const showLibrary = useCallback(() => setOpen(true), [setOpen])
|
||||
const hideLibrary = useCallback(() => setOpen(false), [setOpen])
|
||||
const initTemplateData = useTemplatesStore(
|
||||
(state) => state.initTemplateData,
|
||||
)
|
||||
const fetchTaxonomies = useTaxonomyStore((state) => state.fetchTaxonomies)
|
||||
|
||||
// When the uuid of the user comes back from the database, we can
|
||||
// assume that the state object is ready. This is important to check
|
||||
// as the library may be "open" when loaded, but not ready.
|
||||
const userHasHydrated = useUserStore((state) => state._hasHydrated)
|
||||
const taxonomiesReady = useTemplatesStore(
|
||||
(state) => Object.keys(state.taxonomyDefaultState).length > 0,
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
fetchTaxonomies().then(() => {
|
||||
useTemplatesStore.getState().setupDefaultTaxonomies()
|
||||
})
|
||||
}, [open, fetchTaxonomies])
|
||||
|
||||
useEffect(() => {
|
||||
if (userHasHydrated && taxonomiesReady) {
|
||||
initTemplateData()
|
||||
setReady(true)
|
||||
}
|
||||
}, [userHasHydrated, taxonomiesReady, initTemplateData, setReady])
|
||||
|
||||
useEffect(() => {
|
||||
const search = new URLSearchParams(window.location.search)
|
||||
if (show || search.has('ext-open')) {
|
||||
setOpen(true)
|
||||
}
|
||||
}, [show, setOpen])
|
||||
|
||||
useEffect(() => {
|
||||
GeneralApi.metaData().then((data) => {
|
||||
useGlobalStore.setState({
|
||||
metaData: data,
|
||||
})
|
||||
})
|
||||
}, [])
|
||||
|
||||
// Let the visibility to be controlled from outside the application
|
||||
useEffect(() => {
|
||||
window.addEventListener('extendify::open-library', showLibrary)
|
||||
window.addEventListener('extendify::close-library', hideLibrary)
|
||||
return () => {
|
||||
window.removeEventListener('extendify::open-library', showLibrary)
|
||||
window.removeEventListener('extendify::close-library', hideLibrary)
|
||||
}
|
||||
}, [hideLibrary, showLibrary])
|
||||
|
||||
return <MainWindow />
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
import { useTemplatesStore } from '@extendify/state/Templates'
|
||||
import { useUserStore } from '@extendify/state/User'
|
||||
import { Axios as api } from './axios'
|
||||
|
||||
export const General = {
|
||||
metaData() {
|
||||
return api.get('meta-data')
|
||||
},
|
||||
ping(action) {
|
||||
const categories =
|
||||
useTemplatesStore.getState()?.searchParams?.taxonomies ?? []
|
||||
return api.post('simple-ping', {
|
||||
action,
|
||||
categories,
|
||||
sdk_partner: useUserStore.getState()?.sdkPartner ?? '',
|
||||
})
|
||||
},
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
import { Axios as api } from './axios'
|
||||
|
||||
export const Plugins = {
|
||||
getInstalled() {
|
||||
return api.get('plugins')
|
||||
},
|
||||
installAndActivate(plugins = []) {
|
||||
const formData = new FormData()
|
||||
formData.append('plugins', JSON.stringify(plugins))
|
||||
return api.post('plugins', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
})
|
||||
},
|
||||
getActivated() {
|
||||
return api.get('active-plugins')
|
||||
},
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
import { Axios as api } from './axios'
|
||||
|
||||
export const SiteSettings = {
|
||||
getData() {
|
||||
return api.get('site-settings')
|
||||
},
|
||||
setData(data) {
|
||||
const formData = new FormData()
|
||||
formData.append('data', JSON.stringify(data))
|
||||
return api.post('site-settings', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
import { Axios as api } from './axios'
|
||||
|
||||
export const Taxonomies = {
|
||||
async get() {
|
||||
return await api.get('taxonomies')
|
||||
},
|
||||
}
|
@@ -0,0 +1,76 @@
|
||||
import { useTemplatesStore } from '@extendify/state/Templates'
|
||||
import { useUserStore } from '@extendify/state/User'
|
||||
import { Axios as api } from './axios'
|
||||
|
||||
let count = 0
|
||||
|
||||
export const Templates = {
|
||||
async get(searchParams, options = {}) {
|
||||
count++
|
||||
const defaultpageSize = searchParams.type === 'pattern' ? '8' : '4'
|
||||
const taxonomyType =
|
||||
searchParams.type === 'pattern' ? 'patternType' : 'layoutType'
|
||||
const args = Object.assign(
|
||||
{
|
||||
filterByFormula: prepareFilterFormula(
|
||||
searchParams,
|
||||
taxonomyType,
|
||||
),
|
||||
pageSize: defaultpageSize,
|
||||
categories: searchParams.taxonomies,
|
||||
search: searchParams.search,
|
||||
type: searchParams.type,
|
||||
offset: '',
|
||||
initial: count === 1,
|
||||
request_count: count,
|
||||
sdk_partner: useUserStore.getState().sdkPartner ?? '',
|
||||
},
|
||||
options,
|
||||
)
|
||||
return await api.post('templates', args)
|
||||
},
|
||||
|
||||
// TODO: Refactor this later to combine the following three
|
||||
maybeImport(template) {
|
||||
const categories =
|
||||
useTemplatesStore.getState()?.searchParams?.taxonomies ?? []
|
||||
return api.post(`templates/${template.id}`, {
|
||||
template_id: template?.id,
|
||||
categories,
|
||||
maybe_import: true,
|
||||
type: template.fields?.type,
|
||||
sdk_partner: useUserStore.getState().sdkPartner ?? '',
|
||||
pageSize: '1',
|
||||
template_name: template.fields?.title,
|
||||
})
|
||||
},
|
||||
import(template) {
|
||||
const categories =
|
||||
useTemplatesStore.getState()?.searchParams?.taxonomies ?? []
|
||||
return api.post(`templates/${template.id}`, {
|
||||
template_id: template.id,
|
||||
categories,
|
||||
imported: true,
|
||||
basePattern:
|
||||
template.fields?.basePattern ??
|
||||
template.fields?.baseLayout ??
|
||||
'',
|
||||
type: template.fields.type,
|
||||
sdk_partner: useUserStore.getState().sdkPartner ?? '',
|
||||
pageSize: '1',
|
||||
template_name: template.fields?.title,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
const prepareFilterFormula = ({ taxonomies }, type) => {
|
||||
const siteType = taxonomies?.siteType?.slug
|
||||
const formula = [
|
||||
`{type}="${type.replace('Type', '')}"`,
|
||||
`{siteType}="${siteType}"`,
|
||||
]
|
||||
if (taxonomies[type]?.slug) {
|
||||
formula.push(`{${type}}="${taxonomies[type].slug}"`)
|
||||
}
|
||||
return `AND(${formula.join(', ')})`.replace(/\r?\n|\r/g, '')
|
||||
}
|
@@ -0,0 +1,67 @@
|
||||
import { Axios as api } from './axios'
|
||||
|
||||
export const User = {
|
||||
async getData() {
|
||||
// Zustand changed their persist middleware to bind to the store
|
||||
// so api was undefined here. That's why using fetch for this one request.
|
||||
const data = await fetch(`${window.extendifyData.root}/user`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-WP-Nonce': window.extendifyData.nonce,
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-Extendify': true,
|
||||
},
|
||||
})
|
||||
return await data.json()
|
||||
},
|
||||
getMeta(key) {
|
||||
return api.get('user-meta', {
|
||||
params: {
|
||||
key,
|
||||
},
|
||||
})
|
||||
},
|
||||
authenticate(email, key) {
|
||||
const formData = new FormData()
|
||||
formData.append('email', email)
|
||||
formData.append('key', key)
|
||||
return api.post('login', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
})
|
||||
},
|
||||
register(email) {
|
||||
const formData = new FormData()
|
||||
formData.append('data', email)
|
||||
return api.post('register', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
})
|
||||
},
|
||||
setData(data) {
|
||||
const formData = new FormData()
|
||||
formData.append('data', JSON.stringify(data))
|
||||
return api.post('user', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
})
|
||||
},
|
||||
deleteData() {
|
||||
return api.post('clear-user')
|
||||
},
|
||||
registerMailingList(email) {
|
||||
const formData = new FormData()
|
||||
formData.append('email', email)
|
||||
return api.post('register-mailing-list', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
})
|
||||
},
|
||||
allowedImports() {
|
||||
return api.get('max-free-imports')
|
||||
},
|
||||
}
|
@@ -0,0 +1,73 @@
|
||||
import axios from 'axios'
|
||||
import { useUserStore } from '@extendify/state/User'
|
||||
|
||||
const Axios = axios.create({
|
||||
baseURL: window.extendifyData.root,
|
||||
headers: {
|
||||
'X-WP-Nonce': window.extendifyData.nonce,
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-Extendify': true,
|
||||
},
|
||||
})
|
||||
|
||||
function findResponse(response) {
|
||||
return Object.prototype.hasOwnProperty.call(response, 'data')
|
||||
? response.data
|
||||
: response
|
||||
}
|
||||
|
||||
function handleErrors(error) {
|
||||
if (!error.response) {
|
||||
return
|
||||
}
|
||||
console.error(error.response)
|
||||
// TODO: add a global error message system
|
||||
return Promise.reject(findResponse(error.response))
|
||||
}
|
||||
|
||||
function addDefaults(request) {
|
||||
const userState = useUserStore.getState()
|
||||
const remainingImports = userState.apiKey
|
||||
? 'unlimited'
|
||||
: userState.remainingImports()
|
||||
if (request.data) {
|
||||
request.data.remaining_imports = remainingImports
|
||||
request.data.entry_point = userState.entryPoint
|
||||
request.data.total_imports = userState.imports
|
||||
request.data.participating_tests = userState.activeTestGroups()
|
||||
}
|
||||
return request
|
||||
}
|
||||
|
||||
function checkDevMode(request) {
|
||||
request.headers['X-Extendify-Dev-Mode'] =
|
||||
window.location.search.indexOf('DEVMODE') > -1
|
||||
request.headers['X-Extendify-Local-Mode'] =
|
||||
window.location.search.indexOf('LOCALMODE') > -1
|
||||
return request
|
||||
}
|
||||
|
||||
function checkForSoftError(response) {
|
||||
if (Object.prototype.hasOwnProperty.call(response, 'soft_error')) {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('extendify::softerror-encountered', {
|
||||
detail: response.soft_error,
|
||||
bubbles: true,
|
||||
}),
|
||||
)
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
Axios.interceptors.response.use(
|
||||
(response) => checkForSoftError(findResponse(response)),
|
||||
(error) => handleErrors(error),
|
||||
)
|
||||
|
||||
// TODO: setup a pipe function instead of this nested pattern
|
||||
Axios.interceptors.request.use(
|
||||
(request) => checkDevMode(addDefaults(request)),
|
||||
(error) => error,
|
||||
)
|
||||
|
||||
export { Axios }
|
@@ -0,0 +1,179 @@
|
||||
/* Adding CSS classes should be done with consideration and rarely */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.extendify {
|
||||
--tw-ring-inset: var(--tw-empty, /*!*/ /*!*/);
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: transparent;
|
||||
--tw-ring-color: var(--wp-admin-theme-color);
|
||||
}
|
||||
|
||||
.extendify *,
|
||||
.extendify *:after,
|
||||
.extendify *:before {
|
||||
box-sizing: border-box;
|
||||
border: 0 solid #e5e7eb;
|
||||
}
|
||||
.extendify .button-focus {
|
||||
@apply outline-none focus:shadow-none focus:ring-wp focus:ring-wp-theme-500;
|
||||
}
|
||||
.extendify select.button-focus,
|
||||
.extendify input.button-focus {
|
||||
@apply focus:outline-none focus:border-transparent focus:shadow-none;
|
||||
}
|
||||
|
||||
div.extendify button.extendify-skip-to-sr-link:focus {
|
||||
@apply fixed top-0 z-high bg-white p-4;
|
||||
}
|
||||
|
||||
.button-extendify-main {
|
||||
@apply button-focus cursor-pointer whitespace-nowrap rounded bg-extendify-main p-1.5 px-3 text-base text-white no-underline transition duration-200 hover:bg-extendify-main-dark hover:text-white focus:text-white active:bg-gray-900 active:text-white;
|
||||
}
|
||||
#extendify-search-input:focus ~ svg,
|
||||
#extendify-search-input:not(:placeholder-shown) ~ svg {
|
||||
@apply hidden;
|
||||
}
|
||||
#extendify-search-input::-webkit-textfield-decoration-container {
|
||||
@apply mr-3;
|
||||
}
|
||||
|
||||
/* WP tweaks and overrides */
|
||||
.extendify .components-panel__body > .components-panel__body-title {
|
||||
/* Override WP aggressive boder:none and border:0 */
|
||||
border-bottom: 1px solid #e0e0e0 !important;
|
||||
@apply bg-transparent;
|
||||
}
|
||||
.extendify .components-modal__header {
|
||||
@apply border-b border-gray-300;
|
||||
}
|
||||
|
||||
/* A shim to ensure live previews w/o iframes display properly */
|
||||
.block-editor-block-preview__content
|
||||
.block-editor-block-list__layout.is-root-container
|
||||
> .ext {
|
||||
@apply max-w-none;
|
||||
}
|
||||
|
||||
/* Ensure our patterns display fullwidth on old themes + < 5.9 */
|
||||
.block-editor-block-list__layout.is-root-container
|
||||
.ext.block-editor-block-list__block {
|
||||
@apply max-w-full;
|
||||
}
|
||||
|
||||
.block-editor-block-preview__content-iframe
|
||||
.block-editor-block-list__layout.is-root-container
|
||||
.ext.block-editor-block-list__block {
|
||||
@apply max-w-none;
|
||||
}
|
||||
|
||||
.extendify .block-editor-block-preview__container {
|
||||
/* no important */
|
||||
@apply opacity-0;
|
||||
animation: extendifyOpacityIn 200ms cubic-bezier(0.694, 0, 0.335, 1)
|
||||
forwards 0ms;
|
||||
}
|
||||
|
||||
/* Remove excess margin for top-level patterns on < 5.9 */
|
||||
.extendify .is-root-container > [data-block],
|
||||
.extendify .is-root-container > [data-align="full"],
|
||||
.extendify .is-root-container > [data-align="full"] > .wp-block {
|
||||
@apply my-0 !important;
|
||||
}
|
||||
|
||||
/* Remove excess margin for top-level patterns on 5.9+ */
|
||||
.editor-styles-wrapper:not(.block-editor-writing-flow)
|
||||
> .is-root-container
|
||||
:where(.wp-block)[data-align="full"] {
|
||||
@apply my-0 !important;
|
||||
}
|
||||
|
||||
@keyframes extendifyOpacityIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.extendify .with-light-shadow::after {
|
||||
content: "";
|
||||
@apply absolute inset-0 border-0 shadow-inner-sm;
|
||||
}
|
||||
.extendify .with-light-shadow:hover::after {
|
||||
@apply shadow-inner-md;
|
||||
}
|
||||
|
||||
/* Fallback as FireFox does not support backdrop filter */
|
||||
@supports not (
|
||||
(-webkit-backdrop-filter: saturate(2) blur(24px)) or
|
||||
(backdrop-filter: saturate(2) blur(24px))
|
||||
) {
|
||||
div.extendify .bg-extendify-transparent-white {
|
||||
@apply bg-gray-100;
|
||||
}
|
||||
}
|
||||
|
||||
/* Style contentType/pageType control in sidebar */
|
||||
.components-panel__body.ext-type-control .components-panel__body-toggle {
|
||||
@apply pl-0 pr-0;
|
||||
}
|
||||
|
||||
.components-panel__body.ext-type-control .components-panel__body-title {
|
||||
@apply m-0 border-b-0 px-5;
|
||||
}
|
||||
|
||||
.components-panel__body.ext-type-control
|
||||
.components-panel__body-title
|
||||
.components-button {
|
||||
@apply m-0 border-b-0 py-2 text-xss font-medium uppercase text-extendify-gray;
|
||||
}
|
||||
|
||||
.components-panel__body.ext-type-control
|
||||
.components-button
|
||||
.components-panel__arrow {
|
||||
@apply right-0 text-extendify-gray;
|
||||
}
|
||||
|
||||
.extendify .animate-pulse {
|
||||
animation: extendifyPulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
@keyframes extendifyPulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.is-template--inactive::before,
|
||||
.is-template--in-review::before {
|
||||
content: "";
|
||||
@apply absolute top-0 left-0 bottom-0 right-0 z-40 h-full w-full border-8 border-solid border-extendify-secondary;
|
||||
}
|
||||
|
||||
.is-template--inactive::before {
|
||||
border-color: #fdeab6;
|
||||
}
|
||||
|
||||
.extendify-tooltip-default:not(.is-without-arrow)[data-y-axis="bottom"]::after {
|
||||
border-bottom-color: #1e1e1e;
|
||||
}
|
||||
.extendify-tooltip-default:not(.is-without-arrow)::before {
|
||||
@apply border-transparent;
|
||||
}
|
||||
.extendify-tooltip-default:not(.is-without-arrow) .components-popover__content {
|
||||
min-width: 250px;
|
||||
@apply border-transparent bg-gray-900 p-4 text-white;
|
||||
}
|
||||
.extendify-bottom-arrow::after {
|
||||
content: "";
|
||||
bottom: -15px;
|
||||
@apply absolute inline-block h-0 w-0 -translate-y-px transform border-8 border-transparent;
|
||||
border-top-color: #1e1e1e !important;
|
||||
}
|
@@ -0,0 +1,45 @@
|
||||
import { rawHandler } from '@wordpress/blocks'
|
||||
import { render } from '@wordpress/element'
|
||||
import ExtendifyLibrary from '@extendify/ExtendifyLibrary'
|
||||
import '@extendify/blocks/blocks'
|
||||
import '@extendify/buttons'
|
||||
import '@extendify/listeners'
|
||||
import { useWantedTemplateStore } from '@extendify/state/Importing'
|
||||
import { injectTemplateBlocks } from '@extendify/util/templateInjection'
|
||||
|
||||
window._wpLoadBlockEditor &&
|
||||
window.wp.domReady(() => {
|
||||
// Insert into the editor (note: Modal opens in a portal)
|
||||
const extendify = Object.assign(document.createElement('div'), {
|
||||
id: 'extendify-root',
|
||||
})
|
||||
document.body.append(extendify)
|
||||
render(<ExtendifyLibrary />, extendify)
|
||||
|
||||
// Add an extra div to use for utility modals, etc
|
||||
extendify.parentNode.insertBefore(
|
||||
Object.assign(document.createElement('div'), {
|
||||
id: 'extendify-util',
|
||||
}),
|
||||
extendify.nextSibling,
|
||||
)
|
||||
|
||||
// Insert a template on page load if it exists in localstorage
|
||||
// Note 6/28/21 - this was moved to after the render to possibly
|
||||
// fix a bug where imports would go from 3->0.
|
||||
if (useWantedTemplateStore.getState().importOnLoad) {
|
||||
const template = useWantedTemplateStore.getState().wantedTemplate
|
||||
setTimeout(() => {
|
||||
injectTemplateBlocks(
|
||||
rawHandler({ HTML: template.fields.code }),
|
||||
template,
|
||||
)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
// Reset template state after checking if we need an import
|
||||
useWantedTemplateStore.setState({
|
||||
importOnLoad: false,
|
||||
wantedTemplate: {},
|
||||
})
|
||||
})
|
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { registerBlockCollection } from '@wordpress/blocks'
|
||||
import { Icon } from '@wordpress/components'
|
||||
import { brandBlockIcon } from '@extendify/components/icons'
|
||||
|
||||
/**
|
||||
* Function to register a block collection for our block(s).
|
||||
*/
|
||||
registerBlockCollection('extendify', {
|
||||
title: 'Extendify',
|
||||
icon: <Icon icon={brandBlockIcon} />,
|
||||
})
|
@@ -0,0 +1,2 @@
|
||||
import './block-category.js'
|
||||
import './library/block.js'
|
@@ -0,0 +1,140 @@
|
||||
import { registerBlockType } from '@wordpress/blocks'
|
||||
import { useDispatch } from '@wordpress/data'
|
||||
import { useEffect } from '@wordpress/element'
|
||||
import { __, _x, sprintf } from '@wordpress/i18n'
|
||||
import {
|
||||
Icon,
|
||||
gallery,
|
||||
postAuthor,
|
||||
mapMarker,
|
||||
button,
|
||||
cover,
|
||||
overlayText,
|
||||
} from '@wordpress/icons'
|
||||
import { brandBlockIcon } from '@extendify/components/icons'
|
||||
import { setModalVisibility } from '@extendify/util/general'
|
||||
import metadata from './block.json'
|
||||
|
||||
export const openModal = (source) => setModalVisibility(source, 'open')
|
||||
|
||||
registerBlockType(metadata, {
|
||||
icon: brandBlockIcon,
|
||||
category: 'extendify',
|
||||
example: {
|
||||
attributes: {
|
||||
preview: window.extendifyData.asset_path + '/preview.png',
|
||||
},
|
||||
},
|
||||
variations: [
|
||||
{
|
||||
name: 'gallery',
|
||||
icon: <Icon icon={gallery} />,
|
||||
category: 'extendify',
|
||||
attributes: { search: 'gallery' },
|
||||
title: __('Gallery Patterns', 'extendify'),
|
||||
description: __('Add gallery patterns and layouts.', 'extendify'),
|
||||
keywords: [__('slideshow', 'extendify'), __('images', 'extendify')],
|
||||
},
|
||||
{
|
||||
name: 'team',
|
||||
icon: <Icon icon={postAuthor} />,
|
||||
category: 'extendify',
|
||||
attributes: { search: 'team' },
|
||||
title: __('Team Patterns', 'extendify'),
|
||||
description: __('Add team patterns and layouts.', 'extendify'),
|
||||
keywords: [
|
||||
_x('crew', 'As in team', 'extendify'),
|
||||
__('colleagues', 'extendify'),
|
||||
__('members', 'extendify'),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'hero',
|
||||
icon: <Icon icon={cover} />,
|
||||
category: 'extendify',
|
||||
attributes: { search: 'hero' },
|
||||
title: _x(
|
||||
'Hero Patterns',
|
||||
'Hero being a hero/top section of a webpage',
|
||||
'extendify',
|
||||
),
|
||||
description: __('Add hero patterns and layouts.', 'extendify'),
|
||||
keywords: [__('heading', 'extendify'), __('headline', 'extendify')],
|
||||
},
|
||||
{
|
||||
name: 'text',
|
||||
icon: <Icon icon={overlayText} />,
|
||||
category: 'extendify',
|
||||
attributes: { search: 'text' },
|
||||
title: _x(
|
||||
'Text Patterns',
|
||||
'Relating to patterns that feature text only',
|
||||
'extendify',
|
||||
),
|
||||
description: __('Add text patterns and layouts.', 'extendify'),
|
||||
keywords: [__('simple', 'extendify'), __('paragraph', 'extendify')],
|
||||
},
|
||||
{
|
||||
name: 'about',
|
||||
icon: <Icon icon={mapMarker} />,
|
||||
category: 'extendify',
|
||||
attributes: { search: 'about' },
|
||||
title: _x(
|
||||
'About Page Patterns',
|
||||
'Add patterns relating to an about us page',
|
||||
'extendify',
|
||||
),
|
||||
description: __('About patterns and layouts.', 'extendify'),
|
||||
keywords: [__('who we are', 'extendify'), __('team', 'extendify')],
|
||||
},
|
||||
{
|
||||
name: 'call-to-action',
|
||||
icon: <Icon icon={button} />,
|
||||
category: 'extendify',
|
||||
attributes: { search: 'call-to-action' },
|
||||
title: __('Call to Action Patterns', 'extendify'),
|
||||
description: __(
|
||||
'Add call to action patterns and layouts.',
|
||||
'extendify',
|
||||
),
|
||||
keywords: [
|
||||
_x('cta', 'Initialism for call to action', 'extendify'),
|
||||
__('callout', 'extendify'),
|
||||
__('buttons', 'extendify'),
|
||||
],
|
||||
},
|
||||
],
|
||||
edit: function Edit({ clientId, attributes }) {
|
||||
const { removeBlock } = useDispatch('core/block-editor')
|
||||
useEffect(() => {
|
||||
if (attributes.preview) {
|
||||
return
|
||||
}
|
||||
if (attributes.search) {
|
||||
addTermToSearchParams(attributes.search)
|
||||
}
|
||||
openModal('library-block')
|
||||
removeBlock(clientId)
|
||||
}, [clientId, attributes, removeBlock])
|
||||
return (
|
||||
<img
|
||||
style={{ display: 'block', maxWidth: '100%' }}
|
||||
src={attributes.preview}
|
||||
alt={sprintf(
|
||||
__('%s Pattern Library', 'extendify'),
|
||||
'Extendify',
|
||||
)}
|
||||
/>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const addTermToSearchParams = (term) => {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
params.append('ext-patternType', term)
|
||||
window.history.replaceState(
|
||||
null,
|
||||
null,
|
||||
window.location.pathname + '?' + params.toString(),
|
||||
)
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||
"apiVersion": 2,
|
||||
"name": "extendify/library",
|
||||
"title": "Pattern Library",
|
||||
"description": "Add block patterns and full page layouts with the Extendify Library.",
|
||||
"keywords": ["template", "layouts"],
|
||||
"textdomain": "extendify",
|
||||
"attributes": {
|
||||
"preview": {
|
||||
"type": "string"
|
||||
},
|
||||
"search": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,101 @@
|
||||
import { PluginSidebarMoreMenuItem } from '@wordpress/edit-post'
|
||||
import { render } from '@wordpress/element'
|
||||
import { __ } from '@wordpress/i18n'
|
||||
import { Icon } from '@wordpress/icons'
|
||||
import { registerPlugin } from '@wordpress/plugins'
|
||||
import LibraryAccessModal from '@extendify/components/LibraryAccessModal'
|
||||
import { CtaButton, MainButtonWrapper } from '@extendify/components/MainButtons'
|
||||
import { brandMark } from '@extendify/components/icons/'
|
||||
|
||||
const userState = window.extendifyData?.user?.state
|
||||
const isAdmin = () => window.extendifyData.user === null || userState?.isAdmin
|
||||
const isGlobalLibraryEnabled = () =>
|
||||
window.extendifyData.sitesettings === null ||
|
||||
window.extendifyData?.sitesettings?.state?.enabled
|
||||
const isLibraryEnabled = () =>
|
||||
window.extendifyData.user === null
|
||||
? isGlobalLibraryEnabled()
|
||||
: userState?.enabled
|
||||
|
||||
// Add the MAIN button when Gutenberg is available and ready
|
||||
if (window._wpLoadBlockEditor) {
|
||||
const finish = window.wp.data.subscribe(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (!isGlobalLibraryEnabled() && !isAdmin()) {
|
||||
return
|
||||
}
|
||||
if (document.getElementById('extendify-templates-inserter')) {
|
||||
return
|
||||
}
|
||||
if (!document.querySelector('.edit-post-header-toolbar')) {
|
||||
return
|
||||
}
|
||||
const buttonContainer = Object.assign(
|
||||
document.createElement('div'),
|
||||
{ id: 'extendify-templates-inserter' },
|
||||
)
|
||||
document
|
||||
.querySelector('.edit-post-header-toolbar')
|
||||
.append(buttonContainer)
|
||||
render(<MainButtonWrapper />, buttonContainer)
|
||||
|
||||
if (!isLibraryEnabled()) {
|
||||
document
|
||||
.getElementById('extendify-templates-inserter-btn')
|
||||
.classList.add('hidden')
|
||||
}
|
||||
finish()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// The CTA button inside patterns
|
||||
if (window._wpLoadBlockEditor) {
|
||||
window.wp.data.subscribe(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (!isGlobalLibraryEnabled() && !isAdmin()) {
|
||||
return
|
||||
}
|
||||
if (!document.querySelector('[id$=patterns-view]')) {
|
||||
return
|
||||
}
|
||||
if (document.getElementById('extendify-cta-button')) {
|
||||
return
|
||||
}
|
||||
const ctaButtonContainer = Object.assign(
|
||||
document.createElement('div'),
|
||||
{ id: 'extendify-cta-button-container' },
|
||||
)
|
||||
|
||||
document
|
||||
.querySelector('[id$=patterns-view]')
|
||||
.prepend(ctaButtonContainer)
|
||||
render(<CtaButton />, ctaButtonContainer)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// This will add a button to enable or disable the library button
|
||||
const LibraryEnableDisable = () => {
|
||||
function setOpenSiteSettingsModal() {
|
||||
const util = document.getElementById('extendify-util')
|
||||
render(<LibraryAccessModal />, util)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PluginSidebarMoreMenuItem
|
||||
onClick={setOpenSiteSettingsModal}
|
||||
icon={<Icon icon={brandMark} size={24} />}>
|
||||
{' '}
|
||||
{__('Extendify', 'extendify')}
|
||||
</PluginSidebarMoreMenuItem>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// Load this button always, which is used to enable or disable
|
||||
window._wpLoadBlockEditor &&
|
||||
registerPlugin('extendify-settings-enable-disable', {
|
||||
render: LibraryEnableDisable,
|
||||
})
|
@@ -0,0 +1,37 @@
|
||||
import { useEffect, useState } from '@wordpress/element'
|
||||
import { __, sprintf } from '@wordpress/i18n'
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard'
|
||||
|
||||
/** Overlay for pattern import button */
|
||||
export const DevButtonOverlay = ({ template }) => {
|
||||
const basePatternId = template?.fields?.basePattern?.length
|
||||
? template?.fields?.basePattern[0]
|
||||
: ''
|
||||
const [idText, setIdText] = useState(basePatternId)
|
||||
|
||||
useEffect(() => {
|
||||
if (!basePatternId?.length || idText === basePatternId) return
|
||||
setTimeout(() => setIdText(basePatternId), 1000)
|
||||
}, [idText, basePatternId])
|
||||
|
||||
if (!basePatternId) return null
|
||||
|
||||
return (
|
||||
<div className="absolute bottom-0 left-0 z-50 mb-4 ml-4 flex items-center space-x-2 opacity-0 transition duration-100 group-hover:opacity-100 space-x-0.5">
|
||||
<CopyToClipboard
|
||||
text={template?.fields?.basePattern}
|
||||
onCopy={() => setIdText(__('Copied!', 'extendify'))}>
|
||||
<button className="text-sm rounded-md border border-black bg-white py-1 px-2.5 font-medium text-black no-underline m-0 cursor-pointer">
|
||||
{sprintf(__('Base: %s', 'extendify'), idText)}
|
||||
</button>
|
||||
</CopyToClipboard>
|
||||
<a
|
||||
target="_blank"
|
||||
className="text-sm rounded-md border border-black bg-white py-1 px-2.5 font-medium text-black no-underline m-0"
|
||||
href={template?.fields?.editURL}
|
||||
rel="noreferrer">
|
||||
{__('Edit', 'extendify')}
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
@@ -0,0 +1,109 @@
|
||||
import { safeHTML } from '@wordpress/dom'
|
||||
import { useEffect, memo, useRef } from '@wordpress/element'
|
||||
import { __, _n, sprintf } from '@wordpress/i18n'
|
||||
import { Icon } from '@wordpress/icons'
|
||||
import classnames from 'classnames'
|
||||
import { General } from '@extendify/api/General'
|
||||
import { User as UserApi } from '@extendify/api/User'
|
||||
import { useUserStore } from '@extendify/state/User'
|
||||
import { growthArrow } from './icons'
|
||||
import { alert, download } from './icons/'
|
||||
|
||||
export const ImportCounter = memo(function ImportCounter() {
|
||||
const remainingImports = useUserStore((state) => state.remainingImports)
|
||||
const allowedImports = useUserStore((state) => state.allowedImports)
|
||||
const count = remainingImports()
|
||||
const status = count > 0 ? 'has-imports' : 'no-imports'
|
||||
const buttonRef = useRef()
|
||||
|
||||
useEffect(() => {
|
||||
if (allowedImports < 1 || !allowedImports) {
|
||||
const fallback = 5
|
||||
UserApi.allowedImports()
|
||||
.then((allowedImports) => {
|
||||
allowedImports = /^[1-9]\d*$/.test(allowedImports)
|
||||
? allowedImports
|
||||
: fallback
|
||||
useUserStore.setState({ allowedImports })
|
||||
})
|
||||
.catch(() =>
|
||||
useUserStore.setState({ allowedImports: fallback }),
|
||||
)
|
||||
}
|
||||
}, [allowedImports])
|
||||
|
||||
if (!allowedImports) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
// tabIndex for group focus animations
|
||||
<div tabIndex="0" className="group relative">
|
||||
<a
|
||||
target="_blank"
|
||||
ref={buttonRef}
|
||||
rel="noreferrer"
|
||||
className={classnames(
|
||||
'button-focus hidden w-full justify-between rounded py-3 px-4 text-sm text-white no-underline sm:flex',
|
||||
{
|
||||
'bg-wp-theme-500 hover:bg-wp-theme-600': count > 0,
|
||||
'bg-extendify-alert': !count,
|
||||
},
|
||||
)}
|
||||
onClick={async () => await General.ping('import-counter-click')}
|
||||
href={`https://www.extendify.com/pricing/?utm_source=${encodeURIComponent(
|
||||
window.extendifyData.sdk_partner,
|
||||
)}&utm_medium=library&utm_campaign=import-counter&utm_content=get-more&utm_term=${status}&utm_group=${useUserStore
|
||||
.getState()
|
||||
.activeTestGroupsUtmValue()}`}>
|
||||
<span className="flex items-center space-x-2 text-xs no-underline">
|
||||
<Icon icon={count > 0 ? download : alert} size={14} />
|
||||
<span>
|
||||
{sprintf(
|
||||
_n('%s Import', '%s Imports', count, 'extendify'),
|
||||
count,
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
<span className="outline-none flex items-center text-sm font-medium text-white no-underline">
|
||||
{__('Get more', 'extendify')}
|
||||
<Icon icon={growthArrow} size={24} className="-mr-1.5" />
|
||||
</span>
|
||||
</a>
|
||||
<div
|
||||
className="extendify-bottom-arrow invisible absolute top-0 w-full -translate-y-full transform opacity-0 shadow-md transition-all delay-200 duration-300 ease-in-out group-hover:visible group-hover:-top-2.5 group-hover:opacity-100 group-focus:visible group-focus:-top-2.5 group-focus:opacity-100"
|
||||
tabIndex="-1">
|
||||
<a
|
||||
href={`https://www.extendify.com/pricing/?utm_source=${encodeURIComponent(
|
||||
window.extendifyData.sdk_partner,
|
||||
)}&utm_medium=library&utm_campaign=import-counter-tooltip&utm_content=get-50-off&utm_term=${status}&utm_group=${useUserStore
|
||||
.getState()
|
||||
.activeTestGroupsUtmValue()}`}
|
||||
className="block bg-gray-900 text-white p-4 no-underline rounded bg-cover"
|
||||
onClick={async () =>
|
||||
await General.ping('import-counter-tooltip-click')
|
||||
}
|
||||
style={{
|
||||
backgroundImage: `url(${window.extendifyData.asset_path}/logo-tips.png)`,
|
||||
backgroundSize: '100% 100%',
|
||||
}}>
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: safeHTML(
|
||||
sprintf(
|
||||
__(
|
||||
'%1$sGet %2$s off%3$s Extendify Pro when you upgrade today!',
|
||||
'extendify',
|
||||
),
|
||||
'<strong>',
|
||||
'50%',
|
||||
'</strong>',
|
||||
),
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
@@ -0,0 +1,205 @@
|
||||
import { BlockPreview } from '@wordpress/block-editor'
|
||||
import { rawHandler } from '@wordpress/blocks'
|
||||
import { useEffect, useState, useRef, useMemo } from '@wordpress/element'
|
||||
import { __, sprintf } from '@wordpress/i18n'
|
||||
import classNames from 'classnames'
|
||||
import { Templates as TemplatesApi } from '@extendify/api/Templates'
|
||||
import { useIsDevMode } from '@extendify/hooks/helpers'
|
||||
import { AuthorizationCheck, Middleware } from '@extendify/middleware'
|
||||
import { useGlobalStore } from '@extendify/state/GlobalState'
|
||||
import { useUserStore } from '@extendify/state/User'
|
||||
import { injectTemplateBlocks } from '@extendify/util/templateInjection'
|
||||
import { DevButtonOverlay } from './DevHelpers'
|
||||
import { NoImportModal } from './modals/NoImportModal'
|
||||
import { ProModal } from './modals/ProModal'
|
||||
|
||||
const canImportMiddleware = Middleware([
|
||||
'hasRequiredPlugins',
|
||||
'hasPluginsActivated',
|
||||
])
|
||||
|
||||
export function ImportTemplateBlock({ template, maxHeight }) {
|
||||
const importButtonRef = useRef(null)
|
||||
const once = useRef(false)
|
||||
const hasAvailableImports = useUserStore(
|
||||
(state) => state.hasAvailableImports,
|
||||
)
|
||||
const loggedIn = useUserStore((state) => state.apiKey.length)
|
||||
const setOpen = useGlobalStore((state) => state.setOpen)
|
||||
const pushModal = useGlobalStore((state) => state.pushModal)
|
||||
const removeAllModals = useGlobalStore((state) => state.removeAllModals)
|
||||
const blocks = useMemo(
|
||||
() => rawHandler({ HTML: template.fields.code }),
|
||||
[template.fields.code],
|
||||
)
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
const devMode = useIsDevMode()
|
||||
const [topValue, setTopValue] = useState(0)
|
||||
|
||||
const focusTrapInnerBlocks = () => {
|
||||
if (once.current) return
|
||||
once.current = true
|
||||
Array.from(
|
||||
importButtonRef.current.querySelectorAll(
|
||||
'a, button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])',
|
||||
),
|
||||
).forEach((el) => el.setAttribute('tabIndex', '-1'))
|
||||
}
|
||||
|
||||
const importTemplates = async () => {
|
||||
await canImportMiddleware.check(template)
|
||||
AuthorizationCheck(canImportMiddleware)
|
||||
.then(() => {
|
||||
setTimeout(() => {
|
||||
injectTemplateBlocks(blocks, template)
|
||||
.then(() => removeAllModals())
|
||||
.then(() => setOpen(false))
|
||||
.then(() => canImportMiddleware.reset())
|
||||
}, 100)
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
const handleKeyDown = (event) => {
|
||||
if (['Enter', 'Space', ' '].includes(event.key)) {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
importTemplate()
|
||||
}
|
||||
}
|
||||
|
||||
const importTemplate = () => {
|
||||
// Make a note that they attempted to import
|
||||
TemplatesApi.maybeImport(template)
|
||||
|
||||
if (template?.fields?.pro && !loggedIn) {
|
||||
pushModal(<ProModal />)
|
||||
return
|
||||
}
|
||||
if (!hasAvailableImports()) {
|
||||
pushModal(<NoImportModal />)
|
||||
return
|
||||
}
|
||||
|
||||
importTemplates()
|
||||
}
|
||||
|
||||
// Trigger resize event on the live previews to add
|
||||
// Grammerly/Loom/etc compatability
|
||||
// TODO: This can probably be removed after WP 5.9
|
||||
useEffect(() => {
|
||||
const rafIds = []
|
||||
const timeouts = []
|
||||
let rafId1, rafId2, rafId3, rafId4
|
||||
rafId1 = window.requestAnimationFrame(() => {
|
||||
rafId2 = window.requestAnimationFrame(() => {
|
||||
importButtonRef.current
|
||||
.querySelectorAll('iframe')
|
||||
.forEach((frame) => {
|
||||
const inner = frame.contentWindow.document.body
|
||||
const rafId = window.requestAnimationFrame(() => {
|
||||
const maybeRoot =
|
||||
inner.querySelector('.is-root-container')
|
||||
if (maybeRoot) {
|
||||
const height = maybeRoot?.offsetHeight
|
||||
if (height) {
|
||||
rafId4 = window.requestAnimationFrame(
|
||||
() => {
|
||||
frame.contentWindow.dispatchEvent(
|
||||
new Event('resize'),
|
||||
)
|
||||
},
|
||||
)
|
||||
const id = window.setTimeout(() => {
|
||||
frame.contentWindow.dispatchEvent(
|
||||
new Event('resize'),
|
||||
)
|
||||
}, 2000)
|
||||
timeouts.push(id)
|
||||
}
|
||||
}
|
||||
frame.contentWindow.dispatchEvent(
|
||||
new Event('resize'),
|
||||
)
|
||||
})
|
||||
rafIds.push(rafId)
|
||||
})
|
||||
rafId3 = window.requestAnimationFrame(() => {
|
||||
window.dispatchEvent(new Event('resize'))
|
||||
setLoaded(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
return () => {
|
||||
;[...rafIds, rafId1, rafId2, rafId3, rafId4].forEach((id) =>
|
||||
window.cancelAnimationFrame(id),
|
||||
)
|
||||
timeouts.forEach((id) => window.clearTimeout(id))
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!Number.isInteger(maxHeight)) return
|
||||
const button = importButtonRef.current
|
||||
const handleIn = () => {
|
||||
// The live component changes over time so easier to query on demand
|
||||
const height = button.offsetHeight
|
||||
button.style.transitionDuration = height * 1.5 + 'ms'
|
||||
setTopValue(Math.abs(height - maxHeight) * -1)
|
||||
}
|
||||
const handleOut = () => {
|
||||
const height = button.offsetHeight
|
||||
button.style.transitionDuration = height / 1.5 + 'ms'
|
||||
setTopValue(0)
|
||||
}
|
||||
button.addEventListener('focus', handleIn)
|
||||
button.addEventListener('mouseenter', handleIn)
|
||||
button.addEventListener('blur', handleOut)
|
||||
button.addEventListener('mouseleave', handleOut)
|
||||
return () => {
|
||||
button.removeEventListener('focus', handleIn)
|
||||
button.removeEventListener('mouseenter', handleIn)
|
||||
button.removeEventListener('blur', handleOut)
|
||||
button.removeEventListener('mouseleave', handleOut)
|
||||
}
|
||||
}, [maxHeight])
|
||||
|
||||
return (
|
||||
<div className="group relative">
|
||||
<div
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
aria-label={sprintf(
|
||||
__('Press to import %s', 'extendify'),
|
||||
template?.fields?.type,
|
||||
)}
|
||||
style={{ maxHeight }}
|
||||
className="button-focus relative m-0 cursor-pointer overflow-hidden bg-gray-100 ease-in-out"
|
||||
onFocus={focusTrapInnerBlocks}
|
||||
onClick={importTemplate}
|
||||
onKeyDown={handleKeyDown}>
|
||||
<div
|
||||
ref={importButtonRef}
|
||||
style={{ top: topValue, transitionProperty: 'all' }}
|
||||
className={classNames('with-light-shadow relative', {
|
||||
[`is-template--${template.fields.status}`]:
|
||||
template?.fields?.status && devMode,
|
||||
'p-6 md:p-8': Number.isInteger(maxHeight),
|
||||
})}>
|
||||
<BlockPreview
|
||||
blocks={blocks}
|
||||
live={false}
|
||||
viewportWidth={1400}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Show dev info after the preview is loaded to trigger observer */}
|
||||
{devMode && loaded && <DevButtonOverlay template={template} />}
|
||||
{template?.fields?.pro && (
|
||||
<div className="pointer-events-none absolute top-4 right-4 z-20 rounded-md border border-none bg-white bg-wp-theme-500 py-1 px-2.5 font-medium text-white no-underline shadow-sm">
|
||||
{__('Pro', 'extendify')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
@@ -0,0 +1,111 @@
|
||||
import { Modal } from '@wordpress/components'
|
||||
import { ToggleControl } from '@wordpress/components'
|
||||
import { useSelect } from '@wordpress/data'
|
||||
import { unmountComponentAtNode, useState, useEffect } from '@wordpress/element'
|
||||
import { __ } from '@wordpress/i18n'
|
||||
import { useSiteSettingsStore } from '@extendify/state/SiteSettings'
|
||||
import { useUserStore } from '@extendify/state/User'
|
||||
|
||||
const LibraryAccessModal = () => {
|
||||
const isAdmin = useSelect((select) =>
|
||||
select('core').canUser('create', 'users'),
|
||||
)
|
||||
|
||||
const [libraryforMyself, setLibraryforMyself] = useState(
|
||||
useUserStore.getState().enabled,
|
||||
)
|
||||
const [libraryforEveryone, setLibraryforEveryone] = useState(
|
||||
useSiteSettingsStore.getState().enabled,
|
||||
)
|
||||
|
||||
const closeModal = () => {
|
||||
const util = document.getElementById('extendify-util')
|
||||
unmountComponentAtNode(util)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
hideButton(!libraryforMyself)
|
||||
}, [libraryforMyself])
|
||||
|
||||
function hideButton(state = true) {
|
||||
const button = document.getElementById(
|
||||
'extendify-templates-inserter-btn',
|
||||
)
|
||||
if (!button) return
|
||||
if (state) {
|
||||
button.classList.add('hidden')
|
||||
} else {
|
||||
button.classList.remove('hidden')
|
||||
}
|
||||
}
|
||||
|
||||
async function saveUser(value) {
|
||||
await useUserStore.setState({ enabled: value })
|
||||
}
|
||||
|
||||
async function saveSetting(value) {
|
||||
await useSiteSettingsStore.setState({ enabled: value })
|
||||
}
|
||||
|
||||
async function saveToggle(state, type) {
|
||||
if (type === 'global') {
|
||||
await saveSetting(state)
|
||||
} else {
|
||||
await saveUser(state)
|
||||
}
|
||||
}
|
||||
|
||||
function handleToggle(type) {
|
||||
if (type === 'global') {
|
||||
setLibraryforEveryone((state) => {
|
||||
saveToggle(!state, type)
|
||||
return !state
|
||||
})
|
||||
} else {
|
||||
setLibraryforMyself((state) => {
|
||||
hideButton(!state)
|
||||
saveToggle(!state, type)
|
||||
return !state
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={__('Extendify Settings', 'extendify')}
|
||||
onRequestClose={closeModal}>
|
||||
<ToggleControl
|
||||
label={
|
||||
isAdmin
|
||||
? __('Enable the library for myself', 'extendify')
|
||||
: __('Enable the library', 'extendify')
|
||||
}
|
||||
help={__(
|
||||
'Publish with hundreds of patterns & page layouts',
|
||||
'extendify',
|
||||
)}
|
||||
checked={libraryforMyself}
|
||||
onChange={() => handleToggle('user')}
|
||||
/>
|
||||
|
||||
{isAdmin && (
|
||||
<>
|
||||
<br />
|
||||
<ToggleControl
|
||||
label={__(
|
||||
'Allow all users to publish with the library',
|
||||
)}
|
||||
help={__(
|
||||
'Everyone publishes with patterns & page layouts',
|
||||
'extendify',
|
||||
)}
|
||||
checked={libraryforEveryone}
|
||||
onChange={() => handleToggle('global')}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default LibraryAccessModal
|
@@ -0,0 +1,112 @@
|
||||
import { Button } from '@wordpress/components'
|
||||
import { useState, useEffect, useRef } from '@wordpress/element'
|
||||
import { __ } from '@wordpress/i18n'
|
||||
import { Icon } from '@wordpress/icons'
|
||||
import { General } from '@extendify/api/General'
|
||||
import { useTestGroup } from '@extendify/hooks/useTestGroup'
|
||||
import { useGlobalStore } from '@extendify/state/GlobalState'
|
||||
import { useUserStore } from '@extendify/state/User'
|
||||
import { openModal } from '@extendify/util/general'
|
||||
import { brandMark } from './icons'
|
||||
import { NewImportsPopover } from './popovers/NewImportsPopover'
|
||||
|
||||
export const MainButtonWrapper = () => {
|
||||
const [showTooltip, setShowTooltip] = useState(false)
|
||||
const once = useRef(false)
|
||||
const buttonRef = useRef()
|
||||
const loggedIn = useUserStore((state) => state.apiKey.length)
|
||||
const hasImported = useUserStore((state) => state.imports > 0)
|
||||
const open = useGlobalStore((state) => state.open)
|
||||
const hasPendingNewImports = useUserStore(
|
||||
(state) => state.allowedImports === 0,
|
||||
)
|
||||
const uuid = useUserStore((state) => state.uuid)
|
||||
const buttonText = useTestGroup('main-button-text', ['A', 'B', 'C'], true)
|
||||
const [libraryButtonText, setLibraryButtonText] = useState()
|
||||
|
||||
const handleTooltipClose = async () => {
|
||||
await General.ping('mb-tooltip-closed')
|
||||
setShowTooltip(false)
|
||||
// If they close the tooltip, we can set the allowed imports
|
||||
// to -1 and when it opens it will fetch and update. Meanwhile,
|
||||
// -1 will be ignored by the this component.
|
||||
useUserStore.setState({
|
||||
allowedImports: -1,
|
||||
})
|
||||
}
|
||||
useEffect(() => {
|
||||
if (!uuid) return
|
||||
const text = () => {
|
||||
switch (buttonText) {
|
||||
case 'A':
|
||||
return __('Library', 'extendify')
|
||||
case 'B':
|
||||
return __('Add section', 'extendify')
|
||||
case 'C':
|
||||
return __('Add template', 'extendify')
|
||||
}
|
||||
}
|
||||
setLibraryButtonText(text())
|
||||
}, [buttonText, uuid])
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setShowTooltip(false)
|
||||
once.current = true
|
||||
}
|
||||
if (!loggedIn && hasPendingNewImports && hasImported) {
|
||||
once.current || setShowTooltip(true)
|
||||
once.current = true
|
||||
}
|
||||
}, [loggedIn, hasPendingNewImports, hasImported, open])
|
||||
|
||||
return (
|
||||
<>
|
||||
<MainButton buttonRef={buttonRef} text={libraryButtonText} />
|
||||
{showTooltip && (
|
||||
<NewImportsPopover
|
||||
anchorRef={buttonRef}
|
||||
onClick={async () => {
|
||||
await General.ping('mb-tooltip-pressed')
|
||||
openModal('main-button-tooltip')
|
||||
}}
|
||||
onPressX={handleTooltipClose}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
const MainButton = ({ buttonRef, text }) => {
|
||||
return (
|
||||
<Button
|
||||
isPrimary
|
||||
ref={buttonRef}
|
||||
style={{ padding: '12px' }}
|
||||
onClick={() => openModal('main-button')}
|
||||
id="extendify-templates-inserter-btn"
|
||||
icon={
|
||||
<Icon
|
||||
style={{ marginRight: '4px' }}
|
||||
icon={brandMark}
|
||||
size={24}
|
||||
/>
|
||||
}>
|
||||
{text}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
export const CtaButton = () => {
|
||||
return (
|
||||
<Button
|
||||
id="extendify-cta-button"
|
||||
style={{
|
||||
margin: '1rem 1rem 0',
|
||||
width: 'calc(100% - 2rem)',
|
||||
justifyContent: ' center',
|
||||
}}
|
||||
onClick={() => openModal('patterns-cta')}
|
||||
isSecondary>
|
||||
{__('Discover patterns in Extendify Library', 'extendify')}
|
||||
</Button>
|
||||
)
|
||||
}
|
@@ -0,0 +1,90 @@
|
||||
import { safeHTML } from '@wordpress/dom'
|
||||
import { useEffect, memo } from '@wordpress/element'
|
||||
import { __, _n, _x, sprintf } from '@wordpress/i18n'
|
||||
import { Icon } from '@wordpress/icons'
|
||||
import classNames from 'classnames'
|
||||
import { General } from '@extendify/api/General'
|
||||
import { User as UserApi } from '@extendify/api/User'
|
||||
import { useUserStore } from '@extendify/state/User'
|
||||
import { brandMark } from './icons'
|
||||
|
||||
export const SidebarNotice = memo(function SidebarNotice() {
|
||||
const remainingImports = useUserStore((state) => state.remainingImports)
|
||||
const allowedImports = useUserStore((state) => state.allowedImports)
|
||||
const count = remainingImports()
|
||||
const link = `https://www.extendify.com/pricing/?utm_source=${encodeURIComponent(
|
||||
window.extendifyData.sdk_partner,
|
||||
)}&utm_medium=library&utm_campaign=import-counter&utm_content=get-more&utm_term=${
|
||||
count > 0 ? 'has-imports' : 'no-imports'
|
||||
}&utm_group=${useUserStore.getState().activeTestGroupsUtmValue()}`
|
||||
|
||||
useEffect(() => {
|
||||
if (allowedImports < 1 || !allowedImports) {
|
||||
const fallback = 5
|
||||
UserApi.allowedImports()
|
||||
.then((allowedImports) => {
|
||||
allowedImports = /^[1-9]\d*$/.test(allowedImports)
|
||||
? allowedImports
|
||||
: fallback
|
||||
useUserStore.setState({ allowedImports })
|
||||
})
|
||||
.catch(() =>
|
||||
useUserStore.setState({ allowedImports: fallback }),
|
||||
)
|
||||
}
|
||||
}, [allowedImports])
|
||||
|
||||
if (!allowedImports) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
target="_blank"
|
||||
className="absolute bottom-4 left-0 mx-5 block bg-white rounded border-solid border border-gray-200 p-4 text-left no-underline group button-focus"
|
||||
rel="noreferrer"
|
||||
onClick={async () => await General.ping('fp-sb-click')}
|
||||
href={link}>
|
||||
<span className="flex -ml-1.5 space-x-1.5">
|
||||
<Icon icon={brandMark} />
|
||||
<span className="mb-1 text-gray-800 font-medium text-sm">
|
||||
{__('Free Plan', 'extendify')}
|
||||
</span>
|
||||
</span>
|
||||
<span className="text-gray-700 block ml-6 mb-1.5">
|
||||
{sprintf(
|
||||
_n(
|
||||
'You have %s free pattern and layout import remaining this month.',
|
||||
'You have %s free pattern and layout imports remaining this month.',
|
||||
count,
|
||||
'extendify',
|
||||
),
|
||||
count,
|
||||
)}
|
||||
</span>
|
||||
<span
|
||||
className={classNames(
|
||||
'block font-semibold ml-6 text-sm group-hover:underline',
|
||||
{
|
||||
'text-red-500': count < 2,
|
||||
'text-wp-theme-500': count > 1,
|
||||
},
|
||||
)}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: safeHTML(
|
||||
sprintf(
|
||||
_x(
|
||||
'Upgrade today %s',
|
||||
'The replacement string is a right arrow and context is not lost if removed.',
|
||||
'extendify',
|
||||
),
|
||||
`<span class="text-base">
|
||||
${String.fromCharCode(8250)}
|
||||
</span>`,
|
||||
),
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</a>
|
||||
)
|
||||
})
|
@@ -0,0 +1,241 @@
|
||||
import { useEffect, useState, useRef, useMemo } from '@wordpress/element'
|
||||
import { __ } from '@wordpress/i18n'
|
||||
import classNames from 'classnames'
|
||||
import Fuse from 'fuse.js'
|
||||
import { useTemplatesStore } from '@extendify/state/Templates'
|
||||
import { useUserStore } from '@extendify/state/User'
|
||||
|
||||
const searchMemo = new Map()
|
||||
|
||||
export const SiteTypeSelector = ({ value, setValue, terms }) => {
|
||||
const preferredOptionsHistory = useUserStore(
|
||||
(state) =>
|
||||
state.preferredOptionsHistory?.siteType?.filter((t) => t.slug) ??
|
||||
{},
|
||||
)
|
||||
const searchParams = useTemplatesStore((state) => state.searchParams)
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const searchRef = useRef()
|
||||
const [fuzzy, setFuzzy] = useState({})
|
||||
const [tempValue, setTempValue] = useState('')
|
||||
const [visibleChoices, setVisibleChoices] = useState([])
|
||||
|
||||
const examples = useMemo(() => {
|
||||
return terms
|
||||
.filter((t) => t?.featured)
|
||||
.sort((a, b) => {
|
||||
if (a.slug < b.slug) return -1
|
||||
if (a.slug > b.slug) return 1
|
||||
return 0
|
||||
})
|
||||
}, [terms])
|
||||
|
||||
const updateSearch = (term) => {
|
||||
setTempValue(term)
|
||||
filter(term)
|
||||
}
|
||||
|
||||
const filter = (term = '') => {
|
||||
if (searchMemo.has(term)) {
|
||||
setVisibleChoices(searchMemo.get(term))
|
||||
return
|
||||
}
|
||||
const results = fuzzy.search(term)
|
||||
searchMemo.set(
|
||||
term,
|
||||
results?.length ? results.map((t) => t.item) : examples,
|
||||
)
|
||||
setVisibleChoices(searchMemo.get(term))
|
||||
}
|
||||
|
||||
const showRecent = () =>
|
||||
visibleChoices === examples &&
|
||||
Object.keys(preferredOptionsHistory).length > 0
|
||||
const unknown = value.slug === 'unknown' || !value?.slug
|
||||
|
||||
useEffect(() => {
|
||||
setFuzzy(
|
||||
new Fuse(terms, {
|
||||
keys: ['slug', 'title', 'keywords'],
|
||||
minMatchCharLength: 2,
|
||||
threshold: 0.3,
|
||||
}),
|
||||
)
|
||||
}, [terms])
|
||||
|
||||
useEffect(() => {
|
||||
if (!tempValue?.length) setVisibleChoices(examples)
|
||||
}, [examples, tempValue])
|
||||
|
||||
useEffect(() => {
|
||||
expanded && searchRef.current.focus()
|
||||
}, [expanded])
|
||||
|
||||
const contentHeader = (description) => {
|
||||
return (
|
||||
<>
|
||||
<span className="flex flex-col text-left">
|
||||
<span className="mb-1 text-sm">
|
||||
{__('Site Type', 'extendify')}
|
||||
</span>
|
||||
<span className="text-xs font-light">{description}</span>
|
||||
</span>
|
||||
<span className="flex items-center space-x-4">
|
||||
{unknown && !expanded && (
|
||||
<svg
|
||||
className="text-wp-alert-red"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
width="21"
|
||||
height="21"
|
||||
viewBox="0 0 21 21"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
className="stroke-current"
|
||||
d="M10.9982 4.05371C7.66149 4.05371 4.95654 6.75866 4.95654 10.0954C4.95654 13.4321 7.66149 16.137 10.9982 16.137C14.3349 16.137 17.0399 13.4321 17.0399 10.0954C17.0399 6.75866 14.3349 4.05371 10.9982 4.05371V4.05371Z"
|
||||
strokeWidth="1.25"
|
||||
/>
|
||||
<path
|
||||
className="fill-current"
|
||||
d="M10.0205 12.8717C10.0205 12.3287 10.4508 11.8881 10.9938 11.8881C11.5368 11.8881 11.9774 12.3287 11.9774 12.8717C11.9774 13.4147 11.5368 13.8451 10.9938 13.8451C10.4508 13.8451 10.0205 13.4147 10.0205 12.8717Z"
|
||||
/>
|
||||
<path
|
||||
className="fill-current"
|
||||
d="M11.6495 10.2591C11.6086 10.6177 11.3524 10.9148 10.9938 10.9148C10.625 10.9148 10.3791 10.6074 10.3483 10.2591L10.0205 7.31855C9.95901 6.81652 10.4918 6.34521 10.9938 6.34521C11.4959 6.34521 12.0286 6.81652 11.9774 7.31855L11.6495 10.2591Z"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
<svg
|
||||
className={classNames('stroke-current text-gray-700', {
|
||||
'-translate-x-1 rotate-90 transform': expanded,
|
||||
})}
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
width="8"
|
||||
height="13"
|
||||
viewBox="0 0 8 13"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M1.24194 11.5952L6.24194 6.09519L1.24194 0.595215"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const choicesList = (choices, title = __('Suggestions', 'extendify')) => {
|
||||
if (choices === examples) {
|
||||
title = __('Examples', 'extendify')
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<h4 className="mt-4 mb-2 text-left text-xss font-medium uppercase text-gray-700">
|
||||
{title}
|
||||
</h4>
|
||||
<ul className="m-0">
|
||||
{choices.map((item) => {
|
||||
const label = item?.title ?? item.slug
|
||||
const current =
|
||||
searchParams?.taxonomies?.siteType?.slug ===
|
||||
item.slug
|
||||
return (
|
||||
<li
|
||||
key={item.slug + item?.title}
|
||||
className="m-0 mb-1">
|
||||
<button
|
||||
type="button"
|
||||
className={classNames(
|
||||
'm-0 w-full cursor-pointer bg-transparent pl-0 text-left text-sm hover:text-wp-theme-500',
|
||||
{ 'text-gray-800': !current },
|
||||
)}
|
||||
onClick={() => {
|
||||
setExpanded(false)
|
||||
setValue(item)
|
||||
}}>
|
||||
{label}
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full rounded bg-extendify-transparent-black">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpanded((expanded) => !expanded)}
|
||||
className="button-focus m-0 flex w-full cursor-pointer items-center justify-between rounded bg-transparent p-4 text-gray-800 hover:bg-extendify-transparent-black-100">
|
||||
{contentHeader(
|
||||
expanded
|
||||
? __('What kind of site is this?', 'extendify')
|
||||
: value?.title ?? value.slug ?? 'Unknown',
|
||||
)}
|
||||
</button>
|
||||
{expanded && (
|
||||
<div className="max-h-96 overflow-y-auto p-4 pt-0">
|
||||
<div className="relative my-2">
|
||||
<label htmlFor="site-type-search" className="sr-only">
|
||||
{__('Search', 'extendify')}
|
||||
</label>
|
||||
<input
|
||||
ref={searchRef}
|
||||
id="site-type-search"
|
||||
value={tempValue ?? ''}
|
||||
onChange={(event) =>
|
||||
updateSearch(event.target.value)
|
||||
}
|
||||
type="text"
|
||||
className="button-focus m-0 w-full rounded border-0 bg-white p-3.5 py-2.5 text-sm"
|
||||
placeholder={__('Search', 'extendify')}
|
||||
/>
|
||||
<svg
|
||||
className="pointer-events-none absolute top-2 right-2 hidden lg:block"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24"
|
||||
role="img"
|
||||
aria-hidden="true"
|
||||
focusable="false">
|
||||
<path d="M13.5 6C10.5 6 8 8.5 8 11.5c0 1.1.3 2.1.9 3l-3.4 3 1 1.1 3.4-2.9c1 .9 2.2 1.4 3.6 1.4 3 0 5.5-2.5 5.5-5.5C19 8.5 16.5 6 13.5 6zm0 9.5c-2.2 0-4-1.8-4-4s1.8-4 4-4 4 1.8 4 4-1.8 4-4 4z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
{tempValue?.length > 1 && visibleChoices === examples && (
|
||||
<p className="text-left">
|
||||
{__('Nothing found...', 'extendify')}
|
||||
</p>
|
||||
)}
|
||||
{showRecent() && (
|
||||
<div className="mb-8">
|
||||
{choicesList(
|
||||
preferredOptionsHistory,
|
||||
__('Recent', 'extendify'),
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{visibleChoices?.length > 0 && (
|
||||
<div>{choicesList(visibleChoices)}</div>
|
||||
)}
|
||||
{unknown ? null : (
|
||||
<button
|
||||
type="button"
|
||||
className="mt-4 w-full cursor-pointer bg-transparent pl-0 text-left text-sm text-wp-theme-500 hover:text-wp-theme-500"
|
||||
onClick={() => {
|
||||
setExpanded(false)
|
||||
setValue({ slug: '', title: 'Unknown' })
|
||||
}}>
|
||||
{__('Reset', 'extendify')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
@@ -0,0 +1,49 @@
|
||||
import { PanelBody, PanelRow } from '@wordpress/components'
|
||||
import classNames from 'classnames'
|
||||
import { useTemplatesStore } from '@extendify/state/Templates'
|
||||
import { getTaxonomyName } from '@extendify/util/general'
|
||||
|
||||
export default function TaxonomySection({ taxType, taxonomies, taxLabel }) {
|
||||
const updateTaxonomies = useTemplatesStore(
|
||||
(state) => state.updateTaxonomies,
|
||||
)
|
||||
const searchParams = useTemplatesStore((state) => state.searchParams)
|
||||
|
||||
if (!taxonomies?.length > 0) return null
|
||||
return (
|
||||
<PanelBody
|
||||
title={getTaxonomyName(taxLabel ?? taxType)}
|
||||
className="ext-type-control p-0"
|
||||
initialOpen={true}>
|
||||
<PanelRow>
|
||||
<div className="relative w-full overflow-hidden">
|
||||
<ul className="m-0 w-full px-5 py-1">
|
||||
{taxonomies.map((tax) => {
|
||||
const isCurrentTax =
|
||||
searchParams?.taxonomies[taxType]?.slug ===
|
||||
tax?.slug
|
||||
return (
|
||||
<li className="m-0 w-full" key={tax.slug}>
|
||||
<button
|
||||
type="button"
|
||||
className="button-focus m-0 flex w-full cursor-pointer items-center justify-between bg-transparent px-0 py-2 text-left text-sm leading-none transition duration-200 hover:text-wp-theme-500"
|
||||
onClick={() =>
|
||||
updateTaxonomies({ [taxType]: tax })
|
||||
}>
|
||||
<span
|
||||
className={classNames({
|
||||
'text-wp-theme-500':
|
||||
isCurrentTax,
|
||||
})}>
|
||||
{tax?.title ?? tax.slug}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</PanelRow>
|
||||
</PanelBody>
|
||||
)
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
import { __ } from '@wordpress/i18n'
|
||||
import classNames from 'classnames'
|
||||
import { useGlobalStore } from '@extendify/state/GlobalState'
|
||||
import { useTemplatesStore } from '@extendify/state/Templates'
|
||||
|
||||
export const TypeSelect = ({ className }) => {
|
||||
const updateType = useTemplatesStore((state) => state.updateType)
|
||||
const currentType = useGlobalStore(
|
||||
(state) => state?.currentType ?? 'pattern',
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<h4 className="sr-only">{__('Type select', 'extendify')}</h4>
|
||||
<button
|
||||
type="button"
|
||||
className={classNames({
|
||||
'button-focus m-0 min-w-sm cursor-pointer rounded-tl-sm rounded-bl-sm border border-black py-2.5 px-4 text-xs leading-none': true,
|
||||
'bg-gray-900 text-white': currentType === 'pattern',
|
||||
'bg-transparent text-black': currentType !== 'pattern',
|
||||
})}
|
||||
onClick={() => updateType('pattern')}>
|
||||
<span className="">{__('Patterns', 'extendify')}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={classNames({
|
||||
'outline-none button-focus m-0 -ml-px min-w-sm cursor-pointer items-center rounded-tr-sm rounded-br-sm border border-black py-2.5 px-4 text-xs leading-none': true,
|
||||
'bg-gray-900 text-white': currentType === 'template',
|
||||
'bg-transparent text-black': currentType !== 'template',
|
||||
})}
|
||||
onClick={() => updateType('template')}>
|
||||
<span className="">{__('Page Layouts', 'extendify')}</span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
export { default as alert } from './library/alert'
|
||||
export { default as brandBlockIcon } from './library/brand-block-icon'
|
||||
export { default as brandMark } from './library/brand-mark'
|
||||
export { default as brandLogo } from './library/brand-logo'
|
||||
export { default as diamond } from './library/diamond'
|
||||
export { default as download } from './library/download'
|
||||
export { default as download2 } from './library/download2'
|
||||
export { default as featured } from './library/featured'
|
||||
export { default as growthArrow } from './library/growth-arrow'
|
||||
export { default as layouts } from './library/layouts'
|
||||
export { default as patterns } from './library/patterns'
|
||||
export { default as success } from './library/success'
|
||||
export { default as support } from './library/support'
|
||||
export { default as star } from './library/star'
|
||||
export { default as user } from './library/user'
|
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { Path, SVG } from '@wordpress/primitives'
|
||||
|
||||
const alert = (
|
||||
<SVG viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg">
|
||||
<Path
|
||||
d="M7.32457 0.907043C3.98785 0.907043 1.2829 3.61199 1.2829 6.94871C1.2829 10.2855 3.98785 12.9904 7.32457 12.9904C10.6613 12.9904 13.3663 10.2855 13.3663 6.94871C13.3663 3.61199 10.6613 0.907043 7.32457 0.907043V0.907043Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.25"
|
||||
fill="none"
|
||||
/>
|
||||
<Path
|
||||
d="M6.34684 9.72526C6.34684 9.18224 6.77716 8.74168 7.32018 8.74168C7.8632 8.74168 8.30377 9.18224 8.30377 9.72526C8.30377 10.2683 7.8632 10.6986 7.32018 10.6986C6.77716 10.6986 6.34684 10.2683 6.34684 9.72526Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<Path
|
||||
d="M7.9759 7.11261C7.93492 7.47121 7.67878 7.76834 7.32018 7.76834C6.95134 7.76834 6.70544 7.46097 6.6747 7.11261L6.34684 4.1721C6.28537 3.67006 6.81814 3.19876 7.32018 3.19876C7.82222 3.19876 8.35499 3.67006 8.30377 4.1721L7.9759 7.11261Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</SVG>
|
||||
)
|
||||
|
||||
export default alert
|
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { Path, SVG } from '@wordpress/primitives'
|
||||
|
||||
const brandBlockIcon = (
|
||||
<SVG fill="none" viewBox="0 0 25 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<Path
|
||||
clipRule="evenodd"
|
||||
d="m14.4063 2h4.1856c1.1856 0 1.6147.12701 2.0484.36409.4336.23802.7729.58706 1.0049 1.03111.2319.445.3548.8853.3548 2.10175v4.29475c0 1.2165-.1238 1.6567-.3548 2.1017-.232.445-.5722.7931-1.0049 1.0312-.1939.1064-.3873.1939-.6476.2567v3.4179c0 1.8788-.1912 2.5588-.5481 3.246-.3582.6873-.8836 1.2249-1.552 1.5925-.6697.3676-1.3325.5623-3.1634.5623h-6.46431c-1.83096 0-2.49367-.1962-3.16346-.5623-.6698-.3676-1.19374-.9067-1.552-1.5925s-.54943-1.3672-.54943-3.246v-6.63138c0-1.87871.19117-2.55871.54801-3.24597.35827-.68727.88362-1.22632 1.55342-1.59393.66837-.36615 1.3325-.56231 3.16346-.56231h2.76781c.0519-.55814.1602-.86269.3195-1.16946.232-.445.5721-.79404 1.0058-1.03206.4328-.23708.8628-.36409 2.0483-.36409zm-2.1512 2.73372c0-.79711.6298-1.4433 1.4067-1.4433h5.6737c.777 0 1.4068.64619 1.4068 1.4433v5.82118c0 .7971-.6298 1.4433-1.4068 1.4433h-5.6737c-.7769 0-1.4067-.6462-1.4067-1.4433z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</SVG>
|
||||
)
|
||||
|
||||
export default brandBlockIcon
|
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { Path, SVG, G } from '@wordpress/primitives'
|
||||
|
||||
const brandLogo = (
|
||||
<SVG
|
||||
fill="none"
|
||||
width="150"
|
||||
height="30"
|
||||
viewBox="0 0 2524 492"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<G fill="currentColor">
|
||||
<Path d="m609.404 378.5c-24.334 0-46-5.5-65-16.5-18.667-11.333-33.334-26.667-44-46-10.667-19.667-16-42.167-16-67.5 0-25.667 5.166-48.333 15.5-68 10.333-19.667 24.833-35 43.5-46 18.666-11.333 40-17 64-17 25 0 46.5 5.333 64.5 16 18 10.333 31.833 24.833 41.5 43.5 10 18.667 15 41 15 67v18.5l-212 .5 1-39h150.5c0-17-5.5-30.667-16.5-41-10.667-10.333-25.167-15.5-43.5-15.5-14.334 0-26.5 3-36.5 9-9.667 6-17 15-22 27s-7.5 26.667-7.5 44c0 26.667 5.666 46.833 17 60.5 11.666 13.667 28.833 20.5 51.5 20.5 16.666 0 30.333-3.167 41-9.5 11-6.333 18.166-15.333 21.5-27h56.5c-5.334 27-18.667 48.167-40 63.5-21 15.333-47.667 23-80 23z" />
|
||||
<path d="m797.529 372h-69.5l85-121-85-126h71l54.5 84 52.5-84h68.5l-84 125.5 81.5 121.5h-70l-53-81.5z" />
|
||||
<path d="m994.142 125h155.998v51h-155.998zm108.498 247h-61v-324h61z" />
|
||||
<path d="m1278.62 378.5c-24.33 0-46-5.5-65-16.5-18.66-11.333-33.33-26.667-44-46-10.66-19.667-16-42.167-16-67.5 0-25.667 5.17-48.333 15.5-68 10.34-19.667 24.84-35 43.5-46 18.67-11.333 40-17 64-17 25 0 46.5 5.333 64.5 16 18 10.333 31.84 24.833 41.5 43.5 10 18.667 15 41 15 67v18.5l-212 .5 1-39h150.5c0-17-5.5-30.667-16.5-41-10.66-10.333-25.16-15.5-43.5-15.5-14.33 0-26.5 3-36.5 9-9.66 6-17 15-22 27s-7.5 26.667-7.5 44c0 26.667 5.67 46.833 17 60.5 11.67 13.667 28.84 20.5 51.5 20.5 16.67 0 30.34-3.167 41-9.5 11-6.333 18.17-15.333 21.5-27h56.5c-5.33 27-18.66 48.167-40 63.5-21 15.333-47.66 23-80 23z" />
|
||||
<path d="m1484.44 372h-61v-247h56.5l5 32c7.67-12.333 18.5-22 32.5-29 14.34-7 29.84-10.5 46.5-10.5 31 0 54.34 9.167 70 27.5 16 18.333 24 43.333 24 75v152h-61v-137.5c0-20.667-4.66-36-14-46-9.33-10.333-22-15.5-38-15.5-19 0-33.83 6-44.5 18-10.66 12-16 28-16 48z" />
|
||||
<path d="m1798.38 378.5c-24 0-44.67-5.333-62-16-17-11-30.34-26.167-40-45.5-9.34-19.333-14-41.833-14-67.5s4.66-48.333 14-68c9.66-20 23.5-35.667 41.5-47s39.33-17 64-17c17.33 0 33.16 3.5 47.5 10.5 14.33 6.667 25.33 16.167 33 28.5v-156.5h60.5v372h-56l-4-38.5c-7.34 14-18.67 25-34 33-15 8-31.84 12-50.5 12zm13.5-56c14.33 0 26.66-3 37-9 10.33-6.333 18.33-15.167 24-26.5 6-11.667 9-24.833 9-39.5 0-15-3-28-9-39-5.67-11.333-13.67-20.167-24-26.5-10.34-6.667-22.67-10-37-10-14 0-26.17 3.333-36.5 10-10.34 6.333-18.34 15.167-24 26.5-5.34 11.333-8 24.333-8 39s2.66 27.667 8 39c5.66 11.333 13.66 20.167 24 26.5 10.33 6.333 22.5 9.5 36.5 9.5z" />
|
||||
<path d="m1996.45 372v-247h61v247zm30-296.5c-10.34 0-19.17-3.5-26.5-10.5-7-7.3333-10.5-16.1667-10.5-26.5s3.5-19 10.5-26c7.33-6.99999 16.16-10.49998 26.5-10.49998 10.33 0 19 3.49999 26 10.49998 7.33 7 11 15.6667 11 26s-3.67 19.1667-11 26.5c-7 7-15.67 10.5-26 10.5z" />
|
||||
<path d="m2085.97 125h155v51h-155zm155.5-122.5v52c-3.33 0-6.83 0-10.5 0-3.33 0-6.83 0-10.5 0-15.33 0-25.67 3.6667-31 11-5 7.3333-7.5 17.1667-7.5 29.5v277h-60.5v-277c0-22.6667 3.67-40.8333 11-54.5 7.33-14 17.67-24.1667 31-30.5 13.33-6.66666 28.83-10 46.5-10 5 0 10.17.166671 15.5.5 5.67.333329 11 .99999 16 2z" />
|
||||
<path d="m2330.4 125 80.5 228-33 62.5-112-290.5zm-58 361.5v-50.5h36.5c8 0 15-1 21-3 6-1.667 11.34-5 16-10 5-5 9.17-12.333 12.5-22l102.5-276h63l-121 302c-9 22.667-20.33 39.167-34 49.5-13.66 10.333-30.66 15.5-51 15.5-8.66 0-16.83-.5-24.5-1.5-7.33-.667-14.33-2-21-4z" />
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="m226.926 25.1299h83.271c23.586 0 32.123 2.4639 40.751 7.0633 8.628 4.6176 15.378 11.389 19.993 20.0037 4.615 8.6329 7.059 17.1746 7.059 40.7738v83.3183c0 23.599-2.463 32.141-7.059 40.774-4.615 8.633-11.383 15.386-19.993 20.003-3.857 2.065-7.704 3.764-12.884 4.981v66.308c0 36.447-3.803 49.639-10.902 62.972-7.128 13.333-17.579 23.763-30.877 30.894-13.325 7.132-26.51 10.909-62.936 10.909h-128.605c-36.4268 0-49.6113-3.805-62.9367-10.909-13.3254-7.131-23.749-17.589-30.8765-30.894-7.12757-13.304-10.9308-26.525-10.9308-62.972v-128.649c0-36.447 3.80323-49.639 10.9026-62.972 7.1275-13.333 17.5793-23.7909 30.9047-30.9224 13.2972-7.1034 26.5099-10.9088 62.9367-10.9088h55.064c1.033-10.8281 3.188-16.7362 6.357-22.6877 4.615-8.6329 11.382-15.4043 20.01-20.0219 8.61-4.5994 17.165-7.0633 40.751-7.0633zm-42.798 53.0342c0-15.464 12.53-28 27.986-28h112.877c15.457 0 27.987 12.536 27.987 28v112.9319c0 15.464-12.53 28-27.987 28h-112.877c-15.456 0-27.986-12.536-27.986-28z"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</G>
|
||||
</SVG>
|
||||
)
|
||||
|
||||
export default brandLogo
|
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { Path, SVG } from '@wordpress/primitives'
|
||||
|
||||
const brandMark = (
|
||||
<SVG fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<Path
|
||||
clipRule="evenodd"
|
||||
d="m13.505 4h3.3044c.936 0 1.2747.10161 1.6171.29127.3424.19042.6102.46965.7934.82489.1831.356.2801.70824.2801 1.6814v3.43584c0 .9731-.0977 1.3254-.2801 1.6814-.1832.356-.4517.6344-.7934.8248-.153.0852-.3057.1552-.5112.2054v2.7344c0 1.503-.151 2.047-.4327 2.5968-.2828.5498-.6976.9799-1.2252 1.274-.5288.294-1.052.4498-2.4975.4498h-5.10341c-1.44549 0-1.96869-.1569-2.49747-.4498-.52878-.2941-.94242-.7254-1.22526-1.274-.28284-.5487-.43376-1.0938-.43376-2.5968v-5.3051c0-1.50301.15092-2.04701.43264-2.59682.28284-.54981.6976-.98106 1.22638-1.27514.52767-.29293 1.05198-.44985 2.49747-.44985h2.18511c.041-.44652.1265-.69015.2522-.93557.1832-.356.4517-.63523.7941-.82565.3417-.18966.6812-.29127 1.6171-.29127zm-1.6984 2.18698c0-.63769.4973-1.15464 1.1106-1.15464h4.4793c.6133 0 1.1106.51695 1.1106 1.15464v4.65692c0 .6377-.4973 1.1547-1.1106 1.1547h-4.4793c-.6133 0-1.1106-.517-1.1106-1.1547z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</SVG>
|
||||
)
|
||||
|
||||
export default brandMark
|
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { SVG } from '@wordpress/primitives'
|
||||
|
||||
const alert = (
|
||||
<SVG
|
||||
fill="none"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="m11.9893 2.59931c-.1822.00285-.3558.07789-.4827.20864s-.1967.30653-.1941.48871v1.375c-.0013.0911.0156.18155.0495.26609.034.08454.0844.16149.1484.22637s.1402.11639.2242.15156c.0841.03516.1743.05327.2654.05327s.1813-.01811.2654-.05327c.084-.03517.1603-.08668.2242-.15156.064-.06488.1144-.14183.1484-.22637s.0508-.17499.0495-.26609v-1.375c.0013-.09202-.0158-.18337-.0505-.26863-.0346-.08526-.086-.1627-.1511-.22773s-.1426-.11633-.2279-.15085c-.0853-.03453-.1767-.05158-.2687-.05014zm-5.72562.46013c-.1251.00033-.24775.0348-.35471.09968-.10697.06488-.19421.15771-.25232.2685-.05812.1108-.0849.23534-.07747.36023.00744.12488.0488.24537.11964.34849l.91667 1.375c.04939.07667.11354.14274.18872.19437.07517.05164.15987.0878.24916.10639.08928.01858.18137.01922.27091.00187.08953-.01734.17472-.05233.2506-.10292.07589-.05059.14095-.11577.1914-.19174.05045-.07598.08528-.16123.10246-.2508.01719-.08956.01638-.18165-.00237-.2709s-.05507-.17388-.10684-.24897l-.91666-1.375c-.06252-.09667-.14831-.1761-.2495-.231-.1012-.0549-.21456-.08351-.32969-.0832zm11.45212 0c-.1117.00307-.2209.03329-.3182.08804-.0973.05474-.1798.13237-.2404.22616l-.9167 1.375c-.0518.07509-.0881.15972-.1068.24897-.0188.08925-.0196.18134-.0024.2709.0172.08957.052.17482.1024.2508.0505.07597.1156.14115.1914.19174.0759.05059.1611.08558.2506.10292.0896.01735.1817.01671.271-.00187.0892-.01859.1739-.05475.2491-.10639.0752-.05163.1393-.1177.1887-.19437l.9167-1.375c.0719-.10456.1135-.22698.1201-.3537s-.022-.25281-.0826-.36429c-.0606-.11149-.1508-.20403-.2608-.26738-.11-.06334-.2353-.09502-.3621-.09153zm-9.61162 3.67472c-.09573-.00001-.1904.01998-.27795.05867-.08756.03869-.16607.09524-.23052.16602l-4.58333 5.04165c-.11999.1319-.18407.3052-.17873.4834.00535.1782.0797.3473.20738.4718l8.47917 8.25c.1284.1251.3006.1951.4798.1951.1793 0 .3514-.07.4798-.1951l8.4792-8.25c.1277-.1245.202-.2936.2074-.4718.0053-.1782-.0588-.3515-.1788-.4834l-4.5833-5.04165c-.0644-.07078-.1429-.12733-.2305-.16602s-.1822-.05868-.278-.05867h-3.877zm.30436 1.375h2.21646l-2.61213 3.48314c-.04258.0557-.07639.1176-.10026.1835h-2.83773zm4.96646 0h2.2165l3.3336 3.66664h-2.8368c-.0241-.066-.0582-.1278-.1011-.1835zm-1.375.45833 2.4063 3.20831h-4.81254zm-6.78637 4.58331h2.70077c.00665.0188.01412.0374.02238.0555l2.11442 4.6505zm4.20826 0h5.15621l-2.5781 5.6719zm6.66371 0h2.7008l-4.8376 4.706 2.1144-4.6505c.0083-.0181.0158-.0367.0224-.0555z"
|
||||
fill="#000"
|
||||
/>
|
||||
</SVG>
|
||||
)
|
||||
|
||||
export default alert
|
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { Path, SVG } from '@wordpress/primitives'
|
||||
|
||||
const download = (
|
||||
<SVG viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<Path
|
||||
d="M7.32457 0.907043C3.98785 0.907043 1.2829 3.61199 1.2829 6.94871C1.2829 10.2855 3.98785 12.9904 7.32457 12.9904C10.6613 12.9904 13.3663 10.2855 13.3663 6.94871C13.3663 3.61199 10.6613 0.907043 7.32457 0.907043V0.907043Z"
|
||||
stroke="white"
|
||||
strokeWidth="1.25"
|
||||
/>
|
||||
<Path
|
||||
d="M7.32458 10.0998L4.82458 7.59977M7.32458 10.0998V3.79764V10.0998ZM7.32458 10.0998L9.82458 7.59977L7.32458 10.0998Z"
|
||||
stroke="white"
|
||||
strokeWidth="1.25"
|
||||
/>
|
||||
</SVG>
|
||||
)
|
||||
|
||||
export default download
|
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { SVG } from '@wordpress/primitives'
|
||||
|
||||
const download2 = (
|
||||
<SVG viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M7.93298 20.2773L17.933 20.2773C18.1982 20.2773 18.4526 20.172 18.6401 19.9845C18.8276 19.7969 18.933 19.5426 18.933 19.2773C18.933 19.0121 18.8276 18.7578 18.6401 18.5702C18.4526 18.3827 18.1982 18.2773 17.933 18.2773L7.93298 18.2773C7.66777 18.2773 7.41341 18.3827 7.22588 18.5702C7.03834 18.7578 6.93298 19.0121 6.93298 19.2773C6.93298 19.5426 7.03834 19.7969 7.22588 19.9845C7.41341 20.172 7.66777 20.2773 7.93298 20.2773Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M12.933 4.27734C12.6678 4.27734 12.4134 4.3827 12.2259 4.57024C12.0383 4.75777 11.933 5.01213 11.933 5.27734L11.933 12.8673L9.64298 10.5773C9.55333 10.4727 9.44301 10.3876 9.31895 10.3276C9.19488 10.2676 9.05975 10.2339 8.92203 10.2285C8.78431 10.2232 8.64698 10.2464 8.51865 10.2967C8.39033 10.347 8.27378 10.4232 8.17632 10.5207C8.07887 10.6181 8.00261 10.7347 7.95234 10.863C7.90206 10.9913 7.87886 11.1287 7.88418 11.2664C7.8895 11.4041 7.92323 11.5392 7.98325 11.6633C8.04327 11.7874 8.12829 11.8977 8.23297 11.9873L12.233 15.9873C12.3259 16.0811 12.4365 16.1555 12.5584 16.2062C12.6803 16.257 12.811 16.2831 12.943 16.2831C13.075 16.2831 13.2057 16.257 13.3276 16.2062C13.4494 16.1555 13.56 16.0811 13.653 15.9873L17.653 11.9873C17.8168 11.796 17.9024 11.55 17.8927 11.2983C17.883 11.0466 17.7786 10.8079 17.6005 10.6298C17.4224 10.4517 17.1837 10.3474 16.932 10.3376C16.6804 10.3279 16.4343 10.4135 16.243 10.5773L13.933 12.8673L13.933 5.27734C13.933 5.01213 13.8276 4.75777 13.6401 4.57024C13.4525 4.3827 13.1982 4.27734 12.933 4.27734Z"
|
||||
fill="white"
|
||||
/>
|
||||
</SVG>
|
||||
)
|
||||
export default download2
|
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { Path, SVG, G } from '@wordpress/primitives'
|
||||
|
||||
const featured = (
|
||||
<SVG fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<Path
|
||||
d="m11.2721 16.9866.6041 2.2795.6042-2.2795.6213-2.3445c.0001-.0002.0001-.0004.0002-.0006.2404-.9015.8073-1.5543 1.4638-1.8165.0005-.0002.0009-.0004.0013-.0006l1.9237-.7555 1.4811-.5818-1.4811-.5817-1.9264-.7566c0-.0001-.0001-.0001-.0001-.0001-.0001 0-.0001 0-.0001 0-.654-.25727-1.2213-.90816-1.4621-1.81563-.0001-.00006-.0001-.00011-.0001-.00017l-.6215-2.34519-.6042-2.27947-.6041 2.27947-.6216 2.34519v.00017c-.2409.90747-.80819 1.55836-1.46216 1.81563-.00002 0-.00003 0-.00005 0-.00006 0-.00011 0-.00017.0001l-1.92637.7566-1.48108.5817 1.48108.5818 1.92637.7566c.00007 0 .00015.0001.00022.0001.65397.2572 1.22126.9082 1.46216 1.8156v.0002z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.25"
|
||||
fill="none"
|
||||
/>
|
||||
<G fill="currentColor">
|
||||
<Path d="m18.1034 18.3982-.2787.8625-.2787-.8625c-.1314-.4077-.4511-.7275-.8589-.8589l-.8624-.2786.8624-.2787c.4078-.1314.7275-.4512.8589-.8589l.2787-.8624.2787.8624c.1314.4077.4511.7275.8589.8589l.8624.2787-.8624.2786c-.4078.1314-.7269.4512-.8589.8589z" />
|
||||
<Path d="m6.33141 6.97291-.27868.86242-.27867-.86242c-.13142-.40775-.45116-.72749-.8589-.85891l-.86243-.27867.86243-.27868c.40774-.13141.72748-.45115.8589-.8589l.27867-.86242.27868.86242c.13142.40775.45116.72749.8589.8589l.86242.27868-.86242.27867c-.40774.13142-.7269.45116-.8589.85891z" />
|
||||
</G>
|
||||
</SVG>
|
||||
)
|
||||
|
||||
export default featured
|
@@ -0,0 +1,17 @@
|
||||
import { Path, SVG } from '@wordpress/primitives'
|
||||
|
||||
const growthArrow = (
|
||||
<SVG
|
||||
fill="none"
|
||||
height="25"
|
||||
viewBox="0 0 25 25"
|
||||
width="25"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<Path
|
||||
d="m16.2382 9.17969.7499.00645.0066-.75988-.7599.00344zm-5.5442.77506 5.5475-.02507-.0067-1.49998-5.5476.02506zm4.7942-.78152-.0476 5.52507 1.5.0129.0475-5.52506zm.2196-.52387-7.68099 7.68104 1.06066 1.0606 7.68103-7.68098z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</SVG>
|
||||
)
|
||||
|
||||
export default growthArrow
|
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { Path, SVG, G } from '@wordpress/primitives'
|
||||
|
||||
const layouts = (
|
||||
<SVG
|
||||
fill="none"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<G stroke="currentColor" strokeWidth="1.5">
|
||||
<Path d="m6 4.75h12c.6904 0 1.25.55964 1.25 1.25v12c0 .6904-.5596 1.25-1.25 1.25h-12c-.69036 0-1.25-.5596-1.25-1.25v-12c0-.69036.55964-1.25 1.25-1.25z" />
|
||||
<Path d="m9.25 19v-14" />
|
||||
</G>
|
||||
</SVG>
|
||||
)
|
||||
|
||||
export default layouts
|
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { Path, SVG } from '@wordpress/primitives'
|
||||
|
||||
const patterns = (
|
||||
<SVG
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<Path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M7.49271 18.0092C6.97815 17.1176 7.28413 15.9755 8.17569 15.4609C9.06724 14.946 10.2094 15.252 10.7243 16.1435C11.2389 17.0355 10.9329 18.1772 10.0413 18.6922C9.14978 19.2071 8.00764 18.9011 7.49271 18.0092V18.0092Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<Path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M16.5073 6.12747C17.0218 7.01903 16.7158 8.16117 15.8243 8.67573C14.9327 9.19066 13.7906 8.88467 13.2757 7.99312C12.7611 7.10119 13.0671 5.95942 13.9586 5.44449C14.8502 4.92956 15.9923 5.23555 16.5073 6.12747V6.12747Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<Path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M4.60135 11.1355C5.11628 10.2439 6.25805 9.93793 7.14998 10.4525C8.04153 10.9674 8.34752 12.1096 7.83296 13.0011C7.31803 13.8927 6.17588 14.1987 5.28433 13.6841C4.39278 13.1692 4.08679 12.0274 4.60135 11.1355V11.1355Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<Path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M19.3986 13.0011C18.8837 13.8927 17.7419 14.1987 16.85 13.6841C15.9584 13.1692 15.6525 12.027 16.167 11.1355C16.682 10.2439 17.8241 9.93793 18.7157 10.4525C19.6072 10.9674 19.9132 12.1092 19.3986 13.0011V13.0011Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<Path
|
||||
d="M9.10857 8.92594C10.1389 8.92594 10.9742 8.09066 10.9742 7.06029C10.9742 6.02992 10.1389 5.19464 9.10857 5.19464C8.0782 5.19464 7.24292 6.02992 7.24292 7.06029C7.24292 8.09066 8.0782 8.92594 9.10857 8.92594Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<Path
|
||||
d="M14.8913 18.942C15.9217 18.942 16.7569 18.1067 16.7569 17.0763C16.7569 16.046 15.9217 15.2107 14.8913 15.2107C13.8609 15.2107 13.0256 16.046 13.0256 17.0763C13.0256 18.1067 13.8609 18.942 14.8913 18.942Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<Path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10.3841 13.0011C9.86951 12.1096 10.1755 10.9674 11.067 10.4525C11.9586 9.93793 13.1007 10.2439 13.6157 11.1355C14.1302 12.0274 13.8242 13.1692 12.9327 13.6841C12.0411 14.1987 10.899 13.8927 10.3841 13.0011V13.0011Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</SVG>
|
||||
)
|
||||
|
||||
export default patterns
|
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { Path, SVG } from '@wordpress/primitives'
|
||||
|
||||
const star = (
|
||||
<SVG
|
||||
fill="none"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<Path
|
||||
d="m11.7758 3.45425c.0917-.18582.3567-.18581.4484 0l2.3627 4.78731c.0364.07379.1068.12493.1882.13676l5.2831.76769c.2051.02979.287.28178.1386.42642l-3.8229 3.72637c-.0589.0575-.0858.1402-.0719.2213l.9024 5.2618c.0351.2042-.1793.36-.3627.2635l-4.7254-2.4842c-.0728-.0383-.1598-.0383-.2326 0l-4.7254 2.4842c-.18341.0965-.39776-.0593-.36274-.2635l.90247-5.2618c.01391-.0811-.01298-.1638-.0719-.2213l-3.8229-3.72637c-.14838-.14464-.0665-.39663.13855-.42642l5.28312-.76769c.08143-.01183.15182-.06297.18823-.13676z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</SVG>
|
||||
)
|
||||
|
||||
export default star
|
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { Path, SVG, G, Circle, Rect } from '@wordpress/primitives'
|
||||
|
||||
const download = (
|
||||
<SVG
|
||||
fill="none"
|
||||
viewBox="0 0 151 148"
|
||||
width="151"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<Circle cx="65.6441" cy="66.6114" fill="#0b4a43" r="65.3897" />
|
||||
<G fill="#cbc3f5" stroke="#0b4a43">
|
||||
<Path
|
||||
d="m61.73 11.3928 3.0825 8.3304.1197.3234.3234.1197 8.3304 3.0825-8.3304 3.0825-.3234.1197-.1197.3234-3.0825 8.3304-3.0825-8.3304-.1197-.3234-.3234-.1197-8.3304-3.0825 8.3304-3.0825.3234-.1197.1197-.3234z"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
<Path
|
||||
d="m84.3065 31.2718c0 5.9939-12.4614 22.323-18.6978 22.323h-17.8958v56.1522c3.5249.9 11.6535 0 17.8958 0h6.2364c11.2074 3.33 36.0089 7.991 45.5529 0l-9.294-62.1623c-2.267-1.7171-5.949-6.6968-2.55-12.8786 3.4-6.1817 2.55-18.0406 0-24.5756-1.871-4.79616-8.3289-8.90882-14.4482-8.90882s-7.0825 4.00668-6.7993 6.01003z"
|
||||
strokeWidth="1.75"
|
||||
/>
|
||||
<Rect
|
||||
height="45.5077"
|
||||
rx="9.13723"
|
||||
strokeWidth="1.75"
|
||||
transform="matrix(0 1 -1 0 191.5074 -96.0026)"
|
||||
width="18.2745"
|
||||
x="143.755"
|
||||
y="47.7524"
|
||||
/>
|
||||
<Rect
|
||||
height="42.3038"
|
||||
rx="8.73674"
|
||||
strokeWidth="1.75"
|
||||
transform="matrix(0 1 -1 0 241.97 -50.348)"
|
||||
width="17.4735"
|
||||
x="146.159"
|
||||
y="95.811"
|
||||
/>
|
||||
<Rect
|
||||
height="55.9204"
|
||||
rx="8.73674"
|
||||
strokeWidth="1.75"
|
||||
transform="matrix(0 1 -1 0 213.1347 -85.5913)"
|
||||
width="17.4735"
|
||||
x="149.363"
|
||||
y="63.7717"
|
||||
/>
|
||||
<Rect
|
||||
height="51.1145"
|
||||
rx="8.73674"
|
||||
strokeWidth="1.75"
|
||||
transform="matrix(0 1 -1 0 229.1545 -69.5715)"
|
||||
width="17.4735"
|
||||
x="149.363"
|
||||
y="79.7915"
|
||||
/>
|
||||
<Path
|
||||
d="m75.7483 105.349c.9858-25.6313-19.2235-42.0514-32.8401-44.0538v12.0146c8.5438 1.068 24.8303 9.7642 24.8303 36.0442 0 23.228 19.4905 33.374 29.6362 33.641v-10.413s-22.6122-1.602-21.6264-27.233z"
|
||||
strokeWidth="1.75"
|
||||
/>
|
||||
<Path
|
||||
d="m68.5388 109.354c.9858-25.6312-19.2234-42.0513-32.8401-44.0537v12.0147c8.5438 1.0679 24.8303 9.7641 24.8303 36.044 0 23.228 19.4905 33.374 29.6362 33.641v-10.413s-22.6122-1.602-21.6264-27.233z"
|
||||
strokeWidth="1.75"
|
||||
/>
|
||||
</G>
|
||||
</SVG>
|
||||
)
|
||||
|
||||
export default download
|
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { SVG, Circle } from '@wordpress/primitives'
|
||||
|
||||
const layouts = (
|
||||
<SVG
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<Circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="7.25"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
<Circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="4.25"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
<Circle
|
||||
cx="11.9999"
|
||||
cy="12.2"
|
||||
r="6"
|
||||
transform="rotate(-45 11.9999 12.2)"
|
||||
stroke="currentColor"
|
||||
strokeWidth="3"
|
||||
strokeDasharray="1.5 4"
|
||||
/>
|
||||
</SVG>
|
||||
)
|
||||
|
||||
export default layouts
|
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { Path, SVG } from '@wordpress/primitives'
|
||||
|
||||
const user = (
|
||||
<SVG fill="none" viewBox="0 0 25 25" xmlns="http://www.w3.org/2000/svg">
|
||||
<Path
|
||||
clipRule="evenodd"
|
||||
d="m13 4c4.9545 0 9 4.04545 9 9 0 4.9545-4.0455 9-9 9-4.95455 0-9-4.0455-9-9 0-4.95455 4.04545-9 9-9zm5.0909 13.4545c-1.9545 3.8637-8.22726 3.8637-10.22726 0-.04546-.1818-.04546-.3636 0-.5454 2-3.8636 8.27276-3.8636 10.22726 0 .0909.1818.0909.3636 0 .5454zm-5.0909-8.90905c-1.2727 0-2.3182 1.04546-2.3182 2.31815 0 1.2728 1.0455 2.3182 2.3182 2.3182s2.3182-1.0454 2.3182-2.3182c0-1.27269-1.0455-2.31815-2.3182-2.31815z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</SVG>
|
||||
)
|
||||
|
||||
export default user
|
@@ -0,0 +1,105 @@
|
||||
import { Icon } from '@wordpress/components'
|
||||
import { safeHTML } from '@wordpress/dom'
|
||||
import { useState, useRef } from '@wordpress/element'
|
||||
import { __, sprintf } from '@wordpress/i18n'
|
||||
import { General } from '@extendify/api/General'
|
||||
import { Plugins } from '@extendify/api/Plugins'
|
||||
import { download2, brandLogo } from '@extendify/components/icons'
|
||||
import { useGlobalStore } from '@extendify/state/GlobalState'
|
||||
import { useUserStore } from '@extendify/state/User'
|
||||
import { SplitModal } from './SplitModal'
|
||||
|
||||
export const InstallStandaloneModal = () => {
|
||||
const [text, setText] = useState(__('Install Extendify', 'extendify'))
|
||||
const [success, setSuccess] = useState(false)
|
||||
const [disabled, setDisabled] = useState(false)
|
||||
const initialFocus = useRef(null)
|
||||
const markNoticeSeen = useUserStore((state) => state.markNoticeSeen)
|
||||
const giveFreebieImports = useUserStore((state) => state.giveFreebieImports)
|
||||
const removeAllModals = useGlobalStore((state) => state.removeAllModals)
|
||||
|
||||
const installAndActivate = () => {
|
||||
setText(__('Installing...', 'extendify'))
|
||||
setDisabled(true)
|
||||
Promise.all([
|
||||
General.ping('stln-modal-install'),
|
||||
Plugins.installAndActivate(['extendify']),
|
||||
new Promise((resolve) => setTimeout(resolve, 1000)),
|
||||
])
|
||||
.then(async () => {
|
||||
setText(__('Success! Reloading...', 'extendify'))
|
||||
setSuccess(true)
|
||||
giveFreebieImports(10)
|
||||
await General.ping('stln-modal-success')
|
||||
window.location.reload()
|
||||
})
|
||||
.catch(async (error) => {
|
||||
console.error(error)
|
||||
setText(__('Error. See console.', 'extendify'))
|
||||
await General.ping('stln-modal-fail')
|
||||
})
|
||||
}
|
||||
|
||||
const dismiss = async () => {
|
||||
removeAllModals()
|
||||
markNoticeSeen('standalone', 'modalNotices')
|
||||
await General.ping('stln-modal-x')
|
||||
}
|
||||
return (
|
||||
<SplitModal ref={initialFocus} onClose={dismiss}>
|
||||
<div>
|
||||
<div className="mb-10 flex items-center space-x-2 text-extendify-black">
|
||||
{brandLogo}
|
||||
</div>
|
||||
<h3 className="text-xl">
|
||||
{__(
|
||||
'Get the brand new Extendify plugin today!',
|
||||
'extendify',
|
||||
)}
|
||||
</h3>
|
||||
<p
|
||||
className="text-sm text-black"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: safeHTML(
|
||||
sprintf(
|
||||
__(
|
||||
'Install the new Extendify Library plugin to get the latest we have to offer — right from WordPress.org. Plus, well send you %1$s10 more imports%2$s. Nice.',
|
||||
'extendify',
|
||||
),
|
||||
'<strong>',
|
||||
'</strong>',
|
||||
),
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<button
|
||||
onClick={installAndActivate}
|
||||
ref={initialFocus}
|
||||
disabled={disabled}
|
||||
className="button-extendify-main button-focus mt-2 inline-flex justify-center px-4 py-3"
|
||||
style={{ minWidth: '225px' }}>
|
||||
{text}
|
||||
{success || (
|
||||
<Icon
|
||||
icon={download2}
|
||||
size={24}
|
||||
className="ml-2 w-6 flex-grow-0"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full justify-end rounded-tr-sm rounded-br-sm bg-extendify-secondary">
|
||||
<img
|
||||
alt={__('Upgrade Now', 'extendify')}
|
||||
className="roudned-br-sm max-w-full rounded-tr-sm"
|
||||
src={
|
||||
window.extendifyData.asset_path +
|
||||
'/modal-extendify-purple.png'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</SplitModal>
|
||||
)
|
||||
}
|
@@ -0,0 +1,75 @@
|
||||
import { Button } from '@wordpress/components'
|
||||
import { Fragment, useRef, forwardRef } from '@wordpress/element'
|
||||
import { __ } from '@wordpress/i18n'
|
||||
import { Icon, close } from '@wordpress/icons'
|
||||
import { Dialog, Transition } from '@headlessui/react'
|
||||
import { useGlobalStore } from '@extendify/state/GlobalState'
|
||||
|
||||
export const Modal = forwardRef(
|
||||
({ isOpen, heading, onClose, children }, initialFocus) => {
|
||||
const focusBackup = useRef(null)
|
||||
const defaultClose = useGlobalStore((state) => state.removeAllModals)
|
||||
onClose = onClose ?? defaultClose
|
||||
|
||||
return (
|
||||
<Transition
|
||||
appear
|
||||
show={isOpen}
|
||||
as={Fragment}
|
||||
className="extendify">
|
||||
<Dialog
|
||||
initialFocus={initialFocus ?? focusBackup}
|
||||
onClose={onClose}>
|
||||
<div className="fixed inset-0 z-high flex">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-200 transition"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100">
|
||||
<Dialog.Overlay className="fixed inset-0 bg-black bg-opacity-40" />
|
||||
</Transition.Child>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300 translate transform"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-5"
|
||||
enterTo="opacity-100 translate-y-0">
|
||||
<div className="relative m-auto w-full">
|
||||
<div className="relative m-auto w-full max-w-lg items-center justify-center rounded-sm bg-white shadow-modal">
|
||||
{heading ? (
|
||||
<div className="flex items-center justify-between border-b py-2 pl-6 pr-3 leading-none">
|
||||
<span className="whitespace-nowrap text-base text-extendify-black">
|
||||
{heading}
|
||||
</span>
|
||||
<CloseButton onClick={onClose} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="absolute top-0 right-0 block px-4 py-4 ">
|
||||
<CloseButton
|
||||
ref={focusBackup}
|
||||
onClick={onClose}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
const CloseButton = forwardRef((props, focusRef) => {
|
||||
return (
|
||||
<Button
|
||||
{...props}
|
||||
icon={<Icon icon={close} />}
|
||||
ref={focusRef}
|
||||
className="text-extendify-black opacity-75 hover:opacity-100"
|
||||
showTooltip={false}
|
||||
label={__('Close dialog', 'extendify')}
|
||||
/>
|
||||
)
|
||||
})
|
@@ -0,0 +1,104 @@
|
||||
import { Icon } from '@wordpress/components'
|
||||
import { Button } from '@wordpress/components'
|
||||
import { useRef } from '@wordpress/element'
|
||||
import { __ } from '@wordpress/i18n'
|
||||
import { General } from '@extendify/api/General'
|
||||
import { useGlobalStore } from '@extendify/state/GlobalState'
|
||||
import { useUserStore } from '@extendify/state/User'
|
||||
import {
|
||||
growthArrow,
|
||||
patterns,
|
||||
layouts,
|
||||
support,
|
||||
star,
|
||||
brandLogo,
|
||||
diamond,
|
||||
} from '../icons'
|
||||
import { SplitModal } from './SplitModal'
|
||||
import { SettingsModal } from './settings/SettingsModal'
|
||||
|
||||
export const NoImportModal = () => {
|
||||
const pushModal = useGlobalStore((state) => state.pushModal)
|
||||
const initialFocus = useRef(null)
|
||||
return (
|
||||
<SplitModal
|
||||
isOpen={true}
|
||||
ref={initialFocus}
|
||||
leftContainerBgColor="bg-white">
|
||||
<div>
|
||||
<div className="mb-5 flex items-center space-x-2 text-extendify-black">
|
||||
{brandLogo}
|
||||
</div>
|
||||
|
||||
<h3 className="mt-0 text-xl">
|
||||
{__("You're out of imports", 'extendify')}
|
||||
</h3>
|
||||
<p className="text-sm text-black">
|
||||
{__(
|
||||
'Sign up today and get unlimited access to our entire collection of patterns and page layouts.',
|
||||
'extendify',
|
||||
)}
|
||||
</p>
|
||||
<div>
|
||||
<a
|
||||
target="_blank"
|
||||
ref={initialFocus}
|
||||
className="button-extendify-main button-focus mt-2 inline-flex justify-center px-4 py-3"
|
||||
style={{ minWidth: '225px' }}
|
||||
href={`https://extendify.com/pricing/?utm_source=${
|
||||
window.extendifyData.sdk_partner
|
||||
}&utm_medium=library&utm_campaign=no-imports-modal&utm_content=get-unlimited-imports&utm_group=${useUserStore
|
||||
.getState()
|
||||
.activeTestGroupsUtmValue()}`}
|
||||
onClick={async () =>
|
||||
await General.ping('no-imports-modal-click')
|
||||
}
|
||||
rel="noreferrer">
|
||||
{__('Get Unlimited Imports', 'extendify')}
|
||||
<Icon icon={growthArrow} size={24} className="-mr-1" />
|
||||
</a>
|
||||
<p className="mb-0 text-left text-sm text-extendify-gray">
|
||||
{__('Have an account?', 'extendify')}
|
||||
<Button
|
||||
onClick={() => pushModal(<SettingsModal />)}
|
||||
className="pl-2 text-sm text-extendify-gray underline hover:no-underline">
|
||||
{__('Sign in', 'extendify')}
|
||||
</Button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-full flex-col justify-center space-y-2 p-10 text-black">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Icon icon={patterns} size={24} />
|
||||
<span className="text-sm leading-none">
|
||||
{__("Access to 100's of Patterns", 'extendify')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Icon icon={diamond} size={24} />
|
||||
<span className="text-sm leading-none">
|
||||
{__('Access to "Pro" catalog', 'extendify')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Icon icon={layouts} size={24} />
|
||||
<span className="text-sm leading-none">
|
||||
{__('Beautiful full page layouts', 'extendify')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Icon icon={support} size={24} />
|
||||
<span className="text-sm leading-none">
|
||||
{__('Fast and friendly support', 'extendify')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Icon icon={star} size={24} />
|
||||
<span className="text-sm leading-none">
|
||||
{__('14-Day guarantee', 'extendify')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</SplitModal>
|
||||
)
|
||||
}
|
@@ -0,0 +1,61 @@
|
||||
import { Icon } from '@wordpress/components'
|
||||
import { useRef } from '@wordpress/element'
|
||||
import { __ } from '@wordpress/i18n'
|
||||
import { General } from '@extendify/api/General'
|
||||
import { growthArrow, brandLogo } from '@extendify/components/icons'
|
||||
import { useUserStore } from '@extendify/state/User'
|
||||
import { SplitModal } from './SplitModal'
|
||||
|
||||
export const ProModal = () => {
|
||||
const initialFocus = useRef(null)
|
||||
return (
|
||||
<SplitModal isOpen={true} invertedButtonColor={true} ref={initialFocus}>
|
||||
<div>
|
||||
<div className="mb-5 flex items-center space-x-2 text-extendify-black">
|
||||
{brandLogo}
|
||||
</div>
|
||||
<h3 className="mt-0 text-xl">
|
||||
{__(
|
||||
'Get unlimited access to all our Pro patterns & layouts',
|
||||
'extendify',
|
||||
)}
|
||||
</h3>
|
||||
<p className="text-sm text-black">
|
||||
{__(
|
||||
"Upgrade to Extendify Pro and use all the patterns and layouts you'd like, including our exclusive Pro catalog.",
|
||||
'extendify',
|
||||
)}
|
||||
</p>
|
||||
<div>
|
||||
<a
|
||||
target="_blank"
|
||||
ref={initialFocus}
|
||||
className="button-extendify-main button-focus mt-2 inline-flex justify-center px-4 py-3"
|
||||
style={{ minWidth: '225px' }}
|
||||
href={`https://extendify.com/pricing/?utm_source=${
|
||||
window.extendifyData.sdk_partner
|
||||
}&utm_medium=library&utm_campaign=pro-modal&utm_content=upgrade-now&utm_group=${useUserStore
|
||||
.getState()
|
||||
.activeTestGroupsUtmValue()}`}
|
||||
onClick={async () =>
|
||||
await General.ping('pro-modal-click')
|
||||
}
|
||||
rel="noreferrer">
|
||||
{__('Upgrade Now', 'extendify')}
|
||||
<Icon icon={growthArrow} size={24} className="-mr-1" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="justify-endrounded-tr-sm flex w-full rounded-br-sm bg-black">
|
||||
<img
|
||||
alt={__('Upgrade Now', 'extendify')}
|
||||
className="max-w-full rounded-tr-sm rounded-br-sm"
|
||||
src={
|
||||
window.extendifyData.asset_path +
|
||||
'/modal-extendify-black.png'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</SplitModal>
|
||||
)
|
||||
}
|
@@ -0,0 +1,77 @@
|
||||
import { Fragment, forwardRef, useRef } from '@wordpress/element'
|
||||
import { __ } from '@wordpress/i18n'
|
||||
import { Icon, close } from '@wordpress/icons'
|
||||
import { Dialog, Transition } from '@headlessui/react'
|
||||
import { useGlobalStore } from '@extendify/state/GlobalState'
|
||||
|
||||
export const SplitModal = forwardRef(
|
||||
(
|
||||
{
|
||||
onClose,
|
||||
isOpen,
|
||||
invertedButtonColor,
|
||||
children,
|
||||
leftContainerBgColor = 'bg-white',
|
||||
rightContainerBgColor = 'bg-gray-100',
|
||||
},
|
||||
initialFocus,
|
||||
) => {
|
||||
const focusBackup = useRef(null)
|
||||
const defaultClose = useGlobalStore((state) => state.removeAllModals)
|
||||
onClose = onClose ?? defaultClose
|
||||
|
||||
return (
|
||||
<Transition.Root appear show={true} as={Fragment}>
|
||||
<Dialog
|
||||
as="div"
|
||||
static
|
||||
open={isOpen}
|
||||
className="extendify"
|
||||
initialFocus={initialFocus ?? focusBackup}
|
||||
onClose={onClose}>
|
||||
<div className="fixed inset-0 z-high flex">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-50 transition"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100">
|
||||
<Dialog.Overlay className="fixed inset-0 bg-black bg-opacity-40 transition-opacity" />
|
||||
</Transition.Child>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300 translate transform"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-5"
|
||||
enterTo="opacity-100 translate-y-0">
|
||||
<div className="m-auto">
|
||||
<div className="relative m-8 max-w-md justify-between rounded-sm shadow-modal md:m-0 md:flex md:max-w-2xl">
|
||||
<button
|
||||
onClick={onClose}
|
||||
ref={focusBackup}
|
||||
className="absolute top-0 right-0 block cursor-pointer rounded-md bg-transparent p-4 text-gray-700 opacity-30 hover:opacity-100"
|
||||
style={
|
||||
invertedButtonColor && {
|
||||
filter: 'invert(1)',
|
||||
}
|
||||
}>
|
||||
<span className="sr-only">
|
||||
{__('Close', 'extendify')}
|
||||
</span>
|
||||
<Icon icon={close} />
|
||||
</button>
|
||||
<div
|
||||
className={`w-7/12 p-12 ${leftContainerBgColor}`}>
|
||||
{children[0]}
|
||||
</div>
|
||||
<div
|
||||
className={`hidden w-6/12 md:block ${rightContainerBgColor}`}>
|
||||
{children[1]}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
)
|
||||
},
|
||||
)
|
@@ -0,0 +1,88 @@
|
||||
import { Button } from '@wordpress/components'
|
||||
import { useState } from '@wordpress/element'
|
||||
import { useIsDevMode } from '@extendify/hooks/helpers'
|
||||
import { useTaxonomyStore } from '@extendify/state/Taxonomies'
|
||||
import { useTemplatesStore } from '@extendify/state/Templates'
|
||||
import { useUserStore } from '@extendify/state/User'
|
||||
|
||||
export const DevSettings = () => {
|
||||
const [processing, setProcessing] = useState(false)
|
||||
const [canHydrate, setCanHydrate] = useState(false)
|
||||
const devMode = useIsDevMode()
|
||||
|
||||
const handleReset = async () => {
|
||||
if (processing) return
|
||||
setProcessing(true)
|
||||
if (canHydrate) {
|
||||
setCanHydrate(false)
|
||||
useUserStore.setState({
|
||||
participatingTestsGroups: [],
|
||||
})
|
||||
await useUserStore.persist.rehydrate()
|
||||
window.extendifyData._canRehydrate = false
|
||||
setProcessing(false)
|
||||
return
|
||||
}
|
||||
useUserStore.persist.clearStorage()
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
window.extendifyData._canRehydrate = true
|
||||
setCanHydrate(true)
|
||||
setProcessing(false)
|
||||
}
|
||||
|
||||
const handleServerSwitch = async () => {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
params.delete('LOCALMODE', 1)
|
||||
params[params.has('DEVMODE') || devMode ? 'delete' : 'append'](
|
||||
'DEVMODE',
|
||||
1,
|
||||
)
|
||||
window.history.replaceState(
|
||||
null,
|
||||
null,
|
||||
window.location.pathname + '?' + params.toString(),
|
||||
)
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
window.dispatchEvent(new Event('popstate'))
|
||||
useTemplatesStore.getState().resetTemplates()
|
||||
useTemplatesStore.getState().updateSearchParams({})
|
||||
useTaxonomyStore.persist.clearStorage()
|
||||
useTaxonomyStore.persist.rehydrate()
|
||||
useTemplatesStore.setState({
|
||||
taxonomyDefaultState: {},
|
||||
})
|
||||
useTaxonomyStore
|
||||
.getState()
|
||||
.fetchTaxonomies()
|
||||
.then(() => {
|
||||
useTemplatesStore.getState().setupDefaultTaxonomies()
|
||||
})
|
||||
}
|
||||
|
||||
if (!window.extendifyData.devbuild) return null
|
||||
|
||||
return (
|
||||
<section className="p-6 flex flex-col space-y-6 border-l-8 border-extendify-secondary">
|
||||
<div>
|
||||
<p className="text-base m-0 text-extendify-black">
|
||||
Development Settings
|
||||
</p>
|
||||
<p className="text-sm italic m-0 text-gray-500">
|
||||
Only available on dev builds
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<Button isSecondary onClick={handleServerSwitch}>
|
||||
Switch to {devMode ? 'Live' : 'Dev'} Server
|
||||
</Button>
|
||||
<Button isSecondary onClick={handleReset}>
|
||||
{processing
|
||||
? 'Processing...'
|
||||
: canHydrate
|
||||
? 'OK! Press to rehydrate app'
|
||||
: 'Reset User Data'}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
@@ -0,0 +1,240 @@
|
||||
import { Spinner, Button } from '@wordpress/components'
|
||||
import { useState, useEffect, useRef } from '@wordpress/element'
|
||||
import { __ } from '@wordpress/i18n'
|
||||
import { Icon } from '@wordpress/icons'
|
||||
import classNames from 'classnames'
|
||||
import { General } from '@extendify/api/General'
|
||||
import { User as UserApi } from '@extendify/api/User'
|
||||
import { useIsDevMode } from '@extendify/hooks/helpers'
|
||||
import { useUserStore } from '@extendify/state/User'
|
||||
import { user } from '../../icons'
|
||||
import { success as successIcon } from '../../icons'
|
||||
|
||||
export default function LoginInterface({ actionCallback, initialFocus }) {
|
||||
const loggedIn = useUserStore((state) => state.apiKey.length)
|
||||
const [email, setEmail] = useState('')
|
||||
const [apiKey, setApiKey] = useState('')
|
||||
const [feedback, setFeedback] = useState('')
|
||||
const [feedbackType, setFeedbackType] = useState('info')
|
||||
const [isWorking, setIsWorking] = useState(false)
|
||||
const [success, setSuccess] = useState(false)
|
||||
const viewPatternsButtonRef = useRef(null)
|
||||
const licenseKeyRef = useRef(null)
|
||||
const devMode = useIsDevMode()
|
||||
|
||||
useEffect(() => {
|
||||
setEmail(useUserStore.getState().email)
|
||||
// This will reset the default error state to info
|
||||
return () => setFeedbackType('info')
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
success && viewPatternsButtonRef?.current?.focus()
|
||||
}, [success])
|
||||
|
||||
const logout = () => {
|
||||
setApiKey('')
|
||||
useUserStore.setState({ apiKey: '' })
|
||||
setTimeout(() => {
|
||||
licenseKeyRef?.current?.focus()
|
||||
}, 0)
|
||||
}
|
||||
|
||||
const confirmKey = async (event) => {
|
||||
event.preventDefault()
|
||||
setIsWorking(true)
|
||||
setFeedback('')
|
||||
const { token, error, exception, message } = await UserApi.authenticate(
|
||||
email,
|
||||
apiKey,
|
||||
)
|
||||
|
||||
if (typeof message !== 'undefined') {
|
||||
setFeedbackType('error')
|
||||
setIsWorking(false)
|
||||
setFeedback(
|
||||
message?.length
|
||||
? message
|
||||
: 'Error: Are you interacting with the wrong server?',
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (error || exception) {
|
||||
setFeedbackType('error')
|
||||
setIsWorking(false)
|
||||
setFeedback(error?.length ? error : exception)
|
||||
return
|
||||
}
|
||||
|
||||
if (!token || typeof token !== 'string') {
|
||||
setFeedbackType('error')
|
||||
setIsWorking(false)
|
||||
setFeedback(__('Something went wrong', 'extendify'))
|
||||
return
|
||||
}
|
||||
|
||||
setFeedbackType('success')
|
||||
setFeedback('Success!')
|
||||
setSuccess(true)
|
||||
setIsWorking(false)
|
||||
useUserStore.setState({
|
||||
email: email,
|
||||
apiKey: token,
|
||||
})
|
||||
}
|
||||
|
||||
if (success) {
|
||||
return (
|
||||
<section className="space-y-6 p-6 text-center flex flex-col items-center">
|
||||
<Icon icon={successIcon} size={148} />
|
||||
<p className="text-center text-lg font-semibold m-0 text-extendify-black">
|
||||
{__("You've signed in to Extendify", 'extendify')}
|
||||
</p>
|
||||
<Button
|
||||
ref={viewPatternsButtonRef}
|
||||
className="cursor-pointer rounded bg-extendify-main p-2 px-4 text-center text-white"
|
||||
onClick={actionCallback}>
|
||||
{__('View patterns', 'extendify')}
|
||||
</Button>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
if (loggedIn) {
|
||||
return (
|
||||
<section className="w-full space-y-6 p-6">
|
||||
<p className="text-base m-0 text-extendify-black">
|
||||
{__('Account', 'extendify')}
|
||||
</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="-ml-2 flex items-center space-x-2">
|
||||
<Icon icon={user} size={48} />
|
||||
<p className="text-extendify-black">
|
||||
{email?.length
|
||||
? email
|
||||
: __('Logged In', 'extendify')}
|
||||
</p>
|
||||
</div>
|
||||
{devMode && (
|
||||
<Button
|
||||
className="cursor-pointer rounded bg-extendify-main px-4 py-3 text-center text-white hover:bg-extendify-main-dark"
|
||||
onClick={logout}>
|
||||
{__('Sign out', 'extendify')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="space-y-6 p-6 text-left">
|
||||
<div>
|
||||
<p className="text-center text-lg font-semibold m-0 text-extendify-black">
|
||||
{__('Sign in to Extendify', 'extendify')}
|
||||
</p>
|
||||
<p className="space-x-1 text-center text-sm m-0 text-extendify-gray">
|
||||
<span>{__("Don't have an account?", 'extendify')}</span>
|
||||
<a
|
||||
href={`https://extendify.com/pricing?utm_source=${
|
||||
window.extendifyData.sdk_partner
|
||||
}&utm_medium=library&utm_campaign=sign-in-form&utm_content=sign-up&utm_group=${useUserStore
|
||||
.getState()
|
||||
.activeTestGroupsUtmValue()}`}
|
||||
target="_blank"
|
||||
onClick={async () =>
|
||||
await General.ping(
|
||||
'sign-up-link-from-login-modal-click',
|
||||
)
|
||||
}
|
||||
className="underline hover:no-underline text-extendify-gray"
|
||||
rel="noreferrer">
|
||||
{__('Sign up', 'extendify')}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<form
|
||||
onSubmit={confirmKey}
|
||||
className="flex flex-col items-center justify-center space-y-2">
|
||||
<div className="flex items-center">
|
||||
<label className="sr-only" htmlFor="extendify-login-email">
|
||||
{__('Email address', 'extendify')}
|
||||
</label>
|
||||
<input
|
||||
ref={initialFocus}
|
||||
id="extendify-login-email"
|
||||
name="extendify-login-email"
|
||||
style={{ minWidth: '320px' }}
|
||||
type="email"
|
||||
className="w-full rounded border-2 p-2"
|
||||
placeholder={__('Email address', 'extendify')}
|
||||
value={email.length ? email : ''}
|
||||
onChange={(event) => setEmail(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<label
|
||||
className="sr-only"
|
||||
htmlFor="extendify-login-license">
|
||||
{__('License key', 'extendify')}
|
||||
</label>
|
||||
<input
|
||||
ref={licenseKeyRef}
|
||||
id="extendify-login-license"
|
||||
name="extendify-login-license"
|
||||
style={{ minWidth: '320px' }}
|
||||
type="text"
|
||||
className="w-full rounded border-2 p-2"
|
||||
placeholder={__('License key', 'extendify')}
|
||||
value={apiKey}
|
||||
onChange={(event) => setApiKey(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-center pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
className="relative flex w-72 max-w-full cursor-pointer justify-center rounded bg-extendify-main p-2 py-3 text-center text-base text-white hover:bg-extendify-main-dark ">
|
||||
<span>{__('Sign In', 'extendify')}</span>
|
||||
{isWorking && (
|
||||
<div className="absolute right-2.5">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{feedback && (
|
||||
<div
|
||||
className={classNames({
|
||||
'border-gray-900 text-gray-900':
|
||||
feedbackType === 'info',
|
||||
'border-wp-alert-red text-wp-alert-red':
|
||||
feedbackType === 'error',
|
||||
'border-extendify-main text-extendify-main':
|
||||
feedbackType === 'success',
|
||||
})}>
|
||||
{feedback}
|
||||
</div>
|
||||
)}
|
||||
<div className="pt-4 text-center">
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href={`https://extendify.com/guides/sign-in?utm_source=${
|
||||
window.extendifyData.sdk_partner
|
||||
}&utm_medium=library&utm_campaign=sign-in-form&utm_content=need-help&utm_group=${useUserStore
|
||||
.getState()
|
||||
.activeTestGroupsUtmValue()}`}
|
||||
onClick={async () =>
|
||||
await General.ping(
|
||||
'need-help-link-from-login-modal-click',
|
||||
)
|
||||
}
|
||||
className="underline hover:no-underline text-sm text-extendify-gray">
|
||||
{__('Need Help?', 'extendify')}
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
)
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
import { useRef } from '@wordpress/element'
|
||||
import { __ } from '@wordpress/i18n'
|
||||
import { useGlobalStore } from '@extendify/state/GlobalState'
|
||||
import { Modal } from '../Modal'
|
||||
import { DevSettings } from './DevSettings'
|
||||
import LoginInterface from './LoginInterface'
|
||||
|
||||
export const SettingsModal = () => {
|
||||
const initialFocus = useRef(null)
|
||||
const actionCallback = useGlobalStore((state) => state.removeAllModals)
|
||||
|
||||
return (
|
||||
<Modal
|
||||
heading={__('Settings', 'extendify')}
|
||||
isOpen={true}
|
||||
ref={initialFocus}>
|
||||
<div className="flex justify-center flex-col divide-y">
|
||||
<DevSettings />
|
||||
<LoginInterface
|
||||
initialFocus={initialFocus}
|
||||
actionCallback={actionCallback}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
@@ -0,0 +1,36 @@
|
||||
import { Button } from '@wordpress/components'
|
||||
import { __ } from '@wordpress/i18n'
|
||||
import { General } from '@extendify/api/General'
|
||||
import { useUserStore } from '@extendify/state/User'
|
||||
|
||||
export default function FeedbackNotice() {
|
||||
return (
|
||||
<>
|
||||
<span className="text-black">
|
||||
{__(
|
||||
'Tell us how to make the Extendify Library work better for you',
|
||||
'extendify',
|
||||
)}
|
||||
</span>
|
||||
<span className="px-2 opacity-50" aria-hidden="true">
|
||||
|
|
||||
</span>
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<Button
|
||||
variant="link"
|
||||
className="h-auto p-0 text-black underline hover:no-underline"
|
||||
href={`https://extendify.com/feedback/?utm_source=${
|
||||
window.extendifyData.sdk_partner
|
||||
}&utm_medium=library&utm_campaign=feedback-notice&utm_content=give-feedback&utm_group=${useUserStore
|
||||
.getState()
|
||||
.activeTestGroupsUtmValue()}`}
|
||||
onClick={async () =>
|
||||
await General.ping('feedback-notice-click')
|
||||
}
|
||||
target="_blank">
|
||||
{__('Give feedback', 'extendify')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
@@ -0,0 +1,109 @@
|
||||
import { Button } from '@wordpress/components'
|
||||
import { useState, useEffect, useRef } from '@wordpress/element'
|
||||
import { __ } from '@wordpress/i18n'
|
||||
import { Icon, closeSmall } from '@wordpress/icons'
|
||||
import { General } from '@extendify/api/General'
|
||||
import { useGlobalStore } from '@extendify/state/GlobalState'
|
||||
import { useUserStore } from '@extendify/state/User'
|
||||
import FeedbackNotice from './FeedbackNotice'
|
||||
import { InstallStandaloneNotice } from './InstallStandaloneNotice'
|
||||
import PromotionNotice from './PromotionNotice'
|
||||
|
||||
// import WelcomeNotice from './WelcomeNotice'
|
||||
|
||||
const NoticesByPriority = {
|
||||
// welcome: WelcomeNotice,
|
||||
promotion: PromotionNotice,
|
||||
feedback: FeedbackNotice,
|
||||
standalone: InstallStandaloneNotice,
|
||||
}
|
||||
|
||||
export default function FooterNotice({ className = '' }) {
|
||||
const [hasNotice, setHasNotice] = useState(null)
|
||||
const once = useRef(false)
|
||||
const promotionData = useGlobalStore(
|
||||
(state) => state.metaData?.banners?.footer,
|
||||
)
|
||||
|
||||
const showFeedback = () => {
|
||||
const imports = useUserStore.getState().imports ?? 0
|
||||
const firstLoadedOn =
|
||||
useUserStore.getState()?.firstLoadedOn ?? new Date()
|
||||
const timeDifference =
|
||||
new Date().getTime() - new Date(firstLoadedOn).getTime()
|
||||
const daysSinceActivated = timeDifference / 86_400_000 // 24 hours
|
||||
|
||||
return imports >= 3 && daysSinceActivated > 3
|
||||
}
|
||||
|
||||
// Find the first notice key to use
|
||||
// TODO: extract this logic into the individual component instead of controlling it here
|
||||
const componentKey =
|
||||
Object.keys(NoticesByPriority).find((key) => {
|
||||
if (key === 'promotion') {
|
||||
return (
|
||||
// When checking promotions, use the key sent from the server
|
||||
// to check whether it's been dismissed
|
||||
promotionData?.key &&
|
||||
!useUserStore.getState().noticesDismissedAt[
|
||||
promotionData.key
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
if (key === 'feedback') {
|
||||
return (
|
||||
showFeedback() &&
|
||||
!useUserStore.getState().noticesDismissedAt[key]
|
||||
)
|
||||
}
|
||||
|
||||
if (key === 'standalone') {
|
||||
return (
|
||||
!window.extendifyData.standalone &&
|
||||
!useUserStore.getState().noticesDismissedAt[key]
|
||||
)
|
||||
}
|
||||
|
||||
return !useUserStore.getState().noticesDismissedAt[key]
|
||||
}) ?? null
|
||||
const Notice = NoticesByPriority[componentKey]
|
||||
|
||||
const dismiss = async () => {
|
||||
setHasNotice(false)
|
||||
// The noticesDismissedAt key will either be the key from NoticesByPriority,
|
||||
// or a key passed in from the server, such as 'holiday-sale2077'
|
||||
const key =
|
||||
componentKey === 'promotion' ? promotionData.key : componentKey
|
||||
useUserStore.getState().markNoticeSeen(key, 'notices')
|
||||
await General.ping(`footer-notice-x-${key}`)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// Only show the notice once on main render and only if a notice exists.
|
||||
if (NoticesByPriority[componentKey] && !once.current) {
|
||||
setHasNotice(true)
|
||||
once.current = true
|
||||
}
|
||||
}, [componentKey])
|
||||
|
||||
if (!hasNotice) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={`${className} relative mx-auto hidden max-w-screen-4xl items-center justify-center space-x-4 bg-extendify-secondary py-3 px-5 lg:flex`}>
|
||||
{/* Pass all data to all components and let them decide what they use */}
|
||||
<Notice promotionData={promotionData} />
|
||||
<div className="absolute right-1">
|
||||
<Button
|
||||
className="text-extendify-black opacity-50 hover:opacity-100 focus:opacity-100"
|
||||
icon={<Icon icon={closeSmall} />}
|
||||
label={__('Dismiss this notice', 'extendify')}
|
||||
onClick={dismiss}
|
||||
showTooltip={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@@ -0,0 +1,66 @@
|
||||
import { Button } from '@wordpress/components'
|
||||
import { useState } from '@wordpress/element'
|
||||
import { __ } from '@wordpress/i18n'
|
||||
import classNames from 'classnames'
|
||||
import { General } from '@extendify/api/General'
|
||||
import { Plugins } from '@extendify/api/Plugins'
|
||||
import { useUserStore } from '@extendify/state/User'
|
||||
|
||||
export const InstallStandaloneNotice = () => {
|
||||
const [text, setText] = useState('')
|
||||
const giveFreebieImports = useUserStore((state) => state.giveFreebieImports)
|
||||
const installAndActivate = () => {
|
||||
setText(__('Installing...', 'extendify'))
|
||||
Promise.all([
|
||||
General.ping('stln-footer-install'),
|
||||
Plugins.installAndActivate(['extendify']),
|
||||
new Promise((resolve) => setTimeout(resolve, 1000)),
|
||||
])
|
||||
.then(async () => {
|
||||
giveFreebieImports(10)
|
||||
setText(__('Success! Reloading...', 'extendify'))
|
||||
await General.ping('stln-footer-success')
|
||||
window.location.reload()
|
||||
})
|
||||
.catch(async (error) => {
|
||||
console.error(error)
|
||||
setText(__('Error. See console.', 'extendify'))
|
||||
await General.ping('stln-footer-fail')
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span className="text-black">
|
||||
{__(
|
||||
'Install the new Extendify Library plugin to get the latest we have to offer',
|
||||
'extendify',
|
||||
)}
|
||||
</span>
|
||||
<span className="px-2 opacity-50" aria-hidden="true">
|
||||
|
|
||||
</span>
|
||||
<div className="relative inline-flex items-center space-x-2">
|
||||
<Button
|
||||
variant="link"
|
||||
className={classNames(
|
||||
'h-auto p-0 text-black underline hover:no-underline',
|
||||
{ 'opacity-0': text },
|
||||
)}
|
||||
onClick={installAndActivate}>
|
||||
{__('Install Extendify standalone plugin', 'extendify')}
|
||||
</Button>
|
||||
{/* Little hacky to keep the text in place. Might need to tweak this */}
|
||||
{text ? (
|
||||
<Button
|
||||
variant="link"
|
||||
disabled={true}
|
||||
className="absolute left-0 h-auto p-0 text-black underline opacity-100 hover:no-underline"
|
||||
onClick={() => {}}>
|
||||
{text}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@@ -0,0 +1,32 @@
|
||||
import { Button } from '@wordpress/components'
|
||||
import { useUserStore } from '@extendify/state/User'
|
||||
import { General } from '../../api/General'
|
||||
|
||||
export default function PromotionNotice({ promotionData }) {
|
||||
return (
|
||||
<>
|
||||
<span className="text-black">{promotionData?.text ?? ''}</span>
|
||||
<span className="px-2 opacity-50" aria-hidden="true">
|
||||
|
|
||||
</span>
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
{promotionData?.url && (
|
||||
<Button
|
||||
variant="link"
|
||||
className="h-auto p-0 text-black underline hover:no-underline"
|
||||
href={`${promotionData.url}&utm_source=${
|
||||
window.extendifyData.sdk_partner
|
||||
}&utm_group=${useUserStore
|
||||
.getState()
|
||||
.activeTestGroupsUtmValue()}`}
|
||||
onClick={async () =>
|
||||
await General.ping('promotion-notice-click')
|
||||
}
|
||||
target="_blank">
|
||||
{promotionData?.button_text}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
@@ -0,0 +1,58 @@
|
||||
import { Button } from '@wordpress/components'
|
||||
import { __ } from '@wordpress/i18n'
|
||||
import { General } from '@extendify/api/General'
|
||||
import { useGlobalStore } from '@extendify/state/GlobalState'
|
||||
import { useUserStore } from '@extendify/state/User'
|
||||
|
||||
export default function WelcomeNotice() {
|
||||
const setOpen = useGlobalStore((state) => state.setOpen)
|
||||
|
||||
const disableLibrary = () => {
|
||||
const button = document.getElementById(
|
||||
'extendify-templates-inserter-btn',
|
||||
)
|
||||
button.classList.add('invisible')
|
||||
useUserStore.setState({ enabled: false })
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className="text-black">
|
||||
{__('Welcome to the Extendify Library', 'extendify')}
|
||||
</span>
|
||||
<span className="px-2 opacity-50" aria-hidden="true">
|
||||
|
|
||||
</span>
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<Button
|
||||
variant="link"
|
||||
className="h-auto p-0 text-black underline hover:no-underline"
|
||||
href={`https://extendify.com/welcome/?utm_source=${
|
||||
window.extendifyData.sdk_partner
|
||||
}&utm_medium=library&utm_campaign=welcome-notice&utm_content=tell-me-more&utm_group=${useUserStore
|
||||
.getState()
|
||||
.activeTestGroupsUtmValue()}`}
|
||||
onClick={async () =>
|
||||
await General.ping('welcome-notice-tell-me-more-click')
|
||||
}
|
||||
target="_blank">
|
||||
{__('Tell me more', 'extendify')}
|
||||
</Button>
|
||||
{window.extendifyData.standalone ? null : (
|
||||
<>
|
||||
<span className="font-bold" aria-hidden="true">
|
||||
•
|
||||
</span>
|
||||
<Button
|
||||
variant="link"
|
||||
className="h-auto p-0 text-black underline hover:no-underline"
|
||||
onClick={disableLibrary}>
|
||||
{__('Turn off the library', 'extendify')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
@@ -0,0 +1,74 @@
|
||||
import { Button, Popover } from '@wordpress/components'
|
||||
import { safeHTML } from '@wordpress/dom'
|
||||
import { __, sprintf } from '@wordpress/i18n'
|
||||
import { Icon, close } from '@wordpress/icons'
|
||||
|
||||
export const NewImportsPopover = ({
|
||||
anchorRef,
|
||||
onPressX,
|
||||
onClick,
|
||||
onClickOutside,
|
||||
}) => {
|
||||
if (!anchorRef.current) return null
|
||||
return (
|
||||
<Popover
|
||||
anchorRef={anchorRef.current}
|
||||
shouldAnchorIncludePadding={true}
|
||||
className="extendify-tooltip-default"
|
||||
focusOnMount={false}
|
||||
onFocusOutside={onClickOutside}
|
||||
onClick={onClick}
|
||||
position="bottom center"
|
||||
noArrow={false}>
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: '0.5rem',
|
||||
}}>
|
||||
<span
|
||||
style={{
|
||||
textTransform: 'uppercase',
|
||||
color: '#8b8b8b',
|
||||
}}>
|
||||
{__('Monthly Imports', 'extendify')}
|
||||
</span>
|
||||
<Button
|
||||
style={{
|
||||
color: 'white',
|
||||
position: 'relative',
|
||||
right: '-5px',
|
||||
padding: '0',
|
||||
minWidth: '0',
|
||||
height: '20px',
|
||||
width: '20px',
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
onPressX()
|
||||
}}
|
||||
icon={<Icon icon={close} size={12} />}
|
||||
showTooltip={false}
|
||||
label={__('Close callout', 'extendify')}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: safeHTML(
|
||||
sprintf(
|
||||
__(
|
||||
"%1$sGood news!%2$s We've added more imports to your library. Enjoy!",
|
||||
'extendify',
|
||||
),
|
||||
'<strong>',
|
||||
'</strong>',
|
||||
),
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
</Popover>
|
||||
)
|
||||
}
|
@@ -0,0 +1,85 @@
|
||||
import { useRef, useEffect, useState } from '@wordpress/element'
|
||||
|
||||
export function useIsMounted() {
|
||||
const isMounted = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
isMounted.current = true
|
||||
return () => (isMounted.current = false)
|
||||
})
|
||||
return isMounted
|
||||
}
|
||||
|
||||
export const useIsDevMode = () => {
|
||||
const [devMode, setDevMode] = useState(false)
|
||||
const check = () => {
|
||||
return (
|
||||
window.location.search.indexOf('DEVMODE') > -1 ||
|
||||
window.location.search.indexOf('LOCALMODE') > -1
|
||||
)
|
||||
}
|
||||
useEffect(() => {
|
||||
const handle = () => setDevMode(check())
|
||||
handle()
|
||||
window.addEventListener('popstate', handle)
|
||||
return () => {
|
||||
window.removeEventListener('popstate', handle)
|
||||
}
|
||||
}, [])
|
||||
return devMode
|
||||
}
|
||||
|
||||
export const useWhenIdle = (time) => {
|
||||
const [userInteracted, setUserInteracted] = useState(true)
|
||||
const [idle, setIdle] = useState(false)
|
||||
const isMounted = useIsMounted()
|
||||
const timerId = useRef()
|
||||
|
||||
useEffect(() => {
|
||||
const handleMovement = () => setUserInteracted(true)
|
||||
const passive = { passive: true }
|
||||
window.addEventListener('keydown', handleMovement, passive)
|
||||
window.addEventListener('mousemove', handleMovement, passive)
|
||||
window.addEventListener('touchmove', handleMovement, passive)
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleMovement)
|
||||
window.removeEventListener('mousemove', handleMovement)
|
||||
window.removeEventListener('touchmove', handleMovement)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!userInteracted) return
|
||||
setUserInteracted(false)
|
||||
setIdle(false)
|
||||
window.clearTimeout(timerId.current)
|
||||
timerId.current = window.setTimeout(() => {
|
||||
isMounted.current && setIdle(true)
|
||||
}, time)
|
||||
}, [userInteracted, time, isMounted])
|
||||
|
||||
return idle
|
||||
}
|
||||
|
||||
/** Dev debugging tool to identify leaky renders: https://usehooks.com/useWhyDidYouUpdate/ */
|
||||
export const useWhyDidYouUpdate = (name, props) => {
|
||||
const previousProps = useRef()
|
||||
useEffect(() => {
|
||||
if (previousProps.current) {
|
||||
const allKeys = Object.keys({ ...previousProps.current, ...props })
|
||||
const changesObj = {}
|
||||
allKeys.forEach((key) => {
|
||||
if (previousProps.current[key] !== props[key]) {
|
||||
changesObj[key] = {
|
||||
from: previousProps.current[key],
|
||||
to: props[key],
|
||||
}
|
||||
}
|
||||
})
|
||||
if (Object.keys(changesObj).length) {
|
||||
console.log('[why-did-you-update]', name, changesObj)
|
||||
}
|
||||
}
|
||||
previousProps.current = props
|
||||
})
|
||||
}
|
@@ -0,0 +1,48 @@
|
||||
import { useEffect, useState } from '@wordpress/element'
|
||||
import { InstallStandaloneModal } from '@extendify/components/modals/InstallStandaloneModal'
|
||||
import { useGlobalStore } from '@extendify/state/GlobalState'
|
||||
import { useUserStore } from '@extendify/state/User'
|
||||
|
||||
/** Return any pending modals and check if any need to show */
|
||||
export const useModal = () => {
|
||||
const [modal, setModal] = useState(null)
|
||||
const open = useGlobalStore((state) => state.open)
|
||||
const pushModal = useGlobalStore((state) => state.pushModal)
|
||||
const removeAllModals = useGlobalStore((state) => state.removeAllModals)
|
||||
|
||||
// Watches modals added anywhere
|
||||
useEffect(
|
||||
() =>
|
||||
useGlobalStore.subscribe(
|
||||
(state) => state.modals,
|
||||
(value) => setModal(value?.length > 0 ? value[0] : null),
|
||||
),
|
||||
[],
|
||||
)
|
||||
|
||||
// Checks for modals that need to be shown on load
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
removeAllModals()
|
||||
return
|
||||
}
|
||||
const ModalNoticesByPriority = {
|
||||
standalone: InstallStandaloneModal,
|
||||
}
|
||||
const componentKey =
|
||||
Object.keys(ModalNoticesByPriority).find((key) => {
|
||||
if (key === 'standalone') {
|
||||
return (
|
||||
!window.extendifyData.standalone &&
|
||||
!useUserStore.getState().modalNoticesDismissedAt[key]
|
||||
)
|
||||
}
|
||||
return !useUserStore.getState().modalNoticesDismissedAt[key]
|
||||
}) ?? null
|
||||
|
||||
const Modal = ModalNoticesByPriority[componentKey]
|
||||
if (Modal) pushModal(<Modal />)
|
||||
}, [open, pushModal, removeAllModals])
|
||||
|
||||
return modal
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
import { useState, useLayoutEffect } from '@wordpress/element'
|
||||
import { useGlobalStore } from '@extendify/state/GlobalState'
|
||||
import { useUserStore as user } from '@extendify/state/User'
|
||||
|
||||
export const useTestGroup = (key, options, override) => {
|
||||
const [group, setGroup] = useState()
|
||||
const ready = useGlobalStore((state) => state.ready)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (
|
||||
override ||
|
||||
(ready && !group) ||
|
||||
// Let the devbuild reset this
|
||||
window.extendifyData._canRehydrate
|
||||
) {
|
||||
const testGroup = user.getState().testGroup(key, options)
|
||||
setGroup(testGroup)
|
||||
}
|
||||
}, [key, options, group, ready, override])
|
||||
|
||||
return group
|
||||
}
|
@@ -0,0 +1,4 @@
|
||||
import { softErrorHandler } from './softerror-encountered'
|
||||
import { templateHandler } from './template-inserted'
|
||||
|
||||
;[templateHandler, softErrorHandler].forEach((listener) => listener.register())
|
@@ -0,0 +1,24 @@
|
||||
import { render } from '@wordpress/element'
|
||||
import { camelCase } from 'lodash'
|
||||
import RequiredPluginsModal from '@extendify/middleware/hasRequiredPlugins/RequiredPluginsModal'
|
||||
|
||||
// use this to trigger an error from outside the application
|
||||
export const softErrorHandler = {
|
||||
register() {
|
||||
window.addEventListener('extendify::softerror-encountered', (event) => {
|
||||
this[camelCase(event.detail.type)](event.detail)
|
||||
})
|
||||
},
|
||||
versionOutdated(error) {
|
||||
render(
|
||||
<RequiredPluginsModal
|
||||
title={error.data.title}
|
||||
requiredPlugins={['extendify']}
|
||||
message={error.data.message}
|
||||
buttonLabel={error.data.buttonLabel}
|
||||
forceOpen={true}
|
||||
/>,
|
||||
document.getElementById('extendify-root'),
|
||||
)
|
||||
},
|
||||
}
|
@@ -0,0 +1,24 @@
|
||||
import { dispatch } from '@wordpress/data'
|
||||
import { __ } from '@wordpress/i18n'
|
||||
import { Templates } from '@extendify/api/Templates'
|
||||
import { useUserStore } from '@extendify/state/User'
|
||||
|
||||
// This fires after a template is inserted
|
||||
export const templateHandler = {
|
||||
register() {
|
||||
const { createNotice } = dispatch('core/notices')
|
||||
const increaseImports = useUserStore.getState().incrementImports
|
||||
window.addEventListener('extendify::template-inserted', (event) => {
|
||||
createNotice('info', __('Page layout added'), {
|
||||
isDismissible: true,
|
||||
type: 'snackbar',
|
||||
})
|
||||
// This is put off to the stack in attempt to fix a bug where
|
||||
// some users are having their imports go from 3->0 in an instant
|
||||
setTimeout(() => {
|
||||
increaseImports()
|
||||
Templates.import(event.detail?.template)
|
||||
}, 0)
|
||||
})
|
||||
},
|
||||
}
|
@@ -0,0 +1,65 @@
|
||||
import { Modal, Button } from '@wordpress/components'
|
||||
import { render } from '@wordpress/element'
|
||||
import { __, sprintf } from '@wordpress/i18n'
|
||||
import ExtendifyLibrary from '@extendify/ExtendifyLibrary'
|
||||
import { useWantedTemplateStore } from '@extendify/state/Importing'
|
||||
import { getPluginDescription } from '@extendify/util/general'
|
||||
|
||||
export default function NeedsPermissionModal() {
|
||||
const wantedTemplate = useWantedTemplateStore(
|
||||
(store) => store.wantedTemplate,
|
||||
)
|
||||
const closeModal = () =>
|
||||
render(
|
||||
<ExtendifyLibrary show={true} />,
|
||||
document.getElementById('extendify-root'),
|
||||
)
|
||||
const requiredPlugins = wantedTemplate?.fields?.required_plugins || []
|
||||
return (
|
||||
<Modal
|
||||
title={__('Plugins required', 'extendify')}
|
||||
isDismissible={false}>
|
||||
<p
|
||||
style={{
|
||||
maxWidth: '400px',
|
||||
}}>
|
||||
{sprintf(
|
||||
__(
|
||||
'In order to add this %s to your site, the following plugins are required to be installed and activated.',
|
||||
'extendify',
|
||||
),
|
||||
wantedTemplate?.fields?.type ?? 'template',
|
||||
)}
|
||||
</p>
|
||||
<ul>
|
||||
{
|
||||
// Hardcoded temporarily to not force EP install
|
||||
// requiredPlugins.map((plugin) =>
|
||||
requiredPlugins
|
||||
.filter((p) => p !== 'editorplus')
|
||||
.map((plugin) => (
|
||||
<li key={plugin}>{getPluginDescription(plugin)}</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
<p
|
||||
style={{
|
||||
maxWidth: '400px',
|
||||
fontWeight: 'bold',
|
||||
}}>
|
||||
{__(
|
||||
'Please contact a site admin for assistance in adding these plugins to your site.',
|
||||
'extendify',
|
||||
)}
|
||||
</p>
|
||||
<Button
|
||||
isPrimary
|
||||
onClick={closeModal}
|
||||
style={{
|
||||
boxShadow: 'none',
|
||||
}}>
|
||||
{__('Return to library', 'extendify')}
|
||||
</Button>
|
||||
</Modal>
|
||||
)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user