PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / 3.8.0
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! v3.8.0
3.8.0 3.7.5 3.7.4 3.7.3 3.7.2 1-final 3.7.1 3.7.0 3.6.8 3.6.7 3.6.6 3.6.5 3.6.4 3.6.3 3.6.2 3.6.1 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.1 All 112 releases
templately / includes / Core / Admin.php

Admin.php in Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! 3.8.0, at includes/Core/Admin.php

700 lines 30.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Templately\Core;
3
4 use Exception;
5 use PriyoMukul\WPNotice\Notices;
6 use PriyoMukul\WPNotice\Utils\CacheBank;
7 use PriyoMukul\WPNotice\Utils\NoticeRemover;
8 use Templately\Modules\Auth\REST\Login;
9 use Templately\Modules\FullSiteImport\FullSiteImport;
10 use Templately\Admin\Roles;
11 use Templately\Core\Capabilities;
12 use Templately\Utils\Base;
13 use Templately\Utils\Helper;
14 use Templately\Utils\Options;
15 use Templately\Modules\ElementorIntegration\Platform\Elementor;
16 use Templately\Modules\GutenbergIntegration\Platform\Gutenberg;
17
18 class Admin extends Base {
19
20 /**
21 * @var CacheBank
22 */
23 private static $cache_bank;
24
25 /**
26 * Initially invoked function.
27 * Menu, Assets and maybe redirect on plugin activation is initialized.
28 */
29 public function __construct() {
30 add_action( 'admin_enqueue_scripts', [ $this, 'scripts' ] );
31 add_action( 'load-toplevel_page_templately', [ $this, 'before_scripts' ] );
32 add_action( 'admin_menu', [ $this, 'admin_menu' ] );
33
34 self::$cache_bank = CacheBank::get_instance();
35
36 try {
37 add_action( 'admin_init', [ $this, 'notices' ], 11 );
38 } catch ( Exception $e ) {
39 unset( $e );
40 }
41
42 add_filter( 'plugin_action_links_' . TEMPLATELY_PLUGIN_BASENAME, [$this, 'handleActionLinks'], 10, 2 );
43
44 // Post-import feedback mount point moved to
45 // modules/post-import-feedback/Widget.php::render_mount_point() (spec 027) —
46 // hooked independently on admin_footer.
47
48 // Remove OLD notice from 1.0.0 (if other WPDeveloper plugin has notice)
49 NoticeRemover::get_instance( '1.0.0' );
50 }
51
52 public function register_post_type() {
53 $labels = [
54 'name' => _x( 'Theme Builders', 'Post type general name', 'templately' ),
55 'singular_name' => _x( 'Theme Builder', 'Post type singular name', 'templately' ),
56 'menu_name' => _x( 'Theme Builder', 'Admin Menu text', 'templately' ),
57 'name_admin_bar' => _x( 'Book', 'Add New on Toolbar', 'templately' ),
58 'add_new' => __( 'Add New', 'templately' ),
59 'add_new_item' => __( 'Add New Template', 'templately' ),
60 'new_item' => __( 'New Template', 'templately' ),
61 'edit_item' => __( 'Edit Template', 'templately' ),
62 'view_item' => __( 'View Template', 'templately' ),
63 'all_items' => __( 'All Templates', 'templately' ),
64 'search_items' => __( 'Search Templates', 'templately' ),
65 'parent_item_colon' => __( 'Parent Templates:', 'templately' ),
66 'not_found' => __( 'No templates found.', 'templately' ),
67 'not_found_in_trash' => __( 'No templates found in Trash.', 'templately' ),
68 'featured_image' => _x( 'Template Cover Image', 'Overrides the “Featured Image” phrase for this post type. Added in 4.3', 'templately' ),
69 'set_featured_image' => _x( 'Set cover image', 'Overrides the “Set featured image” phrase for this post type. Added in 4.3', 'templately' ),
70 'remove_featured_image' => _x( 'Remove cover image', 'Overrides the “Remove featured image” phrase for this post type. Added in 4.3', 'templately' ),
71 'use_featured_image' => _x( 'Use as cover image', 'Overrides the “Use as featured image” phrase for this post type. Added in 4.3', 'templately' ),
72 'archives' => _x( 'Template archives', 'The post type archive label used in nav menus. Default “Post Archives”. Added in 4.4', 'templately' ),
73 'insert_into_item' => _x( /** @lang text */ "Insert Into Theme Builder", 'Overrides the “Insert into post”/”Insert into page” phrase (used when inserting media into a post). Added in 4.4', 'templately' ),
74 'uploaded_to_this_item' => _x( 'Uploaded to this template', 'Overrides the “Uploaded to this post”/”Uploaded to this page” phrase (used when viewing media attached to a post). Added in 4.4', 'templately' ),
75 'filter_items_list' => _x( 'Filter templates list', 'Screen reader text for the filter links heading on the post type listing screen. Default “Filter posts list”/”Filter pages list”. Added in 4.4', 'templately' ),
76 'items_list_navigation' => _x( 'Templates list navigation', 'Screen reader text for the pagination heading on the post type listing screen. Default “Posts list navigation”/”Pages list navigation”. Added in 4.4', 'templately' ),
77 'items_list' => _x( 'Templates list', 'Screen reader text for the items list heading on the post type listing screen. Default “Posts list”/”Pages list”. Added in 4.4', 'templately' ),
78 ];
79
80 $args = [
81 'labels' => $labels,
82 'public' => true,
83 'publicly_queryable' => true,
84 'show_ui' => true,
85 'show_in_menu' => false,
86 'query_var' => true,
87 'rewrite' => [ 'slug' => 'templately-library' ],
88 // Kept in sync with the theme-builder module's own registration of this
89 // same post type (modules/theme-builder/Source.php) — whichever runs last
90 // wins, so both must carry the administrator-only capability map.
91 'capability_type' => 'post',
92 'capabilities' => Roles::builder_post_type_capabilities(),
93 'map_meta_cap' => true,
94 'has_archive' => true,
95 'hierarchical' => false,
96 'menu_position' => 10,
97 'supports' => [ 'title', 'editor', 'author', 'thumbnail', 'custom-field' ],
98 ];
99
100 register_post_type( 'templately_library', $args );
101 }
102
103 public function before_scripts() {
104 header("x-frame-options: ALLOW-FROM *.templately.com");
105 }
106
107 /**
108 * Enqueuing Assets
109 *
110 * @param string $hook
111 * @return void
112 */
113 public function scripts( $hook = '' ) {
114 $allowed_hooks = apply_filters(
115 'templately_admin_enqueue_allowed_hooks',
116 [ 'index.php', 'edit.php', 'toplevel_page_templately', 'elementor', 'gutenberg', 'templately_page_templately_settings' ],
117 $hook
118 );
119
120 if ( !empty($hook) && ! in_array( $hook, $allowed_hooks, true ) ) {
121 return;
122 }
123
124 $templately_settings = [];
125 // if('templately_page_templately_settings' === $hook){
126 $kit = null;
127 $el_settings = [];
128 if(defined('ELEMENTOR_VERSION')){
129 $kit = \Elementor\Plugin::$instance->kits_manager->get_active_kit();
130 if($kit){
131 $el_settings = $kit->get_settings();
132 }
133 }
134 $site_logo_id = get_theme_mod( 'custom_logo' );
135 $eb_settings = get_option('eb_global_styles', []);
136 $eb_settings = is_array($eb_settings) ? $eb_settings: [];
137 $eb_settings = array_map(function($item) { return json_decode($item, true); }, $eb_settings);
138 $site_logo = [
139 'id' => $site_logo_id,
140 'url' => $site_logo_id ? wp_get_attachment_url($site_logo_id) : '',
141 // 'size' => (int) get_option('templately_site_logo_size', ''),
142 ];
143
144 $templately_settings = [
145 'nonce' => wp_create_nonce( 'templately_nonce' ),
146 'has_revert' => FullSiteImport::has_revert(),
147 'has_elementor' => $this->check_plugin_status( 'elementor/elementor.php' ),
148 'has_eb' => $this->check_plugin_status( 'essential-blocks/essential-blocks.php' ),
149 'site_logo' => $site_logo,
150 'elementor' => $el_settings,
151 'gutenberg' => $eb_settings,
152 'siteTitle' => get_option('blogname', ''),
153 'siteTagline' => get_option('blogdescription', ''),
154 'customCSS' => get_option('templately_custom_css', ''),
155 // 'imported_platform' => get_option('templately_import_platform', ''),
156 ];
157 // }
158
159 if('index.php' === $hook){
160 // need separate condition so we can return if page is index.php
161 // Post-import feedback widget enqueue/localize moved to
162 // modules/post-import-feedback/Widget.php (spec 027) — hooked independently
163 // on admin_enqueue_scripts.
164 return;
165 }
166
167 $templately = [];
168
169 if ( 'edit.php' === $hook ) {
170 global $current_screen;
171 if ( $current_screen->post_type !== 'templately_library' ) {
172 return;
173 }
174
175 $types = templately()->theme_builder::$templates_manager->get_template_types();
176 $types = array_filter( $types, function ( $item ) {
177 return $item::get_property( 'builder' ) || $item::get_property( 'builder' ) === null;
178 } );
179
180 $types = array_reduce( $types, function ( $carry, $item ) {
181 return array_merge( $carry, [
182 [
183 'value' => call_user_func( [ $item, 'get_type' ] ),
184 'label' => call_user_func( [ $item, 'get_title' ] )
185 ]
186 ] );
187 }, [] );
188
189 $templately = [
190 'types' => $types
191 ];
192 }
193
194 $script_dependencies = ['regenerator-runtime'];
195 $_localize_handle = 'templately';
196 $_current_screen = 'templately';
197
198 // The webpack "commons" chunk (splitChunks) holds the react-src core + vendor code SHARED
199 // across the main bundle AND every module bundle — extracted once instead of duplicated
200 // into each. It MUST load before any of them. Making the main 'templately' handle depend
201 // on it means every module bundle (which depends on 'templately') gets it transitively, in
202 // the right order. Its own .asset.php lists no deps (DEWP doesn't attribute externals to
203 // shared chunks), so declare the WordPress script globals its shared code references here.
204 if ( file_exists( TEMPLATELY_PATH . 'assets/js/templately-commons.js' ) ) {
205 templately()->assets->register( 'templately-commons', 'js/templately-commons.js', [
206 'react', 'react-dom', 'react-jsx-runtime', 'wp-element', 'wp-hooks', 'wp-i18n',
207 'wp-api-fetch', 'wp-url', 'wp-data', 'wp-components', 'lodash',
208 ], true );
209 $script_dependencies[] = 'templately-commons';
210 }
211
212 if ( $hook === 'elementor' || $hook === 'gutenberg' ) {
213 $_current_screen = $hook;
214 $_localize_handle = 'templately-' . $hook;
215 $script_dependencies[] = $_localize_handle;
216 }
217
218 if ( 'templately_page_templately_settings' === $hook ) {
219 templately()->assets->register( 'settings-vendor', 'js/chunks/settings-vendor.js', [], true );
220 $script_dependencies[] = 'settings-vendor';
221 }
222
223 if ( $hook === 'toplevel_page_templately' || $hook == 'edit.php' || 'templately_page_templately_settings' === $hook ) {
224 templately()->assets->enqueue( 'templately-admin', 'css/admin.css', [ 'templately' ] );
225 }
226
227 // Google Fonts Enqueueing
228 // DM Sans - Used in general UI
229 templately()->assets->enqueue(
230 'templately-dmsans',
231 set_url_scheme( '//fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&display=swap' )
232 );
233
234 // Inter - Used in Tailwind components and various UI elements
235 templately()->assets->enqueue(
236 'templately-inter',
237 set_url_scheme( '//fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap' )
238 );
239
240 // DM Mono - Used in settings and code-related UI
241 templately()->assets->enqueue(
242 'templately-dmmono',
243 set_url_scheme( '//fonts.googleapis.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&display=swap' )
244 );
245
246 wp_enqueue_style('wp-components');
247
248 wp_enqueue_media();
249 templately()->assets->enqueue( 'templately', 'js/templately.js', $script_dependencies, true );
250 // splitChunks moves the shared component CSS into the commons chunk's stylesheet
251 // (templately-commons.css) and leaves the per-entry templately.css empty/absent. Register
252 // the shared CSS under the SAME 'templately' style handle so every stylesheet that depends
253 // on it (templately-tailwind, templately-admin) still resolves — WP won't enqueue a style
254 // whose dependency handle is unregistered. Fall back to templately.css if a build didn't
255 // split (no commons chunk).
256 $main_css = file_exists( TEMPLATELY_PATH . 'assets/css/templately-commons.css' ) ? 'css/templately-commons.css' : 'css/templately.css';
257 templately()->assets->enqueue( 'templately', $main_css, ['templately-dmsans', 'templately-inter'] );
258
259 $tailwind_hooks = apply_filters( 'templately_tailwind_hooks', [ 'toplevel_page_templately', 'elementor', 'gutenberg', 'templately_page_templately_settings', 'edit.php' ] );
260 if ( in_array( $hook, $tailwind_hooks, true ) ) {
261 templately()->assets->enqueue( 'templately-tailwind', 'css/tailwind.css', ['templately', 'templately-inter'] );
262 }
263
264 // MODULE STYLES. @wordpress/scripts splits extracted CSS by ORIGIN, not by feature:
265 // `{entry}.css` holds CSS reached from a stylesheet entry point, while
266 // `style-{entry}.css` holds CSS `import`ed from JS. Every module's SCSS is imported from
267 // its own index (e.g. modules/auth/assets/js/index.tsx imports ../scss/style.scss), so
268 // with the modules folded into the `templately` graph ALL of it now lands in
269 // style-templately.css — a file nothing enqueued.
270 //
271 // This is the regression that shipped the sign-in screen completely unstyled: the header
272 // and banner came from templately.css and looked fine, so the page rendered but the
273 // module-owned views did not. Before the merge these styles were emitted per module to
274 // assets/css/modules/*.css and picked up by enqueue_module_assets()'s glob; that glob is
275 // gone, and this is its replacement.
276 //
277 // ORDER MATTERS, and this must stay AFTER the Tailwind enqueue. The retired glob ran at
278 // the very end of this method — after Tailwind — so module rules beat Tailwind utilities
279 // on equal specificity. Enqueueing it earlier silently inverts that: the AI sidebar chat
280 // bubble lost its overflow-wrap to a Tailwind utility and long URLs broke out of the
281 // bubble (caught by 020-ai-chat-bubble-overflow.spec.js, not by eye).
282 if ( file_exists( TEMPLATELY_PATH . 'assets/css/style-templately.css' ) ) {
283 templately()->assets->enqueue( 'templately-modules', 'css/style-templately.css', [ 'templately' ] );
284 }
285
286 // Module assets are no longer enqueued separately: every feature module compiles into
287 // the 'templately' bundle via react-src/generated/moduleManifest.ts (Constitution XIII).
288 // The former enqueue_module_assets() globbed assets/js/modules/*.js and loaded all of
289 // them unconditionally wherever this ran, so folding them into the main handle is a
290 // parity change, not a scope change.
291
292 /**
293 * @var Elementor|Gutenberg $platform
294 */
295 $platform = $this->platform( $_current_screen );
296
297 $global_user = Options::get_instance()->get( 'user', false, get_current_user_id() );
298 $hide_buttons = get_option('templately-gutenberg-hide-buttons', 'no');
299 $hide_buttons = $hide_buttons === 'yes' ? 'yes' : 'no';
300
301 $templately = array_merge( [
302 'url' => home_url(),
303 'site_url' => site_url(),
304 'nonce' => wp_create_nonce( 'templately_nonce' ),
305 'is_ssl' => is_ssl(),
306 'rest_args' => [
307 'nonce' => wp_create_nonce( 'wp_rest' ),
308 'endpoint' => get_rest_url( null, 'templately/v1/' )
309 ],
310 "icons" => [
311 'construction' => templately()->assets->icon( 'icons/construction.gif' ),
312 'profile' => templately()->assets->icon( 'icons/profile.svg' ),
313 'warning' => templately()->assets->icon( 'icons/warning.png' )
314 ],
315 'locale' => get_locale(),
316 'promo_image' => templately()->assets->icon( 'single-page-promo.png' ),
317 'default_image' => templately()->assets->icon( 'clouds/cloud-item.svg' ),
318 'not_found' => templately()->assets->icon( 'no-item-found.png' ),
319 'no_items' => templately()->assets->icon( 'no-items.png' ),
320 'loadingImage' => templately()->assets->icon( 'logos/loading-logo.gif' ),
321 'current_url' => admin_url( 'admin.php?page=templately' ),
322 'wp_dashboard_url' => admin_url( 'index.php' ),
323 'templately_library_url' => admin_url( 'edit.php?post_type=templately_library' ),
324 'is_signed' => Login::is_signed(),
325 'is_globally_signed' => Login::is_globally_signed(),
326 'signed_as_global' => Login::signed_as_global(),
327 'current_screen' => $_current_screen,
328 'can_fsi' => current_user_can('install_plugins') && current_user_can('install_themes'),
329 'post_type' => get_post_type(),
330 'has_elementor' => rest_sanitize_boolean( is_plugin_active( 'elementor/elementor.php' ) ),
331 'has_elementor_pro' => rest_sanitize_boolean( is_plugin_active( 'elementor-pro/elementor-pro.php' ) ),
332 'theme' => $_current_screen == 'templately' ? 'light' : $platform->ui_theme(),
333 'is_wp_support_gutenberg' => version_compare( get_bloginfo( 'version' ), '5.0.0', '>=' ),
334 'hide_buttons' => $hide_buttons,
335 'settings' => $templately_settings,
336 'wp_user_avatar' => get_avatar_url( get_current_user_id(), [ 'size' => 96 ] ),
337 'allowed_mime_types' => get_allowed_mime_types(),
338 'is_free_user' => $global_user === false || isset( $global_user['plan'] ) && $global_user['plan'] == 'free',
339 'is_disconnected' => ! empty( $global_user['is_disconnected'] ),
340 'tour_status' => Options::get_instance()->get( 'tour_status', [] ),
341 // Host-capability map (spec 053) — the ONE delivery point for every
342 // Templately bundle (admin SPA and editor surfaces all localize here).
343 // Always sourced from the registry, never hand-built (FR-025).
344 'caps' => Capabilities::get_instance()->get_map(),
345 // Mirrors the account-scoped server gate so the client can hide what it
346 // would only be 403'd for: Subscription (a plan purchase is made FOR the
347 // account, and Checkout gates `subscription` on `manage_options`).
348 // Hidden rather than shown-and-refused — offering a button that cannot
349 // work is worse than not offering it.
350 'can_manage_account' => current_user_can( 'manage_options' ),
351 // Which Templately site the plugin talks to. `templatelyDeveloper.dev_api`
352 // carries the same flag, but only when developer mode is on and only from
353 // its own script handle — so anything rendered before that handle runs (the
354 // footer CTA, for one) linked to the live site while the plugin was pointed
355 // at dev. Kept here, on the main localized object, `webURL()` is right from
356 // the first paint. See `react-src/utils/helper.ts#webURL`.
357 'dev_api' => Helper::is_dev_api(),
358 ], $templately );
359
360 // Apply filter to allow network admin modifications
361 $templately = apply_filters( 'templately_admin_localized_data', $templately );
362
363 templately()->assets->localize( $_localize_handle, 'templately', $templately );
364
365 do_action( 'templately_admin_scripts', $hook );
366 }
367
368 /**
369 * Check the status of a plugin.
370 *
371 * @param string $plugin_path Plugin path relative to the plugins directory.
372 * @return mixed true if active, false if not installed, 0 if installed but not active.
373 */
374 private function check_plugin_status( $plugin_path ) {
375 $plugins = get_plugins();
376 if ( isset( $plugins[ $plugin_path ] ) ) {
377 return is_plugin_active( $plugin_path ) ? true : 0;
378 }
379 return false;
380 }
381
382 /**
383 * Admin notices for Review and others.
384 *
385 * @since 2.0.0
386 * @return void
387 * @throws Exception
388 * @since 2.0.0
389 */
390 public function notices() {
391 $notices = new Notices( [
392 // 'dev_mode' => true, // also need to clear templately_notices from option/site meta table.
393 'id' => 'templately',
394 'storage_key' => 'notices',
395 'lifetime' => 3,
396 'stylesheet_url' => TEMPLATELY_ASSETS . 'css/notices.css',
397 'styles' => TEMPLATELY_ASSETS . 'css/notices.css',
398 'priority' => 2,
399 ] );
400
401 $global_user = Options::get_instance()->get( 'user', false, get_current_user_id() );
402 $download_counts = Options::get_instance()->get( 'total_download_counts', 0, get_current_user_id() );
403 $cloud_items = 0;
404 if ( isset( $global_user['my_cloud']['usages'] ) ) {
405 $cloud_items = intval( $global_user['my_cloud']['usages'] );
406 }
407
408 if ( $cloud_items >= 5 || $download_counts >= 4 ) {
409 $message = sprintf( __( "We hope you're enjoying %s! Could you please do us a favor and give us a review on %s to help us spread the word and boost our motivation?", 'templately' ), '<strong>Templately</strong>', '<strong>WordPress</strong>' );
410
411 $_review_notice = [
412 'thumbnail' => templately()->assets->icon( 'logos/logo.svg' ),
413 'html' => '<p>' . $message . '</p>',
414 'links' => [
415 'later' => [
416 'link' => 'https://wordpress.org/support/plugin/templately/reviews/#new-post',
417 'target' => '_blank',
418 'label' => __( 'Sure, you deserve it!', 'templately' ),
419 'icon_class' => 'dashicons dashicons-external'
420 ],
421 'allready' => [
422 'label' => __( 'I already did', 'templately' ),
423 'icon_class' => 'dashicons dashicons-smiley',
424 'attributes' => [
425 'data-dismiss' => true
426 ]
427 ],
428 'maybe_later' => [
429 'label' => __( 'Maybe Later', 'templately' ),
430 'icon_class' => 'dashicons dashicons-calendar-alt',
431 'attributes' => [
432 'data-later' => true,
433 'class' => 'dismiss-btn'
434 ]
435 ],
436 'support' => [
437 'link' => Helper::web_url( '', [ 'support' => 'open' ] ),
438 'attributes' => [
439 'target' => '_blank'
440 ],
441 'label' => __( 'I need help', 'templately' ),
442 'icon_class' => 'dashicons dashicons-sos'
443 ],
444 'never_show_again' => [
445 'label' => __( 'Never show again', 'templately' ),
446 'icon_class' => 'dashicons dashicons-dismiss',
447 'attributes' => [
448 'data-dismiss' => true
449 ]
450 ]
451 ]
452 ];
453
454 $notices->add( 'review', $_review_notice, [
455 'start' => $notices->strtotime(),
456 'recurrence' => 20,
457 'dismissible' => true,
458 'refresh' => TEMPLATELY_VERSION,
459 'screens' => [
460 'dashboard',
461 'plugins',
462 'themes',
463 'edit-page',
464 'edit-post',
465 'users',
466 'tools',
467 'options-general',
468 'nav-menus'
469 ]
470 ] );
471 }
472
473 if ( $global_user === false || isset( $global_user['plan'] ) && $global_user['plan'] == 'free' ) {
474 $crown = '<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="m15.743 10.938.161-1.578c.086-.842.142-1.399.098-1.749h.015c.727 0 1.316-.622 1.316-1.389s-.589-1.389-1.315-1.389c-.727 0-1.316.622-1.316 1.39 0 .346.12.663.32.907-.287.186-.66.58-1.223 1.171-.434.456-.65.684-.893.72a.7.7 0 0 1-.394-.059c-.223-.104-.372-.385-.67-.95l-1.57-2.97a22 22 0 0 0-.476-.873c.569-.306.958-.93.958-1.65C10.754 1.496 9.97.667 9 .667s-1.754.829-1.754 1.852c0 .72.389 1.344.958 1.65-.139.234-.293.525-.476.873l-1.57 2.97c-.298.565-.447.846-.67.95a.7.7 0 0 1-.394.058c-.242-.035-.46-.263-.893-.719-.563-.592-.937-.985-1.223-1.171.2-.244.32-.56.32-.908 0-.767-.589-1.389-1.316-1.389-.726 0-1.315.622-1.315 1.39 0 .766.589 1.388 1.315 1.388h.016c-.045.35.012.906.098 1.749l.16 1.578c.09.876.164 1.71.255 2.46H15.49c.09-.75.165-1.584.254-2.46m-7.698 6.395h1.908c2.488 0 3.732 0 4.562-.784.362-.342.591-.959.757-1.762H2.727c.166.803.395 1.42.757 1.762.83.784 2.074.784 4.562.784" fill="#fff"/></svg>';
475
476 // $notices->add( 'upsale', wp_sprintf( '<p>%1$s <a target="_blank" href="%3$s">%2$s</a>.</p>', __( '🔥 Get access to 6,500+ Ready Templates & save up to 65% OFF now', 'templately' ), __( 'Upgrade to Pro', 'templately' ), 'https://templately.com/#pricing' ), [
477 // 'start' => $notices->strtotime( '+10 day' ),
478 // 'dismissible' => true,
479 // 'refresh' => TEMPLATELY_VERSION,
480 // 'screens' => [
481 // 'dashboard',
482 // 'plugins',
483 // 'themes',
484 // 'edit-page',
485 // 'edit-post',
486 // 'users',
487 // 'tools',
488 // 'options-general',
489 // 'nav-menus'
490 // ]
491 // ] );
492
493 // spring_savings_2026 notice
494 $_notice_text_spring_savings_2026 = '<p style="margin-top: 0; margin-bottom: 0px;">🌸 Spring Savings: Design Website Quickly With 6,500+ Elementor & Gutenberg Templates, Including AI-Powered Features ⚡️ – Now <strong>Flat 50% OFF!</strong></p>';
495
496 $_spring_savings_2026 = [
497 'thumbnail' => templately()->assets->icon( 'logos/logo-full.svg' ),
498 'html' => $_notice_text_spring_savings_2026,
499 'links' => [
500 'later' => [
501 'link' => Helper::web_url() . '#pricing',
502 'target' => '_blank',
503 'label' => __( 'Upgrade To Pro Now', 'templately' ),
504 'attributes' => [
505 'class' => 'button button-primary',
506 ]
507 ],
508 'never_show_again' => [
509 'label' => __( 'Maybe Later', 'templately' ),
510 'attributes' => [
511 'data-dismiss' => true,
512 'class' => 'button button-link dismiss-btn',
513 ]
514 ],
515 ],
516 ];
517
518 $notices->add( 'spring_savings_2026', $_spring_savings_2026, [
519 'start' => $notices->strtotime('00:00:01AM 8th April, 2026'),
520 'recurrence' => false,
521 'dismissible' => true,
522 'refresh' => TEMPLATELY_VERSION,
523 "expire" => strtotime( '11:59:59pm 10th May, 2026' ),
524 'display_if' => !wp_is_mobile(),
525 'screens' => [
526 'dashboard',
527 ],
528 ] );
529
530 $holiday_text = sprintf(
531 '<p style="margin-top: 0; margin-bottom: 0px;"> 🎁 %s <strong>%s</strong> %s</p>',
532 __('Get', 'templately'),
533 __('Templately PRO', 'templately'),
534 __('with up to 60% OFF & unlock 6500+ ready WordPress templates to power up web design in 2025.', 'templately')
535 );
536
537 $holiday_text .= "<div class='wpnotice-button-wrapper'>";
538 $holiday_text .= sprintf(
539 '<a class="button button-primary" target="_blank" href="%2$s">%1$s</a>',
540 $crown . __('GET PRO Lifetime Access', 'templately'),
541 Helper::web_url() . '#pricing'
542 );
543 $holiday_text .= sprintf(
544 '<button class="button button-link dismiss-btn" data-dismiss="true">%1$s</button>',
545 __('No, I\'ll Pay Full Price Later', 'templately')
546 );
547 $holiday_text .= "</div>";
548
549
550 $_holiday = [
551 'thumbnail' => templately()->assets->icon( 'logos/logo-full.svg' ),
552 'html' => $holiday_text,
553 ];
554
555 // $notices->add( 'holiday', $_holiday, [
556 // 'start' => $notices->strtotime( '+0 minute' ),
557 // 'recurrence' => false,
558 // 'dismissible' => true,
559 // 'refresh' => TEMPLATELY_VERSION,
560 // "expire" => strtotime( '11:59:59pm 10th January, 2025' ),
561 // 'display_if' => !wp_is_mobile(),
562 // 'screens' => [
563 // 'dashboard',
564 // ],
565 // ] );
566 }
567
568 self::$cache_bank->create_account( $notices );
569 self::$cache_bank->calculate_deposits( $notices );
570 }
571
572 /**
573 * Adding Menu In Sidebar ( WordPress Left-side Dashboard Menu )
574 *
575 * @return void
576 */
577 public function admin_menu() {
578 // TODO: Role Management
579
580 // 58.7 is the TOP-LEVEL slot: a float is correct and always has been
581 // there, and the fractional part is what keeps Templately from
582 // colliding with another plugin sitting at the same integer position.
583 add_menu_page( 'Templately', 'Templately', 'delete_posts', 'templately', '', templately()->assets->icon( 'logos/logo-icon.svg' ), 58.7 );
584
585 // The submenu calls pass NO position on purpose. `58.7` was copied down
586 // from the top-level slot, where it never meant anything — a submenu
587 // $position is an index within THIS menu, and 58.7 is past the end of a
588 // four-item list, so every WordPress we support already just appended.
589 // It was not merely useless though: before WP 6.0, $position had to be
590 // an int (`is_int()`, not `is_numeric()` — a float fails it too), so
591 // each call emitted `_doing_it_wrong` on every admin load of a
592 // WP_DEBUG site inside the advertised floor (readme: WP 5.0) and then
593 // discarded the value anyway. Omitting it appends — verified identical
594 // ordering on WP 5.9 and 7.0 — and is silent on every version.
595 add_submenu_page( 'templately', 'Templately', 'Template Library', 'delete_posts', 'templately', [
596 $this,
597 'display'
598 ] );
599
600 add_submenu_page( 'templately', 'Theme Builder', 'Theme Builder', Roles::CAP_BUILDER, 'edit.php?post_type=templately_library', '' );
601
602 // add_submenu_page( 'templately', 'Settings', 'Settings', 'administrator', 'templately-settings', [
603 // Settings::get_instance(),
604 // 'display'
605 // ], '58.7' );
606 }
607
608 public function display() {
609 Helper::views( 'template-library', [
610 'show_grid_skeleton' => self::is_template_library_path(),
611 ] );
612 }
613
614 /**
615 * Whether the current `?path=` targets the template-library grid
616 * (`TemplatesTypeLayout`, e.g. `elementor/packs`) — the only route the
617 * pre-mount skeleton's card grid actually matches. Every other route
618 * (`clouds/*`, `downloads`, `favourites`, `manage-sites`, `subscription`,
619 * `purchased-items`, `ai-sites`, `settings/*`, item-detail pages, and
620 * Saved Templates) gets a generic shell instead, so a hard reload doesn't
621 * flash the wrong page shape before React mounts. See
622 * views/template-library.php.
623 *
624 * Public + static (it was private on the pre-modular branch) purely so the
625 * path parsing can be unit-tested on its own: it reads nothing but `$_GET`
626 * and touches no instance state, so there is nothing for an Admin instance
627 * to contribute, and constructing one would register every admin hook.
628 *
629 * @return bool
630 */
631 public static function is_template_library_path() {
632 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only routing hint for a skeleton; no state is changed.
633 $path = isset( $_GET['path'] ) ? sanitize_text_field( wp_unslash( $_GET['path'] ) ) : '';
634
635 if ( $path === '' ) {
636 return true;
637 }
638
639 $segments = array_values( array_filter( explode( '/', trim( $path, '/' ) ) ) );
640
641 $non_library_prefixes = [ 'clouds', 'downloads', 'favourites', 'manage-sites', 'subscription', 'purchased-items', 'ai-sites', 'settings' ];
642
643 if ( in_array( $segments[0] ?? '', $non_library_prefixes, true ) ) {
644 return false;
645 }
646
647 // `/:platform/:type` (the grid) vs `/:platform/:type/:slug` (item
648 // detail, its own skeleton shape) or `/:platform/templates` (Saved
649 // Templates, a table) — only the 2-segment form matches the grid.
650 return count( $segments ) === 2 && $segments[1] !== 'templates';
651 }
652
653 /**
654 * If Elementor doesn't exists.
655 *
656 * @return void
657 */
658 public static function has_no_elementor() {
659 $plugin_url = \wp_nonce_url( \self_admin_url( 'update.php?action=install-plugin&amp;plugin=elementor' ), 'install-plugin_elementor' );
660 $button_text = __( 'Install Elementor', 'templately' );
661 if ( isset( Helper::get_plugins()['elementor/elementor.php'] ) ) {
662 $plugin_url = \wp_nonce_url( 'plugins.php?action=activate&amp;plugin=elementor/elementor.php', 'activate-plugin_elementor/elementor.php' );
663 $button_text = __( 'Activate Elementor', 'templately' );
664 }
665 $output = '<div class="notice notice-error">';
666 $output .= sprintf(
667 "<p><strong>%s</strong> %s <strong>%s</strong> %s &nbsp;&nbsp;<a class='button-primary' href='%s'>%s</a></p>",
668 __( 'Templately', 'templately' ),
669 __( 'requires', 'templately' ),
670 __( 'Elementor', 'templately' ),
671 __( 'plugin to be installed and activated. Please install Elementor to continue.', 'templately' ),
672 esc_url( $plugin_url ),
673 $button_text
674 );
675 $output .= '</div>';
676 echo wp_kses_post( $output );
677 }
678
679 public function header() {
680 Helper::views( 'header' );
681 }
682
683 /**
684 * Handle links displayed below the plugin name in the WordPress Installed Plugins page.
685 *
686 * @return array
687 * @since 3.1.2
688 * @static
689 *
690 */
691 public static function handleActionLinks($links, $file)
692 {
693 $settingsLink = '<a href="' . admin_url('admin.php?page=templately') . '" aria-label="' . __('Open settings page',
694 'templately') . '">' . __('Templates Library', 'templately') . '</a>';
695
696 $links[] = $settingsLink;
697 return $links;
698 }
699 }
700