2026-07-01 18:12:12 +01:00
|
|
|
/**
|
|
|
|
|
* SchoolHeroMap
|
|
|
|
|
* The location map that sits atop the school-detail hero. Shows a static
|
|
|
|
|
* preview (pin + tiles) that blends down into the title; the whole band — or
|
|
|
|
|
* the parent's "View on map" link, via the imperative `open()` handle — expands
|
|
|
|
|
* it to a fullscreen, interactive map.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import dynamic from 'next/dynamic';
|
|
|
|
|
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
|
|
|
import styles from './SchoolHeroMap.module.css';
|
|
|
|
|
|
|
|
|
|
const LeafletHeroMap = dynamic(() => import('./LeafletHeroMapInner'), {
|
|
|
|
|
ssr: false,
|
|
|
|
|
loading: () => <div className={styles.skeleton} aria-hidden="true" />,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export interface SchoolHeroMapHandle {
|
|
|
|
|
open: () => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface SchoolHeroMapProps {
|
|
|
|
|
lat: number;
|
|
|
|
|
lng: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const SchoolHeroMap = forwardRef<SchoolHeroMapHandle, SchoolHeroMapProps>(
|
|
|
|
|
function SchoolHeroMap({ lat, lng }, ref) {
|
|
|
|
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
2026-07-05 21:27:41 +01:00
|
|
|
const [nativeFullscreen, setNativeFullscreen] = useState(false);
|
|
|
|
|
// iOS Safari has no Element.requestFullscreen — fall back to a
|
|
|
|
|
// fixed-position overlay driven by state instead of the Fullscreen API.
|
|
|
|
|
const [fallbackFullscreen, setFallbackFullscreen] = useState(false);
|
|
|
|
|
const isFullscreen = nativeFullscreen || fallbackFullscreen;
|
2026-07-01 18:12:12 +01:00
|
|
|
|
|
|
|
|
const open = useCallback(() => {
|
2026-07-05 21:27:41 +01:00
|
|
|
const el = wrapperRef.current;
|
|
|
|
|
if (!el) return;
|
|
|
|
|
if (el.requestFullscreen) {
|
|
|
|
|
el.requestFullscreen().catch(() => setFallbackFullscreen(true));
|
|
|
|
|
} else {
|
|
|
|
|
setFallbackFullscreen(true);
|
|
|
|
|
}
|
2026-07-01 18:12:12 +01:00
|
|
|
}, []);
|
|
|
|
|
const close = useCallback(() => {
|
|
|
|
|
if (document.fullscreenElement) document.exitFullscreen().catch(() => {});
|
2026-07-05 21:27:41 +01:00
|
|
|
setFallbackFullscreen(false);
|
2026-07-01 18:12:12 +01:00
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
useImperativeHandle(ref, () => ({ open }), [open]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-07-05 21:27:41 +01:00
|
|
|
const onChange = () => setNativeFullscreen(!!document.fullscreenElement);
|
2026-07-01 18:12:12 +01:00
|
|
|
document.addEventListener('fullscreenchange', onChange);
|
|
|
|
|
return () => document.removeEventListener('fullscreenchange', onChange);
|
|
|
|
|
}, []);
|
|
|
|
|
|
2026-07-05 21:27:41 +01:00
|
|
|
// The fallback overlay sits on top of the page rather than replacing it,
|
|
|
|
|
// so lock body scroll while it is up.
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!fallbackFullscreen) return;
|
|
|
|
|
const prev = document.body.style.overflow;
|
|
|
|
|
document.body.style.overflow = 'hidden';
|
|
|
|
|
return () => { document.body.style.overflow = prev; };
|
|
|
|
|
}, [fallbackFullscreen]);
|
|
|
|
|
|
2026-07-01 18:12:12 +01:00
|
|
|
return (
|
2026-07-05 21:27:41 +01:00
|
|
|
<div
|
|
|
|
|
ref={wrapperRef}
|
|
|
|
|
className={styles.wrapper}
|
|
|
|
|
data-fullscreen={isFullscreen || undefined}
|
|
|
|
|
data-fs-fallback={fallbackFullscreen || undefined}
|
|
|
|
|
>
|
2026-07-01 18:12:12 +01:00
|
|
|
<LeafletHeroMap lat={lat} lng={lng} interactive={isFullscreen} />
|
|
|
|
|
|
|
|
|
|
{isFullscreen ? (
|
|
|
|
|
<button type="button" className={styles.closeBtn} onClick={close} aria-label="Close map">
|
|
|
|
|
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
|
|
|
<path d="M18 6 6 18M6 6l12 12" />
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
{/* Whole-band click target that opens the full map. */}
|
|
|
|
|
<button type="button" className={styles.openBtn} onClick={open} aria-label="Open full map">
|
|
|
|
|
<span className={styles.openHint}>
|
|
|
|
|
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
|
|
|
<path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7" />
|
|
|
|
|
</svg>
|
|
|
|
|
Open full map
|
|
|
|
|
</span>
|
|
|
|
|
</button>
|
|
|
|
|
{/* Diffuse blend into the hero body below. */}
|
|
|
|
|
<div className={styles.fade} aria-hidden="true" />
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
);
|