- Added a lightbox feature for gallery images, allowing users to view photos in a modal with navigation controls. - Updated the SVG sprite to include inline styles for better accessibility. - Increased CSS specificity by doubling the `.vc-app` class to ensure styles override Astra theme defaults. - Introduced new shortcodes for bus tours, about section, and articles in the VC_Sections class. - Improved the handling of featured images in the single tour template for sharper display. - Updated version number to 1.8.15.
824 lines
34 KiB
PHP
824 lines
34 KiB
PHP
<?php
|
|
defined( 'ABSPATH' ) || exit;
|
|
|
|
/**
|
|
* Front-page section shortcodes — "Hop On Hop Off" design.
|
|
*
|
|
* All markup is scoped under a .vc-app wrapper and styled by assets/css/
|
|
* vc-design.css. Assets are enqueued by VC_Display when a page contains one of
|
|
* these shortcodes, so nothing loads on pages that don't use them.
|
|
*/
|
|
class VC_Sections {
|
|
|
|
const HERO = 'viator_hero';
|
|
const TOP_DESTS = 'viator_top_destinations';
|
|
const TOP_ATTRS = 'viator_top_attractions';
|
|
const ACTIVITIES = 'viator_browse_activities';
|
|
const TOP_TOURS = 'viator_top_tours';
|
|
const BUS_TOURS = 'viator_bus_tours';
|
|
const ABOUT = 'viator_about';
|
|
const ARTICLES = 'viator_articles';
|
|
const FAQ = 'viator_faq';
|
|
const REVIEWS = 'viator_testimonials';
|
|
|
|
/** Per-request counter so each rail gets a unique id for its nav buttons. */
|
|
private static $rail_seq = 0;
|
|
|
|
public static function init() {
|
|
add_shortcode( self::HERO, array( __CLASS__, 'hero' ) );
|
|
add_shortcode( 'viator_hero_slider', array( __CLASS__, 'hero' ) ); // back-compat alias
|
|
add_shortcode( self::TOP_DESTS, array( __CLASS__, 'top_destinations' ) );
|
|
add_shortcode( self::TOP_ATTRS, array( __CLASS__, 'top_attractions' ) );
|
|
add_shortcode( self::ACTIVITIES, array( __CLASS__, 'browse_activities' ) );
|
|
add_shortcode( self::TOP_TOURS, array( __CLASS__, 'top_tours' ) );
|
|
add_shortcode( self::BUS_TOURS, array( __CLASS__, 'bus_tours' ) );
|
|
add_shortcode( self::ABOUT, array( __CLASS__, 'about' ) );
|
|
add_shortcode( self::ARTICLES, array( __CLASS__, 'articles' ) );
|
|
add_shortcode( self::FAQ, array( __CLASS__, 'faq' ) );
|
|
add_shortcode( self::REVIEWS, array( __CLASS__, 'testimonials' ) );
|
|
}
|
|
|
|
/**
|
|
* [viator_hero title="Set out on the journey." image="https://…"]
|
|
*
|
|
* The booking hero: a location/date/seat form that routes to the tours
|
|
* archive pre-filtered by the chosen destination.
|
|
*/
|
|
public static function hero( $atts ) {
|
|
$atts = shortcode_atts(
|
|
array(
|
|
'title' => __( 'Set out on the journey.', 'viator-collector' ),
|
|
'eyebrow' => __( 'Tour & Travel', 'viator-collector' ),
|
|
'image' => '',
|
|
'image_id' => '', // WordPress media attachment ID (responsive).
|
|
),
|
|
$atts,
|
|
self::HERO
|
|
);
|
|
|
|
// Prefer a media-library image (responsive srcset): shortcode image_id,
|
|
// then a shortcode URL, then the plugin's Hero image setting. No bundled
|
|
// fallback — an empty source shows the CSS gradient.
|
|
if ( '' !== $atts['image_id'] ) {
|
|
$img = absint( $atts['image_id'] );
|
|
} elseif ( '' !== $atts['image'] ) {
|
|
$img = $atts['image'];
|
|
} else {
|
|
$img = vc_hero_image_source();
|
|
}
|
|
$archive = get_post_type_archive_link( 'viator_tour' );
|
|
$archive = $archive ? $archive : home_url( '/' );
|
|
|
|
$dests = get_terms( array( 'taxonomy' => 'viator_destination', 'hide_empty' => true, 'number' => 12, 'orderby' => 'count', 'order' => 'DESC' ) );
|
|
$dests = is_wp_error( $dests ) ? array() : $dests;
|
|
|
|
ob_start();
|
|
?>
|
|
<div class="vc-app vc-sections">
|
|
<section class="hero">
|
|
<div class="hero__media">
|
|
<?php echo vc_hero_img_html( $img, 'hero__img', true ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
|
<div class="hero__scrim" aria-hidden="true"></div>
|
|
</div>
|
|
<div class="container hero__content">
|
|
<p class="pill hero__eyebrow"><?php echo esc_html( $atts['eyebrow'] ); ?></p>
|
|
<h1 class="hero__title"><?php echo esc_html( $atts['title'] ); ?></h1>
|
|
|
|
<form class="booking" method="get" action="<?php echo esc_url( $archive ); ?>" novalidate>
|
|
<div class="booking__row">
|
|
<div class="field">
|
|
<span class="field__label"><?php echo vc_icon( 'location' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?><label for="vc-f-location"><?php echo esc_html__( 'Location', 'viator-collector' ); ?></label></span>
|
|
<div class="select">
|
|
<select class="select__input" id="vc-f-location" name="dest[]">
|
|
<option value=""><?php echo esc_html__( 'All destinations', 'viator-collector' ); ?></option>
|
|
<?php foreach ( $dests as $t ) : ?>
|
|
<option value="<?php echo esc_attr( $t->slug ); ?>"><?php echo esc_html( $t->name ); ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
<?php echo vc_icon( 'chevron', 'select__chevron' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<span class="field__label"><?php echo vc_icon( 'calendar' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?><label for="vc-f-date"><?php echo esc_html__( 'Date', 'viator-collector' ); ?></label></span>
|
|
<div class="select">
|
|
<input class="select__input select__input--date" id="vc-f-date" name="date" type="text" placeholder="<?php echo esc_attr__( 'Select Date', 'viator-collector' ); ?>" data-datefield>
|
|
<?php echo vc_icon( 'chevron', 'select__chevron' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<span class="field__label"><?php echo vc_icon( 'seat' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?><label for="vc-f-seat"><?php echo esc_html__( 'Seat', 'viator-collector' ); ?></label></span>
|
|
<div class="select">
|
|
<select class="select__input" id="vc-f-seat" name="seat">
|
|
<option value="1"><?php echo esc_html__( 'One Seat', 'viator-collector' ); ?></option>
|
|
<option value="2"><?php echo esc_html__( 'Two Seats', 'viator-collector' ); ?></option>
|
|
<option value="3"><?php echo esc_html__( 'Three Seats', 'viator-collector' ); ?></option>
|
|
<option value="4"><?php echo esc_html__( 'Four Seats', 'viator-collector' ); ?></option>
|
|
</select>
|
|
<?php echo vc_icon( 'chevron', 'select__chevron' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
|
</div>
|
|
</div>
|
|
|
|
<button class="btn btn--dark booking__submit" type="submit"><?php echo esc_html__( 'Book now', 'viator-collector' ); ?></button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<?php echo vc_ring_badge( 'SCROLL DOWN · SCROLL DOWN · ', $archive, 'scroll' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
|
</section>
|
|
</div>
|
|
<?php
|
|
return ob_get_clean();
|
|
}
|
|
|
|
/**
|
|
* Open a titled rail section and return its unique rail id. Caller fills the
|
|
* <ul> with cards and then calls close_rail().
|
|
*/
|
|
private static function open_rail( $title, $lead, $view_all_url, $view_all_text, $rail_class = 'rail--dest' ) {
|
|
$id = 'vcRail' . ( ++self::$rail_seq );
|
|
ob_start();
|
|
?>
|
|
<div class="vc-app vc-sections">
|
|
<section class="section">
|
|
<div class="container">
|
|
<div class="sec-head">
|
|
<h2 class="sec-head__title"><?php echo wp_kses( $title, array( 'br' => array() ) ); ?></h2>
|
|
<?php if ( $lead ) : ?><p class="sec-head__lead"><?php echo esc_html( $lead ); ?></p><?php endif; ?>
|
|
<div class="sec-head__actions">
|
|
<?php if ( $view_all_url ) : ?>
|
|
<a class="btn btn--ghost btn--wide" href="<?php echo esc_url( $view_all_url ); ?>"><span><?php echo esc_html( $view_all_text ); ?></span><?php echo vc_icon( 'arrow-ne' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></a>
|
|
<?php endif; ?>
|
|
<div class="rail-nav">
|
|
<button class="btn btn--ghost rail-nav__btn" type="button" data-rail-prev="<?php echo esc_attr( $id ); ?>" aria-label="<?php echo esc_attr__( 'Previous', 'viator-collector' ); ?>"><?php echo vc_icon( 'arrow-left' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></button>
|
|
<button class="btn btn--ghost rail-nav__btn" type="button" data-rail-next="<?php echo esc_attr( $id ); ?>" aria-label="<?php echo esc_attr__( 'Next', 'viator-collector' ); ?>"><?php echo vc_icon( 'arrow-right' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<ul class="rail <?php echo esc_attr( $rail_class ); ?>" id="<?php echo esc_attr( $id ); ?>">
|
|
<?php
|
|
return array( 'id' => $id, 'head' => ob_get_clean() );
|
|
}
|
|
|
|
private static function close_rail() {
|
|
return '</ul></div></section></div>';
|
|
}
|
|
|
|
/**
|
|
* [viator_top_tours limit="8" title="Most Liked Tours"]
|
|
*/
|
|
public static function top_tours( $atts ) {
|
|
$atts = shortcode_atts(
|
|
array(
|
|
'ids' => '',
|
|
'codes' => '',
|
|
'destination' => '',
|
|
'tag' => '',
|
|
'limit' => 8,
|
|
'orderby' => 'rating',
|
|
'title' => __( 'Most Liked<br>Tours', 'viator-collector' ),
|
|
'lead' => __( 'Hand-picked tours travellers loved most.', 'viator-collector' ),
|
|
),
|
|
$atts,
|
|
self::TOP_TOURS
|
|
);
|
|
return self::tour_rail( $atts );
|
|
}
|
|
|
|
/**
|
|
* [viator_top_attractions limit="8"]
|
|
*/
|
|
public static function top_attractions( $atts ) {
|
|
$atts = shortcode_atts(
|
|
array(
|
|
'ids' => '',
|
|
'codes' => '',
|
|
'destination' => '',
|
|
'tag' => '',
|
|
'limit' => 8,
|
|
'orderby' => 'rating',
|
|
'title' => __( 'Top<br>Attractions', 'viator-collector' ),
|
|
'lead' => __( 'The must-see experiences everyone books.', 'viator-collector' ),
|
|
),
|
|
$atts,
|
|
self::TOP_ATTRS
|
|
);
|
|
return self::tour_rail( $atts );
|
|
}
|
|
|
|
/**
|
|
* [viator_browse_activities limit="8"]
|
|
*/
|
|
public static function browse_activities( $atts ) {
|
|
$atts = shortcode_atts(
|
|
array(
|
|
'ids' => '',
|
|
'codes' => '',
|
|
'destination' => '',
|
|
'tag' => '',
|
|
'limit' => 8,
|
|
'orderby' => 'rating',
|
|
'title' => __( 'Browse By<br>Activity', 'viator-collector' ),
|
|
'lead' => __( 'Find your style — from city sights to river cruises.', 'viator-collector' ),
|
|
),
|
|
$atts,
|
|
self::ACTIVITIES
|
|
);
|
|
return self::tour_rail( $atts );
|
|
}
|
|
|
|
/**
|
|
* Shared: a rail of tour cards from a curated/filtered query.
|
|
*/
|
|
private static function tour_rail( array $atts ) {
|
|
$query = self::tour_query( $atts );
|
|
if ( ! $query->have_posts() ) {
|
|
return '';
|
|
}
|
|
|
|
$archive = get_post_type_archive_link( 'viator_tour' );
|
|
$rail = self::open_rail( $atts['title'], $atts['lead'], $archive, __( 'See all', 'viator-collector' ) );
|
|
|
|
ob_start();
|
|
echo $rail['head']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
|
while ( $query->have_posts() ) {
|
|
$query->the_post();
|
|
echo vc_get_template( 'card-tour.php', array( 'post_id' => get_the_ID(), 'variant' => 'rail' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
|
}
|
|
echo self::close_rail(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
|
wp_reset_postdata();
|
|
return ob_get_clean();
|
|
}
|
|
|
|
/**
|
|
* [viator_top_destinations limit="8" title="Where you wanna go?"]
|
|
*
|
|
* A rail of destination-term cards; each links to the term archive.
|
|
*/
|
|
public static function top_destinations( $atts ) {
|
|
$atts = shortcode_atts(
|
|
array(
|
|
'ids' => '',
|
|
'slugs' => '',
|
|
'limit' => 8,
|
|
'orderby' => 'count',
|
|
'title' => __( 'Where you<br>wanna go?', 'viator-collector' ),
|
|
'lead' => __( 'Popular destinations our travellers explore.', 'viator-collector' ),
|
|
'link_text' => __( 'View tours', 'viator-collector' ),
|
|
),
|
|
$atts,
|
|
self::TOP_DESTS
|
|
);
|
|
|
|
$args = array( 'taxonomy' => 'viator_destination', 'hide_empty' => true );
|
|
if ( '' !== $atts['ids'] ) {
|
|
$args['include'] = array_filter( array_map( 'absint', explode( ',', $atts['ids'] ) ) );
|
|
$args['orderby'] = 'include';
|
|
} elseif ( '' !== $atts['slugs'] ) {
|
|
$args['slug'] = array_filter( array_map( 'sanitize_title', explode( ',', $atts['slugs'] ) ) );
|
|
$args['orderby'] = 'name';
|
|
} else {
|
|
$args['number'] = max( 1, absint( $atts['limit'] ) );
|
|
$args['orderby'] = ( 'name' === $atts['orderby'] ) ? 'name' : 'count';
|
|
$args['order'] = ( 'name' === $atts['orderby'] ) ? 'ASC' : 'DESC';
|
|
}
|
|
|
|
$terms = get_terms( $args );
|
|
if ( is_wp_error( $terms ) || empty( $terms ) ) {
|
|
return '';
|
|
}
|
|
|
|
$archive = get_post_type_archive_link( 'viator_tour' );
|
|
$rail = self::open_rail( $atts['title'], $atts['lead'], $archive, __( 'See all', 'viator-collector' ) );
|
|
|
|
ob_start();
|
|
echo $rail['head']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
|
foreach ( $terms as $term ) {
|
|
$link = get_term_link( $term );
|
|
if ( is_wp_error( $link ) ) {
|
|
continue;
|
|
}
|
|
$image = vc_term_image_url( $term->term_id, 'viator_destination' );
|
|
?>
|
|
<li class="dcard">
|
|
<?php if ( $image ) : ?>
|
|
<img class="dcard__img" src="<?php echo esc_url( $image ); ?>" alt="<?php echo esc_attr( $term->name ); ?>" loading="lazy">
|
|
<?php endif; ?>
|
|
<div class="dcard__body">
|
|
<h3 class="dcard__title"><a href="<?php echo esc_url( $link ); ?>"><?php echo esc_html( $term->name ); ?></a></h3>
|
|
<div class="dcard__meta">
|
|
<div class="meta">
|
|
<span class="meta__k"><?php echo esc_html__( 'Tours', 'viator-collector' ); ?></span>
|
|
<span class="meta__v"><?php echo esc_html( number_format_i18n( (int) $term->count ) ); ?></span>
|
|
</div>
|
|
<?php echo vc_ring_badge( 'VIEW TOURS · VIEW TOURS · ', $link, 'card' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
<?php
|
|
}
|
|
echo self::close_rail(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
|
return ob_get_clean();
|
|
}
|
|
|
|
/**
|
|
* [viator_faq title="FAQ's" items="Question|Answer;;Question 2|Answer 2"]
|
|
*
|
|
* Items may also be supplied via the `vc_faq_items` filter.
|
|
*/
|
|
public static function faq( $atts ) {
|
|
$atts = shortcode_atts(
|
|
array(
|
|
'title' => __( "FAQ's", 'viator-collector' ),
|
|
'lead' => __( 'Everything you need to know before you book.', 'viator-collector' ),
|
|
'items' => '',
|
|
),
|
|
$atts,
|
|
self::FAQ
|
|
);
|
|
|
|
$items = array();
|
|
if ( '' !== $atts['items'] ) {
|
|
foreach ( explode( ';;', $atts['items'] ) as $pair ) {
|
|
$parts = array_map( 'trim', explode( '|', $pair, 2 ) );
|
|
if ( '' !== $parts[0] && isset( $parts[1] ) ) {
|
|
$items[] = array( 'q' => $parts[0], 'a' => $parts[1] );
|
|
}
|
|
}
|
|
}
|
|
if ( empty( $items ) ) {
|
|
$items = apply_filters(
|
|
'vc_faq_items',
|
|
array(
|
|
array( 'q' => __( 'Is the booking process fully online?', 'viator-collector' ), 'a' => __( 'Yes — you can browse, choose a date and complete your booking entirely online in a few minutes.', 'viator-collector' ) ),
|
|
array( 'q' => __( 'How can I book a tour?', 'viator-collector' ), 'a' => __( 'Pick a tour, choose your date and number of seats, then confirm. You will receive your tickets by email.', 'viator-collector' ) ),
|
|
array( 'q' => __( 'Can I cancel or change my booking?', 'viator-collector' ), 'a' => __( 'Most tours offer free cancellation up to 24 hours before the start time. Check each tour for details.', 'viator-collector' ) ),
|
|
array( 'q' => __( 'Do I need to print my ticket?', 'viator-collector' ), 'a' => __( 'No — a mobile ticket is accepted for the vast majority of tours.', 'viator-collector' ) ),
|
|
)
|
|
);
|
|
}
|
|
if ( empty( $items ) ) {
|
|
return '';
|
|
}
|
|
|
|
ob_start();
|
|
?>
|
|
<div class="vc-app vc-sections">
|
|
<section class="faq">
|
|
<span class="faq__watermark" aria-hidden="true">FAQ</span>
|
|
<div class="container faq__inner">
|
|
<div class="faq__intro">
|
|
<h2 class="faq__title"><?php echo esc_html( $atts['title'] ); ?></h2>
|
|
<?php if ( $atts['lead'] ) : ?><p class="faq__lead"><?php echo esc_html( $atts['lead'] ); ?></p><?php endif; ?>
|
|
</div>
|
|
<div class="accordion" data-accordion>
|
|
<?php foreach ( $items as $i => $item ) : ?>
|
|
<div class="acc">
|
|
<h3>
|
|
<button class="acc__q" type="button" aria-expanded="<?php echo 0 === $i ? 'true' : 'false'; ?>">
|
|
<span><?php echo esc_html( $item['q'] ); ?></span>
|
|
<?php echo vc_icon( 'plus', 'icon--14 acc__plus' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
|
<?php echo vc_icon( 'minus', 'icon--14 acc__minus' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
|
</button>
|
|
</h3>
|
|
<div class="acc__a"><p><?php echo esc_html( $item['a'] ); ?></p></div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
<?php
|
|
return ob_get_clean();
|
|
}
|
|
|
|
/**
|
|
* [viator_testimonials title="Your Comments"]
|
|
*
|
|
* Items may be supplied via the `vc_testimonials` filter
|
|
* (each: name, role, text, avatar).
|
|
*/
|
|
public static function testimonials( $atts ) {
|
|
$atts = shortcode_atts(
|
|
array(
|
|
'title' => __( 'Your Comments', 'viator-collector' ),
|
|
'lead' => __( 'What travellers say about their tours.', 'viator-collector' ),
|
|
),
|
|
$atts,
|
|
self::REVIEWS
|
|
);
|
|
|
|
// Avatars are optional; supply them via the vc_testimonials filter
|
|
// ('avatar' => attachment URL) — none are bundled with the plugin.
|
|
$items = apply_filters(
|
|
'vc_testimonials',
|
|
array(
|
|
array( 'name' => 'Malika Favre', 'role' => __( 'Paris Tour', 'viator-collector' ), 'text' => __( 'An unforgettable day — the open-top ride and the guide made the whole city feel new. Booking was effortless.', 'viator-collector' ), 'avatar' => '', 'size' => 'lg' ),
|
|
array( 'name' => 'James Carter', 'role' => __( 'London Tour', 'viator-collector' ), 'text' => __( 'Great value and flexible timing. We hopped on and off all day and saw everything on our list.', 'viator-collector' ), 'avatar' => '', 'size' => '' ),
|
|
array( 'name' => 'Sofia Rossi', 'role' => __( 'Sydney Tour', 'viator-collector' ), 'text' => __( 'Smooth from start to finish. Highly recommended.', 'viator-collector' ), 'avatar' => '', 'size' => 'sm' ),
|
|
array( 'name' => 'Liam Nguyen', 'role' => __( 'San Diego Tour', 'viator-collector' ), 'text' => __( 'Friendly crew, comfortable seats and the best views of the coast.', 'viator-collector' ), 'avatar' => '', 'size' => '' ),
|
|
)
|
|
);
|
|
if ( empty( $items ) ) {
|
|
return '';
|
|
}
|
|
|
|
ob_start();
|
|
?>
|
|
<div class="vc-app vc-sections">
|
|
<section class="section testimonials">
|
|
<div class="container">
|
|
<div class="testimonials__head">
|
|
<h2 class="testimonials__title"><?php echo esc_html( $atts['title'] ); ?></h2>
|
|
<?php if ( $atts['lead'] ) : ?><p class="testimonials__lead"><?php echo esc_html( $atts['lead'] ); ?></p><?php endif; ?>
|
|
</div>
|
|
</div>
|
|
<div class="marquee" data-marquee>
|
|
<div class="marquee__track">
|
|
<?php foreach ( $items as $item ) : ?>
|
|
<?php $size = ! empty( $item['size'] ) ? ' quote--' . sanitize_html_class( $item['size'] ) : ''; ?>
|
|
<figure class="quote<?php echo esc_attr( $size ); ?>">
|
|
<figcaption class="quote__head">
|
|
<?php if ( ! empty( $item['avatar'] ) ) : ?>
|
|
<img class="quote__avatar" src="<?php echo esc_url( $item['avatar'] ); ?>" alt="" loading="lazy">
|
|
<?php endif; ?>
|
|
<span><b><?php echo esc_html( $item['name'] ); ?></b><i><?php echo esc_html( $item['role'] ); ?></i></span>
|
|
</figcaption>
|
|
<blockquote><?php echo esc_html( $item['text'] ); ?></blockquote>
|
|
</figure>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
<?php
|
|
return ob_get_clean();
|
|
}
|
|
|
|
/**
|
|
* [viator_bus_tours limit="4" tag="hop-on-hop-off" image_id="123"]
|
|
*
|
|
* The "Open Top Bus Tours…" promo block + a 4-up grid of tour cards. Backed
|
|
* by the same curated/filtered tour query as the rails. The bus illustration
|
|
* is a heavy image, so it comes from the Media Library via image_id (or a
|
|
* direct image URL) — nothing is bundled with the plugin.
|
|
*/
|
|
public static function bus_tours( $atts ) {
|
|
$atts = shortcode_atts(
|
|
array(
|
|
'ids' => '',
|
|
'codes' => '',
|
|
'destination' => '',
|
|
'tag' => '',
|
|
'limit' => 4,
|
|
'orderby' => 'rating',
|
|
'title' => __( 'Open Top<br>Bus Tours…', 'viator-collector' ),
|
|
'note' => __( 'An open-top hop on hop off bus is the best way to discover the historic and cultural sights of any city.', 'viator-collector' ),
|
|
'chip' => __( 'Very Fun!', 'viator-collector' ),
|
|
'image_id' => '', // WordPress media attachment ID for the bus illustration.
|
|
'image' => '', // …or a direct image URL.
|
|
),
|
|
$atts,
|
|
self::BUS_TOURS
|
|
);
|
|
|
|
$query = self::tour_query( $atts );
|
|
if ( ! $query->have_posts() ) {
|
|
return '';
|
|
}
|
|
|
|
$archive = get_post_type_archive_link( 'viator_tour' );
|
|
$archive = $archive ? $archive : home_url( '/' );
|
|
|
|
$bus_img = '';
|
|
if ( '' !== $atts['image_id'] ) {
|
|
// 'full' (uncropped original) — some sites register a hard-cropped
|
|
// square 'large', which slices decorative illustrations in half.
|
|
$bus_img = wp_get_attachment_image( absint( $atts['image_id'] ), 'full', false, array( 'alt' => '', 'loading' => 'lazy' ) );
|
|
} elseif ( '' !== $atts['image'] ) {
|
|
$bus_img = '<img src="' . esc_url( $atts['image'] ) . '" alt="" loading="lazy">';
|
|
}
|
|
|
|
ob_start();
|
|
?>
|
|
<div class="vc-app vc-sections">
|
|
<section class="section section--tight">
|
|
<div class="container bus">
|
|
<div class="promo">
|
|
<svg class="promo__route" viewBox="0 0 416 526" fill="none" aria-hidden="true" preserveAspectRatio="none">
|
|
<path d="M-20 470 C 90 470, 60 350, 150 330 S 300 300, 300 210 S 210 120, 300 60 S 430 40, 440 -10" stroke="#2CAFC2" stroke-dasharray="3 3" />
|
|
</svg>
|
|
<div class="promo__head">
|
|
<h2 class="promo__title"><?php echo wp_kses( $atts['title'], array( 'br' => array() ) ); ?></h2>
|
|
<a class="btn btn--white" href="<?php echo esc_url( $archive ); ?>"><span><?php echo esc_html__( 'See all', 'viator-collector' ); ?></span><?php echo vc_icon( 'arrow-ne' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></a>
|
|
</div>
|
|
<?php if ( $atts['chip'] ) : ?><span class="chip chip--accent promo__chip"><?php echo esc_html( $atts['chip'] ); ?></span><?php endif; ?>
|
|
<?php if ( $bus_img ) : ?><div class="promo__bus" aria-hidden="true"><?php echo $bus_img; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></div><?php endif; ?>
|
|
<?php if ( $atts['note'] ) : ?><p class="promo__note"><?php echo esc_html( $atts['note'] ); ?></p><?php endif; ?>
|
|
</div>
|
|
|
|
<ul class="bus__grid">
|
|
<?php
|
|
$i = 0;
|
|
while ( $query->have_posts() ) :
|
|
$query->the_post();
|
|
++$i;
|
|
$pid = get_the_ID();
|
|
$price = get_post_meta( $pid, '_vc_price_from', true );
|
|
$currency = get_post_meta( $pid, '_vc_currency', true );
|
|
$terms = get_the_terms( $pid, 'viator_destination' );
|
|
$kicker = ( $terms && ! is_wp_error( $terms ) ) ? $terms[0]->name : '';
|
|
$active = ( 1 === $i );
|
|
$badge = vc_ring_badge( 'BOOK NOW · BOOK NOW · ', get_permalink( $pid ), 'card' );
|
|
if ( $active ) {
|
|
$badge = str_replace( 'ring-badge--card', 'ring-badge--card ring-badge--accent', $badge );
|
|
}
|
|
?>
|
|
<li class="tcard<?php echo $active ? ' is-active' : ''; ?>">
|
|
<?php echo vc_tour_image_html( $pid, 'tcard__img' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
|
<div class="tcard__top">
|
|
<?php if ( $kicker ) : ?><span class="tcard__k"><?php echo esc_html( $kicker ); ?></span><?php endif; ?>
|
|
<h3 class="tcard__title"><a href="<?php echo esc_url( get_permalink( $pid ) ); ?>"><?php echo esc_html( get_the_title( $pid ) ); ?></a></h3>
|
|
</div>
|
|
<div class="tcard__bottom">
|
|
<?php if ( '' !== (string) $price ) : ?>
|
|
<div class="meta">
|
|
<span class="meta__k"><?php echo esc_html__( 'Start at', 'viator-collector' ); ?></span>
|
|
<span class="meta__v"><?php echo esc_html( vc_money( $price, $currency ) ); ?></span>
|
|
</div>
|
|
<?php endif; ?>
|
|
<?php echo $badge; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
|
</div>
|
|
</li>
|
|
<?php endwhile; ?>
|
|
</ul>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
<?php
|
|
wp_reset_postdata();
|
|
return ob_get_clean();
|
|
}
|
|
|
|
/**
|
|
* [viator_about image_id="123" title="Hop On Hop Off" highlight="Memorable"
|
|
* stats="25+|Years Experience;;500+|Tours Conducted"]
|
|
*
|
|
* A static "About" marketing block (headline + chip, paragraph, stat counters
|
|
* and a decorative bus). All copy is editable via attributes; the isometric
|
|
* bus is a heavy image supplied from the Media Library (image_id / image).
|
|
*/
|
|
public static function about( $atts ) {
|
|
$atts = shortcode_atts(
|
|
array(
|
|
'title' => __( 'Hop On Hop Off', 'viator-collector' ),
|
|
'highlight' => __( 'Memorable', 'viator-collector' ),
|
|
'text' => __( 'Discover cities the memorable way — hop on and off our open-top buses at every major sight and explore each place at your own pace, with expert local commentary along the way.', 'viator-collector' ),
|
|
'button_text' => __( 'About us', 'viator-collector' ),
|
|
'button_url' => '',
|
|
'image_id' => '',
|
|
'image' => '',
|
|
'stats' => '', // "25+|Years Experience;;500+|Tours Conducted;;430+|Tours this Year;;15+|Different Tours"
|
|
),
|
|
$atts,
|
|
self::ABOUT
|
|
);
|
|
|
|
$stats = array();
|
|
if ( '' !== $atts['stats'] ) {
|
|
foreach ( explode( ';;', $atts['stats'] ) as $pair ) {
|
|
$p = array_map( 'trim', explode( '|', $pair, 2 ) );
|
|
if ( '' !== $p[0] && isset( $p[1] ) ) {
|
|
$stats[] = array( 'num' => $p[0], 'label' => $p[1] );
|
|
}
|
|
}
|
|
}
|
|
if ( empty( $stats ) ) {
|
|
$stats = array(
|
|
array( 'num' => '25+', 'label' => __( 'Years Experience', 'viator-collector' ) ),
|
|
array( 'num' => '500+', 'label' => __( 'Tours Conducted', 'viator-collector' ) ),
|
|
array( 'num' => '430+', 'label' => __( 'Tours this Year', 'viator-collector' ) ),
|
|
array( 'num' => '15+', 'label' => __( 'Different Tours', 'viator-collector' ) ),
|
|
);
|
|
}
|
|
|
|
$button_url = $atts['button_url'];
|
|
if ( '' === $button_url ) {
|
|
$button_url = get_post_type_archive_link( 'viator_tour' );
|
|
$button_url = $button_url ? $button_url : home_url( '/' );
|
|
}
|
|
|
|
$bus_img = '';
|
|
if ( '' !== $atts['image_id'] ) {
|
|
// 'full' (uncropped original) — some sites register a hard-cropped
|
|
// square 'large', which slices decorative illustrations in half.
|
|
$bus_img = wp_get_attachment_image( absint( $atts['image_id'] ), 'full', false, array( 'alt' => '', 'loading' => 'lazy' ) );
|
|
} elseif ( '' !== $atts['image'] ) {
|
|
$bus_img = '<img src="' . esc_url( $atts['image'] ) . '" alt="" loading="lazy">';
|
|
}
|
|
|
|
ob_start();
|
|
?>
|
|
<div class="vc-app vc-sections">
|
|
<section class="section">
|
|
<div class="container about">
|
|
<div class="about__art" aria-hidden="true">
|
|
<span class="about__glow"></span>
|
|
<span class="about__trail" aria-hidden="true"></span>
|
|
<?php if ( $bus_img ) : ?><span class="about__bus"><?php echo $bus_img; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span><?php endif; ?>
|
|
<svg class="about__arcs" viewBox="0 0 480 440" fill="none" aria-hidden="true">
|
|
<defs>
|
|
<path id="busArc" pathLength="100" d="M20 336.6V221.6C20 96.6 170 23.4 170 148.4V263.4" />
|
|
</defs>
|
|
<g stroke="#FFCC00" stroke-width="9" stroke-linecap="round" fill="none" transform="translate(16 24)">
|
|
<use href="#busArc" opacity="0.1" stroke-dasharray="62 100" />
|
|
<use href="#busArc" opacity="0.25" stroke-dasharray="69 100" transform="translate(48 22)" />
|
|
<use href="#busArc" opacity="0.45" stroke-dasharray="76 100" transform="translate(96 44)" />
|
|
<use href="#busArc" opacity="0.8" stroke-dasharray="83 100" transform="translate(144 66)" />
|
|
</g>
|
|
</svg>
|
|
</div>
|
|
|
|
<div class="about__body">
|
|
<div class="about__head">
|
|
<h2 class="about__title"><?php echo esc_html( $atts['title'] ); ?><?php if ( $atts['highlight'] ) : ?><span class="chip chip--accent about__chip"><?php echo esc_html( $atts['highlight'] ); ?></span><?php endif; ?></h2>
|
|
<a class="btn btn--dark" href="<?php echo esc_url( $button_url ); ?>"><span><?php echo esc_html( $atts['button_text'] ); ?></span><?php echo vc_icon( 'arrow-right' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></a>
|
|
</div>
|
|
|
|
<?php if ( $atts['text'] ) : ?><p class="about__text"><?php echo esc_html( $atts['text'] ); ?></p><?php endif; ?>
|
|
|
|
<dl class="stats">
|
|
<?php
|
|
foreach ( $stats as $s ) :
|
|
$num = $s['num'];
|
|
$suffix = '';
|
|
if ( preg_match( '/^(.*?)(\+)$/', $num, $m ) ) {
|
|
$num = $m[1];
|
|
$suffix = $m[2];
|
|
}
|
|
?>
|
|
<div class="stat">
|
|
<dt class="stat__num"><?php echo esc_html( $num ); ?><?php if ( $suffix ) : ?><span><?php echo esc_html( $suffix ); ?></span><?php endif; ?></dt>
|
|
<dd class="stat__label"><?php echo esc_html( $s['label'] ); ?></dd>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
<?php
|
|
return ob_get_clean();
|
|
}
|
|
|
|
/**
|
|
* [viator_articles limit="8" category="news"]
|
|
*
|
|
* A rail of the latest blog posts, styled as the design's "Latest Articles"
|
|
* cards. Uses each post's featured image, time and excerpt; links to the post.
|
|
*/
|
|
public static function articles( $atts ) {
|
|
$atts = shortcode_atts(
|
|
array(
|
|
'title' => __( 'Latest<br>Articles', 'viator-collector' ),
|
|
'lead' => __( 'News, tips and stories from our tours.', 'viator-collector' ),
|
|
'limit' => 8,
|
|
'category' => '',
|
|
'view_all' => '',
|
|
),
|
|
$atts,
|
|
self::ARTICLES
|
|
);
|
|
|
|
$args = array(
|
|
'post_type' => 'post',
|
|
'post_status' => 'publish',
|
|
'posts_per_page' => max( 1, absint( $atts['limit'] ) ),
|
|
'ignore_sticky_posts' => true,
|
|
'no_found_rows' => true,
|
|
);
|
|
if ( '' !== $atts['category'] ) {
|
|
$args['category_name'] = sanitize_title( $atts['category'] );
|
|
}
|
|
|
|
$query = new WP_Query( $args );
|
|
if ( ! $query->have_posts() ) {
|
|
return '';
|
|
}
|
|
|
|
$view_all = $atts['view_all'];
|
|
if ( '' === $view_all ) {
|
|
$posts_page = (int) get_option( 'page_for_posts' );
|
|
$view_all = $posts_page ? get_permalink( $posts_page ) : home_url( '/' );
|
|
}
|
|
|
|
$rail = self::open_rail( $atts['title'], $atts['lead'], $view_all, __( 'See all', 'viator-collector' ), 'rail--art' );
|
|
|
|
ob_start();
|
|
echo $rail['head']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
|
$i = 0;
|
|
while ( $query->have_posts() ) :
|
|
$query->the_post();
|
|
++$i;
|
|
$pid = get_the_ID();
|
|
$active = ( 1 === $i );
|
|
$img = has_post_thumbnail( $pid ) ? get_the_post_thumbnail( $pid, 'large', array( 'class' => 'acard__img', 'loading' => 'lazy', 'alt' => '' ) ) : '';
|
|
$time = human_time_diff( (int) get_post_time( 'U', true, $pid ), (int) current_time( 'timestamp', true ) );
|
|
$excerpt = get_the_excerpt( $pid );
|
|
$badge = vc_ring_badge( 'READ MORE · READ MORE · ', get_permalink( $pid ), 'card' );
|
|
if ( $active ) {
|
|
$badge = str_replace( 'ring-badge--card', 'ring-badge--card ring-badge--accent', $badge );
|
|
}
|
|
?>
|
|
<li class="acard<?php echo $active ? ' is-active' : ''; ?>">
|
|
<div class="acard__media">
|
|
<?php echo $img; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
|
<span class="chip chip--glass acard__time"><?php printf( /* translators: %s: human-readable time difference, e.g. "3 hours". */ esc_html__( '%s ago', 'viator-collector' ), esc_html( $time ) ); ?></span>
|
|
<div class="acard__overlay">
|
|
<h3 class="acard__title"><a href="<?php echo esc_url( get_permalink( $pid ) ); ?>"><?php echo esc_html( get_the_title( $pid ) ); ?></a></h3>
|
|
<?php echo $badge; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
|
</div>
|
|
</div>
|
|
<?php if ( $excerpt ) : ?><p class="acard__excerpt"><?php echo esc_html( $excerpt ); ?></p><?php endif; ?>
|
|
</li>
|
|
<?php
|
|
endwhile;
|
|
echo self::close_rail(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
|
wp_reset_postdata();
|
|
return ob_get_clean();
|
|
}
|
|
|
|
/**
|
|
* Shared tour query for the landing sections. Supports curation (ids /
|
|
* product codes) and filtering (destination / tag), with a sane fallback.
|
|
*/
|
|
private static function tour_query( array $atts ) {
|
|
$args = array(
|
|
'post_type' => 'viator_tour',
|
|
'post_status' => 'publish',
|
|
'posts_per_page' => max( 1, absint( $atts['limit'] ) ),
|
|
'no_found_rows' => true,
|
|
'ignore_sticky_posts' => true,
|
|
);
|
|
|
|
if ( '' !== $atts['ids'] ) {
|
|
$args['post__in'] = array_filter( array_map( 'absint', explode( ',', $atts['ids'] ) ) );
|
|
$args['orderby'] = 'post__in';
|
|
} elseif ( '' !== $atts['codes'] ) {
|
|
$args['meta_query'] = array(
|
|
'vc_product_codes' => array(
|
|
'key' => '_vc_viator_product_code',
|
|
'value' => array_filter( array_map( 'sanitize_text_field', explode( ',', $atts['codes'] ) ) ),
|
|
'compare' => 'IN',
|
|
),
|
|
);
|
|
}
|
|
|
|
$tax_query = array();
|
|
if ( '' !== $atts['destination'] ) {
|
|
$tax_query[] = array( 'taxonomy' => 'viator_destination', 'field' => 'slug', 'terms' => array_map( 'sanitize_title', explode( ',', $atts['destination'] ) ) );
|
|
}
|
|
if ( '' !== $atts['tag'] ) {
|
|
$tax_query[] = array( 'taxonomy' => 'viator_tag', 'field' => 'slug', 'terms' => array_map( 'sanitize_title', explode( ',', $atts['tag'] ) ) );
|
|
}
|
|
if ( $tax_query ) {
|
|
$args['tax_query'] = $tax_query;
|
|
}
|
|
|
|
if ( ! isset( $args['orderby'] ) ) {
|
|
switch ( $atts['orderby'] ) {
|
|
case 'price':
|
|
$args['meta_key'] = '_vc_price_from';
|
|
$args['orderby'] = 'meta_value_num';
|
|
$args['order'] = 'ASC';
|
|
break;
|
|
case 'rand':
|
|
$args['orderby'] = 'rand';
|
|
break;
|
|
case 'date':
|
|
$args['orderby'] = 'date';
|
|
$args['order'] = 'DESC';
|
|
break;
|
|
default:
|
|
$args['meta_key'] = '_vc_rating';
|
|
$args['orderby'] = 'meta_value_num';
|
|
$args['order'] = 'DESC';
|
|
}
|
|
}
|
|
|
|
$query = new WP_Query( $args );
|
|
|
|
// Ordering by a meta key silently drops posts that lack it; fall back to
|
|
// newest so the section never empties.
|
|
if ( ! $query->have_posts() && isset( $args['meta_key'] ) ) {
|
|
unset( $args['meta_key'] );
|
|
$args['orderby'] = 'date';
|
|
$args['order'] = 'DESC';
|
|
$query = new WP_Query( $args );
|
|
}
|
|
|
|
return $query;
|
|
}
|
|
}
|