Compare commits
7 Commits
e4d2afc2fa
...
6a7f43311b
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a7f43311b | |||
| d5686ed14b | |||
| b0e302be15 | |||
| 01f414e10b | |||
| 911d2b4d2e | |||
| 35368fb5f9 | |||
| 67c88c795b |
4
.gitignore
vendored
@@ -1 +1,3 @@
|
|||||||
.playwright-mcp
|
.playwright-mcp_home.png
|
||||||
|
_tours.png
|
||||||
|
*.tmp.png
|
||||||
|
|||||||
@@ -421,6 +421,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hoho .ring-badge--top .ring-badge__text {
|
.hoho .ring-badge--top .ring-badge__text {
|
||||||
|
fill: #808080;
|
||||||
font-size: 0.625rem;
|
font-size: 0.625rem;
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
}
|
}
|
||||||
@@ -546,15 +547,26 @@
|
|||||||
box-shadow: 0 1.25rem 2.5rem rgba(0, 0, 0, 0.18);
|
box-shadow: 0 1.25rem 2.5rem rgba(0, 0, 0, 0.18);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
transform: translateY(-0.375rem);
|
transform: translateY(-0.4rem) scale(0.97);
|
||||||
transition: opacity 0.2s var(--ease), transform 0.2s var(--ease), visibility 0.2s;
|
transform-origin: top center;
|
||||||
|
/* Exit: fade/slide out, then flip visibility only once the fade is done, so
|
||||||
|
the close animates instead of snapping. */
|
||||||
|
transition:
|
||||||
|
opacity 0.16s var(--ease),
|
||||||
|
transform 0.16s var(--ease),
|
||||||
|
visibility 0s linear 0.16s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hoho .nav__item--has-menu:hover .nav__submenu,
|
.hoho .nav__item--has-menu:hover .nav__submenu,
|
||||||
.hoho .nav__toggle[aria-expanded="true"]+.nav__submenu {
|
.hoho .nav__toggle[aria-expanded="true"]+.nav__submenu {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
transform: translateY(0);
|
transform: translateY(0) scale(1);
|
||||||
|
/* Enter: a slightly longer, soft ease-out so it settles in gently. */
|
||||||
|
transition:
|
||||||
|
opacity 0.24s var(--ease),
|
||||||
|
transform 0.28s cubic-bezier(0.16, 1, 0.3, 1),
|
||||||
|
visibility 0s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hoho .nav__submenu a {
|
.hoho .nav__submenu a {
|
||||||
@@ -1868,14 +1880,24 @@
|
|||||||
box-shadow: 0 1.5rem 3rem rgba(0, 0, 0, 0.22);
|
box-shadow: 0 1.5rem 3rem rgba(0, 0, 0, 0.22);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
transform: translateY(-0.5rem);
|
transform: translateY(-0.6rem) scale(0.98);
|
||||||
transition: opacity 0.25s var(--ease), transform 0.25s var(--ease), visibility 0.25s;
|
transform-origin: top right;
|
||||||
|
/* Exit: fade/slide up, then hide once the fade completes. */
|
||||||
|
transition:
|
||||||
|
opacity 0.18s var(--ease),
|
||||||
|
transform 0.18s var(--ease),
|
||||||
|
visibility 0s linear 0.18s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hoho .nav.is-open {
|
.hoho .nav.is-open {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
transform: translateY(0);
|
transform: translateY(0) scale(1);
|
||||||
|
/* Enter: soft ease-out settle. */
|
||||||
|
transition:
|
||||||
|
opacity 0.26s var(--ease),
|
||||||
|
transform 0.3s cubic-bezier(0.16, 1, 0.3, 1),
|
||||||
|
visibility 0s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hoho .nav__list {
|
.hoho .nav__list {
|
||||||
|
|||||||
@@ -63,6 +63,31 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- header search: expand, then submit ---------- */
|
||||||
|
var search = document.querySelector(".header__search");
|
||||||
|
if (search) {
|
||||||
|
var searchInput = search.querySelector(".header__search-input");
|
||||||
|
var searchBtn = search.querySelector(".header__search-btn");
|
||||||
|
if (searchInput && searchBtn) {
|
||||||
|
searchBtn.addEventListener("click", function (e) {
|
||||||
|
if (!search.classList.contains("is-open")) {
|
||||||
|
e.preventDefault();
|
||||||
|
search.classList.add("is-open");
|
||||||
|
searchInput.focus();
|
||||||
|
} else if (!searchInput.value.trim()) {
|
||||||
|
e.preventDefault();
|
||||||
|
search.classList.remove("is-open"); /* empty → collapse instead of submit */
|
||||||
|
}
|
||||||
|
/* open + has text → let the form submit to the tours archive (?vc_q=) */
|
||||||
|
});
|
||||||
|
document.addEventListener("click", function (e) {
|
||||||
|
if (!search.contains(e.target) && !searchInput.value.trim()) {
|
||||||
|
search.classList.remove("is-open");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- footer: back to top ---------- */
|
/* ---------- footer: back to top ---------- */
|
||||||
var toTop = document.querySelector(".ring-badge--top");
|
var toTop = document.querySelector(".ring-badge--top");
|
||||||
if (toTop) {
|
if (toTop) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<svg class="sprite" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg">
|
<svg class="sprite" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" style="position:absolute;width:0;height:0;overflow:hidden">
|
||||||
<defs>
|
<defs>
|
||||||
<path id="ringPathScroll" fill="none" d="M50,50 m-39,0 a39,39 0 1,1 78,0 a39,39 0 1,1 -78,0" />
|
<path id="ringPathScroll" fill="none" d="M50,50 m-39,0 a39,39 0 1,1 78,0 a39,39 0 1,1 -78,0" />
|
||||||
</defs>
|
</defs>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
@@ -17,19 +17,55 @@ add_action( 'wp_enqueue_scripts', function () {
|
|||||||
// Design typeface (same as the plugin's pages).
|
// Design typeface (same as the plugin's pages).
|
||||||
wp_enqueue_style( 'hoho-fonts', 'https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;700;800&display=swap', array(), null );
|
wp_enqueue_style( 'hoho-fonts', 'https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;700;800&display=swap', array(), null );
|
||||||
|
|
||||||
// Header/footer design, scoped to .hoho so it can't fight Astra.
|
// Header/footer design, scoped to .hoho so it can't fight Astra. Depend on
|
||||||
wp_enqueue_style( 'hoho-chrome', get_stylesheet_directory_uri() . '/assets/chrome.css', array( 'astra-parent' ), '1.0.0' );
|
// the Astra stylesheet (when present) so our rules load AFTER it and win ties.
|
||||||
|
$deps = array( 'astra-parent' );
|
||||||
|
if ( wp_style_is( 'astra-theme-css', 'registered' ) ) {
|
||||||
|
$deps[] = 'astra-theme-css';
|
||||||
|
}
|
||||||
|
wp_enqueue_style( 'hoho-chrome', get_stylesheet_directory_uri() . '/assets/chrome.css', $deps, '1.0.8' );
|
||||||
|
|
||||||
// Header interactions: burger, submenu, sticky, back-to-top.
|
// 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.0', true );
|
wp_enqueue_script( 'hoho-chrome', get_stylesheet_directory_uri() . '/assets/chrome.js', array(), '1.0.8', true );
|
||||||
|
|
||||||
// On pages without a full-bleed hero to overlay, the header renders solid and
|
|
||||||
// sticky (added by chrome.js as .is-solid) so it stays readable and doesn't
|
|
||||||
// cover content. Hero pages keep the transparent overlay + is-stuck on scroll.
|
|
||||||
wp_add_inline_style(
|
wp_add_inline_style(
|
||||||
'hoho-chrome',
|
'hoho-chrome',
|
||||||
'.hoho .site-header.is-solid{position:sticky;inset-block-start:0;padding-block:var(--sp-2);'
|
implode(
|
||||||
. 'background:rgba(0,0,0,.85);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);}'
|
'',
|
||||||
|
array(
|
||||||
|
// Solid, sticky header on pages without a hero to overlay (added by
|
||||||
|
// chrome.js as .is-solid) so it stays readable and doesn't cover content.
|
||||||
|
'.hoho .site-header.is-solid{position:sticky;inset-block-start:0;padding-block:var(--sp-2);background:rgba(0,0,0,.85);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);}',
|
||||||
|
// Force the design font over Astra inside the header/footer.
|
||||||
|
'.hoho,.hoho h1,.hoho h2,.hoho h3,.hoho p,.hoho a,.hoho span,.hoho li,.hoho button,.hoho input{font-family:"Plus Jakarta Sans",system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;}',
|
||||||
|
// Normalize: stop Astra decorating header/footer links & lists.
|
||||||
|
'.hoho a,.hoho.hoho a{text-decoration:none!important;box-shadow:none!important;}',
|
||||||
|
// …but not ring-badges: they set their own colour on the <a>, and this
|
||||||
|
// inherit rule out-specifies .ring-badge--top, turning the footer
|
||||||
|
// "back to top" text white (inherited) on the light area above it.
|
||||||
|
'.hoho a:not(.btn):not(.ring-badge){color:inherit;}',
|
||||||
|
'.hoho :is(ul,ol){list-style:none!important;margin:0!important;padding:0!important;}',
|
||||||
|
'.hoho li{margin:0!important;}',
|
||||||
|
// Let the footer\'s decorative bits (back-to-top ring, support tab)
|
||||||
|
// spill vertically, but keep the horizontal axis clipped so nothing
|
||||||
|
// causes a mobile sideways scroll. overflow-x:clip (unlike hidden)
|
||||||
|
// does not force overflow-y to auto, so the y-axis stays visible.
|
||||||
|
'.hoho,.hoho .footer{overflow-x:clip;overflow-y:visible;}',
|
||||||
|
// Icon buttons: Astra gives <button> a filled background + its own
|
||||||
|
// colour, which turned the header search icon into an empty dark
|
||||||
|
// circle. Reset them (real .btn buttons keep their styling).
|
||||||
|
'.hoho .icon-btn{background:transparent!important;border:0!important;color:#fff!important;padding:.4rem;}',
|
||||||
|
'.hoho .site-header .icon{color:#fff;}',
|
||||||
|
'.hoho button:not(.btn):not(.burger){background:transparent;border:0;color:inherit;}',
|
||||||
|
// Expanding header search field.
|
||||||
|
'.hoho .header__search{display:flex;align-items:center;gap:.25rem;}',
|
||||||
|
// !important beats Astra's input[type=search] styling, which forces a
|
||||||
|
// white field background (and, with a white placeholder, hid the text).
|
||||||
|
'.hoho .header__search-input{width:0;min-width:0;padding:0;border:0!important;outline:0;opacity:0;height:var(--control-h);border-radius:var(--r-pill)!important;background:rgba(255,255,255,.15)!important;color:#fff!important;box-shadow:none!important;transition:width .25s var(--ease),opacity .2s var(--ease),padding .25s var(--ease);}',
|
||||||
|
'.hoho .header__search.is-open .header__search-input{width:min(52vw,15rem);padding:0 1rem;opacity:1;}',
|
||||||
|
'.hoho .header__search-input::placeholder{color:rgba(255,255,255,.7)!important;opacity:1;}',
|
||||||
|
)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}, 20 );
|
}, 20 );
|
||||||
|
|
||||||
@@ -79,9 +115,11 @@ add_action( 'wp', function () {
|
|||||||
}
|
}
|
||||||
add_action( 'astra_header', 'hoho_render_header' );
|
add_action( 'astra_header', 'hoho_render_header' );
|
||||||
|
|
||||||
if ( function_exists( 'astra_footer_markup' ) ) {
|
// Astra renders its footer via the Footer Builder (a class method on the
|
||||||
remove_action( 'astra_footer', 'astra_footer_markup' );
|
// `astra_footer` hook), so removing the legacy astra_footer_markup() alone
|
||||||
}
|
// left a second, default footer. Clear every callback on the hook, then add
|
||||||
|
// ours — a clean, full replacement regardless of classic vs builder footer.
|
||||||
|
remove_all_actions( 'astra_footer' );
|
||||||
add_action( 'astra_footer', 'hoho_render_footer' );
|
add_action( 'astra_footer', 'hoho_render_footer' );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
@@ -120,7 +158,10 @@ function hoho_logo() {
|
|||||||
* Small helper: inline-icon markup referencing the sprite.
|
* Small helper: inline-icon markup referencing the sprite.
|
||||||
*/
|
*/
|
||||||
function hoho_icon( $name, $extra = '' ) {
|
function hoho_icon( $name, $extra = '' ) {
|
||||||
$name = preg_replace( '/[^a-z0-9\-]/', '', 'i-' . ltrim( (string) $name, 'i-' ) );
|
// Strip an existing "i-" prefix (once), then re-add it. ltrim() with a
|
||||||
|
// charlist would eat any leading i/-, turning "instagram" into "nstagram".
|
||||||
|
$slug = preg_replace( '/^i-/', '', (string) $name );
|
||||||
|
$name = preg_replace( '/[^a-z0-9\-]/', '', 'i-' . $slug );
|
||||||
$class = 'icon' . ( $extra ? ' ' . $extra : '' );
|
$class = 'icon' . ( $extra ? ' ' . $extra : '' );
|
||||||
return '<svg class="' . esc_attr( $class ) . '" aria-hidden="true"><use href="#' . esc_attr( $name ) . '"></use></svg>';
|
return '<svg class="' . esc_attr( $class ) . '" aria-hidden="true"><use href="#' . esc_attr( $name ) . '"></use></svg>';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,10 @@ $hoho_footer_col = function ( $location ) {
|
|||||||
<footer class="footer" id="footer">
|
<footer class="footer" id="footer">
|
||||||
<a class="ring-badge ring-badge--top" href="#top" aria-label="<?php echo esc_attr__( 'Back to top', 'hoponhopoff' ); ?>">
|
<a class="ring-badge ring-badge--top" href="#top" aria-label="<?php echo esc_attr__( 'Back to top', 'hoponhopoff' ); ?>">
|
||||||
<svg class="ring-badge__ring" viewBox="0 0 100 100" aria-hidden="true">
|
<svg class="ring-badge__ring" viewBox="0 0 100 100" aria-hidden="true">
|
||||||
<text class="ring-badge__text"><textPath href="#ringPathScroll" startOffset="0">BACK TO TOP · BACK TO TOP · </textPath></text>
|
<text class="ring-badge__text" text-anchor="middle">
|
||||||
|
<textPath href="#ringPathScroll" startOffset="25%"><?php echo esc_html__( 'BACK TO TOP', 'hoponhopoff' ); ?></textPath>
|
||||||
|
<textPath href="#ringPathScroll" startOffset="75%"><?php echo esc_html__( 'BACK TO TOP', 'hoponhopoff' ); ?></textPath>
|
||||||
|
</text>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="ring-badge__dot"><svg class="icon" aria-hidden="true"><use href="#i-arrow-up"></use></svg></span>
|
<span class="ring-badge__dot"><svg class="icon" aria-hidden="true"><use href="#i-arrow-up"></use></svg></span>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -32,12 +32,12 @@ $hoho_archive = $hoho_archive ? $hoho_archive : home_url( '/' );
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="header__actions">
|
<div class="header__actions">
|
||||||
<button class="icon-btn" type="button" aria-label="<?php echo esc_attr__( 'Search', 'hoponhopoff' ); ?>">
|
<form class="header__search" role="search" method="get" action="<?php echo esc_url( $hoho_archive ); ?>">
|
||||||
|
<input class="header__search-input" type="search" name="vc_q" placeholder="<?php echo esc_attr__( 'Search tours…', 'hoponhopoff' ); ?>" aria-label="<?php echo esc_attr__( 'Search tours', 'hoponhopoff' ); ?>">
|
||||||
|
<button class="icon-btn header__search-btn" type="submit" aria-label="<?php echo esc_attr__( 'Search', 'hoponhopoff' ); ?>">
|
||||||
<?php echo hoho_icon( 'search', 'icon--24' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
<?php echo hoho_icon( 'search', 'icon--24' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||||
</button>
|
</button>
|
||||||
<a class="icon-btn" href="<?php echo esc_url( wp_login_url() ); ?>" aria-label="<?php echo esc_attr__( 'Account', 'hoponhopoff' ); ?>">
|
</form>
|
||||||
<?php echo hoho_icon( 'user', 'icon--24' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
|
||||||
</a>
|
|
||||||
<a class="btn btn--accent header__cta" href="<?php echo esc_url( $hoho_archive ); ?>">
|
<a class="btn btn--accent header__cta" href="<?php echo esc_url( $hoho_archive ); ?>">
|
||||||
<span><?php echo esc_html__( 'Book now', 'hoponhopoff' ); ?></span>
|
<span><?php echo esc_html__( 'Book now', 'hoponhopoff' ); ?></span>
|
||||||
<?php echo hoho_icon( 'arrow-right' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
<?php echo hoho_icon( 'arrow-right' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||||
|
|||||||
@@ -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.0
|
Version: 1.0.8
|
||||||
Text Domain: hoponhopoff
|
Text Domain: hoponhopoff
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
70
build-zip.php
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Distribution zipper for the plugin / theme.
|
||||||
|
*
|
||||||
|
* php build-zip.php <source-dir> <output.zip>
|
||||||
|
*
|
||||||
|
* Produces a clean, distributable archive: excludes docs, build tooling,
|
||||||
|
* markdown, VCS and editor/AI metadata — nothing that documents how the code
|
||||||
|
* was produced ships to the client.
|
||||||
|
*
|
||||||
|
* Exclusion rule (keep in sync with the "no docs/AI files in zips" preference):
|
||||||
|
* - any path segment: .git, .claude, .github, docs, build, node_modules
|
||||||
|
* - any *.md file
|
||||||
|
* - dotfiles at any level except .htaccess
|
||||||
|
*/
|
||||||
|
|
||||||
|
$src = isset( $argv[1] ) ? rtrim( str_replace( '\\', '/', $argv[1] ), '/' ) : '';
|
||||||
|
$out = isset( $argv[2] ) ? $argv[2] : '';
|
||||||
|
|
||||||
|
if ( '' === $src || '' === $out || ! is_dir( $src ) ) {
|
||||||
|
fwrite( STDERR, "Usage: php build-zip.php <source-dir> <output.zip>\n" );
|
||||||
|
exit( 1 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$excluded_dirs = array( '.git', '.claude', '.github', 'docs', 'build', 'node_modules' );
|
||||||
|
|
||||||
|
function is_excluded( $rel, array $excluded_dirs ) {
|
||||||
|
$parts = explode( '/', $rel );
|
||||||
|
$base = end( $parts );
|
||||||
|
|
||||||
|
foreach ( $parts as $seg ) {
|
||||||
|
if ( in_array( $seg, $excluded_dirs, true ) ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Markdown / doc files.
|
||||||
|
if ( preg_match( '/\.md$/i', $base ) ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Dotfiles (e.g. .gitignore, .DS_Store) except .htaccess.
|
||||||
|
if ( '' !== $base && '.' === $base[0] && '.htaccess' !== $base ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@unlink( $out );
|
||||||
|
$zip = new ZipArchive();
|
||||||
|
if ( true !== $zip->open( $out, ZipArchive::CREATE | ZipArchive::OVERWRITE ) ) {
|
||||||
|
fwrite( STDERR, "Cannot create $out\n" );
|
||||||
|
exit( 1 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$root = basename( $src );
|
||||||
|
$it = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $src, FilesystemIterator::SKIP_DOTS ) );
|
||||||
|
$added = 0;
|
||||||
|
$skip = 0;
|
||||||
|
foreach ( $it as $file ) {
|
||||||
|
$path = str_replace( '\\', '/', $file->getPathname() );
|
||||||
|
// Path relative to the source's parent, so the archive contains <root>/…
|
||||||
|
$rel_from_src = ltrim( substr( $path, strlen( $src ) ), '/' );
|
||||||
|
if ( is_excluded( $rel_from_src, $excluded_dirs ) ) {
|
||||||
|
$skip++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$zip->addFile( $path, $root . '/' . $rel_from_src );
|
||||||
|
$added++;
|
||||||
|
}
|
||||||
|
$zip->close();
|
||||||
|
fwrite( STDOUT, "Wrote $out — $added files (skipped $skip)\n" );
|
||||||
@@ -58,6 +58,15 @@ html {
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
font-family:
|
||||||
|
"Plus Jakarta Sans",
|
||||||
|
system-ui,
|
||||||
|
-apple-system,
|
||||||
|
"Segoe UI",
|
||||||
|
Roboto,
|
||||||
|
Helvetica,
|
||||||
|
Arial,
|
||||||
|
sans-serif;
|
||||||
font-family:
|
font-family:
|
||||||
"Plus Jakarta Sans",
|
"Plus Jakarta Sans",
|
||||||
system-ui,
|
system-ui,
|
||||||
@@ -624,7 +633,6 @@ dd {
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
align-items: end;
|
align-items: end;
|
||||||
min-height: clamp(34rem, 88svh, 56rem);
|
|
||||||
margin: var(--page-inset);
|
margin: var(--page-inset);
|
||||||
padding-block: calc(var(--header-h) + var(--sp-6))
|
padding-block: calc(var(--header-h) + var(--sp-6))
|
||||||
clamp(3rem, 1.5rem + 6vw, 6rem);
|
clamp(3rem, 1.5rem + 6vw, 6rem);
|
||||||
@@ -836,9 +844,13 @@ dd {
|
|||||||
scroll-snap-type: x mandatory;
|
scroll-snap-type: x mandatory;
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
/* negative margin + padding keeps the card glow from being clipped by the scroller */
|
/* overflow-x:auto forces overflow-y to auto, so the hover glow (a ~3.75rem
|
||||||
margin-inline: calc(var(--gap) * -1);
|
blur) would be clipped by the scroller. Don't switch to overflow:visible —
|
||||||
padding: var(--sp-4) var(--gap);
|
that kills the horizontal scroll. Instead give the rail enough padding for
|
||||||
|
the glow to paint INSIDE the scroll box, and pull it back with an equal
|
||||||
|
negative margin so the visible spacing is unchanged. Scrolling stays intact. */
|
||||||
|
margin: calc(var(--sp-5) * -1) calc(var(--gap) * -1);
|
||||||
|
padding: 4.5rem var(--gap);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rail::-webkit-scrollbar {
|
.rail::-webkit-scrollbar {
|
||||||
@@ -1696,10 +1708,14 @@ dd {
|
|||||||
transition: grid-template-rows 0.3s var(--ease);
|
transition: grid-template-rows 0.3s var(--ease);
|
||||||
}
|
}
|
||||||
|
|
||||||
.acc__q[aria-expanded="true"] + .acc__a {
|
/* The button sits inside an <h3>, so .acc__a is the h3's sibling — not the
|
||||||
|
button's. Match on the .acc wrapper's expanded state instead of an adjacent
|
||||||
|
sibling, which never matched through the heading. */
|
||||||
|
.acc:has(.acc__q[aria-expanded="true"]) .acc__a {
|
||||||
grid-template-rows: 1fr;
|
grid-template-rows: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.acc__a > * {
|
||||||
.acc__a > * {
|
.acc__a > * {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -2044,10 +2060,6 @@ dd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 40rem) {
|
@media (max-width: 40rem) {
|
||||||
.hero {
|
|
||||||
min-height: clamp(30rem, 84svh, 44rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.booking {
|
.booking {
|
||||||
padding: var(--sp-2);
|
padding: var(--sp-2);
|
||||||
}
|
}
|
||||||
@@ -2717,6 +2729,13 @@ dd {
|
|||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
object-position: center 26%;
|
object-position: center 26%;
|
||||||
|
/* Viator only ships ~720px images, which look soft blown up to a full-width
|
||||||
|
banner. Rather than fight it, lean in: a gentle blur turns the photo into an
|
||||||
|
intentional backdrop (the crisp image lives in the gallery lightbox). The
|
||||||
|
scale hides the blur's feathered edges, which overflow:clip on .hero-tour
|
||||||
|
then trims to the rounded frame. */
|
||||||
|
filter: blur(10px) saturate(1.12);
|
||||||
|
transform: scale(1.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-tour__sky {
|
.hero-tour__sky {
|
||||||
@@ -2732,9 +2751,9 @@ dd {
|
|||||||
),
|
),
|
||||||
linear-gradient(
|
linear-gradient(
|
||||||
180deg,
|
180deg,
|
||||||
rgba(27, 147, 166, 0.82) 0%,
|
rgba(27, 147, 166, 0.55) 0%,
|
||||||
rgba(44, 175, 194, 0.62) 45%,
|
rgba(44, 175, 194, 0.42) 45%,
|
||||||
rgba(111, 208, 221, 0.78) 100%
|
rgba(111, 208, 221, 0.72) 100%
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3013,6 +3032,119 @@ dd {
|
|||||||
border-radius: var(--r-card);
|
border-radius: var(--r-card);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gallery img {
|
||||||
|
cursor: zoom-in;
|
||||||
|
transition: transform 0.25s var(--ease), filter 0.25s var(--ease);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery img:hover {
|
||||||
|
filter: brightness(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
Gallery lightbox (built by vc-design.js)
|
||||||
|
========================================================= */
|
||||||
|
.vc-lightbox {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: clamp(1rem, 0.5rem + 3vw, 3.5rem);
|
||||||
|
background: rgba(0, 0, 0, 0.86);
|
||||||
|
-webkit-backdrop-filter: blur(6px);
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition:
|
||||||
|
opacity 0.25s var(--ease),
|
||||||
|
visibility 0s linear 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-lightbox.is-open {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
transition: opacity 0.25s var(--ease);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-lightbox__img {
|
||||||
|
max-width: min(100%, 78rem);
|
||||||
|
max-height: 84vh;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
border-radius: var(--r-card);
|
||||||
|
box-shadow: 0 1.5rem 4rem rgba(0, 0, 0, 0.5);
|
||||||
|
transform: scale(0.96);
|
||||||
|
transition: transform 0.25s var(--ease), opacity 0.15s var(--ease);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-lightbox.is-open .vc-lightbox__img {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-lightbox__btn {
|
||||||
|
position: absolute;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
border-radius: var(--r-pill);
|
||||||
|
color: var(--c-white);
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s var(--ease), transform 0.2s var(--ease);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-lightbox__btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-lightbox__close {
|
||||||
|
top: clamp(1rem, 3vw, 2rem);
|
||||||
|
right: clamp(1rem, 3vw, 2rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-lightbox__prev,
|
||||||
|
.vc-lightbox__next {
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-lightbox__prev {
|
||||||
|
left: clamp(0.5rem, 2vw, 2rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-lightbox__next {
|
||||||
|
right: clamp(0.5rem, 2vw, 2rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-lightbox__prev:hover,
|
||||||
|
.vc-lightbox__next:hover {
|
||||||
|
transform: translateY(-50%) scale(1.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-lightbox__count {
|
||||||
|
position: absolute;
|
||||||
|
bottom: clamp(1rem, 3vw, 2rem);
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* single image → hide the arrows */
|
||||||
|
.vc-lightbox--single .vc-lightbox__prev,
|
||||||
|
.vc-lightbox--single .vc-lightbox__next,
|
||||||
|
.vc-lightbox--single .vc-lightbox__count {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* --- included / not included --- */
|
/* --- included / not included --- */
|
||||||
.lists {
|
.lists {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
BIN
removed-images/bus-iso.png
Normal file
|
After Width: | Height: | Size: 320 KiB |
BIN
removed-images/bus-open-top.png
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
removed-images/ellipse-17-2.png
Normal file
|
After Width: | Height: | Size: 344 B |
BIN
removed-images/ellipse-17.png
Normal file
|
After Width: | Height: | Size: 344 B |
BIN
removed-images/hero.png
Normal file
|
After Width: | Height: | Size: 630 KiB |
BIN
removed-images/hero2.png
Normal file
|
After Width: | Height: | Size: 696 KiB |
BIN
removed-images/hero3.jpg
Normal file
|
After Width: | Height: | Size: 825 KiB |
BIN
removed-images/tower.png
Normal file
|
After Width: | Height: | Size: 164 KiB |
BIN
viator-collector.zip
Normal file
@@ -16,23 +16,31 @@ listing/single/section assets load only on pages that use them.
|
|||||||
| `[viator_top_tours]` | Rail of tour cards | `viator_tour` posts |
|
| `[viator_top_tours]` | Rail of tour cards | `viator_tour` posts |
|
||||||
| `[viator_top_attractions]` | Rail of tour cards | `viator_tour` posts |
|
| `[viator_top_attractions]` | Rail of tour cards | `viator_tour` posts |
|
||||||
| `[viator_browse_activities]` | Rail of tour cards | `viator_tour` posts |
|
| `[viator_browse_activities]` | Rail of tour cards | `viator_tour` posts |
|
||||||
|
| `[viator_bus_tours]` | "Open Top Bus Tours" promo + 4-up card grid | `viator_tour` posts |
|
||||||
|
| `[viator_about]` | Static "About / Memorable" block + stat counters | Shortcode attributes |
|
||||||
|
| `[viator_articles]` | Rail of latest blog-post cards | `post` posts |
|
||||||
| `[viator_faq]` | FAQ accordion | Shortcode `items` / `vc_faq_items` filter |
|
| `[viator_faq]` | FAQ accordion | Shortcode `items` / `vc_faq_items` filter |
|
||||||
| `[viator_testimonials]` | Testimonials marquee | `vc_testimonials` filter |
|
| `[viator_testimonials]` | Testimonials marquee | `vc_testimonials` filter |
|
||||||
|
|
||||||
`[viator_hero_slider]` is kept as a **back-compat alias** for `[viator_hero]`.
|
`[viator_hero_slider]` is kept as a **back-compat alias** for `[viator_hero]`.
|
||||||
|
|
||||||
A typical home page:
|
Home page matching the design mockup (`new front/index.html`), top to bottom:
|
||||||
|
|
||||||
```
|
```
|
||||||
[viator_hero]
|
[viator_hero image_id="125304"]
|
||||||
[viator_top_destinations]
|
[viator_top_destinations]
|
||||||
|
[viator_bus_tours image_id="125310"]
|
||||||
[viator_top_tours]
|
[viator_top_tours]
|
||||||
[viator_top_attractions]
|
[viator_about image_id="125311"]
|
||||||
[viator_browse_activities]
|
|
||||||
[viator_testimonials]
|
[viator_testimonials]
|
||||||
|
[viator_articles]
|
||||||
[viator_faq]
|
[viator_faq]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Heavy decorative images (hero photo, open-top bus, isometric bus) are **not
|
||||||
|
bundled** — upload them in **Media** and pass the attachment `image_id` (exactly
|
||||||
|
like `[viator_hero image_id="…"]`). Icons and tiny decorations are inline SVG.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## `[viator_hero]`
|
## `[viator_hero]`
|
||||||
@@ -99,6 +107,71 @@ are hidden automatically.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## `[viator_bus_tours]`
|
||||||
|
|
||||||
|
The "Open Top Bus Tours…" promo block (route line, headline, "See all", a chip,
|
||||||
|
a bus illustration and note) followed by a 4-up grid of tour cards. Selects tours
|
||||||
|
with the **same attributes as the tour rails** (`ids` / `codes` / `destination` /
|
||||||
|
`tag` / `limit` / `orderby`), plus:
|
||||||
|
|
||||||
|
| Attribute | Default | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `title` | `Open Top<br>Bus Tours…` | Promo headline (`<br>` allowed). |
|
||||||
|
| `note` | *(default copy)* | Paragraph under the headline. |
|
||||||
|
| `chip` | `Very Fun!` | Small accent chip. |
|
||||||
|
| `image_id` | — | **Media attachment ID** of the open-top bus illustration. |
|
||||||
|
| `image` | — | …or a direct image URL (use `image_id` when possible). |
|
||||||
|
|
||||||
|
```
|
||||||
|
[viator_bus_tours image_id="125310" tag="hop-on-hop-off" limit="4"]
|
||||||
|
[viator_bus_tours ids="482,517,690,701"]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `[viator_about]`
|
||||||
|
|
||||||
|
Static "About" marketing block: headline with an accent chip, a paragraph, four
|
||||||
|
stat counters and a decorative isometric bus. All copy is editable.
|
||||||
|
|
||||||
|
| Attribute | Default | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `title` | `Hop On Hop Off` | Headline. |
|
||||||
|
| `highlight` | `Memorable` | Accent chip appended to the headline. |
|
||||||
|
| `text` | *(default copy)* | Paragraph. |
|
||||||
|
| `button_text` | `About us` | Button label. |
|
||||||
|
| `button_url` | *(tours archive)* | Button link. |
|
||||||
|
| `image_id` | — | **Media attachment ID** of the isometric bus illustration. |
|
||||||
|
| `image` | — | …or a direct image URL. |
|
||||||
|
| `stats` | *(4 defaults)* | `25+|Years Experience;;500+|Tours Conducted` — `|` separates number from label, `;;` separates stats. A trailing `+` is styled. |
|
||||||
|
|
||||||
|
```
|
||||||
|
[viator_about image_id="125311" stats="25+|Years Experience;;500+|Tours Conducted;;430+|Tours this Year;;15+|Different Tours"]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `[viator_articles]`
|
||||||
|
|
||||||
|
A rail of the latest blog posts styled as the design's "Latest Articles" cards
|
||||||
|
(featured image, relative time, title, excerpt → links to the post).
|
||||||
|
|
||||||
|
| Attribute | Default | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `title` | `Latest<br>Articles` | Heading (`<br>` allowed). |
|
||||||
|
| `lead` | *(default copy)* | Sub-heading. |
|
||||||
|
| `limit` | `8` | Number of posts. |
|
||||||
|
| `category` | — | Restrict to a category slug. |
|
||||||
|
| `view_all` | *(posts page)* | "See all" link; defaults to the blog page. |
|
||||||
|
|
||||||
|
Cards use each post's **featured image**; set one so the card isn't blank.
|
||||||
|
|
||||||
|
```
|
||||||
|
[viator_articles limit="8" category="travel-tips"]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## `[viator_faq]`
|
## `[viator_faq]`
|
||||||
|
|
||||||
FAQ accordion.
|
FAQ accordion.
|
||||||
|
|||||||
BIN
viator-collector/assets.zip
Normal file
@@ -1,4 +1,4 @@
|
|||||||
<svg class="sprite" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg">
|
<svg class="sprite" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" style="position:absolute;width:0;height:0;overflow:hidden">
|
||||||
<defs>
|
<defs>
|
||||||
<path id="ringPathScroll" fill="none" d="M50,50 m-39,0 a39,39 0 1,1 78,0 a39,39 0 1,1 -78,0" />
|
<path id="ringPathScroll" fill="none" d="M50,50 m-39,0 a39,39 0 1,1 78,0 a39,39 0 1,1 -78,0" />
|
||||||
</defs>
|
</defs>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 440 KiB |
|
Before Width: | Height: | Size: 321 B |
|
Before Width: | Height: | Size: 321 B |
|
Before Width: | Height: | Size: 2.6 MiB |
|
Before Width: | Height: | Size: 2.5 MiB |
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 595 KiB |
@@ -92,4 +92,57 @@
|
|||||||
var g = app.querySelector(".gallery, .tour-gallery");
|
var g = app.querySelector(".gallery, .tour-gallery");
|
||||||
if (g) g.scrollIntoView({ behavior: "smooth" });
|
if (g) g.scrollIntoView({ behavior: "smooth" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* ---------- gallery lightbox (zoom / view photos) ---------- */
|
||||||
|
(function () {
|
||||||
|
var imgs = Array.prototype.slice.call(app.querySelectorAll(".gallery img"));
|
||||||
|
if (!imgs.length) return;
|
||||||
|
|
||||||
|
var host = imgs[0].closest(".vc-app") || document.body;
|
||||||
|
var box = document.createElement("div");
|
||||||
|
box.className = "vc-lightbox" + (imgs.length < 2 ? " vc-lightbox--single" : "");
|
||||||
|
box.setAttribute("role", "dialog");
|
||||||
|
box.setAttribute("aria-modal", "true");
|
||||||
|
box.innerHTML =
|
||||||
|
'<button class="vc-lightbox__btn vc-lightbox__prev" type="button" aria-label="Previous"><svg class="icon icon--24" aria-hidden="true"><use href="#i-arrow-left"></use></svg></button>' +
|
||||||
|
'<img class="vc-lightbox__img" alt="">' +
|
||||||
|
'<button class="vc-lightbox__btn vc-lightbox__next" type="button" aria-label="Next"><svg class="icon icon--24" aria-hidden="true"><use href="#i-arrow-right"></use></svg></button>' +
|
||||||
|
'<button class="vc-lightbox__btn vc-lightbox__close" type="button" aria-label="Close"><svg class="icon icon--24" aria-hidden="true"><use href="#i-x"></use></svg></button>' +
|
||||||
|
'<span class="vc-lightbox__count"></span>';
|
||||||
|
host.appendChild(box);
|
||||||
|
|
||||||
|
var bigImg = box.querySelector(".vc-lightbox__img");
|
||||||
|
var count = box.querySelector(".vc-lightbox__count");
|
||||||
|
var idx = 0;
|
||||||
|
|
||||||
|
function show(i) {
|
||||||
|
idx = (i + imgs.length) % imgs.length;
|
||||||
|
bigImg.src = imgs[idx].currentSrc || imgs[idx].src;
|
||||||
|
bigImg.alt = imgs[idx].alt || "";
|
||||||
|
count.textContent = (idx + 1) + " / " + imgs.length;
|
||||||
|
}
|
||||||
|
function open(i) {
|
||||||
|
show(i);
|
||||||
|
box.classList.add("is-open");
|
||||||
|
document.documentElement.style.overflow = "hidden";
|
||||||
|
}
|
||||||
|
function close() {
|
||||||
|
box.classList.remove("is-open");
|
||||||
|
document.documentElement.style.overflow = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
imgs.forEach(function (img, i) {
|
||||||
|
img.addEventListener("click", function () { open(i); });
|
||||||
|
});
|
||||||
|
box.querySelector(".vc-lightbox__prev").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.addEventListener("click", function (e) { if (e.target === box) close(); });
|
||||||
|
document.addEventListener("keydown", function (e) {
|
||||||
|
if (!box.classList.contains("is-open")) return;
|
||||||
|
if (e.key === "Escape") close();
|
||||||
|
else if (e.key === "ArrowLeft") show(idx - 1);
|
||||||
|
else if (e.key === "ArrowRight") show(idx + 1);
|
||||||
|
});
|
||||||
|
})();
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -17,7 +17,11 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
|
||||||
const SCOPE = process.env.VC_SCOPE || '.vc-app';
|
// Doubled class (`.vc-app.vc-app`) so every ported rule carries an extra class
|
||||||
|
// of specificity — enough to win against Astra's content selectors (e.g.
|
||||||
|
// `.ast-… .entry-content a`) without touching the markup, which still only
|
||||||
|
// needs a single `class="vc-app"`. Override with VC_SCOPE if ever needed.
|
||||||
|
const SCOPE = process.env.VC_SCOPE || '.vc-app.vc-app';
|
||||||
|
|
||||||
function prefixSelector(sel) {
|
function prefixSelector(sel) {
|
||||||
sel = sel.trim();
|
sel = sel.trim();
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ class VC_Display {
|
|||||||
'viator_top_destinations',
|
'viator_top_destinations',
|
||||||
'viator_top_attractions',
|
'viator_top_attractions',
|
||||||
'viator_browse_activities',
|
'viator_browse_activities',
|
||||||
|
'viator_bus_tours',
|
||||||
|
'viator_about',
|
||||||
|
'viator_articles',
|
||||||
'viator_faq',
|
'viator_faq',
|
||||||
'viator_testimonials',
|
'viator_testimonials',
|
||||||
'viator_tour_card',
|
'viator_tour_card',
|
||||||
@@ -24,7 +27,12 @@ class VC_Display {
|
|||||||
);
|
);
|
||||||
|
|
||||||
public static function init() {
|
public static function init() {
|
||||||
add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_assets' ) );
|
// Priority 100 so the theme (Astra + child) has already enqueued ALL of
|
||||||
|
// its stylesheets — including the parent style.css and the child's chrome
|
||||||
|
// CSS, which are enqueued by the theme at priority 20 and would otherwise
|
||||||
|
// print AFTER us (plugins run before themes at equal priority) and win
|
||||||
|
// equal-specificity ties. Loading last makes our design win those ties.
|
||||||
|
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' ) );
|
||||||
add_filter( 'single_template', array( __CLASS__, 'single_template' ) );
|
add_filter( 'single_template', array( __CLASS__, 'single_template' ) );
|
||||||
@@ -62,9 +70,18 @@ class VC_Display {
|
|||||||
|| is_tax( array( 'viator_destination', 'viator_tag', 'viator_category' ) ) ) {
|
|| is_tax( array( 'viator_destination', 'viator_tag', 'viator_category' ) ) ) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// A Page hosting the full-listing shortcode also wants full width.
|
// Any Page hosting a Viator shortcode (hero, sections, listing, …) also
|
||||||
|
// wants the full-width, no-sidebar canvas.
|
||||||
$post = ( is_singular() ) ? get_post() : null;
|
$post = ( is_singular() ) ? get_post() : null;
|
||||||
return $post && has_shortcode( $post->post_content, 'viator_listing' );
|
if ( ! $post ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
foreach ( self::SHORTCODES as $shortcode ) {
|
||||||
|
if ( has_shortcode( $post->post_content, $shortcode ) ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -134,9 +151,19 @@ class VC_Display {
|
|||||||
// Design typeface.
|
// Design typeface.
|
||||||
wp_enqueue_style( 'vc-fonts', 'https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;700;800&display=swap', array(), null );
|
wp_enqueue_style( 'vc-fonts', 'https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;700;800&display=swap', array(), null );
|
||||||
|
|
||||||
// One scoped stylesheet (#vc-app) covers listing, single, and homepage
|
// One scoped stylesheet (.vc-app) covers listing, single, and homepage
|
||||||
// sections — ported from the "Hop On Hop Off" design.
|
// sections — ported from the "Hop On Hop Off" design. Declare every theme
|
||||||
wp_enqueue_style( 'vc-design', VC_PLUGIN_URL . 'assets/css/vc-design.css', array( 'vc-fonts' ), VC_VERSION );
|
// stylesheet that could fight ours as a dependency (when present) so our
|
||||||
|
// rules always print AFTER them and win equal-specificity ties. We enqueue
|
||||||
|
// at priority 100, so by now the theme (priority 20) has registered them.
|
||||||
|
$deps = array( 'vc-fonts' );
|
||||||
|
foreach ( array( 'astra-theme-css', 'astra-parent', 'hoho-chrome' ) as $handle ) {
|
||||||
|
if ( wp_style_is( $handle, 'registered' ) ) {
|
||||||
|
$deps[] = $handle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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() );
|
||||||
|
|
||||||
// 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.
|
||||||
@@ -146,6 +173,111 @@ class VC_Display {
|
|||||||
add_action( 'wp_footer', 'vc_print_icon_sprite' );
|
add_action( 'wp_footer', 'vc_print_icon_sprite' );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CSS that neutralises Astra's content spacing/width on Viator pages and
|
||||||
|
* makes the design typography win over the theme. Injected after vc-design.css.
|
||||||
|
*/
|
||||||
|
private static function astra_overrides() {
|
||||||
|
return implode(
|
||||||
|
'',
|
||||||
|
array(
|
||||||
|
// Full-bleed: strip Astra's page-builder article padding/width so
|
||||||
|
// the hero and sections reach the viewport edges. !important beats
|
||||||
|
// Astra's own high-specificity rules (e.g. the 2em article padding
|
||||||
|
// that leaves a gap above the hero).
|
||||||
|
'.vc-fullwidth .site-main>article,'
|
||||||
|
. '.vc-fullwidth .ast-page-builder-template .site-main>article,'
|
||||||
|
. '.vc-fullwidth.ast-page-builder-template .site-main>article'
|
||||||
|
. '{padding:0!important;margin:0!important;border:0!important;max-width:none!important;}',
|
||||||
|
'.vc-fullwidth #primary,.vc-fullwidth #content{margin:0!important;padding:0!important;}',
|
||||||
|
// display:block undoes Astra's `.site-content .ast-container{display:flex}`
|
||||||
|
// (≥922px), which otherwise sizes our content to its contents instead
|
||||||
|
// of filling the full-width container.
|
||||||
|
'.vc-fullwidth .site-content .ast-container,.vc-fullwidth .ast-container'
|
||||||
|
. '{max-width:none!important;padding-left:0!important;padding-right:0!important;display:block!important;}',
|
||||||
|
'.vc-fullwidth .entry-content{margin-block:0!important;}',
|
||||||
|
'.vc-fullwidth .site-content{padding:0!important;}',
|
||||||
|
// Guard against a stray horizontal scrollbar from any full-bleed child.
|
||||||
|
'body.vc-fullwidth{overflow-x:hidden;}',
|
||||||
|
// A gradient stands in when no hero image is set, so the banner is
|
||||||
|
// never blank.
|
||||||
|
'.vc-app.vc-app .hero__media,.vc-app.vc-app .hero-tour{background:linear-gradient(120deg,#101317,#2cafc2);}',
|
||||||
|
// Normalize: stop Astra from decorating our components. Its
|
||||||
|
// .entry-content rules (underlined links, list bullets/indent)
|
||||||
|
// out-specify our scoped base, so reset them here (!important beats
|
||||||
|
// e.g. `.ast-single-post .entry-content a`). Every selector matches
|
||||||
|
// the doubled `.vc-app.vc-app` scope of the main stylesheet so these
|
||||||
|
// patches keep winning against it, not just against the theme.
|
||||||
|
'.vc-app.vc-app a{text-decoration:none;box-shadow:none;}',
|
||||||
|
// No !important on margin/padding: the doubled .vc-app.vc-app scope
|
||||||
|
// already out-specifies Astra's list rules, and !important here was
|
||||||
|
// steamrolling design lists that need their own spacing — e.g. the
|
||||||
|
// rail (<ul class="rail">) whose vertical padding gives the hover glow
|
||||||
|
// room. list-style stays !important (purely cosmetic; prose lists
|
||||||
|
// re-enable bullets via a higher-specificity rule).
|
||||||
|
'.vc-app.vc-app :is(ul,ol){list-style:none!important;margin:0;padding:0;}',
|
||||||
|
'.vc-app.vc-app li{margin:0;}',
|
||||||
|
// …but keep the tour "Overview" prose (the_content) readable.
|
||||||
|
'.vc-app.vc-app .tour__text a{text-decoration:underline;}',
|
||||||
|
'.vc-app.vc-app .tour__text ul{list-style:disc!important;margin:1em 0!important;padding-inline-start:1.5em!important;}',
|
||||||
|
'.vc-app.vc-app .tour__text ol{list-style:decimal!important;margin:1em 0!important;padding-inline-start:1.5em!important;}',
|
||||||
|
'.vc-app.vc-app .tour__text li{margin:.25em 0!important;}',
|
||||||
|
// Links must inherit the design colour (Astra colours links with its
|
||||||
|
// theme colour, which turned white card titles/destination names into
|
||||||
|
// coloured text). Buttons keep their own colour.
|
||||||
|
// …but not ring-badges: they set their own colour on the <a>, and this
|
||||||
|
// inherit rule out-specifies .ring-badge--top/--scroll, which turned
|
||||||
|
// the badge text white (inherited) on light backgrounds.
|
||||||
|
'.vc-app.vc-app a:not(.btn):not(.ring-badge){color:inherit;}',
|
||||||
|
'.vc-app.vc-app .tour__text a{color:var(--c-teal);}',
|
||||||
|
// Real tour titles are far longer than the mockup\'s city names — clamp
|
||||||
|
// them and stop price/duration values wrapping mid-number.
|
||||||
|
'.vc-app.vc-app .dcard__title{display:-webkit-box;-webkit-line-clamp:2;line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;}',
|
||||||
|
'.vc-app.vc-app .titem__desc{display:-webkit-box;-webkit-line-clamp:2;line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;}',
|
||||||
|
// Real tour descriptions run long; the on-hover card blurb clamps to
|
||||||
|
// three lines with an ellipsis instead of spilling over the card.
|
||||||
|
'.vc-app.vc-app .dcard__desc p{display:-webkit-box;-webkit-line-clamp:3;line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;}',
|
||||||
|
'.vc-app.vc-app .dcard__meta .meta__v{white-space:nowrap;}',
|
||||||
|
// Astra's page-builder layout centres/limits every entry-content child
|
||||||
|
// (max-width:none; margin:auto), which flattened the hero inset. Our
|
||||||
|
// sections are full-bleed, so re-assert 0 side-margins; !important
|
||||||
|
// beats Astra's 4-class selector. The hero keeps its small inset — the
|
||||||
|
// only section with an intentional side margin.
|
||||||
|
'.vc-app.vc-app>*{max-width:none!important;margin-inline:0!important;}',
|
||||||
|
// Heroes keep their small inset (rounded card look) — .hero on the
|
||||||
|
// home/archive pages and .hero-tour on the single-tour page. Without
|
||||||
|
// this they went full-bleed after the margin-inline:0 reset above.
|
||||||
|
'.vc-app.vc-app>.hero,.vc-app.vc-app>.hero-tour{margin:var(--page-inset)!important;}',
|
||||||
|
// Let vertical decorations (rotating badges, card glow, promo bus, FAQ
|
||||||
|
// watermark) escape, but keep the horizontal axis clipped so a
|
||||||
|
// full-bleed section or decorative element can't cause a mobile
|
||||||
|
// sideways scroll. overflow-x:clip (unlike hidden) does NOT force
|
||||||
|
// overflow-y to auto, so the y-axis stays truly visible.
|
||||||
|
'.vc-app.vc-app{overflow-x:clip;overflow-y:visible;}',
|
||||||
|
// Astra wraps the content in article/.entry-content/.ast-container,
|
||||||
|
// some of which clip overflow — that cut the card hover glow at the
|
||||||
|
// rail edges. Open them up on our pages only (page scroll stays
|
||||||
|
// contained by body.vc-fullwidth{overflow-x:hidden}).
|
||||||
|
'.vc-fullwidth .site-main,.vc-fullwidth #primary,.vc-fullwidth #content,'
|
||||||
|
. '.vc-fullwidth .site-content,.vc-fullwidth .ast-container,'
|
||||||
|
. '.vc-fullwidth .entry-content,.vc-fullwidth .site-main>article'
|
||||||
|
. '{overflow:visible!important;}',
|
||||||
|
// FAQ question buttons must stay flat — Astra gives <button> a shadow.
|
||||||
|
'.vc-app.vc-app .acc__q{box-shadow:none!important;}',
|
||||||
|
// Astra colours every heading on the element itself (h1–h6 and
|
||||||
|
// .entry-content :where(h1..h6)), beating our container-inherited
|
||||||
|
// colours. Make headings inherit again — no !important, so design
|
||||||
|
// classes (.hero__title, the white .tcard titles) keep their own.
|
||||||
|
'.vc-app.vc-app :is(h1,h2,h3,h4,h5,h6){color:inherit;}',
|
||||||
|
// Typography: force the design font over Astra inside our scopes.
|
||||||
|
'.vc-app.vc-app,.vc-app.vc-app h1,.vc-app.vc-app h2,.vc-app.vc-app h3,.vc-app.vc-app h4,.vc-app.vc-app h5,.vc-app.vc-app h6,'
|
||||||
|
. '.vc-app.vc-app p,.vc-app.vc-app a,.vc-app.vc-app span,.vc-app.vc-app li,.vc-app.vc-app button,.vc-app.vc-app input,'
|
||||||
|
. '.vc-app.vc-app select,.vc-app.vc-app textarea,.vc-app.vc-app blockquote'
|
||||||
|
. '{font-family:"Plus Jakarta Sans",system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;}',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read + sanitize the current filter state from the query string. Shared by
|
* Read + sanitize the current filter state from the query string. Shared by
|
||||||
* the query builder and the archive template so both stay in sync.
|
* the query builder and the archive template so both stay in sync.
|
||||||
@@ -214,7 +346,7 @@ 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 );
|
$vars = array( 'posts_per_page' => 12, 'ignore_sticky_posts' => true );
|
||||||
|
|
||||||
if ( '' !== $f['q'] ) {
|
if ( '' !== $f['q'] ) {
|
||||||
$vars['s'] = $f['q'];
|
$vars['s'] = $f['q'];
|
||||||
@@ -271,46 +403,39 @@ class VC_Display {
|
|||||||
if ( $f['sellout'] ) {
|
if ( $f['sellout'] ) {
|
||||||
$meta_query[] = array( 'key' => '_vc_flags', 'value' => 'LIKELY_TO_SELL_OUT', 'compare' => 'LIKE' );
|
$meta_query[] = array( 'key' => '_vc_flags', 'value' => 'LIKELY_TO_SELL_OUT', 'compare' => 'LIKE' );
|
||||||
}
|
}
|
||||||
// Featured tours always sort first, whatever the chosen sort. OR/NOT
|
// Sort. Performance: the default (newest) orders by post date only — no
|
||||||
// EXISTS keeps non-featured tours in the results too.
|
// postmeta JOIN and no featured/review tiebreak — so the unfiltered
|
||||||
$meta_query['vc_featured'] = array(
|
// archive stays fast even with a very large catalogue (tens of thousands
|
||||||
'relation' => 'OR',
|
// of tours). Price/rating sorts add a single meta_key JOIN, on demand.
|
||||||
array( 'key' => '_vc_featured', 'value' => '1', 'compare' => '=' ),
|
|
||||||
array( 'key' => '_vc_featured', 'compare' => 'NOT EXISTS' ),
|
|
||||||
);
|
|
||||||
|
|
||||||
$orderby = array( 'vc_featured' => 'DESC' );
|
|
||||||
|
|
||||||
switch ( $f['sort'] ) {
|
switch ( $f['sort'] ) {
|
||||||
case 'price_asc':
|
case 'price_asc':
|
||||||
$meta_query['vc_price'] = array( 'key' => '_vc_price_from', 'type' => 'DECIMAL', 'compare' => 'EXISTS' );
|
$vars['meta_key'] = '_vc_price_from';
|
||||||
$orderby['vc_price'] = 'ASC';
|
$vars['orderby'] = 'meta_value_num';
|
||||||
|
$vars['order'] = 'ASC';
|
||||||
break;
|
break;
|
||||||
case 'price_desc':
|
case 'price_desc':
|
||||||
$meta_query['vc_price'] = array( 'key' => '_vc_price_from', 'type' => 'DECIMAL', 'compare' => 'EXISTS' );
|
$vars['meta_key'] = '_vc_price_from';
|
||||||
$orderby['vc_price'] = 'DESC';
|
$vars['orderby'] = 'meta_value_num';
|
||||||
|
$vars['order'] = 'DESC';
|
||||||
break;
|
break;
|
||||||
case 'rating':
|
case 'rating':
|
||||||
$meta_query['vc_rating_sort'] = array( 'key' => '_vc_rating', 'type' => 'DECIMAL', 'compare' => 'EXISTS' );
|
$vars['meta_key'] = '_vc_rating';
|
||||||
$orderby['vc_rating_sort'] = 'DESC';
|
$vars['orderby'] = 'meta_value_num';
|
||||||
|
$vars['order'] = 'DESC';
|
||||||
break;
|
break;
|
||||||
|
default:
|
||||||
|
$vars['orderby'] = 'date';
|
||||||
|
$vars['order'] = 'DESC';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep the selected sort as the primary order, then prefer the more
|
// Only attach a meta_query when a filter is actually active, so the plain
|
||||||
// established tour when two products otherwise rank the same.
|
// archive query carries no postmeta joins at all.
|
||||||
$meta_query['vc_reviews_sort'] = array(
|
if ( ! empty( $meta_query ) ) {
|
||||||
'key' => '_vc_review_count',
|
|
||||||
'type' => 'NUMERIC',
|
|
||||||
'compare' => 'EXISTS',
|
|
||||||
);
|
|
||||||
$orderby['vc_reviews_sort'] = 'DESC';
|
|
||||||
$orderby['date'] = 'DESC'; // stable tiebreak
|
|
||||||
|
|
||||||
if ( count( $meta_query ) > 1 ) {
|
if ( count( $meta_query ) > 1 ) {
|
||||||
$meta_query['relation'] = 'AND';
|
$meta_query['relation'] = 'AND';
|
||||||
}
|
}
|
||||||
$vars['meta_query'] = $meta_query;
|
$vars['meta_query'] = $meta_query;
|
||||||
$vars['orderby'] = $orderby;
|
}
|
||||||
|
|
||||||
return $vars;
|
return $vars;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ class VC_Sections {
|
|||||||
const TOP_ATTRS = 'viator_top_attractions';
|
const TOP_ATTRS = 'viator_top_attractions';
|
||||||
const ACTIVITIES = 'viator_browse_activities';
|
const ACTIVITIES = 'viator_browse_activities';
|
||||||
const TOP_TOURS = 'viator_top_tours';
|
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 FAQ = 'viator_faq';
|
||||||
const REVIEWS = 'viator_testimonials';
|
const REVIEWS = 'viator_testimonials';
|
||||||
|
|
||||||
@@ -28,6 +31,9 @@ class VC_Sections {
|
|||||||
add_shortcode( self::TOP_ATTRS, array( __CLASS__, 'top_attractions' ) );
|
add_shortcode( self::TOP_ATTRS, array( __CLASS__, 'top_attractions' ) );
|
||||||
add_shortcode( self::ACTIVITIES, array( __CLASS__, 'browse_activities' ) );
|
add_shortcode( self::ACTIVITIES, array( __CLASS__, 'browse_activities' ) );
|
||||||
add_shortcode( self::TOP_TOURS, array( __CLASS__, 'top_tours' ) );
|
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::FAQ, array( __CLASS__, 'faq' ) );
|
||||||
add_shortcode( self::REVIEWS, array( __CLASS__, 'testimonials' ) );
|
add_shortcode( self::REVIEWS, array( __CLASS__, 'testimonials' ) );
|
||||||
}
|
}
|
||||||
@@ -44,14 +50,21 @@ class VC_Sections {
|
|||||||
'title' => __( 'Set out on the journey.', 'viator-collector' ),
|
'title' => __( 'Set out on the journey.', 'viator-collector' ),
|
||||||
'eyebrow' => __( 'Tour & Travel', 'viator-collector' ),
|
'eyebrow' => __( 'Tour & Travel', 'viator-collector' ),
|
||||||
'image' => '',
|
'image' => '',
|
||||||
|
'image_id' => '', // WordPress media attachment ID (responsive).
|
||||||
),
|
),
|
||||||
$atts,
|
$atts,
|
||||||
self::HERO
|
self::HERO
|
||||||
);
|
);
|
||||||
|
|
||||||
$img = $atts['image'] ? $atts['image'] : vc_hero_image_url();
|
// Prefer a media-library image (responsive srcset): shortcode image_id,
|
||||||
if ( ! $img ) {
|
// then a shortcode URL, then the plugin's Hero image setting. No bundled
|
||||||
$img = vc_asset_url( 'img/hero.png' );
|
// 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 = get_post_type_archive_link( 'viator_tour' );
|
||||||
$archive = $archive ? $archive : home_url( '/' );
|
$archive = $archive ? $archive : home_url( '/' );
|
||||||
@@ -64,7 +77,7 @@ class VC_Sections {
|
|||||||
<div class="vc-app vc-sections">
|
<div class="vc-app vc-sections">
|
||||||
<section class="hero">
|
<section class="hero">
|
||||||
<div class="hero__media">
|
<div class="hero__media">
|
||||||
<img class="hero__img" src="<?php echo esc_url( $img ); ?>" alt="" fetchpriority="high">
|
<?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 class="hero__scrim" aria-hidden="true"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="container hero__content">
|
<div class="container hero__content">
|
||||||
@@ -123,7 +136,7 @@ class VC_Sections {
|
|||||||
* Open a titled rail section and return its unique rail id. Caller fills the
|
* Open a titled rail section and return its unique rail id. Caller fills the
|
||||||
* <ul> with cards and then calls close_rail().
|
* <ul> with cards and then calls close_rail().
|
||||||
*/
|
*/
|
||||||
private static function open_rail( $title, $lead, $view_all_url, $view_all_text ) {
|
private static function open_rail( $title, $lead, $view_all_url, $view_all_text, $rail_class = 'rail--dest' ) {
|
||||||
$id = 'vcRail' . ( ++self::$rail_seq );
|
$id = 'vcRail' . ( ++self::$rail_seq );
|
||||||
ob_start();
|
ob_start();
|
||||||
?>
|
?>
|
||||||
@@ -143,7 +156,7 @@ class VC_Sections {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul class="rail rail--dest" id="<?php echo esc_attr( $id ); ?>">
|
<ul class="rail <?php echo esc_attr( $rail_class ); ?>" id="<?php echo esc_attr( $id ); ?>">
|
||||||
<?php
|
<?php
|
||||||
return array( 'id' => $id, 'head' => ob_get_clean() );
|
return array( 'id' => $id, 'head' => ob_get_clean() );
|
||||||
}
|
}
|
||||||
@@ -396,15 +409,15 @@ class VC_Sections {
|
|||||||
self::REVIEWS
|
self::REVIEWS
|
||||||
);
|
);
|
||||||
|
|
||||||
$a1 = vc_asset_url( 'img/ellipse-17.png' );
|
// Avatars are optional; supply them via the vc_testimonials filter
|
||||||
$a2 = vc_asset_url( 'img/ellipse-17-2.png' );
|
// ('avatar' => attachment URL) — none are bundled with the plugin.
|
||||||
$items = apply_filters(
|
$items = apply_filters(
|
||||||
'vc_testimonials',
|
'vc_testimonials',
|
||||||
array(
|
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' => $a1, 'size' => 'lg' ),
|
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' => $a2, 'size' => '' ),
|
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' => $a1, 'size' => 'sm' ),
|
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' => $a2, 'size' => '' ),
|
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 ) ) {
|
if ( empty( $items ) ) {
|
||||||
@@ -443,6 +456,299 @@ class VC_Sections {
|
|||||||
return ob_get_clean();
|
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 /
|
* Shared tour query for the landing sections. Supports curation (ids /
|
||||||
* product codes) and filtering (destination / tag), with a sane fallback.
|
* product codes) and filtering (destination / tag), with a sane fallback.
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ class VC_Settings {
|
|||||||
'timeout' => 20,
|
'timeout' => 20,
|
||||||
'default_post_status' => 'publish',
|
'default_post_status' => 'publish',
|
||||||
'hero_image' => '',
|
'hero_image' => '',
|
||||||
|
'hero_image_id' => 0,
|
||||||
|
'single_hero_figure_id' => 0,
|
||||||
'hero_slides' => '',
|
'hero_slides' => '',
|
||||||
'crawl_destinations' => '',
|
'crawl_destinations' => '',
|
||||||
'rate_limit_per_minute' => 25,
|
'rate_limit_per_minute' => 25,
|
||||||
@@ -122,6 +124,8 @@ class VC_Settings {
|
|||||||
'timeout' => max( 5, absint( $input['timeout'] ?? 20 ) ),
|
'timeout' => max( 5, absint( $input['timeout'] ?? 20 ) ),
|
||||||
'default_post_status' => sanitize_key( $input['default_post_status'] ?? 'publish' ),
|
'default_post_status' => sanitize_key( $input['default_post_status'] ?? 'publish' ),
|
||||||
'hero_image' => esc_url_raw( $input['hero_image'] ?? '' ),
|
'hero_image' => esc_url_raw( $input['hero_image'] ?? '' ),
|
||||||
|
'hero_image_id' => absint( $input['hero_image_id'] ?? 0 ),
|
||||||
|
'single_hero_figure_id' => absint( $input['single_hero_figure_id'] ?? 0 ),
|
||||||
'hero_slides' => self::sanitize_id_list( $input['hero_slides'] ?? '' ),
|
'hero_slides' => self::sanitize_id_list( $input['hero_slides'] ?? '' ),
|
||||||
'crawl_destinations' => self::sanitize_id_list( $input['crawl_destinations'] ?? '' ),
|
'crawl_destinations' => self::sanitize_id_list( $input['crawl_destinations'] ?? '' ),
|
||||||
'rate_limit_per_minute' => max( 1, min( 60, absint( $input['rate_limit_per_minute'] ?? 25 ) ) ),
|
'rate_limit_per_minute' => max( 1, min( 60, absint( $input['rate_limit_per_minute'] ?? 25 ) ) ),
|
||||||
@@ -198,9 +202,10 @@ class VC_Settings {
|
|||||||
<th scope="row"><label for="vc_hero_image"><?php echo esc_html__( 'Hero image', 'viator-collector' ); ?></label></th>
|
<th scope="row"><label for="vc_hero_image"><?php echo esc_html__( 'Hero image', 'viator-collector' ); ?></label></th>
|
||||||
<td>
|
<td>
|
||||||
<input id="vc_hero_image" type="url" class="regular-text" name="vc_settings[hero_image]" value="<?php echo esc_attr( $options['hero_image'] ); ?>" placeholder="https://…">
|
<input id="vc_hero_image" type="url" class="regular-text" name="vc_settings[hero_image]" value="<?php echo esc_attr( $options['hero_image'] ); ?>" placeholder="https://…">
|
||||||
|
<input id="vc_hero_image_id" type="hidden" name="vc_settings[hero_image_id]" value="<?php echo esc_attr( $options['hero_image_id'] ); ?>">
|
||||||
<button type="button" class="button" id="vc-hero-pick"><?php echo esc_html__( 'Select image', 'viator-collector' ); ?></button>
|
<button type="button" class="button" id="vc-hero-pick"><?php echo esc_html__( 'Select image', 'viator-collector' ); ?></button>
|
||||||
<button type="button" class="button" id="vc-hero-clear"><?php echo esc_html__( 'Clear', 'viator-collector' ); ?></button>
|
<button type="button" class="button" id="vc-hero-clear"><?php echo esc_html__( 'Clear', 'viator-collector' ); ?></button>
|
||||||
<p class="description"><?php echo esc_html__( 'Banner shown at the top of the tours listing. Leave empty for a gradient. Destination pages use their own image when one is set.', 'viator-collector' ); ?></p>
|
<p class="description"><?php echo esc_html__( 'Banner at the top of the tours listing / home hero. Pick from the Media Library so the browser gets responsive, device-sized images. Leave empty for a gradient. Destination pages use their own image when one is set.', 'viator-collector' ); ?></p>
|
||||||
<div id="vc-hero-preview" style="margin-top:8px;">
|
<div id="vc-hero-preview" style="margin-top:8px;">
|
||||||
<?php if ( $options['hero_image'] ) : ?>
|
<?php if ( $options['hero_image'] ) : ?>
|
||||||
<img src="<?php echo esc_url( $options['hero_image'] ); ?>" alt="" style="max-width:320px;height:auto;border-radius:8px;">
|
<img src="<?php echo esc_url( $options['hero_image'] ); ?>" alt="" style="max-width:320px;height:auto;border-radius:8px;">
|
||||||
@@ -221,6 +226,7 @@ class VC_Settings {
|
|||||||
frame.on('select', function () {
|
frame.on('select', function () {
|
||||||
var img = frame.state().get('selection').first().toJSON();
|
var img = frame.state().get('selection').first().toJSON();
|
||||||
$('#vc_hero_image').val(img.url);
|
$('#vc_hero_image').val(img.url);
|
||||||
|
$('#vc_hero_image_id').val(img.id);
|
||||||
$('#vc-hero-preview').html($('<img>', { src: img.url, alt: '' }).css({ maxWidth: '320px', height: 'auto', borderRadius: '8px' }));
|
$('#vc-hero-preview').html($('<img>', { src: img.url, alt: '' }).css({ maxWidth: '320px', height: 'auto', borderRadius: '8px' }));
|
||||||
});
|
});
|
||||||
frame.open();
|
frame.open();
|
||||||
@@ -228,12 +234,56 @@ class VC_Settings {
|
|||||||
$('#vc-hero-clear').on('click', function (e) {
|
$('#vc-hero-clear').on('click', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
$('#vc_hero_image').val('');
|
$('#vc_hero_image').val('');
|
||||||
|
$('#vc_hero_image_id').val('');
|
||||||
$('#vc-hero-preview').empty();
|
$('#vc-hero-preview').empty();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><label><?php echo esc_html__( 'Single tour hero figure', 'viator-collector' ); ?></label></th>
|
||||||
|
<td>
|
||||||
|
<input id="vc_single_figure_id" type="hidden" name="vc_settings[single_hero_figure_id]" value="<?php echo esc_attr( $options['single_hero_figure_id'] ); ?>">
|
||||||
|
<button type="button" class="button" id="vc-figure-pick"><?php echo esc_html__( 'Select image', 'viator-collector' ); ?></button>
|
||||||
|
<button type="button" class="button" id="vc-figure-clear"><?php echo esc_html__( 'Clear', 'viator-collector' ); ?></button>
|
||||||
|
<p class="description"><?php echo esc_html__( 'Optional decorative cut-out shown over the single-tour hero (e.g. a landmark PNG with a transparent background). Leave empty to hide it.', 'viator-collector' ); ?></p>
|
||||||
|
<div id="vc-figure-preview" style="margin-top:8px;">
|
||||||
|
<?php
|
||||||
|
if ( $options['single_hero_figure_id'] ) {
|
||||||
|
echo wp_get_attachment_image( (int) $options['single_hero_figure_id'], 'medium', false, array( 'style' => 'max-width:200px;height:auto;' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
jQuery(function ($) {
|
||||||
|
var frame;
|
||||||
|
$('#vc-figure-pick').on('click', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (frame) { frame.open(); return; }
|
||||||
|
frame = wp.media({
|
||||||
|
title: <?php echo wp_json_encode( __( 'Select hero figure', 'viator-collector' ) ); ?>,
|
||||||
|
button: { text: <?php echo wp_json_encode( __( 'Use this image', 'viator-collector' ) ); ?> },
|
||||||
|
library: { type: 'image' },
|
||||||
|
multiple: false
|
||||||
|
});
|
||||||
|
frame.on('select', function () {
|
||||||
|
var img = frame.state().get('selection').first().toJSON();
|
||||||
|
$('#vc_single_figure_id').val(img.id);
|
||||||
|
var src = (img.sizes && img.sizes.medium) ? img.sizes.medium.url : img.url;
|
||||||
|
$('#vc-figure-preview').html($('<img>', { src: src, alt: '' }).css({ maxWidth: '200px', height: 'auto' }));
|
||||||
|
});
|
||||||
|
frame.open();
|
||||||
|
});
|
||||||
|
$('#vc-figure-clear').on('click', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
$('#vc_single_figure_id').val('');
|
||||||
|
$('#vc-figure-preview').empty();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row"><label for="vc_hero_slides"><?php echo esc_html__( 'Hero slider images', 'viator-collector' ); ?></label></th>
|
<th scope="row"><label for="vc_hero_slides"><?php echo esc_html__( 'Hero slider images', 'viator-collector' ); ?></label></th>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -138,6 +138,34 @@ function vc_hero_image_url() {
|
|||||||
return apply_filters( 'vc_hero_image_url', $url );
|
return apply_filters( 'vc_hero_image_url', $url );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hero image *source* for the current view: an attachment ID when available
|
||||||
|
* (so it can be rendered responsively), otherwise a URL. Resolution order:
|
||||||
|
* 1. On a term archive, that term's own image.
|
||||||
|
* 2. The "Hero image" chosen in Settings (attachment ID preferred).
|
||||||
|
* 3. Nothing.
|
||||||
|
*
|
||||||
|
* @return int|string Attachment ID, image URL, or '' .
|
||||||
|
*/
|
||||||
|
function vc_hero_image_source() {
|
||||||
|
if ( is_tax( array( 'viator_destination', 'viator_tag', 'viator_category' ) ) ) {
|
||||||
|
$term = get_queried_object();
|
||||||
|
if ( $term && ! is_wp_error( $term ) ) {
|
||||||
|
$url = vc_term_image_url( $term->term_id, $term->taxonomy );
|
||||||
|
if ( $url ) {
|
||||||
|
return apply_filters( 'vc_hero_image_source', $url );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = (int) vc_get_option( 'hero_image_id', 0 );
|
||||||
|
if ( $id ) {
|
||||||
|
return apply_filters( 'vc_hero_image_source', $id );
|
||||||
|
}
|
||||||
|
|
||||||
|
return apply_filters( 'vc_hero_image_source', (string) vc_get_option( 'hero_image', '' ) );
|
||||||
|
}
|
||||||
|
|
||||||
function vc_term_image_url( $term_id, $taxonomy = '' ) {
|
function vc_term_image_url( $term_id, $taxonomy = '' ) {
|
||||||
$term_id = absint( $term_id );
|
$term_id = absint( $term_id );
|
||||||
if ( ! $term_id ) {
|
if ( ! $term_id ) {
|
||||||
@@ -506,6 +534,45 @@ function vc_asset_url( $rel ) {
|
|||||||
return VC_PLUGIN_URL . 'assets/' . ltrim( (string) $rel, '/' );
|
return VC_PLUGIN_URL . 'assets/' . ltrim( (string) $rel, '/' );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hero <img> markup. When the source is a WordPress media item (an attachment
|
||||||
|
* ID, or a URL that resolves to one), it is rendered with wp_get_attachment_image
|
||||||
|
* so the browser gets responsive `srcset`/`sizes` and device-appropriate files.
|
||||||
|
* Anything else (external URLs, bundled fallbacks) falls back to a plain <img>.
|
||||||
|
*
|
||||||
|
* @param int|string $src Attachment ID or image URL.
|
||||||
|
* @param string $class CSS class.
|
||||||
|
* @param bool $eager Prioritise loading (above-the-fold hero).
|
||||||
|
*/
|
||||||
|
function vc_hero_img_html( $src, $class = 'hero__img', $eager = true ) {
|
||||||
|
$loading = $eager ? 'eager' : 'lazy';
|
||||||
|
|
||||||
|
$id = is_numeric( $src ) ? (int) $src : (int) attachment_url_to_postid( (string) $src );
|
||||||
|
if ( $id ) {
|
||||||
|
$img = wp_get_attachment_image(
|
||||||
|
$id,
|
||||||
|
'full',
|
||||||
|
false,
|
||||||
|
array(
|
||||||
|
'class' => $class,
|
||||||
|
'loading' => $loading,
|
||||||
|
'alt' => '',
|
||||||
|
'sizes' => '100vw',
|
||||||
|
'fetchpriority' => $eager ? 'high' : 'auto',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if ( $img ) {
|
||||||
|
return $img;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! $src ) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
$fetch = $eager ? ' fetchpriority="high"' : '';
|
||||||
|
return '<img class="' . esc_attr( $class ) . '" src="' . esc_url( $src ) . '" alt="" loading="' . esc_attr( $loading ) . '"' . $fetch . '>';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Print the "Hop On Hop Off" SVG icon sprite once per request. The sprite
|
* Print the "Hop On Hop Off" SVG icon sprite once per request. The sprite
|
||||||
* holds every <symbol id="i-…"> plus the #ringPathScroll circular text path
|
* holds every <symbol id="i-…"> plus the #ringPathScroll circular text path
|
||||||
@@ -528,7 +595,10 @@ function vc_print_icon_sprite() {
|
|||||||
* e.g. vc_icon( 'star', 'rating__star' ) => <svg class="icon rating__star">…</svg>
|
* e.g. vc_icon( 'star', 'rating__star' ) => <svg class="icon rating__star">…</svg>
|
||||||
*/
|
*/
|
||||||
function vc_icon( $name, $extra_class = '' ) {
|
function vc_icon( $name, $extra_class = '' ) {
|
||||||
$name = preg_replace( '/[^a-z0-9\-]/', '', 'i-' . ltrim( (string) $name, 'i-' ) );
|
// Strip an existing "i-" prefix (once), then re-add it. ltrim() with a
|
||||||
|
// charlist would eat any leading i/-, turning "instagram" into "nstagram".
|
||||||
|
$slug = preg_replace( '/^i-/', '', (string) $name );
|
||||||
|
$name = preg_replace( '/[^a-z0-9\-]/', '', 'i-' . $slug );
|
||||||
$class = 'icon' . ( $extra_class ? ' ' . $extra_class : '' );
|
$class = 'icon' . ( $extra_class ? ' ' . $extra_class : '' );
|
||||||
return '<svg class="' . esc_attr( $class ) . '" aria-hidden="true"><use href="#' . esc_attr( $name ) . '"></use></svg>';
|
return '<svg class="' . esc_attr( $class ) . '" aria-hidden="true"><use href="#' . esc_attr( $name ) . '"></use></svg>';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,14 +75,11 @@ $vc_page_url = function ( $n ) use ( $vc_action, $vc_base_args ) {
|
|||||||
return empty( $args ) ? $vc_action : add_query_arg( $args, $vc_action );
|
return empty( $args ) ? $vc_action : add_query_arg( $args, $vc_action );
|
||||||
};
|
};
|
||||||
|
|
||||||
$vc_hero_img = vc_hero_image_url();
|
$vc_hero_img = vc_hero_image_source();
|
||||||
if ( ! $vc_hero_img ) {
|
|
||||||
$vc_hero_img = vc_asset_url( 'img/hero2.png' );
|
|
||||||
}
|
|
||||||
?>
|
?>
|
||||||
<section class="hero hero--archive">
|
<section class="hero hero--archive">
|
||||||
<div class="hero__media">
|
<div class="hero__media">
|
||||||
<img class="hero__img" src="<?php echo esc_url( $vc_hero_img ); ?>" alt="" fetchpriority="high">
|
<?php echo vc_hero_img_html( $vc_hero_img, 'hero__img', true ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||||
<div class="hero__scrim" aria-hidden="true"></div>
|
<div class="hero__scrim" aria-hidden="true"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -38,16 +38,30 @@ while ( have_posts() ) :
|
|||||||
?>
|
?>
|
||||||
<div class="vc-app vc-single">
|
<div class="vc-app vc-single">
|
||||||
<section class="hero-tour">
|
<section class="hero-tour">
|
||||||
<?php if ( $primary ) : ?>
|
<?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">
|
<img class="hero-tour__photo" src="<?php echo esc_url( $primary ); ?>" alt="" fetchpriority="high">
|
||||||
<?php endif; ?>
|
<?php
|
||||||
|
endif;
|
||||||
|
?>
|
||||||
<div class="hero-tour__sky" aria-hidden="true"></div>
|
<div class="hero-tour__sky" aria-hidden="true"></div>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="hero-tour__block">
|
<div class="hero-tour__block">
|
||||||
<p class="hero-tour__kicker"><?php echo esc_html__( 'Welcome to', 'viator-collector' ); ?></p>
|
<p class="hero-tour__kicker"><?php echo esc_html__( 'Welcome to', 'viator-collector' ); ?></p>
|
||||||
<div class="hero-tour__stage">
|
<div class="hero-tour__stage">
|
||||||
<h1 class="hero-tour__word"><?php echo esc_html( $big_word ); ?></h1>
|
<h1 class="hero-tour__word"><?php echo esc_html( $big_word ); ?></h1>
|
||||||
<img class="hero-tour__figure" src="<?php echo esc_url( vc_asset_url( 'img/tower.png' ) ); ?>" alt="" loading="eager">
|
<?php
|
||||||
|
$vc_figure_id = (int) vc_get_option( 'single_hero_figure_id', 0 );
|
||||||
|
if ( $vc_figure_id ) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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.8.0
|
* Version: 1.8.15
|
||||||
* 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.8.0');
|
define('VC_VERSION', '1.8.15');
|
||||||
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__));
|
||||||
|
|||||||