← back to Costa Rica App
app/listing/[slug].tsx
121 lines
import { useEffect, useState } from 'react';
import { View, Text, ScrollView, TouchableOpacity, StyleSheet, ActivityIndicator, Linking, Alert, TextInput, Modal } from 'react-native';
import { useLocalSearchParams, router } from 'expo-router';
import { api, money, isAuthed } from '../../src/api';
export default function ListingDetail() {
const { slug } = useLocalSearchParams<{ slug: string }>();
const [l, setL] = useState<any>(null);
const [contact, setContact] = useState<any>(null);
const [msgOpen, setMsgOpen] = useState(false);
const [msg, setMsg] = useState('');
useEffect(() => {
api.listing(slug!).then(r => setL(r.listing)).catch(() => setL(false));
api.contact(slug!).then(r => setContact(r.contact)).catch(() => {});
}, [slug]);
async function sendMessage() {
if (!isAuthed()) { router.push('/login'); return; }
if (!msg.trim()) return;
try { await api.message(slug!, msg.trim()); setMsgOpen(false); setMsg(''); Alert.alert('Sent ✅', 'Your message was delivered. Replies show in your inbox.'); }
catch (e: any) { Alert.alert('Could not send', e.message); }
}
if (l === null) return <ActivityIndicator style={{ marginTop: 40 }} />;
if (l === false) return <Text style={s.err}>Not available.</Text>;
return (
<View style={{ flex: 1 }}>
<ScrollView contentContainerStyle={{ padding: 16 }}>
<Text style={s.name}>{l.name}</Text>
<Text style={s.region}>{l.region} · {String(l.vertical).replace(/_/g, ' ')}</Text>
{l.rating ? <Text style={s.rating}>★ {l.rating}</Text> : null}
{l.description ? <Text style={s.desc}>{l.description}</Text> : null}
<View style={s.box}>
<Text style={s.price}>{money(l.base_price, l.currency)} <Text style={s.per}>/ {l.booking_type === 'nightly' ? 'night' : 'person'}</Text></Text>
{l.cleaning_fee ? <Text style={s.fee}>+ {money(l.cleaning_fee, l.currency)} cleaning</Text> : null}
<Text style={s.small}>Up to {l.max_guests} guests · {l.instant_book ? 'Instant book' : 'Request to book'} · {l.cancellation}</Text>
</View>
{l.address ? <Text style={s.small}>📍 {l.address}</Text> : null}
{/* BEST WAY TO CONTACT — big, prominent */}
<Text style={s.contactHead}>BEST WAY TO CONTACT</Text>
<View style={s.contactGrid}>
{contact?.whatsapp && (
<TouchableOpacity style={[s.cbtn, s.wa]} onPress={() => Linking.openURL(contact.whatsapp)}>
<Text style={s.cemoji}>💬</Text><Text style={s.cbtnTxt}>WhatsApp</Text>
</TouchableOpacity>
)}
{contact?.tel && (
<TouchableOpacity style={[s.cbtn, s.call]} onPress={() => Linking.openURL(contact.tel)}>
<Text style={s.cemoji}>📞</Text><Text style={s.cbtnTxt}>Call</Text>
</TouchableOpacity>
)}
{contact?.mailto && (
<TouchableOpacity style={[s.cbtn, s.email]} onPress={() => Linking.openURL(contact.mailto)}>
<Text style={s.cemoji}>✉️</Text><Text style={s.cbtnTxt}>Email</Text>
</TouchableOpacity>
)}
<TouchableOpacity style={[s.cbtn, s.msg]} onPress={() => (isAuthed() ? setMsgOpen(true) : router.push('/login'))}>
<Text style={s.cemoji}>📨</Text><Text style={s.cbtnTxt}>Message</Text>
</TouchableOpacity>
{contact?.website && (
<TouchableOpacity style={[s.cbtn, s.other]} onPress={() => Linking.openURL(contact.website)}>
<Text style={s.cemoji}>🌐</Text><Text style={s.cbtnTxt}>Other</Text>
</TouchableOpacity>
)}
</View>
</ScrollView>
{/* In-app messenger composer */}
<Modal visible={msgOpen} animationType="slide" transparent onRequestClose={() => setMsgOpen(false)}>
<View style={s.modalWrap}>
<View style={s.modalCard}>
<Text style={s.modalH}>Message {l.name}</Text>
<TextInput style={s.msgInput} placeholder="Ask about availability, price, anything…" value={msg}
onChangeText={setMsg} multiline autoFocus />
<View style={{ flexDirection: 'row', gap: 8, marginTop: 12 }}>
<TouchableOpacity style={[s.cta, { flex: 1, backgroundColor: '#eee', marginTop: 0 }]} onPress={() => setMsgOpen(false)}>
<Text style={[s.ctaTxt, { color: '#333' }]}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity style={[s.cta, { flex: 1, marginTop: 0 }]} onPress={sendMessage}>
<Text style={s.ctaTxt}>Send</Text>
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
<TouchableOpacity style={s.cta} onPress={() => router.push(`/book/${slug}`)}>
<Text style={s.ctaTxt}>Book now</Text>
</TouchableOpacity>
</View>
);
}
const s = StyleSheet.create({
name: { fontSize: 22, fontWeight: '800', color: '#1a2b22' },
region: { color: '#6b756e', marginTop: 4 },
rating: { color: '#b8860b', marginTop: 6 },
desc: { marginTop: 12, lineHeight: 21, color: '#33413a' },
box: { marginTop: 16, backgroundColor: '#eef5f1', borderRadius: 12, padding: 16 },
price: { fontSize: 20, fontWeight: '800', color: '#0a7d55' },
per: { fontWeight: '400', color: '#6b756e', fontSize: 15 },
fee: { color: '#33413a', marginTop: 4 },
small: { color: '#6b756e', marginTop: 8 },
cta: { backgroundColor: '#0a7d55', margin: 16, height: 52, borderRadius: 12, alignItems: 'center', justifyContent: 'center' },
ctaTxt: { color: '#fff', fontSize: 17, fontWeight: '700' },
err: { textAlign: 'center', marginTop: 40, color: '#a33' },
// BEST WAY TO CONTACT
contactHead: { fontSize: 22, fontWeight: '900', letterSpacing: 1, color: '#1a2b22', marginTop: 26, marginBottom: 12, textAlign: 'center' },
contactGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: 10, justifyContent: 'center' },
cbtn: { width: '47%', minHeight: 74, borderRadius: 14, alignItems: 'center', justifyContent: 'center', paddingVertical: 12 },
cemoji: { fontSize: 26 },
cbtnTxt: { color: '#fff', fontWeight: '800', fontSize: 16, marginTop: 4 },
wa: { backgroundColor: '#25D366' },
call: { backgroundColor: '#0a7d55' },
email: { backgroundColor: '#2b6cb0' },
msg: { backgroundColor: '#7a3cff' },
other: { backgroundColor: '#555' },
modalWrap: { flex: 1, backgroundColor: 'rgba(0,0,0,.4)', justifyContent: 'flex-end' },
modalCard: { backgroundColor: '#fff', borderTopLeftRadius: 20, borderTopRightRadius: 20, padding: 20 },
modalH: { fontSize: 18, fontWeight: '800', color: '#1a2b22', marginBottom: 12 },
msgInput: { minHeight: 90, borderWidth: 1, borderColor: '#e2e5e0', borderRadius: 12, padding: 12, textAlignVertical: 'top' },
});