Content & media - Replace placeholder slides with optimized real estate photos (img/gallery) - Rebuild hero video from the HD intro clip + drone "common area" clips (gate -> vineyard -> home front -> Vineyard sign -> pavilion -> fountain), cutting the private backyard segment; add mobile variant + poster - Add "Community & Common Areas" aerial gallery and a community map - Galleries split strictly by style: Vineyard = Tuscan homes only, Custom Homes = non-Tuscan estates HOA / Homeowner Information - Real board roster (sourced from pinecreekhoa.com) in a responsive table - Homeowner Information document library (CC&Rs, Vineyard covenants, safety rules, architectural review form, delinquency policy, event reimbursement) linking the live PDFs; Premier Management Company reference - Real general contact email Design & UX - Modern full-screen video hero with overlay, sticky nav, refined typography - Click-to-zoom lightbox for all galleries (keyboard + swipe) - Google Maps embed in the Kannapolis section - Favicon set generated from the Pine Creek emblem (svg/png/ico) - Positive reframe of the Custom Homes copy + lot-availability note - SEO/Open Graph meta, lazy-loaded images, responsive board/doc layouts Housekeeping - SCSS source kept in sync (new partials: hero, sections, gallery, board, features); remove stale placeholders, old.index.php, and map artifact - Add .gitignore (.DS_Store, .claude/, css map); fix docs (index.php -> index.html) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
172 lines
6.3 KiB
JavaScript
172 lines
6.3 KiB
JavaScript
/* ============================================================
|
|
Pine Creek — site interactions
|
|
- Mobile nav (hamburger) toggle
|
|
- Smooth-scroll for in-page anchors with fixed-header offset
|
|
- Image galleries (prev/next, dots, autoplay, swipe)
|
|
- Sticky-nav shadow on scroll
|
|
============================================================ */
|
|
|
|
const hamburger = document.getElementById('hamburger'),
|
|
mainNav = document.querySelector('#mainNav ul'),
|
|
navItem = document.getElementsByClassName('nav-item')
|
|
|
|
// --- Mobile nav ------------------------------------------------
|
|
const toggleNav = () => {
|
|
mainNav.classList.toggle('in')
|
|
hamburger.classList.toggle('in')
|
|
},
|
|
closeNav = () => {
|
|
mainNav.classList.remove('in')
|
|
hamburger.classList.remove('in')
|
|
}
|
|
|
|
hamburger.onclick = toggleNav
|
|
hamburger.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleNav() }
|
|
})
|
|
|
|
// --- Smooth scroll with fixed-header offset --------------------
|
|
const scrollToTarget = (hash) => {
|
|
const target = hash && document.querySelector(hash)
|
|
if (!target) return false
|
|
const header = document.getElementById('mainNav')
|
|
const headerHeight = header ? header.offsetHeight : 0
|
|
const y = window.pageYOffset + target.getBoundingClientRect().top - headerHeight - 8
|
|
window.scrollTo({ top: y, behavior: 'smooth' })
|
|
return true
|
|
}
|
|
|
|
for (let i = 0; i < navItem.length; i++) {
|
|
navItem[i].onclick = function (e) {
|
|
const href = this.getAttribute('href') || ''
|
|
closeNav()
|
|
if (href.startsWith('#') && scrollToTarget(href)) {
|
|
e.preventDefault()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Also offset any in-page anchor (hero buttons, etc.)
|
|
document.querySelectorAll('a[href^="#"]:not(.nav-item)').forEach((a) => {
|
|
a.addEventListener('click', (e) => {
|
|
const href = a.getAttribute('href')
|
|
if (href.length > 1 && scrollToTarget(href)) e.preventDefault()
|
|
})
|
|
})
|
|
|
|
window.addEventListener('load', () => scrollToTarget(location.hash))
|
|
window.addEventListener('hashchange', () => scrollToTarget(location.hash))
|
|
|
|
// --- Sticky nav shadow ----------------------------------------
|
|
const navEl = document.getElementById('mainNav')
|
|
const onScroll = () => {
|
|
if (window.pageYOffset > 20) navEl.classList.add('scrolled')
|
|
else navEl.classList.remove('scrolled')
|
|
}
|
|
window.addEventListener('scroll', onScroll, { passive: true })
|
|
onScroll()
|
|
|
|
// --- Lightbox --------------------------------------------------
|
|
const lightbox = document.getElementById('lightbox')
|
|
let lbSrcs = [], lbAlts = [], lbIndex = 0
|
|
if (lightbox) {
|
|
const lbImg = lightbox.querySelector('.lightbox-img'),
|
|
lbCap = lightbox.querySelector('.lightbox-caption'),
|
|
lbPrev = lightbox.querySelector('.lightbox-arrow.prev'),
|
|
lbNext = lightbox.querySelector('.lightbox-arrow.next'),
|
|
lbClose = lightbox.querySelector('.lightbox-close')
|
|
|
|
const render = () => {
|
|
lbImg.src = lbSrcs[lbIndex]
|
|
lbImg.alt = lbAlts[lbIndex] || ''
|
|
lbCap.textContent = lbAlts[lbIndex] || ''
|
|
}
|
|
window.openLightbox = (srcs, alts, i) => {
|
|
lbSrcs = srcs; lbAlts = alts; lbIndex = i
|
|
render()
|
|
lightbox.hidden = false
|
|
document.body.style.overflow = 'hidden'
|
|
}
|
|
const close = () => { lightbox.hidden = true; document.body.style.overflow = '' }
|
|
const lbGo = (n) => { lbIndex = (n + lbSrcs.length) % lbSrcs.length; render() }
|
|
|
|
lbPrev.addEventListener('click', (e) => { e.stopPropagation(); lbGo(lbIndex - 1) })
|
|
lbNext.addEventListener('click', (e) => { e.stopPropagation(); lbGo(lbIndex + 1) })
|
|
lbClose.addEventListener('click', close)
|
|
lightbox.addEventListener('click', (e) => {
|
|
if (e.target === lbImg) lbGo(lbIndex + 1)
|
|
else if (!e.target.closest('.lightbox-arrow') && !e.target.closest('.lightbox-close')) close()
|
|
})
|
|
document.addEventListener('keydown', (e) => {
|
|
if (lightbox.hidden) return
|
|
if (e.key === 'Escape') close()
|
|
else if (e.key === 'ArrowLeft') lbGo(lbIndex - 1)
|
|
else if (e.key === 'ArrowRight') lbGo(lbIndex + 1)
|
|
})
|
|
}
|
|
|
|
// --- Galleries -------------------------------------------------
|
|
document.querySelectorAll('.gallery').forEach((gallery) => {
|
|
const imgs = Array.from(gallery.querySelectorAll('.gallery-img')),
|
|
prev = gallery.querySelector('.gallery-arrow.prev'),
|
|
next = gallery.querySelector('.gallery-arrow.next'),
|
|
dotsWrap = gallery.querySelector('.gallery-dots')
|
|
if (!imgs.length) return
|
|
|
|
let index = 0
|
|
let timer = null
|
|
|
|
// Build dots
|
|
const dots = imgs.map((_, i) => {
|
|
const dot = document.createElement('button')
|
|
dot.className = 'gallery-dot' + (i === 0 ? ' is-active' : '')
|
|
dot.setAttribute('aria-label', 'Go to image ' + (i + 1))
|
|
dot.addEventListener('click', () => { go(i); restart() })
|
|
dotsWrap && dotsWrap.appendChild(dot)
|
|
return dot
|
|
})
|
|
|
|
const go = (n) => {
|
|
imgs[index].classList.remove('is-active')
|
|
dots[index] && dots[index].classList.remove('is-active')
|
|
index = (n + imgs.length) % imgs.length
|
|
imgs[index].classList.add('is-active')
|
|
dots[index] && dots[index].classList.add('is-active')
|
|
}
|
|
|
|
const start = () => { timer = setInterval(() => go(index + 1), 6000) }
|
|
const stop = () => { clearInterval(timer); timer = null }
|
|
const restart = () => { stop(); start() }
|
|
|
|
prev && prev.addEventListener('click', () => { go(index - 1); restart() })
|
|
next && next.addEventListener('click', () => { go(index + 1); restart() })
|
|
|
|
// Pause on hover (desktop)
|
|
gallery.addEventListener('mouseenter', stop)
|
|
gallery.addEventListener('mouseleave', start)
|
|
|
|
// Touch swipe
|
|
let startX = 0
|
|
gallery.addEventListener('touchstart', (e) => { startX = e.touches[0].clientX; stop() }, { passive: true })
|
|
gallery.addEventListener('touchend', (e) => {
|
|
const dx = e.changedTouches[0].clientX - startX
|
|
if (Math.abs(dx) > 40) go(index + (dx < 0 ? 1 : -1))
|
|
start()
|
|
})
|
|
|
|
// Click image to open in lightbox (opens at the currently shown slide)
|
|
const track = gallery.querySelector('.gallery-track')
|
|
if (track && window.openLightbox) {
|
|
const srcs = imgs.map((i) => i.getAttribute('src'))
|
|
const alts = imgs.map((i) => i.alt)
|
|
track.classList.add('zoomable')
|
|
track.addEventListener('click', () => window.openLightbox(srcs, alts, index))
|
|
}
|
|
|
|
start()
|
|
})
|
|
|
|
// --- Footer year ----------------------------------------------
|
|
const yearEl = document.getElementById('year')
|
|
if (yearEl) yearEl.textContent = new Date().getFullYear()
|