Files
Art-gallery/client/src/components/ToursPopup.tsx
T
Danila KhodjaefandCursor 5ddc3fd7f0 Add guided tours and unify left-to-right hall wall hang.
Visitors walk published tours in a 3D hall with stop notes; curators edit drafts via Tour editor. All galleries (artist, movement, tour) place the first work left of the entrance view and the last on the right.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-16 20:27:36 +03:00

99 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { api, imageUrl } from '../api/client';
import type { TourSummary } from '../types';
import './ToursPopup.css';
interface Props {
open: boolean;
onClose: () => void;
onSelectTour: (tourId: number) => void;
}
export default function ToursPopup({ open, onClose, onSelectTour }: Props) {
const { t } = useTranslation('tours');
const [tours, setTours] = useState<TourSummary[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!open) return;
let cancelled = false;
setLoading(true);
setError(null);
api
.listPublishedTours()
.then((data) => {
if (!cancelled) setTours(data.tours);
})
.catch((err) => {
if (!cancelled) {
setError(err instanceof Error ? err.message : t('loadFailed'));
setTours([]);
}
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => {
cancelled = true;
};
}, [open, t]);
if (!open) return null;
return (
<div className="tours-popup-backdrop" role="presentation" onClick={onClose}>
<div
className="tours-popup-modal"
role="dialog"
aria-modal="true"
aria-labelledby="tours-popup-title"
onClick={(e) => e.stopPropagation()}
>
<header className="tours-popup-header">
<h2 id="tours-popup-title">{t('popupTitle')}</h2>
<button type="button" className="tours-popup-close" onClick={onClose} aria-label={t('close')}>
×
</button>
</header>
<p className="tours-popup-hint">{t('popupHint')}</p>
{loading && <p className="tours-popup-muted">{t('loading')}</p>}
{error && <p className="tours-popup-error">{error}</p>}
{!loading && !error && tours.length === 0 && (
<p className="tours-popup-muted">{t('noPublished')}</p>
)}
<ul className="tours-popup-list">
{tours.map((tour) => {
const cover = tour.coverThumbnailPath || tour.coverImagePath;
return (
<li key={tour.id}>
<button
type="button"
className="tours-popup-card"
onClick={() => onSelectTour(tour.id)}
>
<div className="tours-popup-cover">
{cover ? (
<img src={imageUrl(cover)} alt="" />
) : (
<span className="tours-popup-cover-empty" aria-hidden />
)}
</div>
<div className="tours-popup-body">
<strong>{tour.title}</strong>
{tour.description ? <p>{tour.description}</p> : null}
<span className="tours-popup-meta">
{t('stopCount', { count: tour.stopCount })}
</span>
</div>
</button>
</li>
);
})}
</ul>
</div>
</div>
);
}