Files
PineCreek_Web_2.0/js/main.js
Chris Olson 50e8b1bcd3 Enhance Pine Creek marketing site: real media, galleries, HOA info
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>
2026-06-22 22:09:46 -04:00

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()