feat(landscape): multi-photo gallery like rooms with uploads and ordering
This commit is contained in:
+140
-56
@@ -6,16 +6,16 @@ interface Place {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
photo: string | null;
|
||||
photos: string[];
|
||||
featured_photo: string | null;
|
||||
distance_km: string | null;
|
||||
visit_time: string | null;
|
||||
suggestion: string | null;
|
||||
sort_order: number;
|
||||
}
|
||||
|
||||
const gold = '#E8A849';
|
||||
const navy = '#010D1E';
|
||||
const cream = '#f5f0e8';
|
||||
const warm = '#a6683c';
|
||||
const navy = '#001321';
|
||||
|
||||
export default function LandscapePage() {
|
||||
const [places, setPlaces] = useState<Place[]>([]);
|
||||
@@ -33,60 +33,144 @@ export default function LandscapePage() {
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<main style={{ minHeight: '100vh', background: navy, color: '#d4c9b8', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<p style={{ fontSize: '18px', fontStyle: 'italic' }}>Loading landscape...</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main style={{ padding: '2rem', maxWidth: '72rem', margin: '0 auto', minHeight: '60vh' }}>
|
||||
<h1 style={{ fontSize: 'clamp(32px, 5vw, 48px)', fontWeight: 400, fontStyle: 'italic', color: navy, margin: '0 0 0.5rem' }}>Landscape</h1>
|
||||
<p style={{ color: '#555', marginBottom: '2rem', maxWidth: '50ch' }}>
|
||||
Trails, rivers, waterfalls, and villages around La Huasca.
|
||||
</p>
|
||||
<main style={{ minHeight: '100vh', background: navy, color: '#f5f0e8' }}>
|
||||
<section
|
||||
style={{
|
||||
background: 'linear-gradient(rgba(0,19,33,0.75), rgba(0,19,33,0.85))',
|
||||
padding: '5rem 1.5rem 3rem',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<h1
|
||||
style={{
|
||||
fontSize: 'clamp(34px, 5vw, 56px)',
|
||||
fontWeight: 400,
|
||||
fontStyle: 'italic',
|
||||
color: gold,
|
||||
margin: '0 0 1rem',
|
||||
letterSpacing: '-0.5px',
|
||||
}}
|
||||
>
|
||||
Landscape
|
||||
</h1>
|
||||
<p style={{ maxWidth: '720px', margin: '0 auto', fontSize: '18px', color: '#d4c9b8', lineHeight: 1.6 }}>
|
||||
Nearby destinations and Andean highlights worth exploring from La Huasca.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{loading && <p style={{ color: warm }}>Loading places...</p>}
|
||||
{error && <p style={{ color: '#c23b22' }}>{error}</p>}
|
||||
|
||||
{!loading && places.length === 0 && <p style={{ color: '#777' }}>No places added yet.</p>}
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))', gap: '1.5rem' }}>
|
||||
{places.map((place) => (
|
||||
<article
|
||||
key={place.id}
|
||||
style={{
|
||||
background: cream,
|
||||
borderRadius: '20px',
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 2px 8px rgba(1,13,30,0.08)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
transition: 'transform 0.2s, box-shadow 0.2s',
|
||||
}}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.transform = 'translateY(-3px)'; e.currentTarget.style.boxShadow = '0 8px 24px rgba(1,13,30,0.14)'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.transform = 'translateY(0)'; e.currentTarget.style.boxShadow = '0 2px 8px rgba(1,13,30,0.08)'; }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: '200px',
|
||||
background: '#ddd',
|
||||
backgroundImage: place.photo ? `url(${place.photo})` : undefined,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
}}
|
||||
/>
|
||||
<div style={{ padding: '1.5rem', flex: 1 }}>
|
||||
<h2 style={{ margin: '0 0 0.5rem', color: navy }}>{place.name}</h2>
|
||||
<p style={{ color: '#555', lineHeight: 1.5 }}>{place.description}</p>
|
||||
<div style={{ marginTop: '1rem', display: 'flex', flexWrap: 'wrap', gap: '0.5rem' }}>
|
||||
{place.distance_km && <span style={{ fontSize: '13px', background: '#e8eff3', color: navy, padding: '0.3rem 0.7rem', borderRadius: '999px' }}>{place.distance_km} km</span>}
|
||||
{place.visit_time && <span style={{ fontSize: '13px', background: '#e8eff3', color: navy, padding: '0.3rem 0.7rem', borderRadius: '999px' }}>{place.visit_time}</span>}
|
||||
</div>
|
||||
{place.suggestion && (
|
||||
<div style={{ marginTop: '1rem', padding: '0.75rem', background: '#fff', borderLeft: '3px solid ' + gold, borderRadius: '0 10px 10px 0' }}>
|
||||
<strong style={{ fontSize: '13px', color: warm }}>Suggestion:</strong>
|
||||
<p style={{ margin: '0.25rem 0 0', fontSize: '14px', color: '#555' }}>{place.suggestion}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
<section style={{ padding: '2rem 1.5rem 5rem', maxWidth: '1200px', margin: '0 auto' }}>
|
||||
{error && <p style={{ textAlign: 'center', color: '#c23b22' }}>{error}</p>}
|
||||
{places.length === 0 ? (
|
||||
<p style={{ textAlign: 'center', color: '#aaa' }}>No places listed yet.</p>
|
||||
) : (
|
||||
<div style={{ display: 'grid', gap: '2rem' }}>
|
||||
{places.map((place) => (
|
||||
<PlaceCard key={place.id} place={place} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function PlaceCard({ place }: { place: Place }) {
|
||||
const [active, setActive] = useState(place.featured_photo || place.photos[0] || '');
|
||||
|
||||
return (
|
||||
<article
|
||||
style={{
|
||||
background: '#f5f0e8',
|
||||
color: navy,
|
||||
borderRadius: '20px',
|
||||
overflow: 'hidden',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
boxShadow: '0 12px 30px rgba(0,0,0,0.25)',
|
||||
}}
|
||||
>
|
||||
<div style={{ position: 'relative', minHeight: '320px' }}>
|
||||
{active ? (
|
||||
<img
|
||||
src={active}
|
||||
alt={place.name}
|
||||
style={{ width: '100%', height: '100%', minHeight: '320px', objectFit: 'cover', display: 'block' }}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
minHeight: '320px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: '#e0d8cc',
|
||||
color: navy,
|
||||
fontStyle: 'italic',
|
||||
}}
|
||||
>
|
||||
No image
|
||||
</div>
|
||||
)}
|
||||
{place.photos.length > 1 && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
display: 'flex',
|
||||
gap: '0.5rem',
|
||||
padding: '0.75rem',
|
||||
overflowX: 'auto',
|
||||
background: 'rgba(0,19,33,0.55)',
|
||||
}}
|
||||
>
|
||||
{place.photos.map((photo, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => setActive(photo)}
|
||||
style={{
|
||||
padding: 0,
|
||||
border: active === photo ? '2px solid #E8A849' : '2px solid transparent',
|
||||
borderRadius: '8px',
|
||||
flexShrink: 0,
|
||||
cursor: 'pointer',
|
||||
background: 'transparent',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={photo}
|
||||
alt={`${place.name} ${idx + 1}`}
|
||||
style={{ width: '64px', height: '64px', objectFit: 'cover', borderRadius: '6px', display: 'block' }}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ padding: '2rem', display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
|
||||
<h2 style={{ fontSize: '28px', fontWeight: 400, fontStyle: 'italic', margin: '0 0 0.75rem', color: navy }}>{place.name}</h2>
|
||||
{place.distance_km && (
|
||||
<p style={{ margin: '0 0 0.5rem', color: '#a6683c', fontWeight: 600, fontSize: '15px' }}>{place.distance_km} km away · {place.visit_time || 'varies'}</p>
|
||||
)}
|
||||
<p style={{ margin: '0 0 1rem', lineHeight: 1.7, color: '#2b3a46' }}>{place.description}</p>
|
||||
{place.suggestion && (
|
||||
<div style={{ background: navy, color: gold, padding: '1rem 1.25rem', borderRadius: '12px', fontSize: '15px' }}>
|
||||
<strong>Suggestion:</strong> {place.suggestion}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user