75 lines
2.4 KiB
TypeScript
75 lines
2.4 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { Link } from "react-router-dom";
|
|
import { formatApiMessage } from "../api/client";
|
|
import * as api from "../api/services";
|
|
import type { AdventureOffering } from "../api/types";
|
|
|
|
export function ToursPage() {
|
|
const [offerings, setOfferings] = useState<AdventureOffering[]>([]);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
(async () => {
|
|
try {
|
|
const data = await api.listAdventureOfferings();
|
|
if (!cancelled) setOfferings(data);
|
|
} catch (e) {
|
|
if (!cancelled) setError(formatApiMessage(e));
|
|
} finally {
|
|
if (!cancelled) setLoading(false);
|
|
}
|
|
})();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, []);
|
|
|
|
return (
|
|
<div className="page tours-catalog">
|
|
<h1>Tours & adventures</h1>
|
|
<p className="lede">
|
|
Guided experiences from GET <code>/api/v1/adventrues/offerings/</code>. Open a listing for marketing
|
|
attribution IDs used at booking time.
|
|
</p>
|
|
{loading && <p className="muted">Loading…</p>}
|
|
{error && <p className="muted">{error}</p>}
|
|
<ul className="card-list">
|
|
{offerings.map((o) => {
|
|
const img =
|
|
o.images.find((i) => i.is_primary)?.image_url ?? o.images[0]?.image_url;
|
|
return (
|
|
<li key={o.id}>
|
|
<article className="card">
|
|
<div
|
|
className="tours-card-image"
|
|
style={
|
|
img
|
|
? { backgroundImage: `url(${img})` }
|
|
: { background: "linear-gradient(135deg, #fef9c3, #facc15)" }
|
|
}
|
|
/>
|
|
<h2>{o.title}</h2>
|
|
<p>{o.vendor_business_name}</p>
|
|
<p className="muted">
|
|
{o.meeting_point} · {o.duration_minutes} min · up to {o.capacity} guests
|
|
</p>
|
|
<p>
|
|
<strong>${o.price_per_person}</strong> / person
|
|
</p>
|
|
<Link to={`/tours/${o.public_id}`} className="text-link">
|
|
View details
|
|
</Link>
|
|
</article>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
{!loading && !error && offerings.length === 0 && (
|
|
<p className="muted">No adventures published yet.</p>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|