/* global React */ // The Gamekeeper's Cottage — Cottage details const { useState, useEffect, useRef } = React; const NS_C = window.GamekeepersCottageDesignSystem_070bc7; const { Button, Badge, Rating, Eyebrow, Card, Amenity } = NS_C; const Photo = window.GKPhoto; const MiniCalendar = window.GKMiniCalendar; const useShell = window.GKuseShell; const useReviews = window.GKuseReviews; const reviewStats = window.GKreviewStats; const reviewMonth = window.GKreviewMonth; const useRates = window.GKuseRates; const useAvailability = window.GKuseAvailability; const quoteStay = window.GKquoteStay; const stayAvailability = window.GKstayAvailability; const RateGuide = window.GKRateGuide; const AvailabilityNote = window.GKAvailabilityNote; const visibleRatePeriods = window.GKvisibleRatePeriods; const IMG = window.GK_IMG; const C = window.GKContact; const ALL_PHOTOS = [ { src: IMG.exterior1, label: 'The cottage' }, { src: IMG.living1, label: 'Sitting room' }, { src: IMG.kitchen, label: 'Kitchen' }, { src: IMG.dining, label: 'Dining room' }, { src: IMG.doubleRoom, label: 'Double bedroom' }, { src: IMG.twinRoom, label: 'Twin bedroom' }, { src: IMG.singleRoom, label: 'Single bedroom' }, { src: IMG.bathroom, label: 'Bathroom' }, { src: IMG.garden2, label: 'The garden' }, { src: IMG.pond, label: 'The pond' }, { src: IMG.woodDen, label: 'Wood den' }, { src: IMG.deer, label: 'Wildlife' }, ]; // Gallery photos — read from assets/gallery.json (Decap-editable) on the live host; // fall back to the embedded snapshot, then the built-in list. The "View all N photos" // counter follows whatever this returns. let _galleryCache = null, _galleryPromise = null; function loadGallery() { if (_galleryCache) return Promise.resolve(_galleryCache); if (_galleryPromise) return _galleryPromise; _galleryPromise = (async () => { try { const res = await fetch('assets/gallery.json', { cache: 'no-store' }); if (res.ok) { const d = await res.json(); if (d && Array.isArray(d.photos) && d.photos.length) { _galleryCache = d.photos; return _galleryCache; } } } catch (e) { /* fall through */ } if (window.GK_GALLERY && Array.isArray(window.GK_GALLERY.photos) && window.GK_GALLERY.photos.length) { _galleryCache = window.GK_GALLERY.photos; return _galleryCache; } _galleryCache = ALL_PHOTOS; return _galleryCache; })(); return _galleryPromise; } function useGallery() { const [photos, setPhotos] = useState(_galleryCache || ALL_PHOTOS); useEffect(() => { let live = true; loadGallery().then(p => { if (live) setPhotos(p); }); return () => { live = false; }; }, []); return photos; } function KeyFacts() { const facts = [['users', 'Sleeps 5'], ['bed-double', '3 bedrooms'], ['bath', '1 bathroom'], ['trees', '4 acres of wood'], ['paw-print', 'No pets']]; return (
{facts.map(([ic, l]) => ( {l} ))}
); } function GalleryBand() { const { openLightbox } = useShell(); const photos = useGallery(); const stats = reviewStats(useReviews()); return (
The Cottage · near Southwold

The Gamekeeper's Cottage

{stats.count > 0 && }
{photos.length > 0 && (
openLightbox(photos, 0)} />
{photos.slice(1, 5).map((p, idx) => ( openLightbox(photos, idx + 1)} /> ))}
)}
); } const _plusDays = (n) => { const d = new Date(); d.setDate(d.getDate() + n); return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0'); }; function BookCard() { const { openBooking } = useShell(); const [bar, setBar] = useState({ arrive: _plusDays(21), depart: _plusDays(28), guests: '4' }); const set = (k) => (e) => setBar((b) => ({ ...b, [k]: e.target.value })); const todayISO = _plusDays(0); const onArrive = (e) => { const v = e.target.value; setBar((b) => { const next = { ...b, arrive: v }; if (v && (!b.depart || b.depart < v)) next.depart = v; return next; }); }; return (
£140 per night

Breaks of 3 nights or more · weekly stays £588–£900

No deposit taken at this stage

Or call Nick on {C.PHONE}
); } function Highlights() { const items = [ ['trees', 'Set in a 100-acre wood', 'Total seclusion, with four acres of private woodland attached'], ['flame', 'Log burner & cosy living', 'A proper sitting room with views over the garden'], ['bird', 'Birdwatching from the windows', 'Feeders bring nuthatches, woodpeckers, deer and owls'], ]; return (
{items.map(([ic, t, d]) => (
{t}

{d}

))}
); } function VideoTour() { return (

Take a look around

); } // Bedroom data — read from assets/bedrooms.json (which Decap CMS edits) on the live // host; fall back to the embedded window.GK_BEDROOMS for local file:// previews. let _bedroomsCache = null, _bedroomsPromise = null; function loadBedrooms() { if (_bedroomsCache) return Promise.resolve(_bedroomsCache); if (_bedroomsPromise) return _bedroomsPromise; _bedroomsPromise = (async () => { try { const res = await fetch('assets/bedrooms.json', { cache: 'no-store' }); if (res.ok) { const data = await res.json(); if (data && Array.isArray(data.bedrooms)) { _bedroomsCache = data.bedrooms; return _bedroomsCache; } } } catch (e) { /* fall through */ } if (window.GK_BEDROOMS && Array.isArray(window.GK_BEDROOMS.bedrooms)) { _bedroomsCache = window.GK_BEDROOMS.bedrooms; return _bedroomsCache; } _bedroomsCache = []; return _bedroomsCache; })(); return _bedroomsPromise; } function useBedrooms() { const [rooms, setRooms] = useState(_bedroomsCache || []); useEffect(() => { let live = true; loadBedrooms().then(r => { if (live) setRooms(r); }); return () => { live = false; }; }, []); return rooms; } // Pull a YouTube video ID from a full URL (youtu.be/ID, watch?v=ID, embed/ID, // shorts/ID) or accept a bare ID. function ytId(s) { if (!s) return ''; const m = String(s).match(/(?:youtu\.be\/|[?&]v=|embed\/|shorts\/)([A-Za-z0-9_-]{6,})/); return m ? m[1] : (/^[A-Za-z0-9_-]{6,}$/.test(String(s).trim()) ? String(s).trim() : ''); } // Build an embed URL that autoplays muted and loops (browsers only allow autoplay // when muted; loop needs playlist=). playsinline keeps it inline on mobile. function ytEmbed(id, start) { return 'https://www.youtube-nocookie.com/embed/' + id + '?rel=0&autoplay=1&mute=1&loop=1&playsinline=1&playlist=' + id + (start ? '&start=' + start : ''); } function BedroomModal({ room, onClose }) { const [active, setActive] = useState(0); const dialogRef = useRef(null); useEffect(() => { const prev = document.activeElement; if (dialogRef.current) dialogRef.current.focus(); const onKey = (e) => e.key === 'Escape' && onClose(); window.addEventListener('keydown', onKey); if (window.lucide) window.lucide.createIcons(); return () => { window.removeEventListener('keydown', onKey); if (prev && prev.focus) prev.focus(); }; }, [onClose, active]); if (!room) return null; const photos = room.photos || []; const vid = ytId(room.video); // Photos and the video form a single gallery; the video is a slide marked with a // play badge and autoplays (muted, looping) when selected. const media = photos.map((src) => ({ type: 'image', src })); if (vid) media.push({ type: 'video', id: vid, thumb: 'https://img.youtube.com/vi/' + vid + '/hqdefault.jpg' }); const cur = media[active] || media[0]; const pick = (i) => setActive(i); const facts = [ ['bed-double', room.bed], ['users', room.sleeps ? 'Sleeps ' + room.sleeps : null], ['home', room.location], ['bath', room.bathroom], ].filter(([, v]) => v); return (
e.stopPropagation()}>
Sleeping accommodation

{room.name}

{media.length > 0 && (
{cur.type === 'image' ? ( {room.name ) : (
)}
{media.length > 1 && (
{media.map((m, i) => ( ))}
)}
)}
{facts.map(([ic, v]) => ( {v} ))}
{room.features && room.features.length > 0 && (
{room.features.map((f) => {f})}
)} {room.details && (

{room.details}

)}
); } function Sleeping() { const rooms = useBedrooms(); const [open, setOpen] = useState(null); return (

Sleeping accommodation

{rooms.map((room, i) => ( ))}

Sleeps 5 in three bedrooms (a double and a twin upstairs, a single downstairs), with one bathroom. A cot and baby bath are available on request.

{open !== null && rooms[open] && setOpen(null)} />}
); } // Full amenities list — read from assets/amenities.json (Decap-editable) on the // live host; fall back to embedded window.GK_AMENITIES for local file:// previews. let _amenCache = null, _amenPromise = null; function loadAmenities() { if (_amenCache) return Promise.resolve(_amenCache); if (_amenPromise) return _amenPromise; _amenPromise = (async () => { try { const res = await fetch('assets/amenities.json', { cache: 'no-store' }); if (res.ok) { const d = await res.json(); if (d && Array.isArray(d.groups)) { _amenCache = d.groups; return _amenCache; } } } catch (e) { /* fall through */ } if (window.GK_AMENITIES && Array.isArray(window.GK_AMENITIES.groups)) { _amenCache = window.GK_AMENITIES.groups; return _amenCache; } _amenCache = []; return _amenCache; })(); return _amenPromise; } function useAmenities() { const [groups, setGroups] = useState(_amenCache || []); useEffect(() => { let live = true; loadAmenities().then(g => { if (live) setGroups(g); }); return () => { live = false; }; }, []); return groups; } function AmenitiesModal({ groups, onClose }) { const dialogRef = useRef(null); useEffect(() => { const prev = document.activeElement; if (dialogRef.current) dialogRef.current.focus(); const onKey = (e) => e.key === 'Escape' && onClose(); window.addEventListener('keydown', onKey); if (window.lucide) window.lucide.createIcons(); return () => { window.removeEventListener('keydown', onKey); if (prev && prev.focus) prev.focus(); }; }, [onClose, groups]); return (
e.stopPropagation()}>
Amenities & services

What this place offers

{groups.map((g) => (

{g.title}

    {(g.items || []).map((it) => (
  • {it}
  • ))}
))}
); } function Offers() { const items = [ ['Full kitchen', 'utensils-crossed', 'Hob, oven, grill, dishwasher & washing machine'], ['Log burner', 'flame', 'Logs provided through the cooler months'], ['Free parking', 'square-parking', 'Space for up to three vehicles'], ['Wi-Fi included', 'wifi', 'Typically ~45 Mb/sec; the dining room doubles as an office'], ['Separate dining room', 'utensils', 'Seats six (or five plus a high chair)'], ['Private garden & woodland', 'trees', 'Four acres of woodland to roam'], ['Smart TV & books', 'tv', 'For quiet evenings by the fire'], ['Security cameras', 'cctv', 'Covering the parking area, never the garden'], ]; const groups = useAmenities(); const [open, setOpen] = useState(false); return (

What this place offers

{items.map(([l, ic, d]) => )}
{groups.length > 0 && (
)} {open && setOpen(false)} />}
); } function Availability() { const { openBooking } = useShell(); const [sel, setSel] = useState({ arrive: '', depart: '' }); const rates = useRates(); const avail = useAvailability(); const quote = quoteStay(sel.arrive, sel.depart, rates); const dateError = sel.arrive && sel.depart && sel.depart < sel.arrive; const stayAvail = dateError ? null : stayAvailability(sel.arrive, sel.depart, avail); useEffect(() => { if (window.lucide) window.lucide.createIcons(); }, [stayAvail, sel.arrive, sel.depart]); const pick = (iso) => { setSel((s) => (!s.arrive || (s.arrive && s.depart)) ? { arrive: iso, depart: '' } : (iso > s.arrive ? { ...s, depart: iso } : { arrive: iso, depart: '' })); }; const fmt = (iso) => iso ? new Date(iso + 'T00:00:00').toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' }) : ''; return (
Availability

Find your dates

Pick your arrival, then your departure, to see the price and whether the cottage is free.

{(sel.arrive || sel.depart) && (
{fmt(sel.arrive) || 'Pick arrival'}{sel.arrive ? (sel.depart ? ' → ' + fmt(sel.depart) : ' → pick departure') : ''}
{dateError &&
Your departure date is before your arrival date — please pick a later departure.
}
)}
); } function ReviewsStrip() { const seed = [ ['One of the loveliest places we have ever stayed — cosy and characterful, with a stunning garden and an abundance of woodland wildlife.', 'JP', 'Jan 2026', 5], ['Such a beautiful, secluded location. We all loved it — just how life should be.', 'Mark Rawlinson', 'Sep 2025', 5], ['We saw deer, buzzard and woodpeckers in the wood. I couldn’t recommend it more.', 'William Ripper', 'Apr 2025', 5], ]; const live = useReviews(); const stats = reviewStats(live); const liveQuotes = (live || []).map((r) => [r.body, r.name, reviewMonth(r.date), r.rating || 5]); const shown = (liveQuotes.length ? liveQuotes : seed).slice(0, 3); return (
Guest reviews

Loved by our guests

{stats.count > 0 && }
{shown.map(([q, who, when, rating], i) => (

“{q}”

{who}{when ? ' · ' + when : ''}
))}
); } function MapNearby() { const nearby = [ ['uploads/The-Eels-Foot-Inn.webp', 'the-eels-foot-inn-in-suffolk', 'The Eels Foot Inn', 'A characterful country pub near Minsmere'], ['uploads/https___www.southwoldpier.co_.uk_.webp', 'southwold-pier', 'Southwold Pier', 'The classic seaside pier, 20 minutes away'], ['uploads/sole-bay-fish-company.webp', 'solebay-fish-company-southwold', 'Solebay Fish Company', 'Fresh-off-the-boat seafood in Southwold'], ]; return (
Where you'll be staying

The Wood, Brampton — near Beccles & Southwold

The exact location is shared once your stay is confirmed. The cottage sits down a private drive, completely surrounded by woodland.

Places to visit nearby

{nearby.map(([src, slug, t, d]) => (
{t}

{d}

))}
); } function AboutNick() { const { openBooking } = useShell(); return (
Your hosts

Hi, I'm Nick — the owner

This was my family home as a teenager, and we look after the cottage ourselves. We keep it more like a home than a rental, and we love sharing the wood with our guests. Please don't hesitate to get in touch — I'll gladly answer any questions before you book.

); } function RateCardModal({ rates, onClose }) { const dialogRef = useRef(null); useEffect(() => { const prev = document.activeElement; if (dialogRef.current) dialogRef.current.focus(); const onKey = (e) => e.key === 'Escape' && onClose(); window.addEventListener('keydown', onKey); if (window.lucide) window.lucide.createIcons(); return () => { window.removeEventListener('keydown', onKey); if (prev && prev.focus) prev.focus(); }; }, [onClose]); const avail = useAvailability(); const cur = (rates && rates.currency) || '£'; const periods = visibleRatePeriods ? visibleRatePeriods(rates, avail) : ((rates && rates.periods) || []); const price = (p) => (p.available === false || p.price == null) ? 'On request' : cur + Number(p.price).toLocaleString('en-GB') + (p.unit === 'stay' ? '' : '/week'); return (
e.stopPropagation()}>
Pricing

Full rate card

{rates && rates.intro &&

{rates.intro}

}
    {periods.map((p, i) => (
  • {p.label} {price(p)}
  • ))}
); } function Pricing() { const rates = useRates(); const [open, setOpen] = useState(false); const rows = [ ['Short breaks (3+ nights)', 'From £140 per night'], ['Low season week', '£588 per week'], ['Mid season week', '£686 per week'], ['High season week', '£900 per week'], ['Christmas & New Year', 'On request'], ]; return (
Pricing

Pricing information

Prices are for the whole cottage, sleeping up to five. A minimum of three nights applies. Linen, towels, Wi-Fi and parking are all included.

{rows.map(([a, b], i) => (
{a} {b}
))}
{rates && rates.periods && rates.periods.length > 0 && (
)}
{open && setOpen(false)} />}
); } // Guest-info panels — read from assets/guestinfo.json (Decap-editable) on the live // host; fall back to embedded window.GK_GUESTINFO for local file:// previews. let _giCache = null, _giPromise = null; function loadGuestInfo() { if (_giCache) return Promise.resolve(_giCache); if (_giPromise) return _giPromise; _giPromise = (async () => { try { const res = await fetch('assets/guestinfo.json', { cache: 'no-store' }); if (res.ok) { const d = await res.json(); if (d && Array.isArray(d.panels)) { _giCache = d.panels; return _giCache; } } } catch (e) { /* fall through */ } if (window.GK_GUESTINFO && Array.isArray(window.GK_GUESTINFO.panels)) { _giCache = window.GK_GUESTINFO.panels; return _giCache; } _giCache = []; return _giCache; })(); return _giPromise; } function useGuestInfo() { const [panels, setPanels] = useState(_giCache || []); useEffect(() => { let live = true; loadGuestInfo().then(p => { if (live) setPanels(p); }); return () => { live = false; }; }, []); return panels; } function InfoModal({ panel, onClose }) { const dialogRef = useRef(null); useEffect(() => { const prev = document.activeElement; if (dialogRef.current) dialogRef.current.focus(); const onKey = (e) => e.key === 'Escape' && onClose(); window.addEventListener('keydown', onKey); if (window.lucide) window.lucide.createIcons(); return () => { window.removeEventListener('keydown', onKey); if (prev && prev.focus) prev.focus(); }; }, [onClose, panel]); if (!panel) return null; return (
e.stopPropagation()}>
Good to know

{panel.title}

{panel.intro &&

{panel.intro}

} {(panel.groups || []).map((g, gi) => (
{g.heading &&

{g.heading}

}
    {(g.items || []).map((it, ii) => (
  • {it}
  • ))}
))}
); } function GuestInfo() { const panels = useGuestInfo(); const [open, setOpen] = useState(null); useEffect(() => { if (window.lucide) window.lucide.createIcons(); }, [panels, open]); if (!panels.length) return null; return (
Good to know

Guest information

{panels.map((p, i) => (

{p.title}

{p.teaser &&

{p.teaser}

} {Array.isArray(p.highlights) && p.highlights.length > 0 && (
    {p.highlights.slice(0, 3).map((h, hi) => (
  • {h}
  • ))}
)}
))}
{open !== null && panels[open] && setOpen(null)} />}
); } function CottageContent() { return (

A cosy woodland home, all to yourselves

The original Gamekeeper's Cottage sits in the middle of a hundred-acre wood, in a wonderfully secluded position with four acres of private woodland attached. It's warm, homely and full of character — and for the length of your stay, the wood is yours.

Inside there's a well-equipped kitchen, a cosy sitting room with a log burner and a separate dining room. Outside, a generous garden gives way to woodland walks, with feeders that bring deer, pheasants, owls and woodpeckers close to the windows.




); } window.GKmount('cottage', CottageContent);