2026-02-02 20:34:35 +00:00
|
|
|
/**
|
|
|
|
|
* SchoolSearchModal Component
|
|
|
|
|
* Modal for searching and adding schools to comparison
|
|
|
|
|
*/
|
|
|
|
|
|
2026-04-07 16:17:56 +01:00
|
|
|
"use client";
|
2026-02-02 20:34:35 +00:00
|
|
|
|
2026-04-07 16:17:56 +01:00
|
|
|
import { useState, useMemo } from "react";
|
|
|
|
|
import { Modal } from "./Modal";
|
|
|
|
|
import { useComparison } from "@/hooks/useComparison";
|
|
|
|
|
import { debounce } from "@/lib/utils";
|
|
|
|
|
import { fetchSchools } from "@/lib/api";
|
2026-05-19 22:04:22 +01:00
|
|
|
import { track } from "@/lib/analytics";
|
2026-09-30 11:56:54 +01:00
|
|
|
import { MAX_PER_GROUP } from "@/lib/compareLogic";
|
2026-04-07 16:17:56 +01:00
|
|
|
import type { School } from "@/lib/types";
|
|
|
|
|
import styles from "./SchoolSearchModal.module.css";
|
2026-02-02 20:34:35 +00:00
|
|
|
|
|
|
|
|
interface SchoolSearchModalProps {
|
|
|
|
|
isOpen: boolean;
|
|
|
|
|
onClose: () => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function SchoolSearchModal({ isOpen, onClose }: SchoolSearchModalProps) {
|
2026-09-30 11:56:54 +01:00
|
|
|
const { addSchool, selectedSchools, fullGroupFor } = useComparison();
|
2026-04-07 16:17:56 +01:00
|
|
|
const [searchTerm, setSearchTerm] = useState("");
|
2026-02-02 20:34:35 +00:00
|
|
|
const [results, setResults] = useState<School[]>([]);
|
|
|
|
|
const [isSearching, setIsSearching] = useState(false);
|
|
|
|
|
const [hasSearched, setHasSearched] = useState(false);
|
|
|
|
|
|
|
|
|
|
// Debounced search function
|
|
|
|
|
const performSearch = useMemo(
|
|
|
|
|
() =>
|
|
|
|
|
debounce(async (term: string) => {
|
|
|
|
|
if (!term.trim()) {
|
|
|
|
|
setResults([]);
|
|
|
|
|
setHasSearched(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setIsSearching(true);
|
|
|
|
|
try {
|
2026-04-07 16:17:56 +01:00
|
|
|
const data = await fetchSchools(
|
|
|
|
|
{ search: term, page_size: 10 },
|
|
|
|
|
{ cache: "no-store" },
|
|
|
|
|
);
|
2026-02-02 20:34:35 +00:00
|
|
|
setResults(data.schools || []);
|
|
|
|
|
setHasSearched(true);
|
|
|
|
|
} catch (error) {
|
2026-04-07 16:17:56 +01:00
|
|
|
console.error("Search failed:", error);
|
2026-02-02 20:34:35 +00:00
|
|
|
setResults([]);
|
|
|
|
|
} finally {
|
|
|
|
|
setIsSearching(false);
|
|
|
|
|
}
|
|
|
|
|
}, 300),
|
2026-04-07 16:17:56 +01:00
|
|
|
[],
|
2026-02-02 20:34:35 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handleSearchChange = (value: string) => {
|
|
|
|
|
setSearchTerm(value);
|
|
|
|
|
performSearch(value);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleAddSchool = (school: School) => {
|
|
|
|
|
addSchool(school);
|
2026-05-19 22:04:22 +01:00
|
|
|
track('compare_school_added', {
|
|
|
|
|
urn: school.urn,
|
|
|
|
|
from: 'compare',
|
|
|
|
|
selection_count_after: selectedSchools.length + 1,
|
|
|
|
|
});
|
2026-02-02 20:34:35 +00:00
|
|
|
// Don't close modal, allow adding multiple schools
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const isSchoolSelected = (urn: number) => {
|
|
|
|
|
return selectedSchools.some((s) => s.urn === urn);
|
|
|
|
|
};
|
|
|
|
|
|
2026-09-30 11:56:54 +01:00
|
|
|
// Limits apply per phase group, so a full primary list must not stop a
|
|
|
|
|
// parent adding secondary schools.
|
|
|
|
|
const primaryFull = fullGroupFor({ phase: "Primary" }) !== null;
|
|
|
|
|
const secondaryFull = fullGroupFor({ phase: "Secondary" }) !== null;
|
|
|
|
|
const limitWarning =
|
|
|
|
|
primaryFull && secondaryFull
|
|
|
|
|
? `You're comparing ${MAX_PER_GROUP} primary and ${MAX_PER_GROUP} secondary schools, the most at once. Remove a school to add another.`
|
|
|
|
|
: primaryFull
|
|
|
|
|
? `You're comparing ${MAX_PER_GROUP} primary schools, the most at once. You can still add secondary schools.`
|
|
|
|
|
: secondaryFull
|
|
|
|
|
? `You're comparing ${MAX_PER_GROUP} secondary schools, the most at once. You can still add primary schools.`
|
|
|
|
|
: null;
|
|
|
|
|
|
2026-02-02 20:34:35 +00:00
|
|
|
const handleClose = () => {
|
2026-04-07 16:17:56 +01:00
|
|
|
setSearchTerm("");
|
2026-02-02 20:34:35 +00:00
|
|
|
setResults([]);
|
|
|
|
|
setHasSearched(false);
|
|
|
|
|
onClose();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
2026-07-17 23:02:12 +01:00
|
|
|
<Modal isOpen={isOpen} onClose={handleClose} title="Add School to Comparison">
|
2026-02-02 20:34:35 +00:00
|
|
|
<div className={styles.modalContent}>
|
2026-09-30 11:56:54 +01:00
|
|
|
{limitWarning && (
|
|
|
|
|
<div className={styles.warning}>{limitWarning}</div>
|
2026-02-02 20:34:35 +00:00
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Search Input */}
|
|
|
|
|
<div className={styles.searchContainer}>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={searchTerm}
|
|
|
|
|
onChange={(e) => handleSearchChange(e.target.value)}
|
2026-04-07 20:29:15 +01:00
|
|
|
placeholder="School name or postcode"
|
2026-02-02 20:34:35 +00:00
|
|
|
className={styles.searchInput}
|
|
|
|
|
autoFocus
|
|
|
|
|
/>
|
|
|
|
|
{isSearching && <div className={styles.searchSpinner} />}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Results */}
|
|
|
|
|
<div className={styles.results}>
|
|
|
|
|
{hasSearched && results.length === 0 && (
|
|
|
|
|
<div className={styles.noResults}>
|
|
|
|
|
No schools found matching "{searchTerm}"
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{results.map((school) => {
|
|
|
|
|
const alreadySelected = isSchoolSelected(school.urn);
|
2026-09-30 11:56:54 +01:00
|
|
|
const fullGroup = alreadySelected ? null : fullGroupFor(school);
|
2026-02-02 20:34:35 +00:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div key={school.urn} className={styles.resultItem}>
|
|
|
|
|
<div className={styles.schoolInfo}>
|
|
|
|
|
<div className={styles.schoolName}>{school.school_name}</div>
|
|
|
|
|
<div className={styles.schoolMeta}>
|
|
|
|
|
{school.local_authority && (
|
2026-02-02 22:34:14 +00:00
|
|
|
<span>{school.local_authority}</span>
|
2026-02-02 20:34:35 +00:00
|
|
|
)}
|
2026-04-07 16:17:56 +01:00
|
|
|
{school.school_type && <span>{school.school_type}</span>}
|
2026-02-02 20:34:35 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => handleAddSchool(school)}
|
2026-09-30 11:56:54 +01:00
|
|
|
disabled={alreadySelected || fullGroup !== null}
|
2026-07-17 23:02:12 +01:00
|
|
|
className={`${styles.resultButton} ${
|
2026-04-07 16:17:56 +01:00
|
|
|
alreadySelected ? "btn btn-active" : "btn btn-secondary"
|
2026-07-17 23:02:12 +01:00
|
|
|
}`}
|
2026-02-02 20:34:35 +00:00
|
|
|
>
|
2026-09-30 11:56:54 +01:00
|
|
|
{alreadySelected
|
|
|
|
|
? "✓ Comparing"
|
|
|
|
|
: fullGroup
|
|
|
|
|
? `${fullGroup === "primary" ? "Primary" : "Secondary"} full`
|
|
|
|
|
: "+ Compare"}
|
2026-02-02 20:34:35 +00:00
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{!hasSearched && (
|
|
|
|
|
<div className={styles.hint}>
|
|
|
|
|
Start typing to search for schools...
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</Modal>
|
|
|
|
|
);
|
|
|
|
|
}
|