193 lines
6.9 KiB
TypeScript
193 lines
6.9 KiB
TypeScript
import { memo } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import type { TFunction } from 'i18next';
|
|
|
|
import type { SchoolMetadata } from '../../types';
|
|
import { POI_GROUP_COLORS } from '../../lib/consts';
|
|
import { getPoiIconUrl } from '../../lib/map-utils';
|
|
import { ts } from '../../i18n/server';
|
|
|
|
export interface PoiPopupCardData {
|
|
name: string;
|
|
category: string;
|
|
icon_category?: string;
|
|
group: string;
|
|
emoji: string;
|
|
school?: SchoolMetadata;
|
|
}
|
|
|
|
function getPoiGroupColor(group: string): [number, number, number] {
|
|
const color = POI_GROUP_COLORS[group];
|
|
if (!color) {
|
|
throw new Error(`Missing POI group color for '${group}'`);
|
|
}
|
|
return color;
|
|
}
|
|
|
|
/** Best-effort web URL from a free-text website field. GIAS stores some with
|
|
* "http://", some without, and some as bare hostnames. */
|
|
function normalizeSchoolWebsiteUrl(raw: string): string | null {
|
|
const trimmed = raw.trim();
|
|
if (!trimmed) return null;
|
|
if (/^https?:\/\//i.test(trimmed)) return trimmed;
|
|
if (/^[\w.-]+\.[a-z]{2,}/i.test(trimmed)) return `http://${trimmed}`;
|
|
return null;
|
|
}
|
|
|
|
function renderSchoolMetadata(school: SchoolMetadata, t: TFunction) {
|
|
// First line collects the headline classification (phase, type, religious
|
|
// character) so the popup is scannable even when most fields are absent.
|
|
const headline: string[] = [];
|
|
if (school.phase) headline.push(school.phase);
|
|
if (school.type) headline.push(school.type);
|
|
|
|
const pupilsLine =
|
|
school.pupils !== undefined && school.capacity !== undefined
|
|
? `${school.pupils.toLocaleString()} / ${school.capacity.toLocaleString()} ${t('poiPopup.school.pupils')}`
|
|
: school.pupils !== undefined
|
|
? `${school.pupils.toLocaleString()} ${t('poiPopup.school.pupils')}`
|
|
: school.capacity !== undefined
|
|
? `${t('poiPopup.school.capacity')} ${school.capacity.toLocaleString()}`
|
|
: null;
|
|
|
|
const websiteUrl = school.website ? normalizeSchoolWebsiteUrl(school.website) : null;
|
|
|
|
return (
|
|
<dl className="mt-2 grid grid-cols-[auto_1fr] gap-x-2 gap-y-0.5 text-xs text-warm-600 dark:text-warm-300">
|
|
{headline.length > 0 && (
|
|
<>
|
|
<dt className="text-warm-500 dark:text-warm-400">{t('poiPopup.school.type')}</dt>
|
|
<dd className="dark:text-warm-200">{headline.join(' · ')}</dd>
|
|
</>
|
|
)}
|
|
{school.age_range && (
|
|
<>
|
|
<dt className="text-warm-500 dark:text-warm-400">{t('poiPopup.school.ages')}</dt>
|
|
<dd className="dark:text-warm-200">{school.age_range}</dd>
|
|
</>
|
|
)}
|
|
{school.gender && school.gender !== 'Mixed' && (
|
|
<>
|
|
<dt className="text-warm-500 dark:text-warm-400">{t('poiPopup.school.gender')}</dt>
|
|
<dd className="dark:text-warm-200">{school.gender}</dd>
|
|
</>
|
|
)}
|
|
{pupilsLine && (
|
|
<>
|
|
<dt className="text-warm-500 dark:text-warm-400">{t('poiPopup.school.pupilsLabel')}</dt>
|
|
<dd className="dark:text-warm-200">{pupilsLine}</dd>
|
|
</>
|
|
)}
|
|
{school.fsm_percent !== undefined && (
|
|
<>
|
|
<dt className="text-warm-500 dark:text-warm-400">{t('poiPopup.school.freeMeal')}</dt>
|
|
<dd className="dark:text-warm-200">{school.fsm_percent.toFixed(1)}%</dd>
|
|
</>
|
|
)}
|
|
{school.ofsted_rating && (
|
|
<>
|
|
<dt className="text-warm-500 dark:text-warm-400">{t('poiPopup.school.ofsted')}</dt>
|
|
<dd className="dark:text-warm-200">{school.ofsted_rating}</dd>
|
|
</>
|
|
)}
|
|
{school.sixth_form === 'Has a sixth form' && (
|
|
<>
|
|
<dt className="text-warm-500 dark:text-warm-400">{t('poiPopup.school.sixthForm')}</dt>
|
|
<dd className="dark:text-warm-200">{t('common.yes')}</dd>
|
|
</>
|
|
)}
|
|
{school.religious_character &&
|
|
school.religious_character !== 'Does not apply' &&
|
|
school.religious_character !== 'None' && (
|
|
<>
|
|
<dt className="text-warm-500 dark:text-warm-400">{t('poiPopup.school.religion')}</dt>
|
|
<dd className="dark:text-warm-200">{school.religious_character}</dd>
|
|
</>
|
|
)}
|
|
{school.admissions_policy && (
|
|
<>
|
|
<dt className="text-warm-500 dark:text-warm-400">{t('poiPopup.school.admissions')}</dt>
|
|
<dd className="dark:text-warm-200">{school.admissions_policy}</dd>
|
|
</>
|
|
)}
|
|
{school.trust && (
|
|
<>
|
|
<dt className="text-warm-500 dark:text-warm-400">{t('poiPopup.school.trust')}</dt>
|
|
<dd className="dark:text-warm-200">{school.trust}</dd>
|
|
</>
|
|
)}
|
|
{(school.address || school.postcode) && (
|
|
<>
|
|
<dt className="text-warm-500 dark:text-warm-400">{t('poiPopup.school.address')}</dt>
|
|
<dd className="dark:text-warm-200">
|
|
{[school.address, school.postcode].filter(Boolean).join(', ')}
|
|
</dd>
|
|
</>
|
|
)}
|
|
{school.local_authority && (
|
|
<>
|
|
<dt className="text-warm-500 dark:text-warm-400">
|
|
{t('poiPopup.school.localAuthority')}
|
|
</dt>
|
|
<dd className="dark:text-warm-200">{school.local_authority}</dd>
|
|
</>
|
|
)}
|
|
{school.head_name && (
|
|
<>
|
|
<dt className="text-warm-500 dark:text-warm-400">{t('poiPopup.school.head')}</dt>
|
|
<dd className="dark:text-warm-200">{school.head_name}</dd>
|
|
</>
|
|
)}
|
|
{websiteUrl && (
|
|
<>
|
|
<dt className="text-warm-500 dark:text-warm-400">{t('poiPopup.school.website')}</dt>
|
|
<dd className="truncate">
|
|
<a
|
|
href={websiteUrl}
|
|
target="_blank"
|
|
rel="noreferrer noopener"
|
|
className="pointer-events-auto text-teal-600 hover:underline dark:text-teal-400"
|
|
>
|
|
{websiteUrl.replace(/^https?:\/\//, '')}
|
|
</a>
|
|
</dd>
|
|
</>
|
|
)}
|
|
</dl>
|
|
);
|
|
}
|
|
|
|
export const PoiPopupCardContent = memo(function PoiPopupCardContent({
|
|
poi,
|
|
}: {
|
|
poi: PoiPopupCardData;
|
|
}) {
|
|
const { t } = useTranslation();
|
|
return (
|
|
<div className="px-3 py-2 max-w-[280px]">
|
|
<div className="flex items-center gap-2">
|
|
<img
|
|
src={getPoiIconUrl(poi.category, poi.emoji, poi.icon_category, poi.name)}
|
|
alt=""
|
|
aria-hidden="true"
|
|
loading="lazy"
|
|
referrerPolicy="no-referrer"
|
|
className="h-5 w-5 shrink-0 rounded-[4px] bg-white object-contain p-0.5"
|
|
/>
|
|
<div className="min-w-0">
|
|
<div className="font-semibold dark:text-warm-100">{poi.name}</div>
|
|
<div className="flex items-center gap-1.5 text-xs text-warm-500 dark:text-warm-400">
|
|
<span
|
|
className="inline-block w-2 h-2 rounded-full flex-shrink-0"
|
|
style={{
|
|
backgroundColor: `rgb(${getPoiGroupColor(poi.group).join(',')})`,
|
|
}}
|
|
/>
|
|
{ts(poi.category)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{poi.school && renderSchoolMetadata(poi.school, t)}
|
|
</div>
|
|
);
|
|
});
|