Files
school_compare/nextjs-app/components/SchoolSearchModal.tsx
Tudor 0e698d38d9
All checks were successful
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 34s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 1m14s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 0s
Fix: Use centralized API functions instead of manual URL construction
- ComparisonView now uses fetchComparison from lib/api
- SchoolSearchModal now uses fetchSchools from lib/api
- Fixed bug in fetcher function that incorrectly sliced URLs
  (url.slice(4) was removing '/com' from '/compare')

This fixes the malformed URL issue where '/api/compare' became '/apipare'.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 10:27:45 +00:00

143 lines
4.1 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 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);
// 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}>
<div className={styles.modalContent}>
<h2 className={styles.title}>Add School to Comparison</h2>
{!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="Search by school name or location..."
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.addButton}
>
{alreadySelected ? '✓ Added' : '+ Add'}
</button>
</div>
);
})}
</div>
{!hasSearched && (
<div className={styles.hint}>
Start typing to search for schools...
</div>
)}
</div>
</Modal>
);
}