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>
This commit is contained in:
291
js/main.js
291
js/main.js
@@ -1,178 +1,171 @@
|
||||
/* ============================================================
|
||||
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')
|
||||
|
||||
// Nav toggle, open, close
|
||||
// --- Mobile nav ------------------------------------------------
|
||||
const toggleNav = () => {
|
||||
!(mainNav.classList.contains('in')) ? (
|
||||
mainNav.classList.add('in'),
|
||||
hamburger.classList.add('in')
|
||||
) : (
|
||||
mainNav.classList.remove('in'),
|
||||
hamburger.classList.remove('in')
|
||||
)
|
||||
mainNav.classList.toggle('in')
|
||||
hamburger.classList.toggle('in')
|
||||
},
|
||||
openNav = () => mainNav.classList.add('in'),
|
||||
closeNav = () => mainNav.classList.remove('in')
|
||||
|
||||
// Hamburger menu icon click
|
||||
hamburger.onclick = toggleNav
|
||||
|
||||
// Nav items on click: close nav (mobile) and smooth-scroll to anchor targets
|
||||
for (let i = 0; i < navItem.length; i++) {
|
||||
navItem[i].onclick = function (e) {
|
||||
// prevent default anchor jump; we'll smooth-scroll with offset instead
|
||||
e.preventDefault()
|
||||
// close mobile nav if open
|
||||
closeNav()
|
||||
|
||||
const href = this.getAttribute('href') || ''
|
||||
if (href.startsWith('#')) {
|
||||
const target = document.querySelector(href)
|
||||
if (target) {
|
||||
// compute offset to account for fixed header
|
||||
const header = document.getElementById('mainNav')
|
||||
const headerHeight = header ? header.offsetHeight : 0
|
||||
const extra = 8 // small spacing under header
|
||||
const targetY = window.pageYOffset + target.getBoundingClientRect().top - headerHeight - extra
|
||||
|
||||
window.scrollTo({ top: targetY, behavior: 'smooth' })
|
||||
return
|
||||
closeNav = () => {
|
||||
mainNav.classList.remove('in')
|
||||
hamburger.classList.remove('in')
|
||||
}
|
||||
}
|
||||
|
||||
// fallback: follow the link if it's not an in-page anchor
|
||||
if (href) window.location.href = href
|
||||
}
|
||||
}
|
||||
hamburger.onclick = toggleNav
|
||||
hamburger.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleNav() }
|
||||
})
|
||||
|
||||
// If the page is loaded with a hash or the hash changes, adjust scroll to account for the fixed header
|
||||
const scrollToHashWithOffset = (hash) => {
|
||||
if (!hash) return
|
||||
const target = document.querySelector(hash)
|
||||
if (!target) return
|
||||
// --- 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 extra = 8
|
||||
const targetY = window.pageYOffset + target.getBoundingClientRect().top - headerHeight - extra
|
||||
// Use a short timeout to allow browser default jump to complete before adjusting
|
||||
setTimeout(() => window.scrollTo({ top: targetY, behavior: 'smooth' }), 50)
|
||||
const y = window.pageYOffset + target.getBoundingClientRect().top - headerHeight - 8
|
||||
window.scrollTo({ top: y, behavior: 'smooth' })
|
||||
return true
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => scrollToHashWithOffset(location.hash))
|
||||
window.addEventListener('hashchange', () => scrollToHashWithOffset(location.hash))
|
||||
|
||||
// light auth
|
||||
window.onload = () => {
|
||||
const board = document.getElementById('boardPage'),
|
||||
owner = document.getElementById('ownerPage')
|
||||
|
||||
if (board || owner) {
|
||||
const response = prompt('Please enter the password.')
|
||||
|
||||
switch (response) {
|
||||
case '12345':
|
||||
console.log('Board Member')
|
||||
break
|
||||
case 'abcde':
|
||||
console.log('Owner')
|
||||
break
|
||||
default:
|
||||
console.log('Access Denied')
|
||||
break
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Slides
|
||||
// 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()
|
||||
})
|
||||
})
|
||||
|
||||
const slidesVineyard = document.getElementById('slidesVineyard'),
|
||||
slidesVineyardLeft = document.querySelector('#slidesVineyard .slides-arrow:nth-last-child(2)'),
|
||||
slidesVineyardRight = document.querySelector('#slidesVineyard .slides-arrow:last-child'),
|
||||
slidesVineyardImg = document.querySelector('#slidesVineyard .slides-img'),
|
||||
slidesHouse = document.getElementById('slidesHouse'),
|
||||
slidesHouseLeft = document.querySelector('#slidesHouse .slides-arrow:nth-last-child(2)'),
|
||||
slidesHouseRight = document.querySelector('#slidesHouse .slides-arrow:last-child'),
|
||||
slidesHouseImg = document.querySelector('#slidesHouse .slides-img')
|
||||
window.addEventListener('load', () => scrollToTarget(location.hash))
|
||||
window.addEventListener('hashchange', () => scrollToTarget(location.hash))
|
||||
|
||||
let vineyardImgs = 0
|
||||
// --- 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()
|
||||
|
||||
slidesVineyardRight.onclick = () => {
|
||||
vineyardImgs = vineyardImgs + 1
|
||||
slidesVineyardImg.classList.add('out')
|
||||
setTimeout(() => {
|
||||
slidesVineyardImg.setAttribute('src', 'img/vineyard-' + vineyardImgs + '.jpg')
|
||||
slidesVineyardImg.classList.remove('out')
|
||||
if (vineyardImgs >= 1) {
|
||||
slidesVineyardLeft.classList.remove('out')
|
||||
}
|
||||
}, 150)
|
||||
// --- 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)
|
||||
})
|
||||
}
|
||||
|
||||
slidesVineyardLeft.onclick = () => {
|
||||
slidesVineyardImg.classList.add('out')
|
||||
|
||||
setTimeout(() => {
|
||||
slidesVineyardImg.classList.remove('out')
|
||||
if (vineyardImgs > 0) {
|
||||
vineyardImgs = vineyardImgs - 1
|
||||
slidesVineyardImg.setAttribute('src', 'img/vineyard-' + vineyardImgs + '.jpg')
|
||||
}
|
||||
if (vineyardImgs < 1) {
|
||||
slidesVineyardLeft.classList.add('out')
|
||||
}
|
||||
}, 150)
|
||||
}
|
||||
// --- 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
|
||||
|
||||
const imgVineyardError = (image) => {
|
||||
vineyardImgs = 0
|
||||
image.src = 'img/vineyard-0.jpg'
|
||||
let index = 0
|
||||
let timer = null
|
||||
|
||||
setTimeout(() => {
|
||||
if (vineyardImgs < 1) {
|
||||
slidesVineyardLeft.classList.add('out')
|
||||
}
|
||||
}, 150)
|
||||
return true
|
||||
}
|
||||
// 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
|
||||
})
|
||||
|
||||
let houseImgs = 0
|
||||
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')
|
||||
}
|
||||
|
||||
slidesHouseRight.onclick = () => {
|
||||
houseImgs = houseImgs + 1
|
||||
slidesHouseImg.classList.add('out')
|
||||
setTimeout(() => {
|
||||
slidesHouseImg.setAttribute('src', 'img/house-' + houseImgs + '.jpg')
|
||||
slidesHouseImg.classList.remove('out')
|
||||
if (houseImgs >= 1) {
|
||||
slidesHouseLeft.classList.remove('out')
|
||||
}
|
||||
}, 150)
|
||||
}
|
||||
const start = () => { timer = setInterval(() => go(index + 1), 6000) }
|
||||
const stop = () => { clearInterval(timer); timer = null }
|
||||
const restart = () => { stop(); start() }
|
||||
|
||||
slidesHouseLeft.onclick = () => {
|
||||
slidesHouseImg.classList.add('out')
|
||||
|
||||
setTimeout(() => {
|
||||
slidesHouseImg.classList.remove('out')
|
||||
if (houseImgs > 0) {
|
||||
houseImgs = houseImgs - 1
|
||||
slidesHouseImg.setAttribute('src', 'img/house-' + houseImgs + '.jpg')
|
||||
}
|
||||
if (houseImgs < 1) {
|
||||
slidesHouseLeft.classList.add('out')
|
||||
}
|
||||
}, 150)
|
||||
}
|
||||
prev && prev.addEventListener('click', () => { go(index - 1); restart() })
|
||||
next && next.addEventListener('click', () => { go(index + 1); restart() })
|
||||
|
||||
const imgHouseError = (image) => {
|
||||
houseImgs = 0
|
||||
image.src = 'img/house-0.jpg'
|
||||
// Pause on hover (desktop)
|
||||
gallery.addEventListener('mouseenter', stop)
|
||||
gallery.addEventListener('mouseleave', start)
|
||||
|
||||
setTimeout(() => {
|
||||
if (houseImgs < 1) {
|
||||
slidesHouseLeft.classList.add('out')
|
||||
}
|
||||
}, 150)
|
||||
return true
|
||||
}
|
||||
// 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()
|
||||
|
||||
Reference in New Issue
Block a user