diff --git a/2026-05-19 19.35.50 teeoff.no b0df9e23eb81.jpg b/2026-05-19 19.35.50 teeoff.no b0df9e23eb81.jpg new file mode 100644 index 0000000..74f2896 Binary files /dev/null and b/2026-05-19 19.35.50 teeoff.no b0df9e23eb81.jpg differ diff --git a/frontend/src/app/golfbaner/[slug]/FacilityDetailView.tsx b/frontend/src/app/golfbaner/[slug]/FacilityDetailView.tsx index 30eb76f..030b14a 100644 --- a/frontend/src/app/golfbaner/[slug]/FacilityDetailView.tsx +++ b/frontend/src/app/golfbaner/[slug]/FacilityDetailView.tsx @@ -13,7 +13,7 @@ * --------------------------------------------------------------------------- */ -import { useState, useEffect } from 'react'; +import { useEffect, useRef, useState } from 'react'; import dynamic from "next/dynamic"; import { STATUS_MAP, FALLBACK_IMAGE } from "@/config/constants"; import { STATUS_ICON_PATHS, buildMapUrl, getPrimaryStatus, getPublicCourseDisplayName, parseJson as parseSharedJson, slugify } from "@/app/facilityData"; @@ -307,7 +307,10 @@ const ICONS = { camera: <>, webcam: <>, chart: <>, - weather: <> + weather: <>, + chevronLeft: , + chevronRight: , + close: <>, }; const SOCIAL_ICONS: Record = { @@ -336,6 +339,8 @@ export default function FacilityDetailView({ const [showBackToTop, setShowBackToTop] = useState(false); const [currentSlide, setCurrentSlide] = useState(0); const [activeMedia, setActiveMedia] = useState(null); + const lightboxTouchStartX = useRef(null); + const lightboxTouchStartY = useRef(null); const parseJson = (val: any, fallback: any) => { return parseSharedJson(val, fallback); @@ -498,7 +503,7 @@ export default function FacilityDetailView({ const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') { - setActiveMedia(null); + closeMedia(); return; } @@ -545,6 +550,7 @@ export default function FacilityDetailView({ const activeImage = activeMedia?.type === 'image' ? imageGalleryItems[activeMedia.index] : null; const activeVideo = activeMedia?.type === 'video' ? facilityVideos[activeMedia.index] : null; const activeMediaCount = activeMedia?.type === 'image' ? imageGalleryItems.length : activeMedia?.type === 'video' ? facilityVideos.length : 0; + const canCycleActiveMedia = activeMediaCount > 1; const cycleActiveMedia = (direction: -1 | 1) => { setActiveMedia((current) => { @@ -556,6 +562,69 @@ export default function FacilityDetailView({ }); }; + const shouldUseNativeFullscreen = () => { + if (typeof window === "undefined") return false; + return window.matchMedia("(max-width: 767px)").matches; + }; + + const requestNativeFullscreen = () => { + if (typeof document === "undefined" || !shouldUseNativeFullscreen()) return; + const root = document.documentElement; + if (document.fullscreenElement || !root.requestFullscreen) return; + void root.requestFullscreen().catch(() => {}); + }; + + const exitNativeFullscreen = () => { + if (typeof document === "undefined" || !document.fullscreenElement || !document.exitFullscreen) return; + void document.exitFullscreen().catch(() => {}); + }; + + const openMedia = (media: ActiveMedia) => { + requestNativeFullscreen(); + setActiveMedia(media); + }; + + const closeMedia = () => { + setActiveMedia(null); + exitNativeFullscreen(); + }; + + const resetLightboxTouchTracking = () => { + lightboxTouchStartX.current = null; + lightboxTouchStartY.current = null; + }; + + const handleLightboxTouchStart = (event: React.TouchEvent) => { + const touch = event.changedTouches[0]; + lightboxTouchStartX.current = touch?.clientX ?? null; + lightboxTouchStartY.current = touch?.clientY ?? null; + }; + + const handleLightboxTouchEnd = (event: React.TouchEvent) => { + if (!canCycleActiveMedia) { + resetLightboxTouchTracking(); + return; + } + + const startX = lightboxTouchStartX.current; + const startY = lightboxTouchStartY.current; + const touch = event.changedTouches[0]; + if (startX === null || startY === null || !touch) { + resetLightboxTouchTracking(); + return; + } + + const deltaX = touch.clientX - startX; + const deltaY = touch.clientY - startY; + resetLightboxTouchTracking(); + + if (Math.abs(deltaX) < 48 || Math.abs(deltaX) <= Math.abs(deltaY)) { + return; + } + + cycleActiveMedia(deltaX < 0 ? 1 : -1); + }; + return (
@@ -899,7 +968,7 @@ export default function FacilityDetailView({ - - - ) : null} - - + + + + {canCycleActiveMedia ? ( + <> + + + + ) : null} + +
{activeMedia.type === 'image' && activeImage ? ( -
+
event.stopPropagation()} + > {getGalleryImageAlt(activeImage)}
) : null} {activeMedia.type === 'video' && activeVideo ? ( -
+
event.stopPropagation()} + >