feat: tour page photos-first layout, mobile booking bar; board 1 fixes

Tour page (boards 2 and 3):
- Replace the blurred 'Welcome to {city}' hero with a breadcrumb and a photo
  gallery (1 large + 2, 'All N photos' opens the lightbox); the H1 title,
  rating with review count and facts follow; booking card stays sticky
- Phones/tablets: swipeable photos, name before price, 2x2 facts, and a
  fixed bottom bar with price + Check availability (vc-tour.css)

Board 1:
- Hide Viator internal tags ('Agent Low Quality Product'...) from the tour
  category, listing filter and activities section (VC_Tags, tested)
- Listings: run the query filter late and set posts_per_archive_page so
  pages show 12 tours as the count says
- Footer columns without a menu show default links (quick links, structure
  cities, contact/about/faq) instead of empty headings

Plugin 1.9.5, theme 1.0.13.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-29 21:01:13 +03:30
parent 2f1ecd99b4
commit de9814cf3d
15 changed files with 367 additions and 57 deletions

Binary file not shown.

View File

@@ -23,10 +23,10 @@ add_action( 'wp_enqueue_scripts', function () {
if ( wp_style_is( 'astra-theme-css', 'registered' ) ) { if ( wp_style_is( 'astra-theme-css', 'registered' ) ) {
$deps[] = 'astra-theme-css'; $deps[] = 'astra-theme-css';
} }
wp_enqueue_style( 'hoho-chrome', get_stylesheet_directory_uri() . '/assets/chrome.css', $deps, '1.0.12' ); wp_enqueue_style( 'hoho-chrome', get_stylesheet_directory_uri() . '/assets/chrome.css', $deps, '1.0.13' );
// Header interactions: burger, submenu, sticky, back-to-top, search toggle. // Header interactions: burger, submenu, sticky, back-to-top, search toggle.
wp_enqueue_script( 'hoho-chrome', get_stylesheet_directory_uri() . '/assets/chrome.js', array(), '1.0.12', true ); wp_enqueue_script( 'hoho-chrome', get_stylesheet_directory_uri() . '/assets/chrome.js', array(), '1.0.13', true );
wp_add_inline_style( wp_add_inline_style(
'hoho-chrome', 'hoho-chrome',

View File

@@ -12,8 +12,51 @@ $hoho_about = apply_filters(
$hoho_socials = hoho_socials(); $hoho_socials = hoho_socials();
$hoho_footer_col = function ( $location ) { /**
* Default links for a footer column that has no menu assigned, so the footer
* is never three empty headings. Pages are linked only if they exist.
*/
$hoho_footer_defaults = function ( $location ) {
$page = function ( $slugs, $label ) {
foreach ( (array) $slugs as $slug ) {
$p = get_page_by_path( $slug );
if ( $p && 'publish' === $p->post_status ) {
return array( $label, get_permalink( $p ) );
}
}
return null;
};
$links = array();
if ( 'hoho_quick' === $location ) {
$links[] = array( __( 'Home', 'hoponhopoff' ), home_url( '/' ) );
if ( get_post_type_archive_link( 'viator_tour' ) ) {
$links[] = array( __( 'All Tours', 'hoponhopoff' ), get_post_type_archive_link( 'viator_tour' ) );
}
$links[] = get_option( 'page_for_posts' ) ? array( __( 'Blog', 'hoponhopoff' ), get_permalink( get_option( 'page_for_posts' ) ) ) : $page( array( 'blogs', 'blog' ), __( 'Blog', 'hoponhopoff' ) );
} elseif ( 'hoho_tours' === $location && class_exists( 'VC_Structure' ) ) {
// The first cities from Settings → Viator Structure.
foreach ( array_slice( VC_Structure::cities( VC_Structure::get() ), 0, 6 ) as $city ) {
$links[] = array( $city['name'], home_url( '/' . $city['slug'] . '/' ) );
}
} elseif ( 'hoho_account' === $location ) {
$links[] = $page( array( 'contact-us', 'contact' ), __( 'Contact Us', 'hoponhopoff' ) );
$links[] = $page( array( 'about-us', 'about' ), __( 'About Us', 'hoponhopoff' ) );
$links[] = $page( array( 'faq', 'faqs' ), __( 'FAQ', 'hoponhopoff' ) );
}
$links = array_filter( $links );
if ( ! $links ) {
return;
}
echo '<ul>';
foreach ( $links as $link ) {
echo '<li><a href="' . esc_url( $link[1] ) . '">' . esc_html( $link[0] ) . '</a></li>';
}
echo '</ul>';
};
$hoho_footer_col = function ( $location ) use ( $hoho_footer_defaults ) {
if ( ! has_nav_menu( $location ) ) { if ( ! has_nav_menu( $location ) ) {
$hoho_footer_defaults( $location );
return; return;
} }
wp_nav_menu( wp_nav_menu(

View File

@@ -4,7 +4,7 @@ Theme URI: https://git.kaamyab.dev/vargha/TourWebsite
Description: Astra child theme that replaces the Astra header and footer with the "Hop On Hop Off" design. Pairs with the Viator Collector plugin. Description: Astra child theme that replaces the Astra header and footer with the "Hop On Hop Off" design. Pairs with the Viator Collector plugin.
Author: Amir Bagheri Author: Amir Bagheri
Template: astra Template: astra
Version: 1.0.12 Version: 1.0.13
Text Domain: hoponhopoff Text Domain: hoponhopoff
*/ */

Binary file not shown.

View File

@@ -0,0 +1,175 @@
/* Single tour — top section (photos + booking card first) and the mobile
booking bar. Hand-written, loaded after vc-design.css on tour pages only. */
/* No hero any more: the header turns solid (chrome.js) and the page starts
with the breadcrumb. */
.vc-app.vc-app .tour--top {
padding-block-start: var(--sp-5);
}
.vc-app.vc-app .tour__crumbs {
margin-block-end: var(--sp-3);
color: var(--c-ink);
}
.vc-app.vc-app .tour__crumbs li[aria-current] {
overflow: hidden;
max-width: 28ch;
text-overflow: ellipsis;
white-space: nowrap;
}
/* ---------- gallery: one large photo + two, the rest in the lightbox ---------- */
.vc-app.vc-app .tgal {
position: relative;
display: grid;
grid-template-columns: 2fr 1fr;
grid-template-rows: repeat(2, clamp(9rem, 6rem + 6vw, 12.5rem));
gap: var(--sp-1);
}
.vc-app.vc-app .tgal img {
width: 100%;
height: 100%;
aspect-ratio: auto;
border-radius: var(--r-card);
object-fit: cover;
}
.vc-app.vc-app .tgal .tgal__main {
grid-row: span 2;
}
.vc-app.vc-app .tgal .tgal__extra {
display: none;
}
.vc-app.vc-app .tgal--few {
grid-template-columns: 1fr;
}
.vc-app.vc-app .tgal--few .tgal__thumb {
display: none;
}
.vc-app.vc-app .tgal__all {
position: absolute;
right: var(--sp-2);
bottom: var(--sp-2);
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.5rem 0.875rem;
border: 0;
border-radius: var(--r-pill);
background: rgba(255, 255, 255, 0.95);
color: var(--c-ink);
font-size: 0.8125rem;
font-weight: 700;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.18);
cursor: pointer;
}
/* ---------- title row under the photos ---------- */
.vc-app.vc-app .tour--top .tour-head__title {
font-size: clamp(1.5rem, 1.1rem + 1.4vw, 2.25rem);
line-height: 1.2;
}
.vc-app.vc-app .tmeta__reviews {
margin-inline-start: 0.25rem;
font-weight: 500;
opacity: 0.6;
}
/* ---------- mobile booking bar ---------- */
.vc-app.vc-app .tbar {
display: none;
}
@media (max-width: 68.75rem) {
/* The bar does the booking now: the full card moves below the content. */
.vc-app.vc-app .tour--top .tour__aside {
order: 1;
}
.vc-app.vc-app .tbar {
position: fixed;
inset: auto 0 0 0;
z-index: 50;
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--sp-3);
padding: 0.75rem var(--gutter) calc(0.75rem + env(safe-area-inset-bottom));
background: var(--c-white);
box-shadow: 0 -6px 24px rgba(0, 0, 0, 0.12);
}
.vc-app.vc-app .tbar__price {
display: flex;
flex-direction: column;
margin: 0;
line-height: 1.15;
}
.vc-app.vc-app .tbar__price span {
font-size: 0.75rem;
opacity: 0.6;
}
.vc-app.vc-app .tbar__price b {
font-size: 1.375rem;
}
.vc-app.vc-app .tbar__cta {
background: var(--c-accent);
color: var(--c-black);
}
/* room for the bar so it never covers the footer */
.vc-app.vc-single {
padding-block-end: 5rem;
}
}
@media (max-width: 48rem) {
/* Swipe through every photo; tap one (or the button) for the lightbox. */
.vc-app.vc-app .tgal {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
scrollbar-width: none;
margin-inline: calc(var(--gutter) * -1);
padding-inline: var(--gutter);
}
.vc-app.vc-app .tgal::-webkit-scrollbar {
display: none;
}
.vc-app.vc-app .tgal img,
.vc-app.vc-app .tgal .tgal__extra,
.vc-app.vc-app .tgal--few .tgal__thumb {
display: block;
flex: 0 0 88%;
aspect-ratio: 4 / 3;
scroll-snap-align: center;
}
/* every photo is already in the swipe row */
.vc-app.vc-app .tgal__all {
display: none;
}
/* the tour name is the page title right below — keep the trail short */
.vc-app.vc-app .tour__crumbs li[aria-current] {
display: none;
}
/* Compact 2 × 2 facts instead of four stacked full-width cards. */
.vc-app.vc-app .tour--top .facts {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}

View File

@@ -138,6 +138,9 @@
box.querySelector(".vc-lightbox__next").addEventListener("click", function () { show(idx + 1); }); box.querySelector(".vc-lightbox__next").addEventListener("click", function () { show(idx + 1); });
box.querySelector(".vc-lightbox__close").addEventListener("click", close); box.querySelector(".vc-lightbox__close").addEventListener("click", close);
box.addEventListener("click", function (e) { if (e.target === box) close(); }); box.addEventListener("click", function (e) { if (e.target === box) close(); });
/* tour page top gallery: "All N photos" opens at the first photo not on show */
var allBtn = app.querySelector(".tgal__all");
if (allBtn) allBtn.addEventListener("click", function () { open(Math.min(3, imgs.length - 1)); });
document.addEventListener("keydown", function (e) { document.addEventListener("keydown", function (e) {
if (!box.classList.contains("is-open")) return; if (!box.classList.contains("is-open")) return;
if (e.key === "Escape") close(); if (e.key === "Escape") close();

View File

@@ -34,7 +34,7 @@ class VC_Display {
// equal-specificity ties. Loading last makes our design win those ties. // equal-specificity ties. Loading last makes our design win those ties.
add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_assets' ), 100 ); add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_assets' ), 100 );
add_action( 'template_redirect', array( __CLASS__, 'maybe_enrich_single' ) ); add_action( 'template_redirect', array( __CLASS__, 'maybe_enrich_single' ) );
add_action( 'pre_get_posts', array( __CLASS__, 'filter_archive_query' ) ); add_action( 'pre_get_posts', array( __CLASS__, 'filter_archive_query' ), 999 );
add_filter( 'single_template', array( __CLASS__, 'single_template' ) ); add_filter( 'single_template', array( __CLASS__, 'single_template' ) );
add_filter( 'archive_template', array( __CLASS__, 'archive_template' ) ); add_filter( 'archive_template', array( __CLASS__, 'archive_template' ) );
add_filter( 'taxonomy_template', array( __CLASS__, 'taxonomy_template' ) ); add_filter( 'taxonomy_template', array( __CLASS__, 'taxonomy_template' ) );
@@ -164,6 +164,10 @@ class VC_Display {
} }
wp_enqueue_style( 'vc-design', VC_PLUGIN_URL . 'assets/css/vc-design.css', $deps, VC_VERSION ); wp_enqueue_style( 'vc-design', VC_PLUGIN_URL . 'assets/css/vc-design.css', $deps, VC_VERSION );
wp_add_inline_style( 'vc-design', self::astra_overrides() ); wp_add_inline_style( 'vc-design', self::astra_overrides() );
if ( $is_single ) {
// Tour page top section + mobile booking bar (after the overrides above).
wp_enqueue_style( 'vc-tour', VC_PLUGIN_URL . 'assets/css/vc-tour.css', array( 'vc-design' ), VC_VERSION );
}
// Front-end interactions: card rails, FAQ accordion, testimonials // Front-end interactions: card rails, FAQ accordion, testimonials
// marquee, date field, and auto-submit on quick filter toggles. // marquee, date field, and auto-submit on quick filter toggles.
@@ -318,6 +322,8 @@ class VC_Display {
* Apply the filter state to the main tours query (archive + taxonomy pages). * Apply the filter state to the main tours query (archive + taxonomy pages).
*/ */
public static function filter_archive_query( $query ) { public static function filter_archive_query( $query ) {
// Runs late (priority 999, see init) so another plugin/theme setting the
// page size can't leave the listing at 10 while it says "1–12".
if ( is_admin() || ! $query->is_main_query() ) { if ( is_admin() || ! $query->is_main_query() ) {
return; return;
} }
@@ -346,7 +352,9 @@ class VC_Display {
*/ */
public static function build_query_vars( array $base_tax = array() ) { public static function build_query_vars( array $base_tax = array() ) {
$f = self::get_filter_state(); $f = self::get_filter_state();
$vars = array( 'posts_per_page' => 12, 'ignore_sticky_posts' => true ); // posts_per_archive_page too: when set (Settings or another plugin), WP_Query
// uses it instead of posts_per_page on archives.
$vars = array( 'posts_per_page' => 12, 'posts_per_archive_page' => 12, 'ignore_sticky_posts' => true );
if ( '' !== $f['q'] ) { if ( '' !== $f['q'] ) {
$vars['s'] = $f['q']; $vars['s'] = $f['q'];
@@ -741,7 +749,7 @@ class VC_Display {
'viator_browse_activities' 'viator_browse_activities'
); );
$terms = self::get_section_terms( 'viator_tag', $atts['count'], $atts['ids'] ); $terms = VC_Tags::public_terms( self::get_section_terms( 'viator_tag', $atts['count'], $atts['ids'] ) );
if ( empty( $terms ) ) { if ( empty( $terms ) ) {
return ''; return '';
} }

View File

@@ -34,6 +34,7 @@ final class VC_Plugin {
'includes/class-vc-cpt.php', 'includes/class-vc-cpt.php',
'includes/class-vc-taxonomies.php', 'includes/class-vc-taxonomies.php',
'includes/class-vc-structure.php', 'includes/class-vc-structure.php',
'includes/class-vc-tags.php',
'includes/class-vc-api.php', 'includes/class-vc-api.php',
'includes/class-vc-importer.php', 'includes/class-vc-importer.php',
'includes/class-vc-cron.php', 'includes/class-vc-cron.php',

View File

@@ -0,0 +1,41 @@
<?php
defined( 'ABSPATH' ) || exit;
/**
* Viator attaches some internal, trade-only tags to products (e.g. "Agent Low
* Quality Product"). They are imported like any tag but must never be shown
* to visitors — not as a tour's category, not as a filter option.
*/
class VC_Tags {
/**
* Patterns (case-insensitive) that mark a tag as internal. Extend with the
* `vc_internal_tag_patterns` filter when WordPress is loaded.
*/
public static function patterns() {
$patterns = array( '/\bagent\b/i', '/\blow quality\b/i', '/\bquality product\b/i' );
return function_exists( 'apply_filters' ) ? (array) apply_filters( 'vc_internal_tag_patterns', $patterns ) : $patterns;
}
public static function is_internal( $name ) {
foreach ( self::patterns() as $pattern ) {
if ( preg_match( $pattern, (string) $name ) ) {
return true;
}
}
return false;
}
/**
* Drop internal tags from a list of term objects (keeps order).
*/
public static function public_terms( $terms ) {
return array_values(
array_filter(
(array) $terms,
function ( $term ) {
return is_object( $term ) && ! self::is_internal( $term->name );
}
)
);
}
}

View File

@@ -36,7 +36,7 @@ if ( is_wp_error( $vc_action ) || ! $vc_action ) {
$vc_destinations = get_terms( array( 'taxonomy' => 'viator_destination', 'hide_empty' => true, 'number' => 20, 'orderby' => 'count', 'order' => 'DESC' ) ); $vc_destinations = get_terms( array( 'taxonomy' => 'viator_destination', 'hide_empty' => true, 'number' => 20, 'orderby' => 'count', 'order' => 'DESC' ) );
$vc_categories = get_terms( array( 'taxonomy' => 'viator_tag', 'hide_empty' => true, 'number' => 20, 'orderby' => 'count', 'order' => 'DESC' ) ); $vc_categories = get_terms( array( 'taxonomy' => 'viator_tag', 'hide_empty' => true, 'number' => 20, 'orderby' => 'count', 'order' => 'DESC' ) );
$vc_destinations = is_wp_error( $vc_destinations ) ? array() : $vc_destinations; $vc_destinations = is_wp_error( $vc_destinations ) ? array() : $vc_destinations;
$vc_categories = is_wp_error( $vc_categories ) ? array() : $vc_categories; $vc_categories = is_wp_error( $vc_categories ) ? array() : VC_Tags::public_terms( $vc_categories );
$vc_total = (int) $vc_query->found_posts; $vc_total = (int) $vc_query->found_posts;
$vc_per = 12; $vc_per = 12;

View File

@@ -25,7 +25,7 @@ while ( have_posts() ) :
$dest_terms = get_the_terms( $post_id, 'viator_destination' ); $dest_terms = get_the_terms( $post_id, 'viator_destination' );
$dest_names = ( $dest_terms && ! is_wp_error( $dest_terms ) ) ? wp_list_pluck( $dest_terms, 'name' ) : array(); $dest_names = ( $dest_terms && ! is_wp_error( $dest_terms ) ) ? wp_list_pluck( $dest_terms, 'name' ) : array();
$cat_terms = get_the_terms( $post_id, 'viator_tag' ); $cat_terms = get_the_terms( $post_id, 'viator_tag' );
$cat_names = ( $cat_terms && ! is_wp_error( $cat_terms ) ) ? array_slice( wp_list_pluck( $cat_terms, 'name' ), 0, 2 ) : array(); $cat_names = ( $cat_terms && ! is_wp_error( $cat_terms ) ) ? array_slice( wp_list_pluck( VC_Tags::public_terms( $cat_terms ), 'name' ), 0, 2 ) : array();
$gallery = vc_json_decode_meta( get_post_meta( $post_id, '_vc_gallery_image_urls', true ), array() ); $gallery = vc_json_decode_meta( get_post_meta( $post_id, '_vc_gallery_image_urls', true ), array() );
$gallery = array_values( array_filter( (array) $gallery ) ); $gallery = array_values( array_filter( (array) $gallery ) );
@@ -33,48 +33,62 @@ while ( have_posts() ) :
if ( ! $primary && ! empty( $gallery ) ) { if ( ! $primary && ! empty( $gallery ) ) {
$primary = $gallery[0]; $primary = $gallery[0];
} }
$more = array_slice( array_values( array_diff( $gallery, array( $primary ) ) ), 0, 5 ); // The featured image (sharp, full size) leads the gallery when set;
$big_word = ! empty( $dest_names ) ? $dest_names[0] : get_the_title(); // Viator's own photos follow.
$lead = has_post_thumbnail( $post_id ) ? (string) get_the_post_thumbnail_url( $post_id, 'large' ) : $primary;
$all = array_values( array_unique( array_filter( array_merge( array( $lead, $primary ), $gallery ) ) ) );
// Breadcrumb: Home › Region › City › Tour (the tour's canonical city).
$crumbs = array( array( __( 'Home', 'viator-collector' ), home_url( '/' ) ) );
$city_slug = class_exists( 'VC_Routing' ) ? VC_Routing::product_city( $post_id ) : '';
$city = $city_slug ? get_term_by( 'slug', $city_slug, 'viator_destination' ) : null;
if ( ! $city && $dest_terms && ! is_wp_error( $dest_terms ) ) {
$city = $dest_terms[0];
}
if ( $city && $city->parent ) {
$region = get_term( $city->parent, 'viator_destination' );
if ( $region instanceof WP_Term && ! is_wp_error( get_term_link( $region ) ) ) {
$crumbs[] = array( $region->name, get_term_link( $region ) );
}
}
if ( $city && ! is_wp_error( get_term_link( $city ) ) ) {
$crumbs[] = array( $city->name, get_term_link( $city ) );
}
$book_href = $booking_url ? $booking_url : '#';
$book_attrs = $booking_url ? ' target="_blank" rel="nofollow noopener"' : '';
?> ?>
<div class="vc-app vc-single"> <div class="vc-app vc-single">
<section class="hero-tour"> <section class="section tour tour--top">
<?php
// Prefer a WordPress featured image (full size, responsive srcset) for a
// sharp hero — Viator's own image URLs cap out around 720px and look soft
// blown up to a full-width banner. Fall back to the Viator primary image.
if ( has_post_thumbnail( $post_id ) ) :
echo get_the_post_thumbnail( $post_id, 'full', array( 'class' => 'hero-tour__photo', 'fetchpriority' => 'high', 'alt' => '', 'sizes' => '100vw' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
elseif ( $primary ) :
?>
<img class="hero-tour__photo" src="<?php echo esc_url( $primary ); ?>" alt="" fetchpriority="high">
<?php
endif;
?>
<div class="hero-tour__sky" aria-hidden="true"></div>
<div class="container"> <div class="container">
<div class="hero-tour__block"> <nav class="crumbs tour__crumbs" aria-label="<?php echo esc_attr__( 'Breadcrumb', 'viator-collector' ); ?>">
<p class="hero-tour__kicker"><?php echo esc_html__( 'Welcome to', 'viator-collector' ); ?></p> <ol class="crumbs__list">
<div class="hero-tour__stage"> <?php foreach ( $crumbs as $crumb ) : ?>
<h1 class="hero-tour__word"><?php echo esc_html( $big_word ); ?></h1> <li><a href="<?php echo esc_url( $crumb[1] ); ?>"><?php echo esc_html( $crumb[0] ); ?></a></li>
<?php <?php endforeach; ?>
$vc_figure_id = (int) vc_get_option( 'single_hero_figure_id', 0 ); <li aria-current="page"><?php the_title(); ?></li>
if ( $vc_figure_id ) { </ol>
echo wp_get_attachment_image( $vc_figure_id, 'medium_large', false, array( 'class' => 'hero-tour__figure', 'loading' => 'eager', 'alt' => '' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped </nav>
}
?>
</div>
</div>
</div> </div>
</section>
<section class="section tour">
<div class="container tour__layout"> <div class="container tour__layout">
<div class="tour__main"> <div class="tour__main">
<?php if ( $all ) : ?>
<?php // .gallery: the lightbox (vc-design.js) opens on any of these images, hidden ones included. ?>
<div class="gallery tgal<?php echo count( $all ) < 3 ? ' tgal--few' : ''; ?>">
<?php foreach ( $all as $i => $url ) : ?>
<img class="<?php echo 0 === $i ? 'tgal__main' : ( $i < 3 ? 'tgal__thumb' : 'tgal__extra' ); ?>" src="<?php echo esc_url( $url ); ?>" alt="<?php echo esc_attr( get_the_title() ); ?>"<?php echo 0 === $i ? ' fetchpriority="high"' : ' loading="lazy"'; ?>>
<?php endforeach; ?>
<?php if ( count( $all ) > 3 ) : ?>
<button class="tgal__all" type="button"><?php echo vc_icon( 'grid' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?><?php echo esc_html( sprintf( __( 'All %d photos', 'viator-collector' ), count( $all ) ) ); ?></button>
<?php endif; ?>
</div>
<?php endif; ?>
<header class="tour-head"> <header class="tour-head">
<h2 class="tour-head__title"><?php the_title(); ?></h2> <h1 class="tour-head__title"><?php the_title(); ?></h1>
<div class="tmeta"> <div class="tmeta">
<?php if ( '' !== (string) $rating ) : ?> <?php if ( '' !== (string) $rating ) : ?>
<span class="tmeta__item"><?php echo vc_icon( 'star', 'icon--12 tmeta__star' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?><b><?php echo esc_html( number_format_i18n( (float) $rating, 1 ) ); ?></b><i>/5</i></span> <span class="tmeta__item"><?php echo vc_icon( 'star', 'icon--12 tmeta__star' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?><b><?php echo esc_html( number_format_i18n( (float) $rating, 1 ) ); ?></b><i>/5</i><?php if ( $reviews ) : ?> <a class="tmeta__reviews" href="#reviews">(<?php echo esc_html( sprintf( _n( '%s review', '%s reviews', $reviews, 'viator-collector' ), number_format_i18n( $reviews ) ) ); ?>)</a><?php endif; ?></span>
<?php endif; ?> <?php endif; ?>
<?php if ( ! empty( $dest_names ) ) : ?> <?php if ( ! empty( $dest_names ) ) : ?>
<span class="tmeta__rule" aria-hidden="true"></span> <span class="tmeta__rule" aria-hidden="true"></span>
@@ -109,18 +123,6 @@ while ( have_posts() ) :
</section> </section>
<?php endif; ?> <?php endif; ?>
<?php if ( $primary ) : ?>
<section class="tour__block">
<h3 class="tour__ct"><?php echo esc_html__( 'Gallery', 'viator-collector' ); ?></h3>
<div class="gallery">
<img class="gallery__hero" src="<?php echo esc_url( $primary ); ?>" alt="<?php echo esc_attr( get_the_title() ); ?>" loading="lazy">
<?php foreach ( $more as $url ) : ?>
<img src="<?php echo esc_url( $url ); ?>" alt="<?php echo esc_attr( get_the_title() ); ?>" loading="lazy">
<?php endforeach; ?>
</div>
</section>
<?php endif; ?>
<?php <?php
$inc = array_filter( array_map( 'vc_feature_text', (array) vc_json_decode_meta( get_post_meta( $post_id, '_vc_inclusions', true ), array() ) ) ); $inc = array_filter( array_map( 'vc_feature_text', (array) vc_json_decode_meta( get_post_meta( $post_id, '_vc_inclusions', true ), array() ) ) );
$exc = array_filter( array_map( 'vc_feature_text', (array) vc_json_decode_meta( get_post_meta( $post_id, '_vc_exclusions', true ), array() ) ) ); $exc = array_filter( array_map( 'vc_feature_text', (array) vc_json_decode_meta( get_post_meta( $post_id, '_vc_exclusions', true ), array() ) ) );
@@ -240,7 +242,7 @@ while ( have_posts() ) :
<span class="price__unit"><?php echo esc_html( ( $currency ? strtoupper( $currency ) . ' ' : '' ) . __( '/ Per person', 'viator-collector' ) ); ?></span> <span class="price__unit"><?php echo esc_html( ( $currency ? strtoupper( $currency ) . ' ' : '' ) . __( '/ Per person', 'viator-collector' ) ); ?></span>
</p> </p>
<?php endif; ?> <?php endif; ?>
<a class="btn btn--dark bookcard__cta" href="<?php echo esc_url( $booking_url ? $booking_url : '#' ); ?>"<?php echo $booking_url ? ' target="_blank" rel="nofollow noopener"' : ''; ?>><?php echo esc_html__( 'Check Availability', 'viator-collector' ); ?></a> <a class="btn btn--dark bookcard__cta" href="<?php echo esc_url( $book_href ); ?>"<?php echo $book_attrs; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>><?php echo esc_html__( 'Check Availability', 'viator-collector' ); ?></a>
</div> </div>
<div class="statcard statcard--teal"> <div class="statcard statcard--teal">
@@ -262,6 +264,14 @@ while ( have_posts() ) :
</aside> </aside>
</div> </div>
</section> </section>
<?php // Phones/tablets: price + booking button stay on screen (hidden on desktop, where the sidebar is sticky). ?>
<div class="tbar">
<?php if ( '' !== (string) $price ) : ?>
<p class="tbar__price"><span><?php echo esc_html__( 'From', 'viator-collector' ); ?></span><b><?php echo esc_html( vc_money( $price, $currency ) ); ?></b></p>
<?php endif; ?>
<a class="btn tbar__cta" href="<?php echo esc_url( $book_href ); ?>"<?php echo $book_attrs; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>><?php echo esc_html__( 'Check Availability', 'viator-collector' ); ?></a>
</div>
</div><!-- #vc-app --> </div><!-- #vc-app -->
<?php <?php
endwhile; endwhile;

View File

@@ -8,6 +8,7 @@
define( 'ABSPATH', __DIR__ . '/' ); define( 'ABSPATH', __DIR__ . '/' );
require_once dirname( __DIR__ ) . '/includes/class-vc-structure.php'; require_once dirname( __DIR__ ) . '/includes/class-vc-structure.php';
require_once dirname( __DIR__ ) . '/includes/class-vc-tags.php';
function vc_assert_same( $expected, $actual, $msg = '' ) { function vc_assert_same( $expected, $actual, $msg = '' ) {
if ( $expected !== $actual ) { if ( $expected !== $actual ) {

View File

@@ -0,0 +1,28 @@
<?php
/**
* VC_Tags unit tests: Viator's internal tags never reach visitors.
*/
function test_internal_tag_agent_low_quality() {
vc_assert_same( true, VC_Tags::is_internal( 'Agent Low Quality Product' ) );
}
function test_internal_tag_variants() {
vc_assert_same( true, VC_Tags::is_internal( 'Low Quality Supplier' ) );
vc_assert_same( true, VC_Tags::is_internal( 'agent quality product' ) );
}
function test_public_tags_kept() {
vc_assert_same( false, VC_Tags::is_internal( 'Audio Guides' ) );
vc_assert_same( false, VC_Tags::is_internal( 'Excellent Quality' ) );
vc_assert_same( false, VC_Tags::is_internal( 'Hop-on Hop-off Tours' ) );
}
function test_public_terms_filters_objects() {
$terms = array(
(object) array( 'name' => 'Agent Low Quality Product' ),
(object) array( 'name' => 'Audio Guides' ),
(object) array( 'name' => 'Bus Tours' ),
);
vc_assert_same( array( 'Audio Guides', 'Bus Tours' ), array_column( VC_Tags::public_terms( $terms ), 'name' ) );
}

View File

@@ -4,13 +4,13 @@ defined('ABSPATH') || exit;
/** /**
* Plugin Name: Viator Collector * Plugin Name: Viator Collector
* Description: Import Viator tours using the Viator Partner API and display them with native WordPress post types, templates, and shortcodes. * Description: Import Viator tours using the Viator Partner API and display them with native WordPress post types, templates, and shortcodes.
* Version: 1.9.4 * Version: 1.9.5
* Author: Amir Bagheri * Author: Amir Bagheri
* License: GPL-2.0-or-later * License: GPL-2.0-or-later
* Text Domain: viator-collector * Text Domain: viator-collector
*/ */
define('VC_VERSION', '1.9.4'); define('VC_VERSION', '1.9.5');
define('VC_PLUGIN_FILE', __FILE__); define('VC_PLUGIN_FILE', __FILE__);
define('VC_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('VC_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('VC_PLUGIN_URL', plugin_dir_url(__FILE__)); define('VC_PLUGIN_URL', plugin_dir_url(__FILE__));