Skip to content
Couple's Passport

Store

/* ============================================================ MANGO GETAWAY STORE V1.1 Dynamic MG-STORE v0.2.0 frontend ============================================================ */ #mgStore { --mg-bg: #0d0d0c; --mg-bg-soft: #141412; --mg-card: #1a1a17; --mg-card-hover: #1e1e1a; --mg-cream: #f3eadb; --mg-cream-soft: #cec4b5; --mg-muted: #918a80; --mg-orange: #f28a2e; --mg-orange-hover: #ff9a3c; --mg-orange-soft: rgba(242, 138, 46, 0.13); --mg-line: rgba(243, 234, 219, 0.12); --mg-line-strong: rgba(243, 234, 219, 0.24); --mg-success: #a7c79f; --mg-warning: #ddb875; --mg-radius: 24px; --mg-radius-small: 14px; width: 100%; overflow: hidden; background: var(--mg-bg); color: var(--mg-cream); font-family: inherit; } #mgStore *, #mgStore *::before, #mgStore *::after { box-sizing: border-box; } #mgStore button, #mgStore a { font: inherit; } #mgStore img { display: block; max-width: 100%; } /* ============================================================ SHELL ============================================================ */ .mg-store-shell { width: min(1240px, calc(100% - 48px)); margin-inline: auto; } /* ============================================================ HERO ============================================================ */ .mg-store-hero { position: relative; min-height: 88vh; display: flex; align-items: center; overflow: hidden; background: radial-gradient( circle at 82% 10%, rgba(242, 138, 46, 0.10), transparent 31% ), linear-gradient( 155deg, #0b0b0a 0%, #10100e 58%, #16120f 100% ); } .mg-store-hero::before { content: ""; position: absolute; inset: 0; background: linear-gradient( rgba(255,255,255,0.018) 1px, transparent 1px ), linear-gradient( 90deg, rgba(255,255,255,0.018) 1px, transparent 1px ); background-size: 78px 78px; mask-image: linear-gradient( to bottom, #000 0%, transparent 95% ); pointer-events: none; } .mg-store-hero__ambient { position: absolute; width: 620px; height: 620px; border-radius: 50%; right: -240px; top: -100px; background: rgba(242, 138, 46, 0.065); filter: blur(60px); pointer-events: none; } .mg-store-hero__inner { position: relative; z-index: 2; padding: 125px 0 82px; } .mg-store-kicker, .mg-store-eyebrow { margin: 0; color: var(--mg-orange); font-size: 0.71rem; line-height: 1; font-weight: 850; letter-spacing: 0.17em; text-transform: uppercase; } .mg-store-hero h1 { max-width: 1010px; margin: 24px 0 0; color: var(--mg-cream); font-size: clamp(3.2rem, 7vw, 7.4rem); line-height: 0.95; letter-spacing: -0.058em; font-weight: 650; } .mg-store-hero h1 span { color: var(--mg-muted); } .mg-store-hero__intro { max-width: 680px; margin: 31px 0 0; color: var(--mg-cream-soft); font-size: clamp(1rem, 1.6vw, 1.18rem); line-height: 1.75; } /* ============================================================ AUDIENCE ============================================================ */ .mg-store-audience { max-width: 920px; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin-top: 52px; } .mg-store-audience__button { appearance: none; display: flex; align-items: center; gap: 16px; padding: 21px; text-align: left; border: 1px solid var(--mg-line); border-radius: 18px; background: rgba(255,255,255,0.025); color: var(--mg-cream); cursor: pointer; transition: background 180ms ease, border-color 180ms ease, transform 180ms ease; } .mg-store-audience__button:hover { transform: translateY(-2px); border-color: var(--mg-line-strong); background: rgba(255,255,255,0.045); } .mg-store-audience__button.is-active { border-color: rgba(242, 138, 46, 0.55); background: var(--mg-orange-soft); } .mg-store-audience__number { width: 42px; height: 42px; flex: 0 0 auto; display: grid; place-items: center; border: 1px solid var(--mg-line-strong); border-radius: 50%; color: var(--mg-muted); font-size: 0.7rem; font-weight: 850; } .mg-store-audience__button.is-active .mg-store-audience__number { border-color: rgba(242,138,46,0.55); color: var(--mg-orange); } .mg-store-audience__button strong { display: block; margin-bottom: 5px; font-size: 0.98rem; } .mg-store-audience__button small { color: var(--mg-muted); font-size: 0.78rem; } .mg-store-scroll { display: inline-flex; align-items: center; gap: 12px; margin-top: 38px; color: var(--mg-cream-soft); font-size: 0.8rem; font-weight: 750; text-decoration: none; } .mg-store-scroll span { color: var(--mg-orange); } /* ============================================================ MAIN ============================================================ */ .mg-store-main { padding: 110px 0 120px; background: var(--mg-bg-soft); } .mg-store-heading { display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(280px, 0.64fr); gap: 64px; align-items: end; margin-bottom: 45px; } .mg-store-heading h2 { max-width: 760px; margin: 13px 0 0; color: var(--mg-cream); font-size: clamp(2.3rem, 4.2vw, 4.2rem); line-height: 1.02; letter-spacing: -0.047em; font-weight: 650; } .mg-store-heading > p { margin: 0; color: var(--mg-cream-soft); font-size: 0.98rem; line-height: 1.75; } /* ============================================================ FILTERS ============================================================ */ .mg-store-filters { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 26px; } .mg-store-filter { appearance: none; padding: 10px 16px; border: 1px solid var(--mg-line); border-radius: 100px; background: transparent; color: var(--mg-muted); font-size: 0.76rem; font-weight: 800; cursor: pointer; transition: color 160ms ease, background 160ms ease, border-color 160ms ease; } .mg-store-filter:hover { color: var(--mg-cream); border-color: var(--mg-line-strong); } .mg-store-filter.is-active { color: #12110e; border-color: var(--mg-cream); background: var(--mg-cream); } /* ============================================================ SYSTEM STATES ============================================================ */ .mg-store-system { margin-bottom: 26px; } .mg-store-loading { min-height: 130px; display: flex; align-items: center; justify-content: center; gap: 14px; color: var(--mg-muted); border: 1px solid var(--mg-line); border-radius: var(--mg-radius); background: rgba(255,255,255,0.015); } .mg-store-loading__mark { width: 14px; height: 14px; border: 2px solid rgba(242,138,46,0.25); border-top-color: var(--mg-orange); border-radius: 50%; animation: mgStoreSpin 720ms linear infinite; } @keyframes mgStoreSpin { to { transform: rotate(360deg); } } .mg-store-error { padding: 32px; border: 1px solid rgba(242,138,46,0.24); border-radius: var(--mg-radius); background: rgba(242,138,46,0.055); } .mg-store-error > span { color: var(--mg-orange); font-size: 0.7rem; font-weight: 850; letter-spacing: 0.12em; text-transform: uppercase; } .mg-store-error h3 { margin: 10px 0 0; font-size: 2rem; letter-spacing: -0.035em; } .mg-store-error p { margin: 10px 0 22px; color: var(--mg-cream-soft); } /* ============================================================ GRID ============================================================ */ .mg-store-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; } /* ============================================================ PRODUCT CARD ============================================================ */ .mg-product { position: relative; min-height: 550px; display: flex; flex-direction: column; overflow: hidden; padding: 28px; border: 1px solid var(--mg-line); border-radius: var(--mg-radius); background: linear-gradient( 145deg, rgba(255,255,255,0.036), rgba(255,255,255,0.012) ), var(--mg-card); transition: transform 200ms ease, border-color 200ms ease, background 200ms ease; } .mg-product:hover { transform: translateY(-4px); border-color: var(--mg-line-strong); background: linear-gradient( 145deg, rgba(255,255,255,0.052), rgba(255,255,255,0.016) ), var(--mg-card-hover); } /* FEATURED PRODUCT */ .mg-product.is-featured { grid-column: 1 / -1; min-height: 610px; padding: 36px; background: linear-gradient( 90deg, rgba(10,10,9,0.96) 0%, rgba(10,10,9,0.76) 50%, rgba(10,10,9,0.25) 100% ), var(--mg-product-image, none), #161411; background-size: cover; background-position: center; } .mg-product.is-featured::after { content: ""; position: absolute; inset: 0; background: radial-gradient( circle at 80% 20%, rgba(242,138,46,0.13), transparent 30% ); pointer-events: none; } .mg-product__inner { position: relative; z-index: 2; height: 100%; display: flex; flex-direction: column; } /* IMAGE CARD */ .mg-product:not(.is-featured) .mg-product__media { height: 230px; margin: -28px -28px 26px; overflow: hidden; border-bottom: 1px solid var(--mg-line); background: #11110f; } .mg-product__media img { width: 100%; height: 100%; object-fit: cover; transition: transform 500ms ease; } .mg-product:hover .mg-product__media img { transform: scale(1.025); } /* TOP */ .mg-product__top { display: flex; justify-content: space-between; align-items: flex-start; gap: 14px; } .mg-product__type { color: var(--mg-cream-soft); font-size: 0.71rem; font-weight: 800; letter-spacing: 0.07em; text-transform: uppercase; } .mg-product__status { flex: 0 0 auto; padding: 7px 10px; border-radius: 100px; background: var(--mg-orange-soft); color: var(--mg-orange); font-size: 0.65rem; font-weight: 850; letter-spacing: 0.04em; text-transform: uppercase; } .mg-product__status--sold { background: rgba(221,184,117,0.10); color: var(--mg-warning); } /* CONTENT */ .mg-product__content { flex: 1; display: flex; flex-direction: column; justify-content: center; padding: 50px 0 42px; } .mg-product.is-featured .mg-product__content { max-width: 800px; } .mg-product__eyebrow { margin: 0 0 14px; color: var(--mg-orange); font-size: 0.68rem; line-height: 1; font-weight: 850; letter-spacing: 0.16em; text-transform: uppercase; } .mg-product__content h3 { margin: 0; color: var(--mg-cream); font-size: clamp(2rem, 3vw, 3.25rem); line-height: 1.02; letter-spacing: -0.043em; font-weight: 650; } .mg-product.is-featured .mg-product__content h3 { max-width: 900px; font-size: clamp(3rem, 5.8vw, 5.8rem); } .mg-product__description { max-width: 620px; margin: 23px 0 0; color: var(--mg-cream-soft); line-height: 1.72; } .mg-product__features { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 28px; } .mg-product__features span { padding: 7px 10px; border: 1px solid var(--mg-line); border-radius: 100px; color: var(--mg-muted); font-size: 0.69rem; } /* FOOTER */ .mg-product__footer { display: flex; justify-content: space-between; align-items: flex-end; gap: 20px; padding-top: 22px; border-top: 1px solid var(--mg-line); } .mg-product__price small { display: block; margin-bottom: 5px; color: var(--mg-muted); font-size: 0.68rem; } .mg-product__price strong { color: var(--mg-cream); font-size: 1.45rem; line-height: 1; letter-spacing: -0.025em; } .mg-product__reference { margin-top: 11px; color: rgba(243,234,219,0.35); font-size: 0.61rem; letter-spacing: 0.08em; } /* ============================================================ BUTTONS ============================================================ */ .mg-button { min-height: 48px; display: inline-flex; align-items: center; justify-content: center; padding: 13px 20px; border: 1px solid transparent; border-radius: 100px; text-decoration: none; font-size: 0.78rem; line-height: 1; font-weight: 850; cursor: pointer; transition: transform 160ms ease, background 160ms ease, border-color 160ms ease, opacity 160ms ease; } .mg-button:hover { transform: translateY(-2px); } .mg-button--primary { background: var(--mg-orange); color: #17120d !important; } .mg-button--primary:hover { background: var(--mg-orange-hover); } .mg-button--secondary { border-color: var(--mg-line-strong); background: transparent; color: var(--mg-cream) !important; } .mg-button[aria-disabled="true"], .mg-button.is-disabled { opacity: 0.48; pointer-events: none; } /* ============================================================ EMPTY ============================================================ */ .mg-store-empty { padding: 90px 30px; text-align: center; border: 1px solid var(--mg-line); border-radius: var(--mg-radius); background: rgba(255,255,255,0.018); } .mg-store-empty h3 { max-width: 620px; margin: 13px auto 0; font-size: clamp(2rem, 4vw, 3.5rem); line-height: 1.02; letter-spacing: -0.045em; } .mg-store-empty p { max-width: 550px; margin: 17px auto 0; color: var(--mg-cream-soft); line-height: 1.7; } /* ============================================================ COMMERCIAL PRINCIPLE ============================================================ */ .mg-store-principle { display: grid; grid-template-columns: 0.38fr 1fr; gap: 60px; margin-top: 74px; padding: 40px 0; border-top: 1px solid var(--mg-line); border-bottom: 1px solid var(--mg-line); } .mg-store-principle__label { color: var(--mg-orange); font-size: 0.7rem; font-weight: 850; letter-spacing: 0.11em; text-transform: uppercase; } .mg-store-principle h2 { max-width: 730px; margin: 0; font-size: clamp(2rem, 4vw, 3.5rem); line-height: 1.03; letter-spacing: -0.045em; } .mg-store-principle p { max-width: 660px; margin: 20px 0 0; color: var(--mg-cream-soft); line-height: 1.76; } /* ============================================================ DISCOVERY ============================================================ */ .mg-store-discovery { display: flex; align-items: center; justify-content: space-between; gap: 55px; margin-top: 34px; padding: 42px; border: 1px solid var(--mg-line); border-radius: var(--mg-radius); background: #11110f; } .mg-store-discovery > div { max-width: 720px; } .mg-store-discovery h2 { margin: 11px 0 0; font-size: clamp(2rem, 3.5vw, 3.2rem); line-height: 1.02; letter-spacing: -0.042em; } .mg-store-discovery p { max-width: 650px; margin: 17px 0 0; color: var(--mg-cream-soft); line-height: 1.72; } /* ============================================================ MEMORY CLOSING ============================================================ */ .mg-store-memory { padding: 150px 0 42px; text-align: center; } .mg-store-memory__label { color: var(--mg-orange); font-size: 0.68rem; font-weight: 850; letter-spacing: 0.18em; } .mg-store-memory h2 { margin: 15px 0 0; color: var(--mg-cream); font-size: clamp(2.8rem, 6vw, 6rem); line-height: 0.97; letter-spacing: -0.058em; } .mg-store-memory p { margin: 18px 0 0; color: var(--mg-muted); font-size: clamp(1rem, 2vw, 1.3rem); } .mg-store-memory strong { display: block; margin-top: 38px; color: var(--mg-orange); font-size: clamp(1rem, 2vw, 1.35rem); } /* ============================================================ DRAWER ============================================================ */ .mg-store-drawer { position: fixed; inset: 0; z-index: 999999; visibility: hidden; pointer-events: none; } .mg-store-drawer.is-open { visibility: visible; pointer-events: auto; } .mg-store-drawer__backdrop { appearance: none; position: absolute; inset: 0; width: 100%; height: 100%; border: 0; background: rgba(0,0,0,0.75); backdrop-filter: blur(7px); opacity: 0; transition: opacity 220ms ease; } .mg-store-drawer.is-open .mg-store-drawer__backdrop { opacity: 1; } .mg-store-drawer__panel { position: absolute; right: 0; top: 0; width: min(640px, 100%); height: 100%; display: flex; flex-direction: column; overflow: hidden; border-left: 1px solid var(--mg-line); background: #131311; transform: translateX(100%); transition: transform 300ms cubic-bezier(.22,.8,.3,1); } .mg-store-drawer.is-open .mg-store-drawer__panel { transform: translateX(0); } .mg-store-drawer__header { min-height: 78px; display: flex; justify-content: space-between; align-items: center; gap: 20px; padding: 18px 27px; border-bottom: 1px solid var(--mg-line); } .mg-store-drawer__header > div > span { display: block; color: var(--mg-cream-soft); font-size: 0.68rem; font-weight: 800; letter-spacing: 0.08em; } .mg-store-drawer__header small { display: block; margin-top: 4px; color: var(--mg-muted); font-size: 0.65rem; } .mg-store-drawer__close { appearance: none; width: 42px; height: 42px; display: grid; place-items: center; border: 1px solid var(--mg-line); border-radius: 50%; background: transparent; color: var(--mg-cream); font-size: 1.35rem; cursor: pointer; } .mg-store-drawer__media { height: 265px; flex: 0 0 auto; overflow: hidden; border-bottom: 1px solid var(--mg-line); } .mg-store-drawer__media img { width: 100%; height: 100%; object-fit: cover; } .mg-store-drawer__body { flex: 1; overflow-y: auto; padding: 44px 38px; } .mg-store-drawer__eyebrow { margin: 0; color: var(--mg-orange); font-size: 0.68rem; font-weight: 850; letter-spacing: 0.15em; text-transform: uppercase; } .mg-store-drawer__body h2 { margin: 12px 0 0; font-size: clamp(2.5rem, 5vw, 4.3rem); line-height: 0.99; letter-spacing: -0.052em; } .mg-store-drawer__description { margin: 24px 0 0; color: var(--mg-cream-soft); line-height: 1.75; } .mg-store-drawer__block { margin-top: 34px; padding-top: 24px; border-top: 1px solid var(--mg-line); } .mg-store-drawer__block > span { display: block; margin-bottom: 11px; color: var(--mg-muted); font-size: 0.68rem; font-weight: 850; letter-spacing: 0.12em; text-transform: uppercase; } .mg-store-drawer__block p { margin: 0; color: var(--mg-cream); line-height: 1.68; } .mg-store-drawer__block ul { margin: 0; padding-left: 18px; color: var(--mg-cream-soft); } .mg-store-drawer__block li { margin: 10px 0; line-height: 1.55; } .mg-store-drawer__note { margin-top: 32px; padding: 18px; border-radius: var(--mg-radius-small); background: var(--mg-orange-soft); color: var(--mg-cream-soft); font-size: 0.81rem; line-height: 1.65; } .mg-store-drawer__footer { display: flex; justify-content: space-between; align-items: center; gap: 24px; padding: 22px 28px; border-top: 1px solid var(--mg-line); background: #10100e; } .mg-store-drawer__price small { display: block; color: var(--mg-muted); font-size: 0.66rem; } .mg-store-drawer__price strong { display: block; margin-top: 4px; color: var(--mg-cream); font-size: 1.28rem; } /* ============================================================ ACCESSIBILITY ============================================================ */ #mgStore button:focus-visible, #mgStore a:focus-visible { outline: 2px solid var(--mg-orange); outline-offset: 4px; } /* ============================================================ TABLET ============================================================ */ @media (max-width: 900px) { .mg-store-heading { grid-template-columns: 1fr; gap: 22px; } .mg-store-grid { grid-template-columns: 1fr; } .mg-product.is-featured { grid-column: auto; } .mg-store-principle { grid-template-columns: 1fr; gap: 22px; } .mg-store-discovery { flex-direction: column; align-items: flex-start; gap: 28px; } } /* ============================================================ MOBILE ============================================================ */ @media (max-width: 650px) { .mg-store-shell { width: min(100% - 28px, 1240px); } .mg-store-hero { min-height: auto; } .mg-store-hero__inner { padding: 95px 0 54px; } .mg-store-hero h1 { font-size: clamp(3rem, 15vw, 5rem); } .mg-store-hero__intro { margin-top: 23px; font-size: 0.95rem; } .mg-store-audience { grid-template-columns: 1fr; margin-top: 36px; } .mg-store-audience__button { padding: 17px; } .mg-store-main { padding: 76px 0 82px; } .mg-store-heading { margin-bottom: 32px; } .mg-store-heading h2 { font-size: 2.6rem; } .mg-product, .mg-product.is-featured { min-height: 0; padding: 22px; } .mg-product:not(.is-featured) .mg-product__media { height: 205px; margin: -22px -22px 24px; } .mg-product__content { padding: 47px 0 40px; } .mg-product__content h3, .mg-product.is-featured .mg-product__content h3 { font-size: 2.6rem; } .mg-product__footer { flex-direction: column; align-items: stretch; } .mg-product__footer .mg-button { width: 100%; } .mg-store-principle { margin-top: 48px; } .mg-store-discovery { padding: 28px 22px; } .mg-store-discovery .mg-button { width: 100%; } .mg-store-memory { padding-top: 105px; } .mg-store-memory h2 { font-size: 3.3rem; } .mg-store-drawer__body { padding: 35px 24px; } .mg-store-drawer__footer { flex-direction: column; align-items: stretch; } .mg-store-drawer__footer .mg-button { width: 100%; } } /* ============================================================ REDUCED MOTION ============================================================ */ @media (prefers-reduced-motion: reduce) { #mgStore *, #mgStore *::before, #mgStore *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; scroll-behavior: auto !important; transition-duration: 0.01ms !important; } } /* ============================================================ MANGO GETAWAY STORE V1.1 MG-STORE v0.2.0 Dynamic Frontend Runtime ============================================================ */ document.addEventListener("DOMContentLoaded", function () { "use strict"; /* ========================================================== ROOT ========================================================== */ const store = document.getElementById("mgStore"); if (!store) { return; } /* ========================================================== CONFIG ========================================================== */ const CONFIG = { /* * Primary MG-STORE v0.2.0 endpoint. * * Expected: * GET /wp-json/mango-getaway/v1/store */ endpoint: "/wp-json/mango-getaway/v1/store", defaultAudience: "couples", allowedAudiences: [ "couples", "business" ], allowedStatuses: [ "live", "sold_out" ], debug: false }; /* ========================================================== STATE ========================================================== */ const state = { products: [], audience: CONFIG.defaultAudience, filter: "all", loading: false, drawerProduct: null, lastFocusedElement: null }; /* ========================================================== ELEMENTS ========================================================== */ const elements = { audienceButtons: Array.from( store.querySelectorAll( ".mg-store-audience__button" ) ), eyebrow: document.getElementById( "mgStoreEyebrow" ), title: document.getElementById( "mgStoreTitle" ), description: document.getElementById( "mgStoreDescription" ), filters: document.getElementById( "mgStoreFilters" ), system: document.getElementById( "mgStoreSystem" ), loading: document.getElementById( "mgStoreLoading" ), error: document.getElementById( "mgStoreError" ), errorMessage: document.getElementById( "mgStoreErrorMessage" ), retry: document.getElementById( "mgStoreRetry" ), grid: document.getElementById( "mgStoreGrid" ), empty: document.getElementById( "mgStoreEmpty" ), emptyTitle: document.getElementById( "mgStoreEmptyTitle" ), emptyText: document.getElementById( "mgStoreEmptyText" ), commercialPrinciple: document.getElementById( "mgStoreCommercialPrinciple" ), discovery: document.getElementById( "mgStoreDiscovery" ), coupleClosing: document.getElementById( "mgStoreCoupleClosing" ), drawer: document.getElementById( "mgStoreDrawer" ), drawerBackdrop: document.getElementById( "mgStoreDrawerBackdrop" ), drawerClose: document.getElementById( "mgStoreDrawerClose" ), drawerReference: document.getElementById( "mgDrawerReference" ), drawerType: document.getElementById( "mgDrawerType" ), drawerMedia: document.getElementById( "mgDrawerMedia" ), drawerImage: document.getElementById( "mgDrawerImage" ), drawerEyebrow: document.getElementById( "mgDrawerEyebrow" ), drawerTitle: document.getElementById( "mgDrawerTitle" ), drawerDescription: document.getElementById( "mgDrawerDescription" ), drawerBestForSection: document.getElementById( "mgDrawerBestForSection" ), drawerBestFor: document.getElementById( "mgDrawerBestFor" ), drawerIncludesSection: document.getElementById( "mgDrawerIncludesSection" ), drawerIncludes: document.getElementById( "mgDrawerIncludes" ), drawerNote: document.getElementById( "mgDrawerNote" ), drawerPriceLabel: document.getElementById( "mgDrawerPriceLabel" ), drawerPrice: document.getElementById( "mgDrawerPrice" ), drawerCTA: document.getElementById( "mgDrawerCTA" ) }; /* ========================================================== AUDIENCE COPY ========================================================== */ const audienceCopy = { couples: { eyebrow: "FOR COUPLES", title: "Choose the experience you want to have together.", description: "Mango Getaway helps you move from researching a place to deciding how you actually want to experience it together.", emptyTitle: "Your next experience is being prepared.", emptyText: "There are no live couple products in this part of the Store yet." }, business: { eyebrow: "FOR TOURISM BUSINESSES", title: "Choose how you want to work with Mango Getaway.", description: "Commercial opportunities are designed around where a tourism business can genuinely strengthen the couple journey.", emptyTitle: "No commercial opportunities are available here yet.", emptyText: "New placements, services and partnerships will appear when they become available." } }; /* ========================================================== PRODUCT TYPE LABELS ========================================================== */ const typeLabels = { itinerary: "Itinerary", placement: "Featured Placement", service: "Mango Getaway Service", partnership: "Partnership", campaign: "Campaign" }; /* ========================================================== HELPERS ========================================================== */ function log() { if (!CONFIG.debug) { return; } console.log.apply( console, ["[MG STORE]"].concat( Array.from(arguments) ) ); } function safeString(value) { if ( value === null || typeof value === "undefined" ) { return ""; } return String(value).trim(); } function safeArray(value) { if (Array.isArray(value)) { return value; } return []; } function normalizeBoolean(value) { return ( value === true || value === 1 || value === "1" || value === "true" ); } function normalizeMedia(media) { if (!media) { return ""; } if (typeof media === "string") { return media; } if (typeof media === "object") { return safeString( media.url || media.src || media.source_url ); } return ""; } function getProductImage(product) { return ( normalizeMedia(product.card_media) || normalizeMedia(product.card_image) || normalizeMedia(product.hero_media) || normalizeMedia(product.hero_image) || "" ); } function getHeroImage(product) { return ( normalizeMedia(product.hero_media) || normalizeMedia(product.hero_image) || getProductImage(product) ); } function getProductReference(product) { return safeString( product.mg_product_id || product.product_id || product.reference ); } function getProductName(product) { return safeString( product.product_name || product.name || product.title ); } function getProductHeadline(product) { return safeString( product.headline || product.product_name || product.name || product.title ); } function getShortDescription(product) { return safeString( product.short_description || product.description ); } function getLongDescription(product) { return safeString( product.long_description || product.description || product.short_description ); } function getFeatures(product) { const possibilities = [ product.features, product.benefits, product.includes ]; for (const value of possibilities) { if (Array.isArray(value)) { return value .map(function (item) { if ( item && typeof item === "object" ) { return safeString( item.feature_text || item.text || item.label || item.title ); } return safeString(item); }) .filter(Boolean); } } return []; } /* ========================================================== PRICE ========================================================== */ function formatAmount( amount, currency ) { const numeric = Number(amount); if (!Number.isFinite(numeric)) { return ""; } try { return new Intl.NumberFormat( undefined, { style: "currency", currency: safeString(currency) || "USD", maximumFractionDigits: numeric % 1 === 0 ? 0 : 2 } ).format(numeric); } catch (error) { return "$" + numeric; } } function getPriceDisplay(product) { const explicit = safeString( product.price_display ); if (explicit) { return explicit; } const mode = safeString( product.pricing_mode ); const amount = product.price_amount; const currency = safeString( product.currency ) || "USD"; if (mode === "fixed") { return formatAmount( amount, currency ); } if (mode === "starting_from") { const formatted = formatAmount( amount, currency ); return formatted ? "From " + formatted : "Starting from"; } if (mode === "custom") { return "By project"; } if (mode === "enquiry") { return "Enquire"; } return ""; } function getPriceLabel(product) { const mode = safeString( product.pricing_mode ); if (mode === "starting_from") { return "Starting from"; } if (mode === "custom") { return "Custom scope"; } if (mode === "enquiry") { return "Commercial enquiry"; } return "Price"; } /* ========================================================== API NORMALIZATION ========================================================== */ function extractProducts(payload) { if (!payload) { return []; } if (Array.isArray(payload)) { return payload; } if (Array.isArray(payload.products)) { return payload.products; } /* * Future-compatible structure: * * { * audiences: { * couples: [...], * business: [...] * } * } */ if ( payload.audiences && typeof payload.audiences === "object" ) { const all = []; Object.keys( payload.audiences ).forEach(function (audience) { const products = safeArray( payload.audiences[audience] ); products.forEach( function (product) { if ( !product.audience ) { product.audience = audience; } all.push(product); } ); }); return all; } return []; } function normalizeProduct(product) { const normalized = Object.assign({}, product); normalized.mg_product_id = getProductReference(product); normalized.product_name = getProductName(product); normalized.headline = getProductHeadline(product); normalized.audience = safeString( product.audience ).toLowerCase(); normalized.product_type = safeString( product.product_type || product.type ).toLowerCase(); normalized.status = safeString( product.status ).toLowerCase(); normalized.is_featured = normalizeBoolean( product.is_featured ); normalized.sort_order = Number( product.sort_order || 0 ); normalized.features = getFeatures(product); return normalized; } /* ========================================================== LOAD ========================================================== */ async function loadStore() { setLoading(true); clearError(); try { const response = await fetch( CONFIG.endpoint, { method: "GET", headers: { "Accept": "application/json" }, credentials: "same-origin" } ); if (!response.ok) { throw new Error( "Store API returned HTTP " + response.status ); } const payload = await response.json(); const products = extractProducts(payload) .map(normalizeProduct) .filter( isPublicProduct ); products.sort( function (a, b) { return ( a.sort_order - b.sort_order ); } ); state.products = products; log( "Loaded", products.length, "products" ); renderStore(); } catch (error) { console.error( "Mango Getaway Store:", error ); showError( "The Store could not connect to the Mango Getaway product registry." ); } finally { setLoading(false); } } function isPublicProduct(product) { if ( !CONFIG.allowedAudiences.includes( product.audience ) ) { return false; } if ( !CONFIG.allowedStatuses.includes( product.status ) ) { return false; } return true; } /* ========================================================== LOADING / ERROR ========================================================== */ function setLoading(isLoading) { state.loading = isLoading; elements.loading.hidden = !isLoading; if (isLoading) { elements.grid.innerHTML = ""; elements.empty.hidden = true; } } function showError(message) { elements.error.hidden = false; elements.errorMessage.textContent = message; elements.grid.innerHTML = ""; elements.empty.hidden = true; } function clearError() { elements.error.hidden = true; } /* ========================================================== AUDIENCE ========================================================== */ function setAudience(audience) { if ( !CONFIG.allowedAudiences.includes( audience ) ) { return; } state.audience = audience; state.filter = "all"; elements.audienceButtons.forEach( function (button) { const selected = button.dataset.audience === audience; button.classList.toggle( "is-active", selected ); button.setAttribute( "aria-selected", selected ? "true" : "false" ); } ); const copy = audienceCopy[audience]; elements.eyebrow.textContent = copy.eyebrow; elements.title.textContent = copy.title; elements.description.textContent = copy.description; const business = audience === "business"; elements.commercialPrinciple.hidden = !business; elements.discovery.hidden = !business; elements.coupleClosing.hidden = business; renderStore(); } /* ========================================================== FILTERS ========================================================== */ function getAvailableTypes() { const types = new Set(); state.products.forEach( function (product) { if ( product.audience !== state.audience ) { return; } if ( product.product_type ) { types.add( product.product_type ); } } ); return Array.from(types); } function renderFilters() { elements.filters.innerHTML = ""; const types = getAvailableTypes(); if (types.length === 0) { return; } createFilterButton( "All", "all" ); types.forEach( function (type) { createFilterButton( typeLabels[type] || humanize(type), type ); } ); } function createFilterButton( label, value ) { const button = document.createElement( "button" ); button.type = "button"; button.className = "mg-store-filter"; if ( state.filter === value ) { button.classList.add( "is-active" ); } button.dataset.filter = value; button.textContent = label; elements.filters.appendChild( button ); } function humanize(value) { return safeString(value) .replace(/_/g, " ") .replace( /\b\w/g, function (char) { return char.toUpperCase(); } ); } /* ========================================================== FILTERED PRODUCTS ========================================================== */ function getVisibleProducts() { return state.products.filter( function (product) { if ( product.audience !== state.audience ) { return false; } if ( state.filter !== "all" && product.product_type !== state.filter ) { return false; } return true; } ); } /* ========================================================== RENDER STORE ========================================================== */ function renderStore() { renderFilters(); renderProducts(); } function renderProducts() { elements.grid.innerHTML = ""; const products = getVisibleProducts(); if (products.length === 0) { showEmpty(); return; } elements.empty.hidden = true; products.forEach( function (product) { elements.grid.appendChild( createProductCard(product) ); } ); } function showEmpty() { const copy = audienceCopy[ state.audience ]; elements.emptyTitle.textContent = copy.emptyTitle; elements.emptyText.textContent = copy.emptyText; elements.empty.hidden = false; } /* ========================================================== CREATE PRODUCT CARD ========================================================== */ function createProductCard(product) { const article = document.createElement( "article" ); article.className = "mg-product"; if ( product.is_featured ) { article.classList.add( "is-featured" ); } article.dataset.product = product.mg_product_id; const image = getProductImage(product); if ( product.is_featured && image ) { article.style.setProperty( "--mg-product-image", 'url("' + image.replace(/"/g, '\\"') + '")' ); } const inner = document.createElement( "div" ); inner.className = "mg-product__inner"; /* * Standard card image. */ if ( image && !product.is_featured ) { const media = document.createElement( "div" ); media.className = "mg-product__media"; const img = document.createElement( "img" ); img.src = image; img.alt = safeString( product.media_alt ) || product.product_name; media.appendChild(img); inner.appendChild(media); } /* * Top row. */ const top = document.createElement( "div" ); top.className = "mg-product__top"; const type = document.createElement( "span" ); type.className = "mg-product__type"; type.textContent = typeLabels[ product.product_type ] || humanize( product.product_type ); top.appendChild(type); if ( product.status === "sold_out" ) { const status = document.createElement( "span" ); status.className = "mg-product__status mg-product__status--sold"; status.textContent = "Filled"; top.appendChild(status); } else if ( product.is_featured ) { const status = document.createElement( "span" ); status.className = "mg-product__status"; status.textContent = "Featured"; top.appendChild(status); } inner.appendChild(top); /* * Content. */ const content = document.createElement( "div" ); content.className = "mg-product__content"; if (product.eyebrow) { const eyebrow = document.createElement( "p" ); eyebrow.className = "mg-product__eyebrow"; eyebrow.textContent = safeString( product.eyebrow ); content.appendChild( eyebrow ); } const headline = document.createElement( "h3" ); headline.textContent = product.headline; content.appendChild( headline ); const description = getShortDescription( product ); if (description) { const paragraph = document.createElement( "p" ); paragraph.className = "mg-product__description"; paragraph.textContent = description; content.appendChild( paragraph ); } if ( product.features.length ) { const features = document.createElement( "div" ); features.className = "mg-product__features"; product.features .slice(0, 5) .forEach( function (feature) { const item = document.createElement( "span" ); item.textContent = feature; features.appendChild( item ); } ); content.appendChild( features ); } inner.appendChild( content ); /* * Footer. */ const footer = document.createElement( "div" ); footer.className = "mg-product__footer"; const priceWrap = document.createElement( "div" ); priceWrap.className = "mg-product__price"; const priceLabel = document.createElement( "small" ); priceLabel.textContent = getPriceLabel( product ); const price = document.createElement( "strong" ); price.textContent = getPriceDisplay( product ) || "Explore"; priceWrap.appendChild( priceLabel ); priceWrap.appendChild( price ); const reference = getProductReference( product ); if (reference) { const ref = document.createElement( "div" ); ref.className = "mg-product__reference"; ref.textContent = reference; priceWrap.appendChild( ref ); } footer.appendChild( priceWrap ); const action = document.createElement( "button" ); action.type = "button"; action.className = "mg-button mg-button--primary"; action.textContent = safeString( product.cta_label ) || "View details"; action.dataset.product = product.mg_product_id; if ( product.status === "sold_out" ) { action.textContent = "Position filled"; action.disabled = true; action.classList.add( "is-disabled" ); } else { action.addEventListener( "click", function () { openProductDrawer( product ); } ); } footer.appendChild( action ); inner.appendChild( footer ); article.appendChild( inner ); return article; } /* ========================================================== DRAWER ========================================================== */ function openProductDrawer( product ) { state.drawerProduct = product; state.lastFocusedElement = document.activeElement; elements.drawerReference.textContent = product.mg_product_id || "Mango Getaway"; elements.drawerType.textContent = typeLabels[ product.product_type ] || humanize( product.product_type ); const hero = getHeroImage( product ); if (hero) { elements.drawerMedia.hidden = false; elements.drawerImage.src = hero; elements.drawerImage.alt = safeString( product.media_alt ) || product.product_name; } else { elements.drawerMedia.hidden = true; elements.drawerImage.src = ""; } elements.drawerEyebrow.textContent = safeString( product.eyebrow ); elements.drawerTitle.textContent = product.product_name || product.headline; elements.drawerDescription.textContent = getLongDescription( product ); /* * Best for. */ const bestFor = safeString( product.best_for ); elements.drawerBestForSection.hidden = !bestFor; elements.drawerBestFor.textContent = bestFor; /* * Includes. */ elements.drawerIncludes.innerHTML = ""; const includes = getFeatures( product ); elements.drawerIncludesSection.hidden = includes.length === 0; includes.forEach( function (feature) { const li = document.createElement( "li" ); li.textContent = feature; elements.drawerIncludes.appendChild( li ); } ); /* * Note. */ const note = safeString( product.commercial_note || product.note ); elements.drawerNote.hidden = !note; elements.drawerNote.textContent = note; /* * Price. */ elements.drawerPriceLabel.textContent = getPriceLabel( product ); elements.drawerPrice.textContent = getPriceDisplay( product ) || "Explore"; configureDrawerCTA( product ); elements.drawer.classList.add( "is-open" ); elements.drawer.setAttribute( "aria-hidden", "false" ); document.body.style.overflow = "hidden"; window.setTimeout( function () { elements.drawerClose.focus(); }, 30 ); } function closeProductDrawer() { elements.drawer.classList.remove( "is-open" ); elements.drawer.setAttribute( "aria-hidden", "true" ); document.body.style.overflow = ""; state.drawerProduct = null; if ( state.lastFocusedElement && typeof state.lastFocusedElement.focus === "function" ) { state.lastFocusedElement.focus(); } } /* ========================================================== CTA ========================================================== */ function configureDrawerCTA( product ) { const cta = elements.drawerCTA; const action = safeString( product.cta_action ).toLowerCase(); const target = safeString( product.cta_target || product.checkout_url || product.url ); cta.textContent = safeString( product.cta_label ) || "Continue"; cta.classList.remove( "is-disabled" ); cta.removeAttribute( "aria-disabled" ); cta.removeAttribute( "target" ); cta.removeAttribute( "rel" ); /* * SOLD OUT */ if ( product.status === "sold_out" ) { cta.href = "#"; cta.textContent = "Position filled"; cta.classList.add( "is-disabled" ); cta.setAttribute( "aria-disabled", "true" ); return; } /* * COMING SOON */ if ( action === "coming_soon" ) { cta.href = "#"; cta.textContent = safeString( product.cta_label ) || "Coming soon"; cta.classList.add( "is-disabled" ); cta.setAttribute( "aria-disabled", "true" ); return; } /* * EXTERNAL URL */ if ( action === "external_url" ) { cta.href = target || "#"; cta.target = "_blank"; cta.rel = "noopener noreferrer"; return; } /* * WOOCOMMERCE * * MG-STORE should ideally resolve the WooCommerce * binding to a public checkout/product URL server-side. */ if ( action === "woocommerce" ) { cta.href = target || "#"; return; } /* * INTERNAL / PARTNER / CONTACT / ENQUIRY */ if ( [ "internal_url", "partner_application", "contact", "enquiry" ].includes(action) ) { cta.href = target || "#"; return; } /* * Default behaviour. */ cta.href = target || "#"; } /* ========================================================== EVENTS ========================================================== */ elements.audienceButtons.forEach( function (button) { button.addEventListener( "click", function () { const audience = button.dataset.audience; setAudience( audience ); document .getElementById( "mgStoreCatalogue" ) .scrollIntoView({ behavior: "smooth", block: "start" }); } ); } ); elements.filters.addEventListener( "click", function (event) { const button = event.target.closest( ".mg-store-filter" ); if (!button) { return; } state.filter = button.dataset.filter; renderStore(); } ); elements.retry.addEventListener( "click", function () { loadStore(); } ); elements.drawerClose.addEventListener( "click", closeProductDrawer ); elements.drawerBackdrop.addEventListener( "click", closeProductDrawer ); document.addEventListener( "keydown", function (event) { if ( event.key === "Escape" && elements.drawer.classList.contains( "is-open" ) ) { closeProductDrawer(); } } ); /* ========================================================== START ========================================================== */ loadStore(); });

Mango Getaway Store

Travel better together.
Or become part of the experience.

Couple-first itineraries, meaningful tourism placements and Mango Getaway commercial opportunities — all managed through the Mango Getaway Store.

Explore the Store
FOR COUPLES

Choose the experience you want to have together.

Mango Getaway helps you move from researching a place to deciding how you actually want to experience it together.

Opening the Mango Getaway Store…

MANGO GETAWAY

The purchase is not the point.

What the two of you do with it is.

Create More Shared Memories.