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:
2026-06-22 22:09:46 -04:00
parent f739de7360
commit 50e8b1bcd3
81 changed files with 1847 additions and 689 deletions

View File

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