Files
hoponhopofftickets/viator-collector/templates/single-viator_tour.php
Amir Bagheri de9814cf3d 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>
2026-10-29 21:01:13 +03:30

280 lines
16 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Single Viator tour — "Hop On Hop Off" design (ported from new front/tour.html).
* Itinerary and Reviews are not in the mockup; they reuse the design's own
* primitives (.notes, .tmeta) so they sit naturally alongside the rest.
*/
defined( 'ABSPATH' ) || exit;
get_header();
while ( have_posts() ) :
the_post();
$post_id = get_the_ID();
$rating = get_post_meta( $post_id, '_vc_rating', true );
$reviews = (int) get_post_meta( $post_id, '_vc_review_count', true );
$price = get_post_meta( $post_id, '_vc_price_from', true );
$currency = get_post_meta( $post_id, '_vc_currency', true );
$supplier = get_post_meta( $post_id, '_vc_supplier', true );
$duration_min = (int) get_post_meta( $post_id, '_vc_duration_minutes', true );
$max_trav = (int) get_post_meta( $post_id, '_vc_max_travelers', true );
$freecancel = (int) get_post_meta( $post_id, '_vc_free_cancellation', true );
$booking_url = get_post_meta( $post_id, '_vc_booking_url', true );
$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();
$cat_terms = get_the_terms( $post_id, 'viator_tag' );
$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 = array_values( array_filter( (array) $gallery ) );
$primary = get_post_meta( $post_id, '_vc_primary_image_url', true );
if ( ! $primary && ! empty( $gallery ) ) {
$primary = $gallery[0];
}
// The featured image (sharp, full size) leads the gallery when set;
// 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">
<section class="section tour tour--top">
<div class="container">
<nav class="crumbs tour__crumbs" aria-label="<?php echo esc_attr__( 'Breadcrumb', 'viator-collector' ); ?>">
<ol class="crumbs__list">
<?php foreach ( $crumbs as $crumb ) : ?>
<li><a href="<?php echo esc_url( $crumb[1] ); ?>"><?php echo esc_html( $crumb[0] ); ?></a></li>
<?php endforeach; ?>
<li aria-current="page"><?php the_title(); ?></li>
</ol>
</nav>
</div>
<div class="container tour__layout">
<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">
<h1 class="tour-head__title"><?php the_title(); ?></h1>
<div class="tmeta">
<?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><?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 if ( ! empty( $dest_names ) ) : ?>
<span class="tmeta__rule" aria-hidden="true"></span>
<span class="tmeta__item"><?php echo vc_icon( 'location', 'tmeta__icon' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?><?php echo esc_html( implode( ', ', array_slice( $dest_names, 0, 2 ) ) ); ?></span>
<?php endif; ?>
<?php if ( $supplier ) : ?>
<span class="tmeta__rule" aria-hidden="true"></span>
<span class="tmeta__item"><?php echo vc_icon( 'person', 'tmeta__icon' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?><?php echo esc_html__( 'from', 'viator-collector' ) . ' ' . esc_html( $supplier ); ?></span>
<?php endif; ?>
</div>
</header>
<ul class="facts">
<?php if ( $duration_min ) : ?>
<li class="fact"><?php echo vc_icon( 'duration', 'fact__icon' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?><div class="fact__text"><span class="fact__k"><?php echo esc_html__( 'Duration', 'viator-collector' ); ?></span><span class="fact__v"><?php echo esc_html( vc_duration_human( $duration_min ) ); ?></span></div></li>
<?php endif; ?>
<?php if ( $max_trav ) : ?>
<li class="fact"><?php echo vc_icon( 'person', 'fact__icon' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?><div class="fact__text"><span class="fact__k"><?php echo esc_html__( 'Max Travelers', 'viator-collector' ); ?></span><span class="fact__v"><?php echo esc_html( sprintf( __( '%d people', 'viator-collector' ), $max_trav ) ); ?></span></div></li>
<?php endif; ?>
<?php if ( $freecancel ) : ?>
<li class="fact"><?php echo vc_icon( 'shield-check', 'fact__icon' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?><div class="fact__text"><span class="fact__k"><?php echo esc_html__( 'Free Cancellation', 'viator-collector' ); ?></span><span class="fact__v"><?php echo esc_html__( 'Up to 24 hrs', 'viator-collector' ); ?></span></div></li>
<?php endif; ?>
<?php if ( ! empty( $cat_names ) ) : ?>
<li class="fact"><?php echo vc_icon( 'grid', 'fact__icon' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?><div class="fact__text"><span class="fact__k"><?php echo esc_html__( 'Category', 'viator-collector' ); ?></span><span class="fact__v"><?php echo esc_html( implode( ', ', $cat_names ) ); ?></span></div></li>
<?php endif; ?>
</ul>
<?php if ( get_the_content() ) : ?>
<section class="tour__block">
<h3 class="tour__ct"><?php echo esc_html__( 'Overview', 'viator-collector' ); ?></h3>
<div class="tour__text"><?php the_content(); ?></div>
</section>
<?php endif; ?>
<?php
$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() ) ) );
if ( $inc || $exc ) :
?>
<div class="lists">
<?php if ( $inc ) : ?>
<section class="ilist">
<h3 class="tour__ct"><?php echo esc_html__( "What's included", 'viator-collector' ); ?></h3>
<ul class="ilist__items">
<?php foreach ( $inc as $row ) : ?>
<li><span class="tick tick--yes" aria-hidden="true"><?php echo vc_icon( 'check', 'icon--12' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span><p><?php echo esc_html( $row ); ?></p></li>
<?php endforeach; ?>
</ul>
</section>
<?php endif; ?>
<?php if ( $exc ) : ?>
<section class="ilist">
<h3 class="tour__ct"><?php echo esc_html__( "What's Not included", 'viator-collector' ); ?></h3>
<ul class="ilist__items">
<?php foreach ( $exc as $row ) : ?>
<li><span class="tick tick--no" aria-hidden="true"><?php echo vc_icon( 'x', 'icon--12' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span><p><?php echo esc_html( $row ); ?></p></li>
<?php endforeach; ?>
</ul>
</section>
<?php endif; ?>
</div>
<?php endif; ?>
<?php
$steps = vc_itinerary_steps( vc_json_decode_meta( get_post_meta( $post_id, '_vc_itinerary', true ), array() ) );
if ( ! empty( $steps ) ) :
?>
<section class="tour__block">
<h3 class="tour__ct"><?php echo esc_html__( 'Itinerary', 'viator-collector' ); ?></h3>
<ul class="notes">
<?php foreach ( $steps as $step ) : ?>
<li class="note"><?php echo vc_icon( 'location', 'note__icon' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?><p><?php
$name = ! empty( $step['name'] ) ? '<strong>' . esc_html( $step['name'] ) . '</strong> ' : '';
echo $name . esc_html( ! empty( $step['description'] ) ? $step['description'] : '' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
?></p></li>
<?php endforeach; ?>
</ul>
</section>
<?php endif; ?>
<?php
$info = (array) vc_json_decode_meta( get_post_meta( $post_id, '_vc_additional_info', true ), array() );
$info_rows = array_filter( array_map( 'vc_feature_text', $info ) );
if ( ! empty( $info_rows ) ) :
?>
<section class="tour__block">
<h3 class="tour__ct"><?php echo esc_html__( 'Good to know', 'viator-collector' ); ?></h3>
<ul class="notes">
<?php foreach ( $info_rows as $row ) : ?>
<li class="note"><?php echo vc_icon( 'info', 'note__icon' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?><p><?php echo esc_html( $row ); ?></p></li>
<?php endforeach; ?>
</ul>
</section>
<?php endif; ?>
<?php
$policy = vc_json_decode_meta( get_post_meta( $post_id, '_vc_cancellation_policy', true ), array() );
$policy_text = is_scalar( $policy ) ? (string) $policy : (string) vc_array_get( $policy, 'description' );
$tiers = is_array( $policy ) ? (array) vc_array_get( $policy, 'refundEligibility', array() ) : array();
if ( $policy_text || $tiers ) :
?>
<section class="tour__block">
<h3 class="tour__ct"><?php echo esc_html__( 'Cancellation Policy', 'viator-collector' ); ?></h3>
<?php if ( $policy_text ) : ?><p class="tour__text"><?php echo esc_html( $policy_text ); ?></p><?php endif; ?>
<?php if ( $tiers ) : ?>
<div class="policy">
<?php
foreach ( $tiers as $tier ) :
$pct = (int) vc_array_get( $tier, 'percentageRefundable', 0 );
$max = vc_array_get( $tier, 'dayRangeMax', null );
$min = vc_array_get( $tier, 'dayRangeMin', null );
$yes = $pct > 0;
$when = ( null !== $max )
? sprintf( __( 'Less than %d hours before the start time', 'viator-collector' ), (int) $max * 24 )
: sprintf( __( '%d hours or more before the start time', 'viator-collector' ), (int) $min * 24 );
?>
<div class="statcard statcard--<?php echo $yes ? 'yes' : 'no'; ?>">
<?php echo vc_icon( $yes ? 'check' : 'x', 'icon--12 statcard__icon' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<div class="statcard__text">
<span class="statcard__k"><?php echo esc_html( sprintf( __( '%d%% Refund', 'viator-collector' ), $pct ) ); ?></span>
<span class="statcard__v"><?php echo esc_html( $when ); ?></span>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</section>
<?php endif; ?>
<?php if ( '' !== (string) $rating && $reviews ) : ?>
<section class="tour__block" id="reviews">
<h3 class="tour__ct"><?php echo esc_html__( 'Reviews', 'viator-collector' ); ?></h3>
<div class="tmeta">
<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__rule" aria-hidden="true"></span>
<span class="tmeta__item"><?php echo esc_html( sprintf( _n( '%s review', '%s reviews', $reviews, 'viator-collector' ), number_format_i18n( $reviews ) ) ); ?></span>
</div>
</section>
<?php endif; ?>
</div>
<aside class="tour__aside">
<div class="bookcard">
<h2 class="bookcard__title"><?php echo vc_icon( 'info' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?><?php echo esc_html__( 'Tour Details', 'viator-collector' ); ?></h2>
<div class="bookcard__price">
<?php if ( '' !== (string) $price ) : ?>
<p class="price">
<span class="price__k"><?php echo esc_html__( 'From', 'viator-collector' ); ?></span>
<span class="price__v"><?php echo esc_html( vc_money( $price, $currency ) ); ?></span>
<span class="price__unit"><?php echo esc_html( ( $currency ? strtoupper( $currency ) . ' ' : '' ) . __( '/ Per person', 'viator-collector' ) ); ?></span>
</p>
<?php endif; ?>
<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 class="statcard statcard--teal">
<?php echo vc_icon( 'shield-check', 'statcard__icon' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<div class="statcard__text">
<span class="statcard__k"><?php echo esc_html__( 'Free Cancellation', 'viator-collector' ); ?></span>
<span class="statcard__v"><?php echo esc_html__( 'Up to 24 hrs', 'viator-collector' ); ?></span>
</div>
</div>
<div class="statcard statcard--yes">
<?php echo vc_icon( 'shield-check', 'statcard__icon' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<div class="statcard__text">
<span class="statcard__k"><?php echo esc_html__( 'Reserve now, pay later!', 'viator-collector' ); ?></span>
<span class="statcard__v"><?php echo esc_html__( 'Secure your spot today', 'viator-collector' ); ?></span>
</div>
</div>
</div>
</aside>
</div>
</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 -->
<?php
endwhile;
get_footer();