PluginProbe
WP VR – 360 Panorama and Virtual Tour Builder / 9.0.1
WP VR – 360 Panorama and Virtual Tour Builder v9.0.1
9.1.1 9.1.0 9.0.3 9.0.2 9.0.1 9.0.0 8.5.79 8.5.78 8.5.77 8.5.76 8.5.75 8.5.74 8.5.73 8.5.72 8.5.71 8.5.70 8.5.69 8.5.68 8.5.35 8.5.36 8.5.37 8.5.38 8.5.39 8.5.4 8.5.40 All 220 releases
wpvr / src / Admin / Admin.php

Admin.php in WP VR – 360 Panorama and Virtual Tour Builder 9.0.1, at src/Admin/Admin.php

580 lines 24.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace RexTheme\WPVR\Admin;
4
5 /**
6 * Manages the custom tour-editor admin page and CPT edit suppression.
7 *
8 * Step 1 — Register hidden admin page (wpvr-tour-editor).
9 * Step 2 — Redirect post.php / post-new.php for wpvr_item to custom page.
10 * Step 3 — Override edit links in WP posts list.
11 * Step 4 — Render page shell (#wpvr-tour-editor-root) + enqueue assets.
12 * Step 5 — New tour: React creates post on mount; history.replaceState URL.
13 */
14 class Admin {
15
16 const POST_TYPE = 'wpvr_item';
17 const PAGE_SLUG = 'wpvr-tour-editor';
18 const HOOK_SUFFIX = 'admin_page_wpvr-tour-editor';
19
20 private $editor_enabled;
21
22 public function __construct( bool $editor_enabled = true ) {
23 $this->editor_enabled = $editor_enabled;
24
25 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_listing_assets' ] );
26
27 if ( $this->editor_enabled ) {
28 add_action( 'admin_footer', [ $this, 'render_listing_modal' ] );
29 }
30
31 if ( ! $this->editor_enabled ) {
32 return;
33 }
34
35 add_action( 'admin_menu', [ $this, 'register_editor_page' ] );
36 add_action( 'load-post.php', [ $this, 'redirect_cpt_edit' ] );
37 add_action( 'load-post-new.php', [ $this, 'redirect_cpt_new' ] );
38 add_filter( 'get_edit_post_link', [ $this, 'filter_edit_link' ], 10, 3 );
39 add_filter( 'post_row_actions', [ $this, 'filter_row_actions' ], 10, 2 );
40 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_tour_editor_assets' ] );
41 add_filter( 'admin_body_class', [ $this, 'add_body_class' ] );
42 add_filter( 'admin_title', [ $this, 'filter_admin_title' ], 10, 2 );
43 add_action( 'current_screen', [ $this, 'set_screen_title' ] );
44 add_filter( 'parent_file', [ $this, 'set_menu_parent' ] );
45 add_filter( 'submenu_file', [ $this, 'set_menu_submenu' ] );
46 add_action( 'admin_head', [ $this, 'editor_head_scripts' ] );
47 add_action( 'admin_footer', [ $this, 'editor_footer_scripts' ] );
48 }
49
50 // -------------------------------------------------------------------------
51 // Step 1 — Register hidden admin page
52 // -------------------------------------------------------------------------
53
54 public function register_editor_page(): void {
55 $tour_id = isset( $_GET['tour_id'] ) ? (int) $_GET['tour_id'] : 0;
56 $page_title = $tour_id
57 ? __( 'Edit Tour', 'wpvr' )
58 : __( 'Add New Tour', 'wpvr' );
59
60 add_submenu_page(
61 '',
62 $page_title,
63 $page_title,
64 'edit_posts',
65 self::PAGE_SLUG,
66 [ $this, 'render_editor_page' ]
67 );
68 }
69
70 // -------------------------------------------------------------------------
71 // Step 2 — Redirect post.php / post-new.php
72 // -------------------------------------------------------------------------
73
74 public function redirect_cpt_edit(): void {
75 $action = isset( $_GET['action'] ) ? sanitize_key( $_GET['action'] ) : '';
76 if ( $action && $action !== 'edit' ) {
77 return;
78 }
79 $post_id = isset( $_GET['post'] ) ? (int) $_GET['post'] : 0;
80 if ( ! $post_id ) {
81 return;
82 }
83 $post = get_post( $post_id );
84 if ( ! $post || $post->post_type !== self::POST_TYPE ) {
85 return;
86 }
87 wp_redirect( $this->editor_url( $post_id ) );
88 exit;
89 }
90
91 public function redirect_cpt_new(): void {
92 $post_type = isset( $_GET['post_type'] ) ? sanitize_key( $_GET['post_type'] ) : '';
93 if ( $post_type !== self::POST_TYPE ) {
94 return;
95 }
96 wp_redirect( $this->editor_url() );
97 exit;
98 }
99
100 // -------------------------------------------------------------------------
101 // Step 3 — Override edit links
102 // -------------------------------------------------------------------------
103
104 public function filter_edit_link( string $url, int $post_id, string $context ): string {
105 $post = get_post( $post_id );
106 if ( ! $post || $post->post_type !== self::POST_TYPE ) {
107 return $url;
108 }
109 return $this->editor_url( $post_id );
110 }
111
112 public function filter_row_actions( array $actions, \WP_Post $post ): array {
113 if ( $post->post_type !== self::POST_TYPE ) {
114 return $actions;
115 }
116 if ( isset( $actions['edit'] ) ) {
117 $actions['edit'] = sprintf(
118 '<a href="%s">%s</a>',
119 esc_url( $this->editor_url( $post->ID ) ),
120 __( 'Edit', 'wpvr' )
121 );
122 }
123 return $actions;
124 }
125
126 // -------------------------------------------------------------------------
127 // Step 4 — Render + enqueue
128 // -------------------------------------------------------------------------
129
130 public function render_editor_page(): void {
131 echo '<div id="wpvr-tour-editor-root"></div>';
132
133 if ( ! wpvr_is_pro_active() ) {
134 require WPVR_PLUGIN_LEGACY_DIR_PATH . 'admin/partials/wpvr-premium-feature-popup.php';
135 }
136
137 // Hidden wp_editor() output forces TinyMCE HTML/scripts to load so
138 // wp.editor.initialize() is available for WYSIWYG fields in React.
139 echo '<div style="display:none">';
140 wp_editor( '', 'wpvr_wysiwyg_seed', [
141 'tinymce' => true,
142 'quicktags' => false,
143 'media_buttons' => false,
144 ] );
145 echo '</div>';
146 }
147
148 public function enqueue_tour_editor_assets( string $hook ): void {
149 if ( ! $this->is_editor_page( $hook ) ) {
150 return;
151 }
152
153 $plugin_url = plugin_dir_url( WPVR_FILE );
154 $plugin_path = plugin_dir_path( WPVR_FILE );
155 $build_dir = $plugin_path . 'build/tour-editor/';
156
157 // Load Pannellum (from legacy bundle).
158 $pano_base = $plugin_url . 'legacy/admin/lib/pannellum/src/';
159 wp_enqueue_style( 'wpvr-pannellum-css', $pano_base . 'css/pannellum.css', [], WPVR_VERSION );
160 wp_enqueue_script( 'wpvr-libpannellum', $pano_base . 'js/libpannellum.js', [], WPVR_VERSION, true );
161 wp_enqueue_script( 'wpvr-pannellum', $pano_base . 'js/pannellum.js', [ 'wpvr-libpannellum' ], WPVR_VERSION, true );
162
163 wp_enqueue_style( 'wpvr-fontawesome', $plugin_url . 'legacy/admin/lib/fontawesome/css/all.min.css', [], WPVR_VERSION );
164
165 // Tour editor React bundle.
166 $js_file = $build_dir . 'index.js';
167 $css_file = $build_dir . 'style-index.css';
168 $asset_file = $build_dir . 'index.asset.php';
169
170 if ( ! file_exists( $js_file ) ) {
171 return;
172 }
173
174 $asset = file_exists( $asset_file )
175 ? require $asset_file
176 : [ 'dependencies' => [ 'wp-element', 'wp-data', 'wp-api-fetch' ], 'version' => WPVR_VERSION ];
177
178 wp_enqueue_script(
179 'wpvr-tour-editor',
180 $plugin_url . 'build/tour-editor/index.js',
181 array_merge( $asset['dependencies'], [ 'wpvr-pannellum' ] ),
182 $asset['version'],
183 true
184 );
185
186 $tour_id_cfg = isset( $_GET['tour_id'] ) ? (int) $_GET['tour_id'] : 0;
187 $tour_status = $tour_id_cfg ? ( get_post_status( $tour_id_cfg ) ?: 'draft' ) : 'draft';
188
189 $fa_icons = [];
190 if ( class_exists( 'Wpvr_fontawesome_icons' ) ) {
191 $fa_icons = ( new \Wpvr_fontawesome_icons() )->icon;
192 }
193
194 wp_localize_script( 'wpvr-tour-editor', 'wpvrTourEditor', [
195 'tourId' => $tour_id_cfg ?: null,
196 'tourStatus' => $tour_status,
197 'nonce' => wp_create_nonce( 'wp_rest' ),
198 'apiBase' => rest_url( 'wpvr/v1' ),
199 'mediaUrl' => rest_url( 'wp/v2/media' ),
200 'isPro' => wpvr_is_pro_active(),
201 'pluginUrl' => $plugin_url,
202 'listUrl' => admin_url( 'edit.php?post_type=' . self::POST_TYPE ),
203 'postEditUrl' => admin_url( 'post.php' ),
204 'faIcons' => $fa_icons,
205 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
206 'exportNonce' => wp_create_nonce( 'wpvr_export_tour' ),
207 'imageResizeWarning' => [
208 'ajaxNonce' => wp_create_nonce( 'wpvr' ),
209 'canManageSettings' => current_user_can( 'manage_options' ),
210 'settingEnabled' => get_option( 'high_res_image' ) === 'true',
211 'heading' => __( 'Keep this panorama in High Resolution', 'wpvr' ),
212 'scaledMessage' => __( 'By default, a resized version is created while the original image is still available.', 'wpvr' ),
213 'disableHandler' => __( 'Disable WordPress Large Image Handler on WP VR for future uploads', 'wpvr' ),
214 'highResolutionNote'=> __( '*Note: Turn this on to use the high-resolution image.', 'wpvr' ),
215 'close' => __( 'Close', 'wpvr' ),
216 ],
217 ] );
218
219 if ( file_exists( $css_file ) ) {
220 wp_enqueue_style(
221 'wpvr-tour-editor',
222 $plugin_url . 'build/tour-editor/style-index.css',
223 [ 'wpvr-pannellum-css' ],
224 $asset['version']
225 );
226 }
227 }
228
229 public function filter_admin_title( string $admin_title, string $title ): string {
230 if ( ! isset( $_GET['page'] ) || sanitize_key( $_GET['page'] ) !== self::PAGE_SLUG ) {
231 return $admin_title;
232 }
233 $tour_id = isset( $_GET['tour_id'] ) ? (int) $_GET['tour_id'] : 0;
234 $page_title = $tour_id ? __( 'Edit Tour', 'wpvr' ) : __( 'Add New Tour', 'wpvr' );
235 return $page_title . ' &lsaquo; ' . get_bloginfo( 'name' ) . ' &#8212; WordPress';
236 }
237
238 public function add_body_class( string $classes ): string {
239 if ( isset( $_GET['page'] ) && sanitize_key( $_GET['page'] ) === self::PAGE_SLUG ) {
240 $classes .= ' wpvr-tour-editor-page';
241 }
242 return $classes;
243 }
244
245 public function set_menu_parent( string $parent_file ): string {
246 if ( isset( $_GET['page'] ) && sanitize_key( $_GET['page'] ) === self::PAGE_SLUG ) {
247 return 'edit.php?post_type=' . self::POST_TYPE;
248 }
249 return $parent_file;
250 }
251
252 public function set_menu_submenu( ?string $submenu_file ): ?string {
253 if ( isset( $_GET['page'] ) && sanitize_key( $_GET['page'] ) === self::PAGE_SLUG ) {
254 return 'post-new.php?post_type=' . self::POST_TYPE;
255 }
256 return $submenu_file;
257 }
258
259 public function editor_head_scripts(): void {
260 if ( ! isset( $_GET['page'] ) || sanitize_key( $_GET['page'] ) !== self::PAGE_SLUG ) {
261 return;
262 }
263 ?>
264 <script>
265 ( function() {
266 var narrowEditor = window.matchMedia( '(max-width: 1279px)' );
267
268 function applyEditorMenuState() {
269 if ( ! document.body ) {
270 window.requestAnimationFrame( applyEditorMenuState );
271 return;
272 }
273
274 document.body.classList.toggle( 'folded', narrowEditor.matches );
275 }
276
277 applyEditorMenuState();
278
279 if ( narrowEditor.addEventListener ) {
280 narrowEditor.addEventListener( 'change', applyEditorMenuState );
281 } else {
282 narrowEditor.addListener( applyEditorMenuState );
283 }
284 }() );
285 </script>
286 <?php
287 }
288
289 public function editor_footer_scripts(): void {
290 if ( ! isset( $_GET['page'] ) || sanitize_key( $_GET['page'] ) !== self::PAGE_SLUG ) {
291 return;
292 }
293 ?>
294 <script>
295 jQuery( window ).on( 'load', function() {
296 // Intercept WP sidebar toggle so it doesn't persist state to user meta.
297 // The editor's initial state is viewport-dependent; manual toggling should
298 // work visually but must not overwrite the user's real preference.
299 jQuery( '#collapse-button' ).off( 'click' ).on( 'click', function() {
300 jQuery( 'body' ).toggleClass( 'folded' );
301 } );
302 } );
303 </script>
304 <?php
305 }
306
307 public function render_mode_switch_notice(): void {
308 if ( ! current_user_can( 'edit_posts' ) ) {
309 return;
310 }
311
312 if ( ! $this->is_listing_page() && ! $this->is_editor_request() ) {
313 return;
314 }
315
316 $tour_id = isset( $_GET['tour_id'] ) ? absint( $_GET['tour_id'] ) : 0;
317 $switch_url = $this->get_ui_mode_switch_url( 'legacy', $tour_id );
318 $button_label = __( 'Switch to Classic UI', 'wpvr' );
319 ?>
320 <div class="notice notice-info">
321 <p>
322 <?php esc_html_e( 'You are using the latest WP VR editor.', 'wpvr' ); ?>
323 <a class="button button-secondary" href="<?php echo esc_url( $switch_url ); ?>" style="margin-left:8px;">
324 <?php echo esc_html( $button_label ); ?>
325 </a>
326 </p>
327 </div>
328 <?php
329 }
330
331 public function handle_ui_mode_switch(): void {
332 if ( ! current_user_can( 'edit_posts' ) ) {
333 wp_die( esc_html__( 'You are not allowed to switch UI modes.', 'wpvr' ) );
334 }
335
336 check_admin_referer( 'wpvr_switch_ui_mode' );
337
338 $mode = isset( $_GET['mode'] ) && sanitize_key( $_GET['mode'] ) === 'latest' ? 'latest' : 'legacy';
339 $tour_id = isset( $_GET['tour_id'] ) ? absint( $_GET['tour_id'] ) : 0;
340
341 update_option( 'wpvr_ui_mode', $mode );
342
343 wp_safe_redirect( $this->get_ui_mode_redirect_url( $mode, $tour_id ) );
344 exit;
345 }
346
347 // -------------------------------------------------------------------------
348 // Step 5 — Set $title global so admin-header.php doesn't get null
349 // -------------------------------------------------------------------------
350
351 public function set_screen_title( \WP_Screen $screen ): void {
352 if ( $screen->id !== 'admin_page_' . self::PAGE_SLUG ) {
353 return;
354 }
355 global $title;
356 $tour_id = isset( $_GET['tour_id'] ) ? (int) $_GET['tour_id'] : 0;
357 $title = $tour_id ? __( 'Edit Tour', 'wpvr' ) : __( 'Add New Tour', 'wpvr' );
358 }
359
360 // -------------------------------------------------------------------------
361 // Helpers
362 // -------------------------------------------------------------------------
363
364 private function editor_url( int $post_id = 0 ): string {
365 $args = [ 'page' => self::PAGE_SLUG ];
366 if ( $post_id > 0 ) {
367 $args['tour_id'] = $post_id;
368 }
369 return add_query_arg( $args, admin_url( 'admin.php' ) );
370 }
371
372 private function is_editor_page( string $hook ): bool {
373 return $hook === self::HOOK_SUFFIX
374 || ( isset( $_GET['page'] ) && sanitize_key( $_GET['page'] ) === self::PAGE_SLUG );
375 }
376
377 private function is_editor_request(): bool {
378 return isset( $_GET['page'] ) && sanitize_key( $_GET['page'] ) === self::PAGE_SLUG;
379 }
380
381 private function is_listing_page(): bool {
382 $screen = get_current_screen();
383 return $screen && $screen->id === 'edit-' . self::POST_TYPE;
384 }
385
386 private function get_ui_mode_switch_url( string $mode, int $tour_id = 0 ): string {
387 $args = [
388 'action' => 'wpvr_switch_ui_mode',
389 'mode' => $mode,
390 ];
391
392 if ( $tour_id > 0 ) {
393 $args['tour_id'] = $tour_id;
394 }
395
396 return wp_nonce_url( add_query_arg( $args, admin_url( 'admin-post.php' ) ), 'wpvr_switch_ui_mode' );
397 }
398
399 private function get_ui_mode_redirect_url( string $mode, int $tour_id = 0 ): string {
400 if ( $mode === 'latest' ) {
401 return $this->editor_url( $tour_id );
402 }
403
404 if ( $tour_id > 0 ) {
405 return add_query_arg(
406 [
407 'post' => $tour_id,
408 'action' => 'edit',
409 ],
410 admin_url( 'post.php' )
411 );
412 }
413
414 return admin_url( 'edit.php?post_type=' . self::POST_TYPE );
415 }
416
417 // -------------------------------------------------------------------------
418 // Listing page — Create Tour modal
419 // -------------------------------------------------------------------------
420
421 public function enqueue_listing_assets( string $hook ): void {
422 if ( ! $this->is_listing_page() ) {
423 return;
424 }
425
426 $plugin_url = plugin_dir_url( WPVR_FILE );
427 $style_dependencies = [];
428 $script_dependencies = [];
429
430 if ( $this->editor_enabled ) {
431 wp_enqueue_style(
432 'wpvr-create-tour-modal',
433 $plugin_url . 'app/admin/create-tour-modal.css',
434 [],
435 WPVR_VERSION
436 );
437
438 wp_enqueue_script(
439 'wpvr-create-tour-modal',
440 $plugin_url . 'app/admin/create-tour-modal.js',
441 [],
442 WPVR_VERSION,
443 true
444 );
445
446 wp_localize_script( 'wpvr-create-tour-modal', 'wpvrListingModal', [
447 'nonce' => wp_create_nonce( 'wp_rest' ),
448 'apiBase' => rest_url( 'wpvr/v1' ),
449 'editorUrl' => admin_url( 'admin.php?page=' . self::PAGE_SLUG ),
450 'isPro' => wpvr_is_pro_active(),
451 'iconsUrl' => $plugin_url . 'app/assets/icons/tour-types/',
452 ] );
453
454 $style_dependencies[] = 'wpvr-create-tour-modal';
455 $script_dependencies[] = 'wpvr-create-tour-modal';
456 }
457
458 wp_enqueue_style(
459 'wpvr-tour-listing',
460 $plugin_url . 'app/admin/tour-listing.css',
461 $style_dependencies,
462 WPVR_VERSION
463 );
464
465 wp_enqueue_script(
466 'wpvr-tour-listing',
467 $plugin_url . 'app/admin/tour-listing.js',
468 $script_dependencies,
469 WPVR_VERSION,
470 true
471 );
472 }
473
474 public function render_listing_modal(): void {
475 if ( ! $this->editor_enabled || ! $this->is_listing_page() ) {
476 return;
477 }
478
479 $is_pro = apply_filters( 'is_wpvr_pro_active', false ) && get_option( 'wpvr_edd_license_status' ) === 'valid';
480 $icons_url = plugin_dir_url( WPVR_FILE ) . 'app/assets/icons/tour-types/';
481 ?>
482 <div id="wpvr-ctm-overlay" class="wpvr-ctm-overlay" style="display:none;" role="dialog" aria-modal="true" aria-label="Create New Tour">
483 <div class="wpvr-ctm">
484
485 <header class="wpvr-ctm__header">
486 <div>
487 <h2 class="wpvr-ctm__title"><?php esc_html_e( 'Create New Tour', 'wpvr' ); ?></h2>
488 <p class="wpvr-ctm__subtitle"><?php esc_html_e( 'Choose your tour type and get started', 'wpvr' ); ?></p>
489 </div>
490 <button class="wpvr-ctm__close" id="wpvr-ctm-close" type="button" aria-label="<?php esc_attr_e( 'Close', 'wpvr' ); ?>">×</button>
491 </header>
492
493 <div class="wpvr-ctm__body">
494
495 <div class="wpvr-ctm__field">
496 <label for="wpvr-ctm-name" class="wpvr-ctm__label">
497 <?php esc_html_e( 'Tour Name', 'wpvr' ); ?>
498 <span class="wpvr-ctm__required">*</span>
499 </label>
500 <input
501 type="text"
502 id="wpvr-ctm-name"
503 class="wpvr-ctm__input"
504 placeholder="<?php esc_attr_e( 'Enter tour name...', 'wpvr' ); ?>"
505 autocomplete="off"
506 />
507 <span class="wpvr-ctm__error" id="wpvr-ctm-name-error" style="display:none;">
508 <?php esc_html_e( 'Tour name is required.', 'wpvr' ); ?>
509 </span>
510 </div>
511
512 <div class="wpvr-ctm__field">
513 <label class="wpvr-ctm__label">
514 <?php esc_html_e( 'Tour Type', 'wpvr' ); ?>
515 <span class="wpvr-ctm__required">*</span>
516 </label>
517 <div class="wpvr-ctm__cards">
518
519 <?php /* 360° Image — always free, selected by default */ ?>
520 <button type="button" class="wpvr-ctm__card wpvr-ctm__card--selected" data-type="image">
521 <img class="wpvr-ctm__card-icon"
522 src="<?php echo esc_url( $icons_url . 'scene-image-filled.svg' ); ?>"
523 data-active="<?php echo esc_url( $icons_url . 'scene-image-filled.svg' ); ?>"
524 data-inactive="<?php echo esc_url( $icons_url . 'scene-image.svg' ); ?>"
525 alt="" />
526 <span class="wpvr-ctm__card-title"><?php esc_html_e( '360° Image', 'wpvr' ); ?></span>
527 <span class="wpvr-ctm__card-desc"><?php esc_html_e( 'Create immersive panoramic image tours', 'wpvr' ); ?></span>
528 <span class="wpvr-ctm__card-radio wpvr-ctm__card-radio--checked"></span>
529 </button>
530
531 <?php /* 360° Video — free */ ?>
532 <button type="button" class="wpvr-ctm__card" data-type="video">
533 <img class="wpvr-ctm__card-icon"
534 src="<?php echo esc_url( $icons_url . 'scene-video.svg' ); ?>"
535 data-active="<?php echo esc_url( $icons_url . 'scene-video-filled.svg' ); ?>"
536 data-inactive="<?php echo esc_url( $icons_url . 'scene-video.svg' ); ?>"
537 alt="" />
538 <span class="wpvr-ctm__card-title"><?php esc_html_e( '360° Video', 'wpvr' ); ?></span>
539 <span class="wpvr-ctm__card-desc"><?php esc_html_e( 'Build dynamic video-based experiences', 'wpvr' ); ?></span>
540 <span class="wpvr-ctm__card-radio"></span>
541 </button>
542
543 <?php /* Street View — PRO only */ ?>
544 <button type="button" class="wpvr-ctm__card<?php echo $is_pro ? '' : ' wpvr-ctm__card--disabled'; ?>"
545 data-type="street-view"
546 <?php echo $is_pro ? '' : 'disabled'; ?>
547 title="<?php echo $is_pro ? '' : esc_attr__( 'Requires WP VR Pro', 'wpvr' ); ?>">
548 <?php if ( ! $is_pro ) : ?>
549 <span class="is-pro" aria-hidden="true">Pro</span>
550 <?php endif; ?>
551 <img class="wpvr-ctm__card-icon"
552 src="<?php echo esc_url( $icons_url . 'scene-street.svg' ); ?>"
553 data-active="<?php echo esc_url( $icons_url . 'scene-street-filled.svg' ); ?>"
554 data-inactive="<?php echo esc_url( $icons_url . 'scene-street.svg' ); ?>"
555 alt="" />
556 <span class="wpvr-ctm__card-title"><?php esc_html_e( 'Street View', 'wpvr' ); ?></span>
557 <span class="wpvr-ctm__card-desc"><?php esc_html_e( 'Integrate Google Street View locations', 'wpvr' ); ?></span>
558 <span class="wpvr-ctm__card-radio"></span>
559 </button>
560
561 </div>
562 </div>
563
564 </div>
565
566 <div class="wpvr-ctm__footer">
567 <button type="button" class="wpvr-ctm__btn wpvr-ctm__btn--secondary" id="wpvr-ctm-cancel">
568 <?php esc_html_e( 'Cancel', 'wpvr' ); ?>
569 </button>
570 <button type="button" class="wpvr-ctm__btn wpvr-ctm__btn--primary" id="wpvr-ctm-submit">
571 <?php esc_html_e( '+ Create Tour', 'wpvr' ); ?>
572 </button>
573 </div>
574
575 </div>
576 </div>
577 <?php
578 }
579 }
580