Files
school_compare/nextjs-app/components/SchoolSearchModal.tsx
T
TudorandClaude Fable 5 e5f7f4c959
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m6s
PR Checks / Backend Smoke (pull_request) Successful in 6s
PR Checks / Build Backend (no push) (pull_request) Successful in 10s
PR Checks / Build Frontend (no push) (pull_request) Successful in 48s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 52s
fix(compare): repair the mobile add-school modal
The 'add school' modal passed no title to Modal, so its header held only
the close button — shoved to the far left by justify-content:space-between
in an otherwise-empty bar — while the real title was rendered separately
inside the content. Pass the title to Modal so the header reads title
(left) + close (right), and drop the duplicate in-content heading.

Also: the mobile full-width result button targeted a dead .addButton
selector (the button never had that class), so it rendered inconsistently
— give the button a real module class and full width on mobile, tighten
the stacked cards, and give the bottom sheet a stable min-height so its
empty state isn't a tiny stub.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146VHeLAWjDVE2B5uU67jCB
2026-07-17 23:02:12 +01:00

150 lines
4.4 KiB
TypeScript

/**
* SchoolSearchModal Component
* Modal for searching and adding schools to comparison
*/
"use client";
import { useState, useMemo } from "react";
import { Modal } from "./Modal";
import { useComparison } from "@/hooks/useComparison";
import { debounce } from "@/lib/utils";
import { fetchSchools } from "@/lib/api";
import { track } from "@/lib/analytics";
import type { School } from "@/lib/types";
import styles from "./SchoolSearchModal.module.css";
interface SchoolSearchModalProps {
isOpen: boolean;
onClose: () => void;
}
export function SchoolSearchModal({ isOpen, onClose }: SchoolSearchModalProps) {
const { addSchool, selectedSchools, canAddMore } = useComparison();
const [searchTerm, setSearchTerm] = useState("");
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 {
const data = await fetchSchools(
{ search: term, page_size: 10 },
{ cache: "no-store" },
);
setResults(data.schools || []);
setHasSearched(true);
} catch (error) {
console.error("Search failed:", error);
setResults([]);
} finally {
setIsSearching(false);
}
}, 300),
[],
);
const handleSearchChange = (value: string) => {
setSearchTerm(value);
performSearch(value);
};
const handleAddSchool = (school: School) => {
addSchool(school);
track('compare_school_added', {
urn: school.urn,
from: 'compare',
selection_count_after: selectedSchools.length + 1,
});
// Don't close modal, allow adding multiple schools
};
const isSchoolSelected = (urn: number) => {
return selectedSchools.some((s) => s.urn === urn);
};
const handleClose = () => {
setSearchTerm("");
setResults([]);
setHasSearched(false);
onClose();
};
return (
<Modal isOpen={isOpen} onClose={handleClose} title="Add School to Comparison">
<div className={styles.modalContent}>
{!canAddMore && (
<div className={styles.warning}>
Maximum 5 schools can be compared. Remove a school to add another.
</div>
)}
{/* Search Input */}
<div className={styles.searchContainer}>
<input
type="text"
value={searchTerm}
onChange={(e) => handleSearchChange(e.target.value)}
placeholder="School name or postcode"
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);
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 && (
<span>{school.local_authority}</span>
)}
{school.school_type && <span>{school.school_type}</span>}
</div>
</div>
<button
onClick={() => handleAddSchool(school)}
disabled={alreadySelected || !canAddMore}
className={`${styles.resultButton} ${
alreadySelected ? "btn btn-active" : "btn btn-secondary"
}`}
>
{alreadySelected ? "✓ Comparing" : "+ Compare"}
</button>
</div>
);
})}
</div>
{!hasSearched && (
<div className={styles.hint}>
Start typing to search for schools...
</div>
)}
</div>
</Modal>
);
}