Files
school_compare/nextjs-app/components/SchoolSearchModal.tsx
Tudor f04e383ea3
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 1m10s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
Fix: Use correct API base URL for client-side fetches
The compare screen and school search modal were not working because
they were fetching from '/api' directly instead of using the
NEXT_PUBLIC_API_URL environment variable that points to the backend.

Fixed client-side fetch calls in:
- ComparisonView: Fetch comparison data with correct API URL
- SchoolSearchModal: Search schools with correct API URL

This ensures client-side requests go to the FastAPI backend at
the configured URL (e.g., http://localhost:8000/api) rather than
trying to hit non-existent Next.js API routes.

Fixes comparison screen showing no data when schools are selected.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-02 22:42:21 +00:00

144 lines
4.2 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 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 apiBaseUrl = process.env.NEXT_PUBLIC_API_URL || '/api';
const response = await fetch(`${apiBaseUrl}/schools?search=${encodeURIComponent(term)}&page_size=10`);
const data = await response.json();
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>
);
}