1const menuButton = document.querySelector(".menu-toggle"); 2const navLinks = document.querySelectorAll(".primary-nav a"); 3const menuLabel = menuButton?.querySelector(".sr-only"); 4const contactForm = document.querySelector(".contact-form"); 5const formStatus = document.querySelector(".form-status"); 6const demoForms = document.querySelectorAll("[data-demo-form]"); 7const carousel = document.querySelector("[data-news-carousel]"); 8const scrollCarousels = document.querySelectorAll("[data-scroll-carousel]"); 9const teamSelector = document.querySelector(".team-selector-nav"); 10const mapFrame = document.querySelector("[data-map-frame]"); 11const mapEnable = document.querySelector("[data-map-enable]"); 12const mapReset = document.querySelector("[data-map-reset]"); 13 14function applyClubIdentityDetails() { 15 document.querySelectorAll(".main-header").forEach((header) => { 16 const mark = header.querySelector(":scope > .club-mark"); 17 if (!mark || header.querySelector(".header-partnership")) return; 18 const lockup = document.createElement("div"); 19 lockup.className = "club-identity-lockup"; 20 mark.before(lockup); 21 lockup.append(mark); 22 lockup.insertAdjacentHTML("beforeend", '<small class="header-partnership">In partnership with <a href="https://www.thehound.london/" target="_blank" rel="noopener">The Hound</a> of the <a href="https://www.ardentpubgroup.com/" target="_blank" rel="noopener">Ardent Pub Group</a>.</small>'); 23 }); 24 document.querySelectorAll(".site-footer").forEach((footer) => { 25 if (footer.querySelector(".footer-legal")) return; 26 const bottom = footer.querySelector(".footer-bottom"); 27 const legal = document.createElement("div"); 28 legal.className = "section-shell footer-legal"; 29 legal.innerHTML = '<div class="footer-legal-copy"><strong>Polytechnic Football Club</strong><span>Registered charity in England and Wales, number 1127155.</span></div><div class="footer-affiliations" aria-label="Club affiliations"><span>Affiliated to</span><div><a href="https://www.thefa.com/" target="_blank" rel="noopener" aria-label="Visit The Football Association website"><img src="/assets/logos/The-FA.png" alt="The Football Association"></a><a href="https://www.amateur-fa.com/" target="_blank" rel="noopener" aria-label="Visit the Amateur Football Alliance website"><img src="/assets/logos/Amateur_Football_Alliance_logo.png" alt="Amateur Football Alliance"></a><a href="https://www.southernamateurleague.co.uk/" target="_blank" rel="noopener" aria-label="Visit the Southern Amateur League website"><img src="/assets/logos/SAL_short.png" alt="Southern Amateur League"></a></div></div>'; 30 if (bottom) bottom.before(legal); else footer.append(legal); 31 }); 32} 33applyClubIdentityDetails(); 34 35function applySecondaryMobileNavigation() { 36 const path = window.location.pathname.replace(/\/+$/, "") || "/"; 37 const isHome = path === "/" || path === "/index.html"; 38 const excluded = document.body.classList.contains("admin-dashboard-page") || document.body.dataset.adminArea || /\/(?:admin[^/]*|login|password-reset)\.html$/.test(path); 39 const nav = document.querySelector(".primary-nav:not(.admin-primary-nav)"); 40 if (!nav || isHome || excluded) return; 41 const login = nav.querySelector(".mobile-login-link"); 42 const routes = [ 43 { label: "Home", href: "/index.html", className: "mobile-global-home" }, 44 { label: "Teams", href: "/teams/index.html", className: "mobile-global-teams" }, 45 { label: "Join", href: "/join.html", className: "mobile-global-join" }, 46 ]; 47 routes.forEach((route) => { 48 const existing = [...nav.querySelectorAll("a")].find((link) => new URL(link.href, window.location.href).pathname === route.href); 49 const link = existing || document.createElement("a"); 50 link.classList.add("mobile-global-route", route.className); 51 if (path === route.href) link.setAttribute("aria-current", "page"); 52 if (!existing) { 53 link.classList.add("mobile-global-injected"); 54 link.href = route.href; 55 link.textContent = route.label; 56 nav.insertBefore(link, login || null); 57 } 58 }); 59} 60applySecondaryMobileNavigation(); 61 62const resultForm=document.querySelector("[data-result-form]"); 63const requiredGoals=resultForm?.querySelector(".result-goals-required"); 64const optionalTeamSheet=resultForm?.querySelector(".is-optional-section[data-result-match-details]"); 65if(requiredGoals&&optionalTeamSheet)resultForm.insertBefore(requiredGoals,optionalTeamSheet); 66 67document.querySelectorAll("option").forEach((option) => { 68 if (option.textContent.trim() === "Development") { 69 option.textContent = "Development XI"; 70 option.value = "Development XI"; 71 } 72}); 73document.querySelectorAll('.team-page-hero h1, #overview h2, .team-card strong').forEach((element) => { 74 if (element.textContent.trim() === "Development") element.textContent = "Development XI"; 75}); 76 77function prepareTeamDataShell() { 78 const team = document.querySelector(".team-page-hero h1")?.textContent.trim(); 79 const grid = document.querySelector("#overview .team-detail-grid"); 80 const stats = grid?.querySelector(".team-stat-grid"); 81 if (!team || !grid || !stats) return; 82 const copy = grid.firstElementChild; 83 copy.classList.add("team-overview-copy"); 84 if (!copy.querySelector(".team-overview-label")) copy.querySelector(".team-intro")?.insertAdjacentHTML("beforebegin", '<p class="team-overview-label">About the team</p>'); 85 stats.className = "team-performance-panel team-performance-loading"; 86 stats.innerHTML = '<div class="team-performance-heading"><div><p class="section-kicker">Season performance</p><h3>Current statistics</h3></div><span>Loading competitive results</span></div><div class="team-record-summary"><div><strong>0</strong><span>Played</span></div><div class="is-won"><strong>0</strong><span>Won</span></div><div class="is-drawn"><strong>0</strong><span>Drawn</span></div><div class="is-lost"><strong>0</strong><span>Lost</span></div></div><div class="team-form-bar"></div><div class="team-performance-metrics"><article><span>Goals</span><strong>0</strong></article><article><span>Clean sheets</span><strong>0</strong></article><article class="is-average"><span>Average goals for</span><strong>0.0</strong></article><article class="is-average"><span>Average goals against</span><strong>0.0</strong></article></div>'; 87 const table = document.querySelector("#stats .team-data-grid"); 88 if (table) { 89 table.className = "section-shell team-player-stats team-player-stats-loading"; 90 table.innerHTML = `<div class="team-player-stats-heading"><div><p class="section-kicker">Player statistics</p><h2>${team} season stats</h2><p>
90Competitive matches only. Select a heading to reorder the table.</p></div><span>Loading matches</span></div><div class="team-stats-number-loading">Loading player statisticsâ¦</div>`; 91 } 92 document.body.classList.add("team-data-shell-ready"); 93} 94 95function initializeTeamMediaLightbox() { 96 const cards = [...document.querySelectorAll("#media .media-card")]; 97 if (!cards.length) return; 98 const images = cards.map((card) => ({ src: card.querySelector("img")?.src, alt: card.querySelector("img")?.alt || "", title: card.querySelector("span")?.textContent.trim() || "Team photo" })).filter((item) => item.src); 99 if (!images.length) return; 100 const dialog = document.createElement("dialog"); 101 dialog.className = "gallery-lightbox team-media-lightbox"; 102 dialog.innerHTML = '<button type="button" class="gallery-lightbox-close" aria-label="Close gallery">Ã</button><button type="button" class="gallery-lightbox-arrow gallery-lightbox-prev" aria-label="Previous photo">â</button><figure><img src="" alt=""><figcaption></figcaption></figure><button type="button" class="gallery-lightbox-arrow gallery-lightbox-next" aria-label="Next photo">â</button>'; 103 document.body.append(dialog); 104 let active = 0; 105 const show = (index) => { active = (index + images.length) % images.length; const item = images[active]; dialog.querySelector("img").src = item.src; dialog.querySelector("img").alt = item.alt; dialog.querySelector("figcaption").textContent = `${active + 1} of ${images.length} · ${item.title}`; }; 106 cards.forEach((card, index) => { 107 card.tabIndex = 0; card.setAttribute("role", "button"); card.setAttribute("aria-label", `Open ${images[index]?.title || "team photo"}`); 108 const open = () => { show(index); dialog.showModal(); }; 109 card.addEventListener("click", open); 110 card.addEventListener("keydown", (event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); open(); } }); 111 }); 112 dialog.querySelector(".gallery-lightbox-close").addEventListener("click", () => dialog.close()); 113 dialog.querySelector(".gallery-lightbox-prev").addEventListener("click", () => show(active - 1)); 114 dialog.querySelector(".gallery-lightbox-next").addEventListener("click", () => show(active + 1)); 115 dialog.addEventListener("click", (event) => { if (event.target === dialog) dialog.close(); }); 116} 117 118function initializeHomeTeamShowcase() { 119 const root = document.querySelector("[data-team-showcase]"); 120 const buttons = [...document.querySelectorAll("[data-home-team]")]; 121 const teamSelect = document.querySelector("[data-home-team-select]"); 122 if (!root || !buttons.length) return; 123 const teams = { 124 first: { title: "1st XI", kicker: "Senior football", image: "assets/images/1s.jpg", alt: "Polytechnic FC 1st XI", href: "teams/first-xi.html", copy: "Polytechnic 1st XI is the clubâs flagship menâs team and its principal representative in competitive football. Bringing together outstanding players from across the club, the side competes ambitiously in the Southern Amateur League and sets the standard for the wider Polytechnic pathway. The team proudly represents the clubâs history, values and traditions, combining high performance with the strong team spirit that defines Polytechnic FC." }, 125 second: { title: "2nd XI", kicker: "Senior football", image: "assets/images/2s.jpg", alt: "Polytechnic FC 2nd XI", href: "teams/second-xi.html", copy: "Polytechnic 2nd XI is a strong, competitive Saturday side and a vital link between the clubâs wider senior squads and the 1st XI. The team gives committed players the opportunity to test themselves in high-quality league football, develop within a demanding and supportive environment, and compete for progression. With its own ambitions and identity, the 2nd XI plays an important role in driving standards across the club." }, 126 third: { title: "3rd XI", kicker: "Saturday football", image: "assets/images/3s.jpg", alt: "Polytechnic FC 3rd XI", href: "teams/third-xi.html", copy: "A strong squad balancing performance, player development and club spirit through regular Southern Amateur League and AFA competition." }, 127 fifth: { title: "Development XI", kicker: "Saturday football", image: "assets/images/5s.jpg", alt: "Polytechnic FC Development XI", href: "teams/fifth-xi.html", copy: "A competitive squad for players building rhythm and enjoying regular football within the clubâs broad Saturday programme." }, 128 sixth: { title: "6th XI", kicker: "Saturday football", image: "assets/images/150th_3s.jpg", alt: "Polytechnic FC 6th XI", href: "teams/sixth-xi.html", copy: "A social but competitive side offering regular fixtures, cup football and a welcoming route into Polytechnicâs Saturday teams." }, 129 seventh: { title: "7th XI", kicker: "Saturday football", image: "assets/images/150th_men.jpg", alt: "Polytechnic FC menâs teams", href: "teams/seventh-xi.html", copy: "A welcoming route into regular football for players across the menâs section, combining competitive matches with the clubâs strong social spirit." }, 130 development: { title: "4th XI", kicker: "Player pathway", image: "assets/images/development.jpg", alt: "Polytechnic FC 4th XI", href: "teams/development.html", copy: "A pathway squad for younger adult players, broadly aged 16â21, focused on coaching, confidence and progression into open-age Polytechnic teams." }, 131 womens: { title: "Womens XI", kicker: "Womenâs football", image: "assets/images/150th_women.jpg", alt: "Polytechnic Womenâs X
131I team", href: "teams/womens-xi.html", copy: "Founded in 2025, Polytechnic Womenâs XI is a friendly, inclusive and ambitious squad preparing for the 2026 London 11s AFA eleven-a-side league. Training takes place on Thursdays from 7.30â9pm at Quintin Hogg Memorial Ground, and everyone is welcome." }, 132 vets: { title: "Vets", kicker: "Veterans football", image: "assets/images/vets.jpg", alt: "Polytechnic FC Veterans", href: "teams/vets.html", copy: "Competitive and social football for experienced players in the clubâs 40+ Veterans section, maintaining long-standing friendships and club connections." }, 133 strollers: { title: "Strollers", kicker: "Casual football", image: "assets/images/strollers.jpg", alt: "Polytechnic FC Strollers", href: "teams/strollers.html", copy: "Casual Saturday football, friendly fixtures and a welcoming place for players who want to keep enjoying the game and the Polytechnic community." }, 134 }; 135 const select = (key) => { 136 const team = teams[key]; if (!team) return; 137 root.querySelector("[data-team-showcase-image]").src = team.image; 138 root.querySelector("[data-team-showcase-image]").alt = team.alt; 139 root.querySelector(".section-kicker").textContent = team.kicker; 140 root.querySelector("[data-team-showcase-title]").textContent = team.title; 141 root.querySelector("[data-team-showcase-copy]").textContent = team.copy; 142 const link = root.querySelector("[data-team-showcase-link]"); 143 link.href = team.href; link.textContent = `Visit the ${team.title} page â`; 144 buttons.forEach((button) => { const active = button.dataset.homeTeam === key; button.classList.toggle("is-active", active); button.setAttribute("aria-selected", String(active)); }); 145 if (teamSelect) teamSelect.value = key; 146 }; 147 buttons.forEach((button) => button.addEventListener("click", () => select(button.dataset.homeTeam))); 148 teamSelect?.addEventListener("change", () => select(teamSelect.value)); 149} 150 151prepareTeamDataShell(); 152initializeTeamMediaLightbox(); 153initializeHomeTeamShowcase(); 154 155function closeMenu() { 156 document.body.classList.remove("menu-open"); 157 menuButton?.setAttribute("aria-expanded", "false"); 158 if (menuLabel) { 159 menuLabel.textContent = "Open menu"; 160 } 161} 162 163menuButton?.addEventListener("click", () => { 164 const isOpen = document.body.classList.toggle("menu-open"); 165 menuButton.setAttribute("aria-expanded", String(isOpen)); 166 if (menuLabel) { 167 menuLabel.textContent = isOpen ? "Close menu" : "Open menu"; 168 } 169}); 170 171navLinks.forEach((link) => { 172 link.addEventListener("click", closeMenu); 173}); 174 175teamSelector?.querySelector(".active")?.scrollIntoView({ 176 block: "nearest", 177 inline: "center", 178}); 179 180document.querySelectorAll('a[href="#top"]').forEach((link) => { 181 link.addEventListener("click", (event) => { 182 event.preventDefault(); 183 closeMenu(); 184 history.replaceState(null, "", `${window.location.pathname}${window.location.search}`); 185 window.scrollTo({ top: 0, behavior: "smooth" }); 186 }); 187}); 188 189document.querySelectorAll('a[href*="#"]').forEach((link) => { 190 link.addEventListener("click", (event) => { 191 const destination = new URL(link.href, window.location.href); 192 const normalisePath = (path) => path.replace(/index\.html$/, "").replace(/\/+$/, ""); 193 if ( 194 destination.origin !== window.location.origin || 195 normalisePath(destination.pathname) !== normalisePath(window.location.pathname) || 196 !destination.hash 197 ) return; 198 199 const target = document.querySelector(destination.hash); 200 if (!target) return; 201 event.preventDefault(); 202 closeMenu(); 203 if (destination.hash === "#top") { 204 history.replaceState(null, "", `${window.location.pathname}${window.location.search}`); 205 window.scrollTo({ top: 0, behavior: "smooth" }); 206 return; 207 } 208 history.replaceState(null, "", destination.hash); 209 target.scrollIntoView({ behavior: "smooth", block: "start" }); 210 }); 211}); 212 213window.addEventListener("resize", () => { 214 if (window.innerWidth > 920) { 215 closeMenu(); 216 } 217}); 218 219contactForm?.addEventListener("submit", (event) => { 220 if ( 221 contactForm.dataset.demoForm !== undefined || 222 contactForm.dataset.authLogin !== undefined || 223 contactForm.dataset.authRegister !== undefined || 224 contactForm.dataset.publicEnquiryForm !== undefined 225 ) return; 226 227 event.preventDefault(); 228 contactForm.reset(); 229 230 if (formStatus) { 231 formStatus.textContent = "Thanks. Your enquiry is ready for the club team to pick up."; 232 } 233}); 234
235demoForms.forEach((form) => { 236 form.addEventListener("submit", (event) => { 237 event.preventDefault(); 238 const status = form.querySelector(".form-status"); 239 form.reset(); 240 241 if (status) { 242 status.textContent = "This form is ready to connect to the club admin system."; 243 } 244 }); 245}); 246 247function initializeNewsCarousel() { 248 if (!carousel || carousel.dataset.carouselReady) return; 249 const slides = Array.from(carousel.querySelectorAll("[data-slide]")); 250 if (!slides.length) return; 251 carousel.dataset.carouselReady = "true"; 252 const triggers = Array.from(carousel.querySelectorAll("[data-carousel-trigger]")); 253 const title = carousel.querySelector("[data-carousel-title]"); 254 const category = carousel.querySelector("[data-carousel-category]"); 255 const summary = carousel.querySelector("[data-carousel-summary]"); 256 const date = carousel.querySelector("[data-carousel-date]"); 257 const link = carousel.querySelector("[data-carousel-link]"); 258 const previousButton = carousel.querySelector("[data-carousel-prev]"); 259 const nextButton = carousel.querySelector("[data-carousel-next]"); 260 let activeIndex = 0; 261 let rotationTimer; 262 let touchStartX = 0; 263 264 function showSlide(index) { 265 activeIndex = (index + slides.length) % slides.length; 266 const activeSlide = slides[activeIndex]; 267 268 slides.forEach((slide, slideIndex) => { 269 slide.classList.toggle("is-active", slideIndex === activeIndex); 270 }); 271 272 triggers.forEach((trigger, triggerIndex) => { 273 const isActive = triggerIndex === activeIndex; 274 trigger.classList.toggle("is-active", isActive); 275 trigger.setAttribute("aria-pressed", String(isActive)); 276 }); 277 278 if (title) title.textContent = activeSlide.dataset.title || ""; 279 if (category) category.textContent = activeSlide.dataset.category || ""; 280 if (summary) summary.textContent = activeSlide.dataset.summary || ""; 281 if (date) date.textContent = activeSlide.dataset.date || ""; 282 if (link) link.href = activeSlide.dataset.link || "#"; 283 } 284 285 function restartRotation() { 286 window.clearInterval(rotationTimer); 287 rotationTimer = window.setInterval(() => { 288 showSlide(activeIndex + 1); 289 }, 6500); 290 } 291 292 triggers.forEach((trigger) => { 293 trigger.addEventListener("click", () => { 294 showSlide(Number(trigger.dataset.slideIndex)); 295 restartRotation(); 296 }); 297 }); 298 299 previousButton?.addEventListener("click", () => { 300 showSlide(activeIndex - 1); 301 restartRotation(); 302 }); 303 304 nextButton?.addEventListener("click", () => { 305 showSlide(activeIndex + 1); 306 restartRotation(); 307 }); 308 309 carousel.addEventListener("mouseenter", () => window.clearInterval(rotationTimer)); 310 carousel.addEventListener("mouseleave", restartRotation); 311 carousel.addEventListener("focusin", () => window.clearInterval(rotationTimer)); 312 carousel.addEventListener("focusout", restartRotation); 313 314 carousel.addEventListener("touchstart", (event) => { 315 touchStartX = event.changedTouches[0].clientX; 316 }, { passive: true }); 317 318 carousel.addEventListener("touchend", (event) => { 319 const touchEndX = event.changedTouches[0].clientX; 320 const swipeDistance = touchEndX - touchStartX; 321 322 if (Math.abs(swipeDistance) > 45) { 323 showSlide(activeIndex + (swipeDistance < 0 ? 1 : -1)); 324 restartRotation(); 325 } 326 }, { passive: true }); 327 328 showSlide(0); 329 restartRotation(); 330} 331 332if (carousel?.dataset.dynamicSpotlight !== undefined) { 333 window.addEventListener("spotlight-ready", initializeNewsCarousel, { once: true }); 334 window.setTimeout(initializeNewsCarousel, 2500); 335} else { 336 initializeNewsCarousel(); 337} 338 339mapEnable?.addEventListener("click", () => { 340 const isUnlocked = mapFrame?.classList.toggle("is-unlocked"); 341 mapEnable.textContent = isUnlocked ? "Lock map" : "Use map"; 342}); 343 344mapReset?.addEventListener("click", () => { 345 if (mapFrame?.dataset.mapSrc) { 346 mapFrame.src = mapFrame.dataset.mapSrc; 347 } 348}); 349 350scrollCarousels.forEach((scrollCarousel) => { 351 const group = scrollCarousel.closest("[data-scroll-group]") || scrollCarousel.closest("section"); 352 const previous = group?.querySelector("[data-scroll-prev]"); 353 const next = group?.querySelector("[data-scroll-next]"); 354 355 function scrollFixtures(direction) { 356 const card = scrollCarousel.querySelector(":scope > *"); 357 const distance = card ? card.getBoundingClientRect().width + 16 : 320; 358 const max = Math.max(0, scrollCarousel.scrollWidth - scrollCarousel.clientWidth); 359 if (max < 2) return; 360 if (direction > 0 && scrollCarousel.scrollLeft >= max - 2) { 361 scrollCarousel.scrollTo({ left: 0, behavior: "smooth" }); 362 return; 363 } 364 if (direction < 0 && scrollCarousel.scrollLeft <= 2) { 365 scrollCarousel.scrollTo({ left: max, behavior: "smooth" }); 366 return; 367 } 368 const target = Math.min(max, Math.max(0, scrollCarousel.scrollLeft + distance * direction)); 369 scrollCarousel.scrollTo({ left: target, behavior: "smooth" }); 370 } 371 372 previous?.addEventListener("click", () => scrollFixtures(-1)); 373 next?.addEventListener("click", () => scrollFixtures(1)); 374});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.