68 lines
4.1 KiB
TypeScript
68 lines
4.1 KiB
TypeScript
import { useState } from 'react';
|
|
import { Dialog, DialogContent, DialogTitle, DialogDescription, DialogClose } from '@/components/ui/dialog';
|
|
import { firImageUrl, firRoomName, type FirRoom } from '@/lib/fir';
|
|
import { firTranslate } from '@/lib/firLocale';
|
|
|
|
const LIBRARY = (import.meta.env.VITE_FIR_API_BASE || '/fir-library').replace(/\/$/, '');
|
|
|
|
type Props = { room: FirRoom; language?: number };
|
|
export function FirRoomDetailsBody({ room, language }: Props) {
|
|
const t = (key: string) => firTranslate(language, key);
|
|
const imageUrl = firImageUrl(room.image_url, LIBRARY);
|
|
const [failedImage, setFailedImage] = useState<string | null>(null);
|
|
const metadata = room.metadata || {};
|
|
const value = (key: string) => metadata[key]?.trim() || '—';
|
|
// Only attach units to scalar values; preserve CSV configuration annotations verbatim.
|
|
const unit = (text: string, suffix: string) => /^[+-]?\d+(?:\.\d+)?$/.test(text) ? `${text} ${suffix}` : text;
|
|
const groups: { title: string; fields: [string, string][] }[] = [
|
|
{ title: 'detailRoom', fields: [
|
|
['detailName', firRoomName(room) || t('unnamed')], ['detailId', room.room_id],
|
|
['detailLocation', value('Room Location')], ['detailType', value('Room Type')],
|
|
['detailDimensions', value('Room Dimensions (m) (l x w x h)')],
|
|
] },
|
|
{ title: 'detailAcoustics', fields: [
|
|
['detailRt60', unit(value('RT60 (s)'), 's')],
|
|
['detailReduced', /^(yes|no)$/i.test(value('Reduced Reverberation?')) ? t(value('Reduced Reverberation?').toLowerCase() === 'yes' ? 'detailYes' : 'detailNo') : value('Reduced Reverberation?')],
|
|
['detailListener', value('Listener')], ['detailDistance', unit(value('Source Distance (m)'), 'm')],
|
|
['detailAzimuthRange', value('Source Azimuth Range')], ['detailElevationRange', value('Source Elevation Range')],
|
|
['detailConfigurations', value('Measurement Configurations')],
|
|
] },
|
|
];
|
|
const renderGroup = (group: typeof groups[number]) => <section key={group.title}>
|
|
<h3 className="mb-3 text-xs font-semibold uppercase tracking-wider text-cyan-300">{t(group.title)}</h3>
|
|
<dl className="grid grid-cols-2 gap-x-6 gap-y-3">
|
|
{group.fields.map(([label, text]) => <div key={label} className="min-w-0 border-b border-white/10 pb-2">
|
|
<dt className="mb-1 text-xs text-white/45">{t(label)}</dt>
|
|
<dd className="break-words text-sm leading-relaxed text-white/90">{text}</dd>
|
|
</div>)}
|
|
</dl>
|
|
</section>;
|
|
const hasImage = imageUrl && failedImage !== imageUrl;
|
|
return <div className="space-y-5">
|
|
{hasImage && <figure className="space-y-2">
|
|
<a href={imageUrl} target="_blank" rel="noopener noreferrer" className="block overflow-hidden rounded-xl border border-white/15 bg-[#f8fafc]" aria-label={t('detailEnlarge')}>
|
|
<img src={imageUrl} alt={firRoomName(room)} className="block h-auto w-full" onError={() => setFailedImage(imageUrl)} />
|
|
</a>
|
|
<figcaption className="flex items-center justify-between gap-3 text-xs text-white/50">
|
|
<span>{t('detailDiagram')}</span>
|
|
<a href={imageUrl} target="_blank" rel="noopener noreferrer" className="shrink-0 text-cyan-300 hover:underline">{t('detailEnlarge')} ↗</a>
|
|
</figcaption>
|
|
</figure>}
|
|
{!hasImage && groups.map(renderGroup)}
|
|
</div>;
|
|
|
|
}
|
|
export default function FirRoomDetails({ room, language, open, onOpenChange }: Props & { open: boolean; onOpenChange: (open: boolean) => void }) {
|
|
const t = (key: string) => firTranslate(language, key);
|
|
return <Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent showCloseButton={false} className="max-h-[85dvh] overflow-y-auto border-white/15 bg-[#19191c] text-white sm:max-w-4xl">
|
|
<div className="flex items-start justify-between gap-4">
|
|
<div><DialogTitle className="text-lg">{firRoomName(room) || t('unnamed')}</DialogTitle>
|
|
<DialogDescription className="mt-1 text-xs text-white/50">{t('detailTitle')}</DialogDescription></div>
|
|
<DialogClose className="shrink-0 rounded-md px-2 py-1 text-sm text-cyan-300 hover:bg-white/10">{t('detailClose')}</DialogClose>
|
|
</div>
|
|
<FirRoomDetailsBody room={room} language={language} />
|
|
</DialogContent>
|
|
</Dialog>;
|
|
}
|