Guided and Non-Guided Tours
:root{ --bg: #F2EBD9; --card: #FBF8EF; --card-line: #DCCFA8; --ink: #2A2015; --ink-soft: #5B5140; --green-deep: #2E4636; --green-deep-2: #24382b; --green-leaf: #6E8F55; --green-leaf-dark:#526b3f; --gold: #D2993D; --gold-dark:#a9741; --rust: #BD5E2C; --barn-red: #8A2E1C; --barn-red-dark:#6e2314; --cream-shadow: rgba(42,32,21,0.12); --focus: #1c5fae; --font-display: 'Fraunces', serif; --font-body: 'Inter', sans-serif; --font-mono: 'Space Mono', monospace; } *{box-sizing:border-box;} html{scroll-behavior:smooth;} @media (prefers-reduced-motion: reduce){ html{scroll-behavior:auto;} *{animation-duration:0.001ms !important; transition-duration:0.001ms !important;} } body{ margin:0; background:var(--bg); color:var(--ink); font-family:var(--font-body); line-height:1.55; -webkit-font-smoothing:antialiased; } img,svg{display:block; max-width:100%;} a{color:inherit;} .container{max-width:1080px; margin:0 auto; padding:0 24px;} h1,h2,h3,h4{font-family:var(--font-display); font-weight:600; margin:0 0 .4em 0; color:var(--green-deep);} .eyebrow{ font-family:var(--font-mono); text-transform:uppercase; letter-spacing:.14em; font-size:.72rem; color:var(--gold-dark); font-weight:700; } button{font-family:inherit; cursor:pointer;} :focus-visible{outline:3px solid var(--focus); outline-offset:2px; border-radius:4px;} /* ---------- Top bar ---------- */ .topbar{ background:var(--green-deep); color:#F4EFDF; font-family:var(--font-mono); font-size:.78rem; padding:8px 0; letter-spacing:.03em; } .topbar .container{display:flex; justify-content:space-between; flex-wrap:wrap; gap:6px;} .topbar a{text-decoration:underline; text-underline-offset:3px;} /* ---------- Header ---------- */ header.site{ background:var(--green-deep); color:#F4EFDF; padding:22px 0 26px; } header.site .container{display:flex; align-items:center; justify-content:space-between; gap:20px; flex-wrap:wrap;} .brand{display:flex; align-items:center; gap:14px;} .brand-mark{width:46px; height:46px; flex-shrink:0;} .brand-text .name{font-family:var(--font-display); font-size:1.35rem; font-weight:700; color:#fff;} .brand-text .tag{font-family:var(--font-mono); font-size:.72rem; letter-spacing:.08em; color:#cfe0c6; text-transform:uppercase;} nav.site a{ color:#eee6cf; text-decoration:none; font-weight:600; font-size:.92rem; margin-left:22px; border-bottom:2px solid transparent; padding-bottom:2px; } nav.site a:hover{border-color:var(--gold);} /* ---------- Hero ---------- */ .hero{position:relative; background:linear-gradient(180deg,var(--green-deep) 0%, var(--green-deep-2) 60%, var(--bg) 60%); padding-bottom:0;} .hero-inner{ display:grid; grid-template-columns:1.1fr .9fr; gap:40px; align-items:center; padding:44px 0 70px; } @media (max-width:860px){ .hero-inner{grid-template-columns:1fr; padding:30px 0 40px;} } .hero h1{color:#fff; font-size:2.7rem; line-height:1.05; margin-bottom:.35em;} @media (max-width:600px){ .hero h1{font-size:2rem;} } .hero p.lede{color:#e7e1cd; font-size:1.08rem; max-width:46ch;} .hero-facts{ display:flex; flex-wrap:wrap; gap:10px; margin:22px 0 26px; } .fact-chip{ background:rgba(255,255,255,0.09); border:1px solid rgba(255,255,255,0.22); color:#f4efdf; font-family:var(--font-mono); font-size:.78rem; padding:7px 12px; border-radius:100px; }.fact-chip{ background:rgba(255,255,255,0.09); border:1px solid rgba(255,255,255,0.22); color:#f4efdf; font-family:var(--font-mono); font-size:.78rem; padding:7px 12px; border-radius:100px; } /* Make hero fact chips match the gold button style */ .hero-facts .fact-chip { background: var(--gold); border: 2px solid var(--gold-dark); color: #2a1c08; font-weight: 700; } .cta-row{display:flex; gap:14px; flex-wrap:wrap;} .btn{ display:inline-flex; align-items:center; gap:8px; font-weight:700; font-size:.95rem; padding:13px 22px; border-radius:8px; border:2px solid transparent; text-decoration:none; transition:transform .15s ease, box-shadow .15s ease; } .btn:hover{transform:translateY(-1px);} .btn-gold{background:var(--gold); color:#2a1c08;} .btn-gold:hover{background:#e0a94c;} .btn-outline{background:gold; border-color:rgba(255,255,255,0.55); color:#fff;} .btn-outline:hover{border-color:#fff;} .btn-green{background:var(--green-deep); color:#fff;} .btn-green:hover{background:var(--green-deep-2);} .btn:disabled{opacity:.5; cursor:not-allowed; transform:none;} .hero-art{width:100%; filter:drop-shadow(0 18px 30px rgba(0,0,0,0.28));} /* ---------- Sections ---------- */ section{padding:64px 0;} section.tight{padding:40px 0;} .section-head{max-width:60ch; margin-bottom:34px;} .section-head h2{font-size:2rem;} .section-head p{color:var(--ink-soft);} /* Grade band cards */ .grade-grid{display:grid; grid-template-columns:repeat(4,1fr); gap:16px;} @media (max-width:900px){ .grade-grid{grid-template-columns:repeat(2,1fr);} } @media (max-width:520px){ .grade-grid{grid-template-columns:1fr;} } .grade-card{ background:var(--card); border:1px solid var(--card-line); border-radius:14px; padding:20px; display:flex; flex-direction:column; gap:8px; } .grade-card .badge{ font-family:var(--font-mono); font-weight:700; font-size:.82rem; color:#fff; background:var(--green-leaf-dark); display:inline-block; padding:4px 10px; border-radius:100px; width:fit-content; } .grade-card h3{font-size:1.15rem; margin:2px 0 0;} .grade-card p{font-size:.9rem; color:var(--ink-soft); margin:0;} /* Included / details */ .details-grid{display:grid; grid-template-columns:1.2fr 1fr; gap:28px;} @media (max-width:860px){ .details-grid{grid-template-columns:1fr;} } .included-card{ background:var(--card); border:1px solid var(--card-line); border-radius:16px; padding:28px; } .included-card ul{margin:14px 0 0; padding:0; list-style:none; display:flex; flex-direction:column; gap:11px;} .included-card li{display:flex; gap:10px; align-items:flex-start; font-size:.97rem;} .included-card li svg{flex-shrink:0; margin-top:3px;} .price-tag{font-family:var(--font-display); font-size:2.6rem; font-weight:700; color:var(--barn-red);} .price-tag span{font-family:var(--font-body); font-size:.9rem; font-weight:600; color:var(--ink-soft); display:block; margin-top:2px;} .logistics-card{ background:var(--green-deep); color:#f0ead6; border-radius:16px; padding:26px; font-size:.92rem; } .logistics-card h3{color:#fff;} .logistics-card dl{margin:16px 0 0; display:grid; grid-template-columns:auto 1fr; gap:8px 14px;} .logistics-card dt{font-family:var(--font-mono); color:var(--gold); font-size:.76rem; text-transform:uppercase; letter-spacing:.06em; white-space:nowrap; padding-top:2px;} .logistics-card dd{margin:0;} /* ---------- Booking calendar (signature element) ---------- */ .booking-section{background:var(--card); border-top:1px solid var(--card-line); border-bottom:1px solid var(--card-line);} .legend{display:flex; flex-wrap:wrap; gap:16px; margin:6px 0 28px; font-size:.82rem; font-family:var(--font-mono);} .legend-item{display:flex; align-items:center; gap:7px;} .swatch{width:14px; height:14px; border-radius:4px; display:inline-block;} .cal-wrap{display:grid; grid-template-columns:repeat(2,1fr); gap:26px;} @media (max-width:860px){ .cal-wrap{grid-template-columns:1fr;} } .month-card{background:var(--bg); border:1px solid var(--card-line); border-radius:16px; padding:18px 18px 22px;} .month-title{font-family:var(--font-display); font-weight:700; font-size:1.2rem; text-align:center; margin-bottom:12px;} .dow-row{display:grid; grid-template-columns:repeat(7,1fr); gap:5px; margin-bottom:6px;} .dow-row span{font-family:var(--font-mono); font-size:.66rem; text-align:center; color:var(--ink-soft); text-transform:uppercase;} .day-grid{display:grid; grid-template-columns:repeat(7,1fr); gap:5px;} .day-cell{ aspect-ratio:1/0.95; border-radius:8px; border:1px solid ; background:transparent; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:2px; padding:2px; font-family:var(--font-body); position:relative; } .day-cell .dnum{font-weight:700; font-size:.88rem;} .day-cell .dleft{font-family:var(--font-mono); font-size:.56rem;} .day-cell.empty{visibility:hidden;} .day-cell.out-of-range{color:#b8ac8c;} .day-cell.weekend{color:#c3b795; background:repeating-linear-gradient(135deg, rgba(0,0,0,0.03) 0 4px, transparent 4px 8px);} .day-cell.bookable{color:#fff; cursor:pointer; transition:transform .12s ease, box-shadow .12s ease;} .day-cell.bookable:hover{transform:translateY(-2px); box-shadow:0 6px 14px var(--cream-shadow);} .day-cell.bookable.selected{outline:3px solid var(--focus); outline-offset:1px;} .day-cell.tier-open{background:var(--green-leaf);} .day-cell.tier-filling{background:var(--gold); color:#2a1c08;} .day-cell.tier-almost{background:var(--rust);} .day-cell.tier-full{background:var(--barn-red); cursor:not-allowed;} .day-cell.tier-full .dleft{content:'FULL';} /* Day panel */ .day-panel{ margin-top:30px; background:var(--bg); border:1px solid var(--card-line); border-radius:16px; padding:26px; display:none; } .day-panel.open{display:block; animation:fadeUp .25s ease;} @keyframes fadeUp{from{opacity:0; transform:translateY(8px);} to{opacity:1; transform:translateY(0);}} .day-panel h3{margin-bottom:4px;} .day-panel .sub{color:var(--ink-soft); font-size:.9rem; margin-bottom:18px;} .slot-grid{display:grid; grid-template-columns:repeat(auto-fill,minmax(150px,1fr)); gap:10px;} .slot-btn{ border:2px solid var(--card-line); background:var(--card); border-radius:10px; padding:12px 10px; text-align:left; font-family:var(--font-body); display:flex; flex-direction:column; gap:4px; } .slot-btn .time{font-weight:700; font-family:var(--font-mono); font-size:.92rem;} .slot-btn .left{font-size:.76rem; color:var(--ink-soft);} .slot-btn:hover:not(:disabled){border-color:var(--green-leaf);} .slot-btn.chosen{border-color:var(--green-deep); background:#eef2e6;} .slot-btn:disabled{opacity:.45; cursor:not-allowed; background:repeating-linear-gradient(135deg, rgba(0,0,0,0.04) 0 4px, transparent 4px 8px);} /* Booking form */ .book-form{margin-top:24px; display:none;} .book-form.open{display:block; animation:fadeUp .25s ease;} .form-grid{display:grid; grid-template-columns:1fr 1fr; gap:16px;} @media (max-width:640px){ .form-grid{grid-template-columns:1fr;} } .field{display:flex; flex-direction:column; gap:6px;} .field.full{grid-column:1/-1;} .field label{font-size:.82rem; font-weight:600;} .field input, .field select, .field textarea{ font-family:var(--font-body); font-size:.95rem; padding:11px 12px; border-radius:8px; border:1.5px solid var(--card-line); background:var(--card); color:var(--ink); } .field textarea{resize:vertical; min-height:70px;} .form-summary{ margin-top:18px; padding:16px 18px; background:var(--card); border:1px dashed var(--card-line); border-radius:10px; display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:10px; } .form-summary .total{font-family:var(--font-display); font-size:1.5rem; color:var(--barn-red); font-weight:700;} .error-msg{color:var(--barn-red-dark); font-size:.85rem; font-weight:600; margin-top:10px; display:none;} .error-msg.show{display:block;} /* Confirmation */ .confirm-panel{display:none; margin-top:24px;} .confirm-panel.open{display:block; animation:fadeUp .3s ease;} .confirm-box{ background:var(--green-deep); color:#f2ecd9; border-radius:16px; padding:32px; position:relative; overflow:hidden; } .confirm-box .code{font-family:var(--font-mono); font-size:1.3rem; color:var(--gold); letter-spacing:.04em; font-weight:700;} .confirm-grid{display:grid; grid-template-columns:repeat(2,1fr); gap:8px 26px; margin:18px 0 22px; font-size:.92rem;} @media (max-width:600px){ .confirm-grid{grid-template-columns:1fr;} } .confirm-grid dt{font-family:var(--font-mono); font-size:.7rem; text-transform:uppercase; color:#cfe0c6; letter-spacing:.06em;} .confirm-grid dd{margin:0 0 8px;} .confirm-actions{display:flex; gap:12px; flex-wrap:wrap;} /* Loading */ .loading-note{font-family:var(--font-mono); font-size:.82rem; color:var(--ink-soft); text-align:center; padding:20px;} /* Footer */ footer{background:var(--green-deep-2); color:#d9d2b8; padding:44px 0 28px; font-size:.9rem;} .footer-grid{display:grid; grid-template-columns:2fr 1fr 1fr; gap:30px;} @media (max-width:700px){ .footer-grid{grid-template-columns:1fr; gap:22px;} } footer h4{color:#fff; font-family:var(--font-mono); text-transform:uppercase; font-size:.76rem; letter-spacing:.08em; margin-bottom:10px;} footer a{text-decoration:none; opacity:.9;} footer a:hover{text-decoration:underline;} .footer-bottom{margin-top:30px; padding-top:20px; border-top:1px solid rgba(255,255,255,0.12); display:flex; justify-content:space-between; flex-wrap:wrap; gap:10px; font-size:.78rem; color:#a9a084;} .footer-bottom button.linklike{background:none; border:none; color:#a9a084; text-decoration:underline; font-size:.78rem; padding:0;} /* Admin modal */ .modal-backdrop{ display:none; position:fixed; inset:0; background:rgba(20,15,8,0.55); z-index:50; align-items:center; justify-content:center; padding:20px; } .modal-backdrop.open{display:flex;} .modal{ background:var(--card); border-radius:16px; padding:26px; max-width:720px; width:100%; max-height:82vh; overflow:auto; } .modal h3{display:flex; justify-content:space-between; align-items:center;} .modal table{width:100%; border-collapse:collapse; font-size:.84rem; margin-top:12px;} .modal th, .modal td{text-align:left; padding:8px 6px; border-bottom:1px solid var(--card-line);} .modal th{font-family:var(--font-mono); font-size:.7rem; text-transform:uppercase; color:var(--ink-soft);} .close-x{background:none; border:none; font-size:1.3rem; line-height:1; color:var(--ink-soft);} .cancel-link{color:var(--barn-red-dark); background:none; border:none; text-decoration:underline; font-size:.8rem;} .passcode-row{display:flex; gap:10px; margin-top:14px; flex-wrap:wrap;} .passcode-row input{flex:1; min-width:160px; padding:10px 12px; border-radius:8px; border:1.5px solid var(--card-line);} .visually-hidden{position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap;}
🍎 10175 Clark Road, Mountain, ON K0E 1S0 1.613.989.5601  Â·  schooltours@mountainorchards.ca
Mountain Orchards
A Family Tradition Since 1970

School & Group Tours · Fall 2026

Bring your class where learning grows on trees.

A guided wagon-ride tour through a working apple orchard — grafting, a real beehive, and hands-on apple picking. Every student gets to pick an apple and bring home a goodies bag. Booking below reserves your spot instantly.

$7.50 / student · teachers & chaperones free Tue–Thu, Sept 8 – Oct 23, 2026 10:00 AM – 1:00 PM start times Up to 35 students per group

What happens on your tour

A hands-on hour in a working orchard

Every group gets the same guided experience, led by our orchard staff — paced to whatever time your group has with us.

Arrival

Meet, settle in, wash up

We meet your bus, get everyone organized into groups, take a quick washroom break, and drop bags at the picnic tables before heading out.

Trees

Grafting & growing

Out to the orchard for a talk on how many trees and varieties we grow, what a tree needs to grow, and a real grafted tree model showing why you can't just plant a seed.

Bees

Bees & pollination

A look at a real beehive and honeycomb, how bees do their jobs and communicate, and how pollination turns a blossom into an apple. Time permitting, we'll also look at apple anatomy up close.

Picking time

One apple each, to take home

Proper picking technique, then everyone picks their own apples in the rows — two per student, packed up to take home.

When the tour is completed. The time left is yours to do as you wish. You can enjoy the play area, the straw jump, corn maze etc....We have many options for you to explore and have a great time with.

What's included

$7.50per student · teachers and parent chaperones are free
  • Guided tour led by our orchard staff, 45 minutes to an hour
  • Wagon rides out to the orchard and back
  • Grafting demonstration with a real grafted tree model
  • Bees & pollination talk with a real beehive and honeycomb
  • Hands-on apple picking — 1 apple per student to pick and enjoy
  • Picnic tables reserved for your group in the picnic area, plus corn maze, straw jump & forest trail as time allows

Good to know

Planning your visit

Days
Tuesday, Wednesday & Thursday only
Season
Sept 8 – Oct 23, 2026
Start times
10:00 AM, 11:00 AM & 12:00 PM
Group size
Up to 35 students per time slot — bigger groups take multiple consecutive slots
Take-home
1 picked apple per student plus goodies bag
Payment
One payment for the group ($7.50 Ă— number of students), due on arrival

Live availability

Pick a date, pick a time

This calendar updates in real time as tours are booked. Dates ripen from open (green) to filling (gold) to almost full (rust) — once the day is full (deep red).

Plenty of spots Filling up Almost full Full Closed (Mon, Fri & weekends closed for bookings)
Loading live availability…

Tours run 10:00 AM – 1:00 PM in three one-hour groups. Enter your student count to see which start times fit — larger groups take more than one slot.

Group & contact details

Selected: —
0 students · chaperones free
$0

Booking confirmed

You're all set!

Confirmation code:

School
Date & time
Contact
Group size
Grade / age group
Total due on arrival
Add to Google Calendar
(function(){ "use strict"; // This file expects to be hosted on the SAME domain as your WordPress site // (e.g. mountainorchards.ca/school-tours.html), so this relative path reaches // the REST routes registered by the "Mountain Orchards - School Tour Booking" plugin. const API_BASE = window.location.origin + '/wp-json/mo-tours/v1'; // Fallback values, used ONLY if the WordPress settings endpoint can't be reached // (e.g. while previewing this file somewhere other than the live WordPress site). let CONFIG = { seasonStart: '2026-09-08', seasonEnd: '2026-10-30', allowedDays: [2,3,4], // Tue, Wed, Thu (0=Sun ... 6=Sat) slots: ['10:00','11:00','12:00'], // start times; each slot is 1 hour studentsPerSlot: 35, pricePerStudent: 7.5, typicalDurationText: '45 minutes to an hour', location: '10175 Clark Road, Mountain, ON K0E 1S0', configured: false }; let availability = {}; // { 'YYYY-MM-DD': { '10:00': true/false, ... }, ... } true = occupied let selectedDate = null; let selectedStartIndex = null; let selectedSlotsNeeded = 1; let backendOffline = false; let staffPasscode = null; function pad(n){ return n = 12 ? 'PM' : 'AM'; const h12 = ((h+11)%12)+1; return `${h12}:${pad(m)} ${period}`; } function addMinutes(hhmm, mins){ const [h,m] = hhmm.split(':').map(Number); const total = h*60 + m + mins; const nh = Math.floor(total/60) % 24; const nm = total % 60; return `${pad(nh)}:${pad(nm)}`; } function fmtDateLong(iso){ const d = new Date(iso+'T00:00:00'); return d.toLocaleDateString('en-CA', { weekday:'long', month:'long', day:'numeric', year:'numeric' }); } function fmtDateShort(iso){ const d = new Date(iso+'T00:00:00'); return d.toLocaleDateString('en-CA', { month:'short', day:'numeric', year:'numeric' }); } function formatMoney(n){ return '$' + Number(n).toLocaleString('en-CA', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } // ---------- Backend calls (WordPress REST routes, backed by Google Calendar) ---------- async function fetchSettings(){ try{ const res = await fetch(API_BASE + '/settings'); if (!res.ok) throw new Error('bad status ' + res.status); const data = await res.json(); CONFIG = Object.assign({}, CONFIG, data); backendOffline = false; }catch(e){ backendOffline = true; console.warn('Could not reach the WordPress settings endpoint — showing fallback demo values.', e); } } async function fetchAvailability(){ try{ const res = await fetch(API_BASE + '/availability'); if (!res.ok) throw new Error('bad status ' + res.status); availability = await res.json(); backendOffline = false; }catch(e){ availability = {}; backendOffline = true; console.warn('Could not reach the WordPress availability endpoint.', e); } } async function submitBooking(payload){ const res = await fetch(API_BASE + '/book', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const data = await res.json(); if (!res.ok){ throw new Error(data.error || 'Something went wrong while booking. Please try again.'); } return data; } async function fetchStaffBookings(passcode){ const res = await fetch(API_BASE + '/bookings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ passcode }) }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'Could not load bookings.'); return data; } async function cancelBookingRemote(eventId, passcode){ const res = await fetch(API_BASE + '/cancel', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ eventId, passcode }) }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'Could not cancel booking.'); return data; } // ---------- Date range / day-eligibility helpers ---------- function inSeason(iso){ return iso >= CONFIG.seasonStart && iso { if (flags[s]) occupied++; }); const capacity = CONFIG.slots.length; return { occupied, capacity, fraction: capacity ? occupied/capacity : 0 }; } function tierFor(fraction){ if (fraction >= 1) return 'full'; if (fraction >= 0.6) return 'almost'; if (fraction > 0) return 'filling'; return 'open'; } // ---------- Reflect live WordPress settings in the page copy ---------- function applyConfigToPage(){ const seasonLong = `${fmtDateShort(CONFIG.seasonStart)} – ${fmtDateShort(CONFIG.seasonEnd)}`; const dayNames = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat']; const daysText = CONFIG.allowedDays.slice().sort().map(d => dayNames[d]).join(', '); const timesJoined = CONFIG.slots.map(displayTime).join(', '); const firstLast = CONFIG.slots.length ? `${displayTime(CONFIG.slots[0])} – ${addMinutesDisplay(CONFIG.slots[CONFIG.slots.length-1])}` : ''; const set = (id, text) => { const el = document.getElementById(id); if (el) el.textContent = text; }; set('factSeason', `${daysText}, ${seasonLong}`); set('factPrice', `${formatMoney(CONFIG.pricePerStudent)} / student · teachers & chaperones free`); set('factTimes', `${firstLast} start times`); set('priceAmount', formatMoney(CONFIG.pricePerStudent)); set('logDays', daysText + ' only'); set('logSeason', seasonLong); set('logTimes', timesJoined); set('logGroupSize', `Up to ${CONFIG.studentsPerSlot} students per time slot — bigger groups take multiple consecutive slots`); set('tourDurationText', CONFIG.typicalDurationText); } function addMinutesDisplay(startSlot){ return displayTime(addMinutes(startSlot, 60)); } // ---------- Calendar rendering ---------- function renderMonth(year, monthIndex){ const monthName = new Date(year, monthIndex, 1).toLocaleDateString('en-CA', { month:'long', year:'numeric' }); const firstDow = new Date(year, monthIndex, 1).getDay(); const daysInMonth = new Date(year, monthIndex+1, 0).getDate(); let cells = ''; for (let i=0; i<firstDow; i++){ cells += `
`; } for (let d=1; d<=daysInMonth; d++){ const iso = isoDate(year, monthIndex, d); const tourDay = isTourDay(year, monthIndex, d); const seasonOk = inSeason(iso); if (!tourDay || !seasonOk){ cells += `
${d}
`; continue; } const { occupied, capacity, fraction } = dayTotals(iso); const tier = tierFor(fraction); const left = capacity - occupied; const label = tier === 'full' ? 'FULL' : `${left} slot${left===1?'':'s'} left`; const isSelected = iso === selectedDate ? ' selected' : ''; cells += ``; } return `
${monthName}
SuMoTuWeThFrSa
${cells}
`; } function renderCalendar(){ const wrap = document.getElementById('calWrap'); const start = new Date(CONFIG.seasonStart + 'T00:00:00'); const end = new Date(CONFIG.seasonEnd + 'T00:00:00'); let html = ''; let cursor = new Date(start.getFullYear(), start.getMonth(), 1); const last = new Date(end.getFullYear(), end.getMonth(), 1); let guard = 0; while (cursor <= last && guard { btn.addEventListener('click', () => { if (btn.classList.contains('tier-full')) return; selectDay(btn.dataset.iso); }); }); } function selectDay(iso){ selectedDate = iso; selectedStartIndex = null; renderCalendar(); document.getElementById('dayPanel').classList.add('open'); document.getElementById('bookForm').classList.remove('open'); document.getElementById('dayPanelTitle').textContent = fmtDateLong(iso); updateSlotOptions(); document.getElementById('dayPanel').scrollIntoView({ behavior:'smooth', block:'start' }); } // Recomputes how many slots are needed and which start times are valid, // based on the current "number of students" value. function updateSlotOptions(){ const grid = document.getElementById('slotGrid'); const note = document.getElementById('slotsNeededNote'); grid.innerHTML = ''; selectedStartIndex = null; document.getElementById('bookForm').classList.remove('open'); if (!selectedDate) return; const studentsVal = parseInt(document.getElementById('numStudents').value || '0', 10); const neededSlots = studentsVal > 0 ? Math.max(1, Math.ceil(studentsVal / CONFIG.studentsPerSlot)) : 1; selectedSlotsNeeded = neededSlots; if (studentsVal > 0){ note.textContent = neededSlots === 1 ? `This group fits in one time slot.` : `This group needs ${neededSlots} consecutive time slots (more than ${CONFIG.studentsPerSlot} students per slot).`; } else { note.textContent = `Enter a student count above to see available start times.`; } const flags = slotFlagsFor(selectedDate); const total = CONFIG.slots.length; const tooBig = neededSlots > total; if (tooBig){ const warn = document.createElement('p'); warn.className = 'error-msg show'; warn.textContent = `A group this size needs more slots than we have in a single day. Please split into two visits or contact us directly.`; grid.appendChild(warn); return; } for (let s = 0; s !flags[t]); const label = neededSlots === 1 ? displayTime(span[0]) : `${displayTime(span[0])} – ${addMinutesDisplay(span[span.length-1])}`; const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'slot-btn'; btn.disabled = !feasible; btn.innerHTML = `${label}${feasible ? (neededSlots > 1 ? neededSlots + ' slots' : 'Available') : 'Booked'}`; btn.addEventListener('click', () => chooseSlot(s, btn)); grid.appendChild(btn); } // Any start times that would run past the end of the day are simply not offered above. } function chooseSlot(startIndex, btnEl){ selectedStartIndex = startIndex; document.querySelectorAll('.slot-btn').forEach(b => b.classList.remove('chosen')); btnEl.classList.add('chosen'); document.getElementById('bookForm').classList.add('open'); const span = CONFIG.slots.slice(startIndex, startIndex + selectedSlotsNeeded); const label = selectedSlotsNeeded === 1 ? displayTime(span[0]) : `${displayTime(span[0])} – ${addMinutesDisplay(span[span.length-1])}`; document.getElementById('summaryDateTime').textContent = `${fmtDateLong(selectedDate)} at ${label}`; updateSummary(); document.getElementById('bookForm').scrollIntoView({ behavior:'smooth', block:'center' }); } function updateSummary(){ const students = parseInt(document.getElementById('numStudents').value || '0', 10); const total = students * CONFIG.pricePerStudent; document.getElementById('summaryCount').textContent = students; document.getElementById('summaryTotal').textContent = formatMoney(total); } document.getElementById('numStudents').addEventListener('input', () => { if (selectedDate) updateSlotOptions(); }); // ---------- ICS + Google Calendar link (for the teacher's own calendar) ---------- // Converts any ISO datetime string (with an offset like -04:00, or already UTC) // into the UTC "YYYYMMDDTHHMMSSZ" format Google Calendar and .ics files expect. function toUtcStamp(isoString){ return new Date(isoString).toISOString().replace(/[-:]/g,'').replace(/\.\d{3}Z$/, 'Z'); } function gcalLink(title, startIso, endIso, details){ const params = new URLSearchParams({ action: 'TEMPLATE', text: title, dates: `${toUtcStamp(startIso)}/${toUtcStamp(endIso)}`, details: details, location: CONFIG.location }); return 'https://calendar.google.com/calendar/render?' + params.toString(); } function icsContent(title, startIso, endIso, details){ return [ 'BEGIN:VCALENDAR','VERSION:2.0','PRODID:-//Mountain Orchards//School Tours//EN', 'BEGIN:VEVENT', 'UID:' + Date.now() + '@mountainorchards.ca', 'DTSTAMP:' + toUtcStamp(new Date().toISOString()), 'DTSTART:' + toUtcStamp(startIso), 'DTEND:' + toUtcStamp(endIso), 'SUMMARY:' + title, 'DESCRIPTION:' + details.replace(/\n/g,'\\n'), 'LOCATION:' + CONFIG.location, 'END:VEVENT','END:VCALENDAR' ].join('\r\n'); } function downloadIcs(filename, content){ const blob = new Blob([content], { type:'text/calendar' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } // ---------- Booking submission ---------- const form = document.getElementById('bookForm'); document.getElementById('cancelFormBtn').addEventListener('click', () => { form.classList.remove('open'); selectedStartIndex = null; document.querySelectorAll('.slot-btn').forEach(b => b.classList.remove('chosen')); }); form.addEventListener('submit', async (e) => { e.preventDefault(); const errorEl = document.getElementById('formError'); errorEl.classList.remove('show'); const students = parseInt(document.getElementById('numStudents').value || '0', 10); if (!selectedDate || selectedStartIndex === null || !students){ errorEl.textContent = 'Please choose a date, enter your student count, and pick a start time first.'; errorEl.classList.add('show'); return; } const schoolName = document.getElementById('schoolName').value.trim(); const contactName = document.getElementById('contactName').value.trim(); const gradeLevel = document.getElementById('gradeLevel').value.trim(); const email = document.getElementById('contactEmail').value.trim(); const phone = document.getElementById('contactPhone').value.trim(); const chaperones = parseInt(document.getElementById('numChaperones').value || '0', 10); const notes = document.getElementById('notes').value.trim(); const slot = CONFIG.slots[selectedStartIndex]; if (!schoolName || !contactName || !email || !phone){ errorEl.textContent = 'Please fill in all required fields.'; errorEl.classList.add('show'); return; } const submitBtn = form.querySelector('button[type="submit"]'); const originalLabel = submitBtn.textContent; submitBtn.disabled = true; submitBtn.textContent = 'Booking…'; try{ const booking = await submitBooking({ date: selectedDate, slot, students, schoolName, contactName, gradeLevel, email, phone, chaperones, notes }); showConfirmation(booking); await fetchAvailability(); renderCalendar(); }catch(err){ errorEl.textContent = err.message; errorEl.classList.add('show'); await fetchAvailability(); if (selectedDate) updateSlotOptions(); renderCalendar(); }finally{ submitBtn.disabled = false; submitBtn.textContent = originalLabel; } }); function showConfirmation(b){ document.getElementById('dayPanel').classList.remove('open'); form.classList.remove('open'); form.reset(); document.getElementById('numChaperones').value = 0; document.getElementById('numStudents').value = ''; const timeLabel = b.slotsUsed > 1 ? `${displayTime(b.slot)} – ${addMinutesDisplay(b.endSlotStart || b.slot)}` : displayTime(b.slot); document.getElementById('confCode').textContent = b.code; document.getElementById('confSchool').textContent = b.schoolName; document.getElementById('confDateTime').textContent = `${fmtDateLong(b.date)} at ${timeLabel}`; document.getElementById('confContact').textContent = `${b.contactName} · ${b.email} · ${b.phone}`; document.getElementById('confCount').textContent = `${b.students} students, ${b.chaperones||0} chaperones (free)`; document.getElementById('confGrade').textContent = b.gradeLevel || '—'; document.getElementById('confTotal').textContent = formatMoney(b.total) + ' due on arrival'; const title = `Mountain Orchards School Tour — ${b.schoolName}`; const details = `Confirmation code: ${b.code}\nStudents: ${b.students}\nChaperones: ${b.chaperones||0}\nContact: ${b.contactName}, ${b.phone}\nTotal due on arrival: $${b.total}`; document.getElementById('gcalBtn').href = gcalLink(title, b.startTime, b.endTime, details); document.getElementById('icsBtn').onclick = () => downloadIcs(`mountain-orchards-tour-${b.code}.ics`, icsContent(title, b.startTime, b.endTime, details)); document.getElementById('printBtn').onclick = () => window.print(); document.getElementById('confirmPanel').classList.add('open'); document.getElementById('confirmPanel').scrollIntoView({ behavior:'smooth', block:'start' }); selectedDate = null; selectedStartIndex = null; } document.getElementById('bookAnotherBtn').addEventListener('click', () => { document.getElementById('confirmPanel').classList.remove('open'); document.getElementById('book').scrollIntoView({ behavior:'smooth', block:'start' }); }); // ---------- Staff / admin view (backed by the WordPress endpoint, passcode set in wp-admin) ---------- const adminBackdrop = document.getElementById('adminBackdrop'); document.getElementById('staffLoginBtn').addEventListener('click', () => { adminBackdrop.classList.add('open'); document.getElementById('adminLoginView').style.display = 'block'; document.getElementById('adminListView').style.display = 'none'; document.getElementById('adminError').classList.remove('show'); }); document.getElementById('adminCloseBtn').addEventListener('click', () => adminBackdrop.classList.remove('open')); adminBackdrop.addEventListener('click', (e) => { if (e.target === adminBackdrop) adminBackdrop.classList.remove('open'); }); async function loadStaffList(){ const list = await fetchStaffBookings(staffPasscode); document.getElementById('adminLoginView').style.display = 'none'; document.getElementById('adminListView').style.display = 'block'; document.getElementById('adminSummary').textContent = `${list.length} booking(s) on file.`; const body = document.getElementById('adminTableBody'); body.innerHTML = ''; list.slice().reverse().forEach(b => { const tr = document.createElement('tr'); const total = (b.students || 0) * CONFIG.pricePerStudent; tr.innerHTML = `${b.code}${b.schoolName}${b.date}${b.time ? displayTime(b.time) : ''}${b.slotsUsed > 1 ? ' ('+b.slotsUsed+' slots)' : ''}${b.students}${formatMoney(total)} `; body.appendChild(tr); }); body.querySelectorAll('.cancel-link').forEach(btn => { btn.addEventListener('click', async () => { if (!confirm('Cancel this booking? This removes it from the calendar and frees up the slot(s).')) return; try{ await cancelBookingRemote(btn.dataset.event, staffPasscode); await fetchAvailability(); renderCalendar(); await loadStaffList(); }catch(err){ alert(err.message); } }); }); } document.getElementById('adminSubmitBtn').addEventListener('click', async () => { const val = document.getElementById('adminPasscode').value; document.getElementById('adminError').classList.remove('show'); try{ staffPasscode = val; await loadStaffList(); }catch(err){ staffPasscode = null; document.getElementById('adminError').textContent = err.message; document.getElementById('adminError').classList.add('show'); } }); // ---------- Init ---------- async function init(){ await fetchSettings(); await fetchAvailability(); applyConfigToPage(); document.getElementById('calLoading').style.display = 'none'; document.getElementById('calWrap').style.display = 'grid'; if (backendOffline || !CONFIG.configured){ const note = document.createElement('p'); note.className = 'loading-note'; note.style.color = 'var(--barn-red-dark)'; note.textContent = backendOffline ? 'Live availability couldn\'t be loaded right now. Showing the calendar without real-time data — please refresh, or contact the farm directly to book.' : 'Heads up: the Google Calendar connection hasn\'t been configured yet in wp-admin, so bookings can\'t be completed until that\'s set up.'; document.getElementById('calWrap').parentNode.insertBefore(note, document.getElementById('calWrap')); } renderCalendar(); } init(); })();
Scroll to Top