Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e8df006d7 | ||
|
|
1f8284adfc | ||
|
|
b2dc4d0779 | ||
|
|
1cdcd85e41 | ||
|
|
a00cbe9161 | ||
|
|
64121592fd | ||
|
|
6828f6cd44 | ||
|
|
331ae8d89f | ||
|
|
3adea73ee0 | ||
|
|
47335fcda0 |
@@ -25,6 +25,16 @@ test('home page loads with hero search', async ({ page }) => {
|
|||||||
await expect(page.getByPlaceholder('School name or postcode').first()).toBeVisible();
|
await expect(page.getByPlaceholder('School name or postcode').first()).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('home hero offers a "use my location" shortcut beside the search box', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
// The geolocation shortcut lives inside the hero search card, right under the
|
||||||
|
// search input — not in a separate strip further down the page.
|
||||||
|
const searchInput = page.getByPlaceholder('School name or postcode').first();
|
||||||
|
await expect(searchInput).toBeVisible();
|
||||||
|
const nearMe = page.getByRole('button', { name: /use my location/i });
|
||||||
|
await expect(nearMe).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
test('searching by name returns school results', async ({ page }) => {
|
test('searching by name returns school results', async ({ page }) => {
|
||||||
await searchByName(page, 'primary');
|
await searchByName(page, 'primary');
|
||||||
await expect(schoolLinks(page).first()).toBeVisible({ timeout: 15_000 });
|
await expect(schoolLinks(page).first()).toBeVisible({ timeout: 15_000 });
|
||||||
@@ -75,6 +85,31 @@ test('school hero map opens fullscreen on mobile without the Fullscreen API', as
|
|||||||
await expect(openMap).toBeVisible();
|
await expect(openMap).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('results map fullscreen falls back to an overlay on iOS', async ({ page }) => {
|
||||||
|
// Same iOS gap as the hero map: no Element.requestFullscreen, so the results
|
||||||
|
// map's fullscreen button must fall back to a CSS overlay.
|
||||||
|
await page.setViewportSize({ width: 390, height: 844 });
|
||||||
|
await page.addInitScript(() => {
|
||||||
|
// @ts-expect-error deliberate API removal
|
||||||
|
delete Element.prototype.requestFullscreen;
|
||||||
|
});
|
||||||
|
|
||||||
|
await searchByName(page, 'B1 1BB');
|
||||||
|
await expect(schoolLinks(page).first()).toBeVisible({ timeout: 15_000 });
|
||||||
|
|
||||||
|
// Switch to the map view, then open the map fullscreen.
|
||||||
|
await page.getByRole('button', { name: 'Map', exact: true }).click();
|
||||||
|
const openFs = page.getByRole('button', { name: 'View map fullscreen' });
|
||||||
|
await expect(openFs).toBeVisible({ timeout: 15_000 });
|
||||||
|
await openFs.click();
|
||||||
|
|
||||||
|
// The button flips to its exit state once the overlay is up.
|
||||||
|
const exitFs = page.getByRole('button', { name: 'Exit fullscreen' });
|
||||||
|
await expect(exitFs).toBeVisible();
|
||||||
|
await exitFs.click();
|
||||||
|
await expect(openFs).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
test('comparing two schools shows both side by side', async ({ page }) => {
|
test('comparing two schools shows both side by side', async ({ page }) => {
|
||||||
// Collect two school URNs from search results, then load the share URL
|
// Collect two school URNs from search results, then load the share URL
|
||||||
await searchByName(page, 'primary');
|
await searchByName(page, 'primary');
|
||||||
@@ -100,15 +135,20 @@ test('compare chart on mobile shows school chips with tap-to-focus', async ({ pa
|
|||||||
links.map((l) => (l as HTMLAnchorElement).getAttribute('href') || '')
|
links.map((l) => (l as HTMLAnchorElement).getAttribute('href') || '')
|
||||||
);
|
);
|
||||||
const urns = [...new Set(hrefs.map((h) => h.match(/\/school\/(\d+)/)?.[1]).filter(Boolean))];
|
const urns = [...new Set(hrefs.map((h) => h.match(/\/school\/(\d+)/)?.[1]).filter(Boolean))];
|
||||||
expect(urns.length).toBeGreaterThanOrEqual(2);
|
// Compare three schools, not two: a "primary" search can return all-through
|
||||||
|
// schools that classify as secondary, and the chips only appear for the
|
||||||
|
// active phase. With three schools across two phases, the auto-selected
|
||||||
|
// majority phase always holds ≥2, so the chip legend is guaranteed to render.
|
||||||
|
expect(urns.length).toBeGreaterThanOrEqual(3);
|
||||||
|
|
||||||
await page.goto(`/compare?urns=${urns[0]},${urns[1]}`);
|
await page.goto(`/compare?urns=${urns[0]},${urns[1]},${urns[2]}`);
|
||||||
await expect(page.locator('canvas:visible').first()).toBeVisible({ timeout: 15_000 });
|
await expect(page.locator('canvas:visible').first()).toBeVisible({ timeout: 15_000 });
|
||||||
|
|
||||||
// The mobile chart legend renders one chip per school inside the chart card.
|
// The mobile chart legend renders one chip per school in the active phase.
|
||||||
const chipGroup = page.getByRole('group', { name: /highlight a school/i });
|
const chipGroup = page.getByRole('group', { name: /highlight a school/i });
|
||||||
const chips = chipGroup.getByRole('button');
|
const chips = chipGroup.getByRole('button');
|
||||||
await expect(chips).toHaveCount(2);
|
await expect(chips.first()).toBeVisible({ timeout: 15_000 });
|
||||||
|
expect(await chips.count()).toBeGreaterThanOrEqual(2);
|
||||||
|
|
||||||
// Tapping a chip focuses that school's line; tapping again releases it.
|
// Tapping a chip focuses that school's line; tapping again releases it.
|
||||||
await chips.first().click();
|
await chips.first().click();
|
||||||
|
|||||||
@@ -21,8 +21,10 @@
|
|||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.chips {
|
.chips {
|
||||||
display: flex;
|
/* Two chips per row so long school names don't crowd into a single
|
||||||
flex-wrap: wrap;
|
line; each chip fills its column and truncates with an ellipsis. */
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
}
|
}
|
||||||
@@ -31,8 +33,8 @@
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
min-height: 44px;
|
min-height: 40px;
|
||||||
max-width: 100%;
|
min-width: 0;
|
||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
border: 1px solid rgba(0, 0, 0, .12);
|
border: 1px solid rgba(0, 0, 0, .12);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
@@ -58,6 +60,8 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
max-width: 9rem;
|
/* min-width:0 lets the name shrink inside the grid cell so the
|
||||||
|
ellipsis kicks in instead of overflowing. */
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,91 @@
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.searchHint {
|
||||||
|
margin: 0.875rem 0 0;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: var(--text-secondary, #5a554d);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchHint strong {
|
||||||
|
color: var(--text-primary, #1a1612);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.searchHint {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nearMeRow {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nearMeBtn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.625rem 1.375rem;
|
||||||
|
background: var(--accent-teal, #2d7d7d);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease, transform 0.15s ease;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nearMeBtn:hover:not(:disabled) {
|
||||||
|
background: #235f5f;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nearMeBtn:disabled {
|
||||||
|
opacity: 0.7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nearMeSpinner {
|
||||||
|
display: inline-block;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.35);
|
||||||
|
border-top-color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: nearMeSpin 0.7s linear infinite;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes nearMeSpin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.geoError {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: var(--accent-coral-dark, #b04a2e);
|
||||||
|
margin: 0;
|
||||||
|
max-width: 340px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.nearMeBtn {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.searchSection {
|
.searchSection {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,21 @@ interface FilterBarProps {
|
|||||||
filters: Filters;
|
filters: Filters;
|
||||||
isHero?: boolean;
|
isHero?: boolean;
|
||||||
resultFilters?: ResultFilters;
|
resultFilters?: ResultFilters;
|
||||||
|
// Geolocation "use my location" affordance, shown beside the hero search box.
|
||||||
|
// The state and handler live in HomeView (which owns the geolocation flow).
|
||||||
|
onNearMe?: () => void;
|
||||||
|
geoState?: "idle" | "requesting" | "error";
|
||||||
|
geoError?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FilterBar({ filters, isHero, resultFilters }: FilterBarProps) {
|
export function FilterBar({
|
||||||
|
filters,
|
||||||
|
isHero,
|
||||||
|
resultFilters,
|
||||||
|
onNearMe,
|
||||||
|
geoState = "idle",
|
||||||
|
geoError,
|
||||||
|
}: FilterBarProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
@@ -182,6 +194,52 @@ export function FilterBar({ filters, isHero, resultFilters }: FilterBarProps) {
|
|||||||
{isPending ? <div className={styles.spinner}></div> : "Search"}
|
{isPending ? <div className={styles.spinner}></div> : "Search"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{isHero && (
|
||||||
|
<>
|
||||||
|
<p className={styles.searchHint}>
|
||||||
|
Search by <strong>school name</strong> — or use your{" "}
|
||||||
|
<strong>postcode</strong> for the nearest schools.
|
||||||
|
</p>
|
||||||
|
{onNearMe && (
|
||||||
|
<div className={styles.nearMeRow}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.nearMeBtn}
|
||||||
|
onClick={onNearMe}
|
||||||
|
disabled={geoState === "requesting"}
|
||||||
|
>
|
||||||
|
{geoState === "requesting" ? (
|
||||||
|
<>
|
||||||
|
<span className={styles.nearMeSpinner} aria-hidden="true" />
|
||||||
|
Locating you…
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<svg
|
||||||
|
width="15"
|
||||||
|
height="15"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2.5"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M12 2a7 7 0 0 1 7 7c0 5.25-7 13-7 13S5 14.25 5 9a7 7 0 0 1 7-7z" />
|
||||||
|
<circle cx="12" cy="9" r="2.5" />
|
||||||
|
</svg>
|
||||||
|
Use my location
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{geoError && (
|
||||||
|
<p className={styles.geoError} role="alert">
|
||||||
|
{geoError}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{!isHero && (
|
{!isHero && (
|
||||||
|
|||||||
@@ -369,6 +369,16 @@
|
|||||||
|
|
||||||
.viewToggle {
|
.viewToggle {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The sort <select> sizes to its widest option ("Highest Reading, Writing
|
||||||
|
& Maths %"), which overflows a phone viewport — beside the view toggle it
|
||||||
|
ran off the right edge. Let it flex into the remaining space and shrink;
|
||||||
|
the selected label truncates instead of pushing past the screen. */
|
||||||
|
.sortSelect {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mapViewContainer {
|
.mapViewContainer {
|
||||||
@@ -496,68 +506,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.discoverySection {
|
|
||||||
padding: 0.5rem 0 0.5rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nearMeRow {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin-bottom: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nearMeBtn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
padding: 0.625rem 1.375rem;
|
|
||||||
background: var(--accent-teal, #2d7d7d);
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
border-radius: 999px;
|
|
||||||
font-size: 0.9375rem;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.2s ease, transform 0.15s ease;
|
|
||||||
font-family: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nearMeBtn:hover:not(:disabled) {
|
|
||||||
background: #235f5f;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nearMeBtn:disabled {
|
|
||||||
opacity: 0.7;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nearMeBtnSpinner {
|
|
||||||
display: inline-block;
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
border: 2px solid rgba(255, 255, 255, 0.35);
|
|
||||||
border-top-color: #fff;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: nearMeSpin 0.7s linear infinite;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes nearMeSpin {
|
|
||||||
to { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.geoError {
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
color: var(--accent-coral-dark, #b04a2e);
|
|
||||||
margin: 0;
|
|
||||||
max-width: 340px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quickSearches {
|
.quickSearches {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -284,37 +284,11 @@ export function HomeView({ initialSchools, filters, totalSchools, howItWorks, ed
|
|||||||
filters={filters}
|
filters={filters}
|
||||||
isHero={!isSearchActive}
|
isHero={!isSearchActive}
|
||||||
resultFilters={initialSchools.result_filters}
|
resultFilters={initialSchools.result_filters}
|
||||||
|
onNearMe={handleNearMe}
|
||||||
|
geoState={geoState}
|
||||||
|
geoError={geoError}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Discovery section shown on landing page before any search */}
|
|
||||||
{!isSearchActive && initialSchools.schools.length === 0 && (
|
|
||||||
<div className={styles.discoverySection}>
|
|
||||||
<div className={styles.nearMeRow}>
|
|
||||||
<button
|
|
||||||
className={styles.nearMeBtn}
|
|
||||||
onClick={handleNearMe}
|
|
||||||
disabled={geoState === 'requesting'}
|
|
||||||
>
|
|
||||||
{geoState === 'requesting' ? (
|
|
||||||
<>
|
|
||||||
<span className={styles.nearMeBtnSpinner} aria-hidden="true" />
|
|
||||||
Locating you…
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" aria-hidden="true">
|
|
||||||
<path d="M12 2a7 7 0 0 1 7 7c0 5.25-7 13-7 13S5 14.25 5 9a7 7 0 0 1 7-7z"/>
|
|
||||||
<circle cx="12" cy="9" r="2.5"/>
|
|
||||||
</svg>
|
|
||||||
Schools near me
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
{geoError && <p className={styles.geoError} role="alert">{geoError}</p>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Admissions countdown strip — only on landing page */}
|
{/* Admissions countdown strip — only on landing page */}
|
||||||
{!isSearchActive && (
|
{!isSearchActive && (
|
||||||
<section className={styles.admissionsStrip}>
|
<section className={styles.admissionsStrip}>
|
||||||
|
|||||||
@@ -10,6 +10,15 @@
|
|||||||
height: 100dvh;
|
height: 100dvh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Fallback fullscreen (iOS Safari — no Element.requestFullscreen): the API
|
||||||
|
can't promote the element, so pin it over the page ourselves. Above the
|
||||||
|
comparison toast (3000) and the bottom nav; below modals (9999+). */
|
||||||
|
.mapWrapper.fsFallback {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 5000;
|
||||||
|
}
|
||||||
|
|
||||||
.fullscreenBtn {
|
.fullscreenBtn {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0.625rem;
|
top: 0.625rem;
|
||||||
|
|||||||
@@ -33,22 +33,52 @@ interface SchoolMapProps {
|
|||||||
|
|
||||||
export function SchoolMap({ schools, center, zoom = 13, referencePoint, onMarkerClick, nationalAvgRwm, laAverages }: SchoolMapProps) {
|
export function SchoolMap({ schools, center, zoom = 13, referencePoint, onMarkerClick, nationalAvgRwm, laAverages }: SchoolMapProps) {
|
||||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
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;
|
||||||
|
|
||||||
// Sync state with browser fullscreen events (e.g. Escape key)
|
// Sync state with browser fullscreen events (e.g. Escape key)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onFsChange = () => setIsFullscreen(!!document.fullscreenElement);
|
const onFsChange = () => setNativeFullscreen(!!document.fullscreenElement);
|
||||||
document.addEventListener('fullscreenchange', onFsChange);
|
document.addEventListener('fullscreenchange', onFsChange);
|
||||||
return () => document.removeEventListener('fullscreenchange', onFsChange);
|
return () => document.removeEventListener('fullscreenchange', onFsChange);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Lock body scroll while the fallback overlay is up.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!fallbackFullscreen) return;
|
||||||
|
const prev = document.body.style.overflow;
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
return () => { document.body.style.overflow = prev; };
|
||||||
|
}, [fallbackFullscreen]);
|
||||||
|
|
||||||
|
// Leaflet re-measures on window resize (trackResize). Native fullscreen fires
|
||||||
|
// one; the CSS fallback overlay changes size without a resize event, so nudge
|
||||||
|
// Leaflet after the layout settles or the map fills only part of the screen.
|
||||||
|
useEffect(() => {
|
||||||
|
const id = requestAnimationFrame(() => window.dispatchEvent(new Event('resize')));
|
||||||
|
return () => cancelAnimationFrame(id);
|
||||||
|
}, [isFullscreen]);
|
||||||
|
|
||||||
const toggleFullscreen = useCallback(() => {
|
const toggleFullscreen = useCallback(() => {
|
||||||
if (!document.fullscreenElement) {
|
if (document.fullscreenElement) {
|
||||||
wrapperRef.current?.requestFullscreen();
|
document.exitFullscreen().catch(() => {});
|
||||||
} else {
|
return;
|
||||||
document.exitFullscreen();
|
|
||||||
}
|
}
|
||||||
}, []);
|
if (fallbackFullscreen) {
|
||||||
|
setFallbackFullscreen(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const el = wrapperRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
if (el.requestFullscreen) {
|
||||||
|
el.requestFullscreen().catch(() => setFallbackFullscreen(true));
|
||||||
|
} else {
|
||||||
|
setFallbackFullscreen(true);
|
||||||
|
}
|
||||||
|
}, [fallbackFullscreen]);
|
||||||
|
|
||||||
// Calculate center if not provided
|
// Calculate center if not provided
|
||||||
const mapCenter: [number, number] = center || (() => {
|
const mapCenter: [number, number] = center || (() => {
|
||||||
@@ -64,7 +94,7 @@ export function SchoolMap({ schools, center, zoom = 13, referencePoint, onMarker
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={wrapperRef} className={`${styles.mapWrapper} ${isFullscreen ? styles.fullscreen : ''}`}>
|
<div ref={wrapperRef} className={`${styles.mapWrapper} ${isFullscreen ? styles.fullscreen : ''} ${fallbackFullscreen ? styles.fsFallback : ''}`}>
|
||||||
<button
|
<button
|
||||||
className={styles.fullscreenBtn}
|
className={styles.fullscreenBtn}
|
||||||
onClick={toggleFullscreen}
|
onClick={toggleFullscreen}
|
||||||
|
|||||||
Reference in New Issue
Block a user