Discover Cambodia
រុករក​កម្ពុជា
🇰🇭   Kingdom of Cambodia   🇰🇭

DiscoverCambodia

រុករក​កម្ពុជា

From the temples of Angkor to tropical islands, misty highlands to vibrant cities — explore every corner of the Kingdom of Wonder.

Explore Provinces
25
Provinces
50+
Attractions
0
Users
8
Categories
2
Languages
Scroll

Interactive Map

Explore CambodiaBy Province

Hover or tap a province to preview it. Click to open places, hotels, food, and travel details.

Cambodia province map BMC OMC PVH STG RTK PLN BTB SR KPT KRT MDK PST KCH KCM TKM KKG KSP KDL PP PV SVG SHV KPT KEP TKO
Selected province Choose a province ចុចលើផែនទីដើម្បីមើលព័ត៌មាន

All 25 Provinces & Cities

Choose YourDestination

Click any province to explore its places, weather, and top hotels.

Explore Everything

All Places

📖 About this Place

✨ Highlights
    🔎 Feature / Specification
      🎒 What Travelers Get
        🍜 Food & Drink
        🗓️ Best Time to Visit
        ✅ Things To Do
          🚫 Things To Avoid
            🏨 Nearby Hotels
            🍽️ Nearby Restaurants
            ⭐ Reviews

            🌤️ Weather
            🟡 Dry🔵 Wet
            📍 Places in this Province
            🏨 Where to Stay
            🍜 Where to Eat
            🇰🇭 Khmer Culture

            Cambodia's Major Festivals

            Discover Cambodia's rich calendar of national festivals — from the iconic Water Festival to Khmer New Year. Each celebration brings the whole country alive, but some provinces are especially famous for their festivities.

            Travel Services
            Book local help for your Cambodia trip

            Tourists can book approved guides and transport with a 50% deposit. Local guides and transport owners can also apply to become approved partners.

            Find & Book a Guide

            Check approved guides, choose one, send passport or ID details, trip plan, address, email, and pay 50% deposit first.

            Become a Guide

            Guides can submit ID card, address, phone, email, Telegram, warranty letter, guarantor, languages, provinces, and experience.

            Book Transportation

            Tourists can book taxi, tuk tuk, van, bus, or other transport with pickup, dropoff, schedule, passenger count, and 50% deposit.

            Partner With Us

            Transporters can request partnership with ID card, vehicle details, address, phone, email, Telegram, warranty letter, and guarantor.

            🌏
            Discover Cambodia (DC)
            Algorithm travel helper, no AI cost
            ${h(p.icon||'??')}
            ${h(lang==='kh'&&p.name_kh?p.name_kh:p.name_en)}
            ${h(lang==='kh'&&p.cat_kh?p.cat_kh:p.cat_en)} ${p.entry_fee?`??? ${h(p.entry_fee)}`:`${h(t('free_entry'))}`} ${p.avg_visit_hrs?`?? ~${h(p.avg_visit_hrs)}h`:''} ${hasPicnic?`?? ${h(t('picnic_ok'))}`:''} ${hasFood?`??? ${h(t('food_onsite'))}`:''}
            ?? `; }).join(''); const lunchHtml=day.lunch?`
            12:00 PM ???
            ${h(t('lunch'))}: ${h(day.lunch.restaurant_name_en||'')}
            ${day.lunch.price_range?`
            ${h(day.lunch.price_range)}
            `:''}
            `:''; return `
            ${lang==='kh'?'??????':'Day'} ${day.day}
            ~$${day.est_cost} � ${(day.places||[]).length} ${h(t('places_lbl'))}
            ${placesHtml||`

            ${h(t('no_places_day'))}

            `}${lunchHtml}
            `; }).join(''); } async function savePlanAsTrip(){ if(!me){openAuth('login');toast(t('login_save_trips'));return} const title=document.getElementById('tr-save-name').value.trim()||(lang==='kh'?`???????????? ${pfState.days}????`:`Cambodia Trip ${pfState.days}D`); const r=await up({action:'create_trip',title}); if(!r.success){toast(t('could_not_create_trip'));return} const tid=r.trip_id; if(currentPlan?.days){ for(const day of currentPlan.days){ for(const p of day.places||[]) await up({action:'add_to_trip',trip_id:tid,place_id:p.id}); } } toast(`${t('trip_saved')} ${title}`); document.getElementById('tr-save-name').value=''; } async function loadTrips(){ document.getElementById('tr-cnt').innerHTML=`

            ${h(t('loading'))}

            `; const r=await up({action:'get_trips'});myTrips=r.data||[]; document.getElementById('tr-cnt').innerHTML=myTrips.length ?myTrips.map(trip=>`
            ??? ${h(trip.title)}
            ${trip.places?.length?trip.places.map(p=>`${h(p.icon)} ${h(lang==='kh'&&p.name_kh?p.name_kh:p.name_en)} `).join(''):`${h(t('no_places_yet'))}`}
            `).join('') :`

            ${h(t('no_trips_yet'))}

            `; } async function createTrip(){if(!me){openAuth('login');return}const title=document.getElementById('nt-title').value.trim()||(lang==='kh'?'?????????????????????':'My Cambodia Trip');const r=await up({action:'create_trip',title});if(r.success){document.getElementById('nt-title').value='';toast(`${t('trip_saved')} ${title}`);await loadTrips()}} async function delTrip(tid){if(!confirm(t('delete_trip_confirm')))return;await up({action:'delete_trip',trip_id:tid});toast(t('deleted'));await loadTrips()} async function rmTrip(tid,pid){await up({action:'remove_from_trip',trip_id:tid,place_id:pid});await loadTrips()} async function openAT(pid,name){ if(!me){openAuth('login');toast(t('log_in_plan_trips'));return} atPid=pid;document.getElementById('at-pname').textContent=name;document.getElementById('at-new').value='';om('at-ov'); const r=await up({action:'get_trips'});myTrips=r.data||[]; document.getElementById('at-list').innerHTML=myTrips.length ?myTrips.map(trip=>`
            ??? ${h(trip.title)} (${trip.places?.length||0})
            `).join('') :`

            ${h(t('no_trips_yet'))}

            `; } async function addToTrip(tid){const r=await up({action:'add_to_trip',trip_id:tid,place_id:atPid});if(r.success){cm('at-ov');toast(t('add_to_trip'))}else toast(r.error||t('already_in_trip'))} async function createTripAndAdd(){const title=document.getElementById('at-new').value.trim()||(lang==='kh'?'?????????????????????':'My Cambodia Trip');const tr=await up({action:'create_trip',title});if(tr.success)await addToTrip(tr.trip_id)} // ---------------------------------------------- // DONATE � ABA // ---------------------------------------------- function openDonate(){document.getElementById('udrop').classList.remove('open');resetDonate();om('dn-ov');if(me){const n=document.getElementById('dn-name');if(n&&!n.value)n.value=me.name;}} function resetDonate(){document.getElementById('dn-s1').style.display='block';document.getElementById('dn-s2').style.display='none';document.getElementById('dn-s3').style.display='none';document.getElementById('dn-amt').value='5';document.querySelectorAll('.apill').forEach(b=>b.classList.remove('active'));const d=document.querySelector('.apill:nth-child(2)');if(d)d.classList.add('active');updAmt();} function setAmt(v,btn){document.getElementById('dn-amt').value=v;document.querySelectorAll('.apill').forEach(b=>b.classList.remove('active'));if(btn)btn.classList.add('active');updAmt()} function updAmt(){const v=parseFloat(document.getElementById('dn-amt')?.value)||5;const el=document.getElementById('dn-preview-amt');if(el)el.textContent=`$${v.toFixed(2)}`} // ---------------------------------------------- // FESTIVALS // ---------------------------------------------- async function loadFestivals(){ try{ const r=await fetch(`${API}?action=places&search=Festival`).then(x=>x.json()); const festivals=(r.data||[]).filter(p=> p.name_en.includes('Festival')|| p.name_en.includes('Lent')|| p.name_kh.includes('?????????') ); const grid=document.getElementById('fest-grid'); if(!grid) return; if(!festivals.length){grid.innerHTML='';return;} const colors=['#8B4513','#1A4A6B','#2D5A1B']; const icons=['??','??','???']; grid.innerHTML=festivals.map((p,i)=>`
            ${icons[i%icons.length]}
            ${h(lang==='kh'&&p.name_kh?p.name_kh:p.name_en)}
            ${h(lang==='kh'?p.name_en:p.name_kh)}

            ${h((p.description_en||'').slice(0,120))}�

            ${p.opening_hours?`⏰ ${h(p.opening_hours)}`:''} ${p.entry_fee?`🏭 ${h(p.entry_fee)}`:''}
            View Details →
            `).join(''); }catch(e){console.warn('Festival load error:',e);} } document.addEventListener('DOMContentLoaded',()=>{initProvinceMap();const a=document.getElementById('dn-amt');if(a)a.addEventListener('input',updAmt)}); function goToQR(){ const amt=parseFloat(document.getElementById('dn-amt').value)||0; if(amt<1){showErr('dn-err','Minimum $1');return} const msg=document.getElementById('dn-msg').value.trim(); document.getElementById('dn-qr-img').src='img.png'; document.getElementById('dn-qr-amt').textContent=`$${amt.toFixed(2)}`; const md=document.getElementById('dn-msg-d'),mt=document.getElementById('dn-msg-t'); if(msg){mt.textContent=msg;md.style.display='block'}else md.style.display='none'; document.getElementById('dn-s1').style.display='none';document.getElementById('dn-s2').style.display='block'; const name=document.getElementById('dn-name').value||'Anonymous'; up({action:'donate',amount_usd:amt,donor_name:name,message:msg}).catch(()=>{}); } function backToStep1(){document.getElementById('dn-s2').style.display='none';document.getElementById('dn-s1').style.display='block'} function confirmDonation(){const a=document.getElementById('dn-qr-amt').textContent;document.getElementById('dn-s2').style.display='none';document.getElementById('dn-s3').style.display='block'} // ---------------------------------------------- // SCROLL REVEAL // ---------------------------------------------- const ro=new IntersectionObserver(es=>{es.forEach(e=>{if(e.isIntersecting)e.target.classList.add('visible')})},{threshold:.08}); function obsRev(){document.querySelectorAll('.reveal').forEach(el=>ro.observe(el))} // keyboard function toggleMobileNav(){ const open = document.body.classList.toggle('nav-open'); const btn = document.getElementById('nav-toggle'); if(btn) btn.setAttribute('aria-expanded', open ? 'true' : 'false'); if(open) armUiHistory(); } document.addEventListener('click', e=>{ if(document.body.classList.contains('nav-open') && e.target.closest('.nav-links a')) closeMobileNavOnly(); if(document.body.classList.contains('nav-open') && !e.target.closest('nav')) closeMobileNavOnly(); }); window.addEventListener('resize',()=>{if(innerWidth>860 && document.body.classList.contains('nav-open')) closeMobileNavOnly();}); document.addEventListener('keydown',e=>{ if(e.key==='Escape'){ document.querySelectorAll('.overlay.open').forEach(o=>o.classList.remove('open')); if(document.getElementById('dp').style.transform==='translateX(0px)'||document.getElementById('dp').style.transform==='translateX(0)') closeDetail(); if(document.getElementById('pp').style.transform==='translateX(0px)'||document.getElementById('pp').style.transform==='translateX(0)') closeProv(); } if(e.key==='Enter'&&document.getElementById('auth-ov').classList.contains('open')){ document.getElementById('t-li').classList.contains('active')?doLogin():doRegister(); } }); // ---------------------------------------------- // FESTIVALS MODAL // ---------------------------------------------- // Fallback data used only if SQL not yet imported const FESTIVALS_DATA=[ { id:266, icon:'??', name_en:'Khmer New Year', name_kh:'??????????????????????', when:'April 13�15', duration:'3 days', vibe:'Joyous � Water fights � Family reunions', desc:'The biggest celebration of the year! Families reunite, temples overflow with offerings, and water fights take over the streets. The whole country shuts down for 3 days of pure celebration.', color:'#C8922A', provinces:[ {code:'PP',name:'Phnom Penh',color:'#CC1F27'}, {code:'SR',name:'Siem Reap',color:'#1B5E3B'}, {code:'BTB',name:'Battambang',color:'#1A4A7A'}, {code:'KPT',name:'Kampot',color:'#7B5E2A'}, {code:'KG',name:'Koh Kong',color:'#2D7A5A'}, {code:'All 25',name:'All Provinces',color:'#8B4513'}, ] }, { id:267, icon:'??', name_en:'Pchum Ben', name_kh:'???????????????????', when:'September � October (15 days)', duration:'15 days', vibe:'Solemn � Ancestral offerings � Temple visits', desc:'One of the most sacred festivals in Cambodia � Ancestors\' Day. Families bring food offerings to temples across 15 days to honor the spirits of deceased relatives. All 15,000+ temples participate.', color:'#CC1F27', provinces:[ {code:'PP',name:'Phnom Penh',color:'#CC1F27'}, {code:'SR',name:'Siem Reap',color:'#1B5E3B'}, {code:'BTB',name:'Battambang',color:'#1A4A7A'}, {code:'UDM',name:'Udong',color:'#8B4513'}, {code:'All 25',name:'Nationwide',color:'#555'}, ] }, { id:268, icon:'???', name_en:'Chol Vossa (Buddhist Lent)', name_kh:'?????????????????????', when:'July � October', duration:'3 months', vibe:'Spiritual � Candle ceremonies � Monk ordinations', desc:'The start of Buddhist Lent � monks remain in their temples for 3 months of intensive meditation and study. Celebrated with candlelit processions and young men ordaining as monks.', color:'#1A4A7A', provinces:[ {code:'PP',name:'Phnom Penh',color:'#CC1F27'}, {code:'SR',name:'Siem Reap',color:'#1B5E3B'}, {code:'BTB',name:'Battambang',color:'#1A4A7A'}, {code:'KPG',name:'Kampong Cham',color:'#7B3A1A'}, ] }, { id:null, icon:'??', name_en:'Water Festival (Bon Om Touk)', name_kh:'???????????????', when:'October � November (full moon)', duration:'3 days', vibe:'Boat races � Fireworks � Huge crowds', desc:'The most spectacular festival in Cambodia! Up to 3 million people flood Phnom Penh to watch the famous dragon boat races on the Tonle Sap and Mekong rivers, plus fireworks and floating lanterns.', color:'#1B5E3B', provinces:[ {code:'PP',name:'Phnom Penh ?',color:'#CC1F27'}, {code:'SR',name:'Siem Reap',color:'#1B5E3B'}, {code:'KRT',name:'Kratie',color:'#5A2A1A'}, {code:'STG',name:'Stung Treng',color:'#2A5A3A'}, ] }, { id:null, icon:'??', name_en:'Royal Ploughing Ceremony', name_kh:'????????????????????????????', when:'May (Royal Palace, PP)', duration:'1 day', vibe:'Royal � Agricultural � Sacred oxen ritual', desc:'A royal ceremony held at the Royal Palace grounds where sacred oxen are led to eat from trays of different foods � their choices predict the year\'s harvest and fortune for the kingdom.', color:'#8B4513', provinces:[ {code:'PP',name:'Phnom Penh ?',color:'#CC1F27'}, ] }, { id:null, icon:'??', name_en:'Festival of Lights (Loy Krathong style)', name_kh:'??????????????? (?????)', when:'Same as Water Festival', duration:'3 nights', vibe:'Lanterns � Riverside � Magical atmosphere', desc:'On the evenings of Water Festival, thousands of floating lanterns and illuminated boats drift down the Tonle Sap. The riverside of Phnom Penh transforms into a river of light.', color:'#C8922A', provinces:[ {code:'PP',name:'Phnom Penh ?',color:'#CC1F27'}, {code:'SR',name:'Siem Reap',color:'#1B5E3B'}, {code:'KPT',name:'Kampot',color:'#7B5E2A'}, ] }, { id:null, icon:'??', name_en:'King\'s Birthday', name_kh:'????????????????????????', when:'May 13�15', duration:'3 days', vibe:'Parades � Fireworks � National pride', desc:'Cambodia celebrates King Sihamoni\'s birthday with fireworks, light displays, and cultural performances. The Royal Palace area and major cities are decorated with lights and flags.', color:'#7B3A7A', provinces:[ {code:'PP',name:'Phnom Penh ?',color:'#CC1F27'}, {code:'SR',name:'Siem Reap',color:'#1B5E3B'}, {code:'BTB',name:'Battambang',color:'#1A4A7A'}, {code:'SVK',name:'Sihanoukville',color:'#1A7A7A'}, ] }, { id:null, icon:'??', name_en:'Phare Circus � Year Round', name_kh:'?????????????? Phare', when:'Year-round (Fri�Sat evenings)', duration:'Ongoing', vibe:'Circus arts � Youth empowerment � Cultural storytelling', desc:'Battambang\'s world-famous Phare Circus � run by young Cambodian artists � performs throughout the year, blending acrobatics, traditional music and visual arts into stunning shows.', color:'#2A5A8A', provinces:[ {code:'BTB',name:'Battambang ?',color:'#1A4A7A'}, {code:'SR',name:'Siem Reap',color:'#1B5E3B'}, ] }, ]; let festivalSlideTimer=null; function festivalImages(f){ const raw=[f.image_url||'',...(String(f.images||'').split('|'))].map(s=>String(s||'').trim()).filter(Boolean); return [...new Set(raw)]; } function parseFestivalStartDate(text){ const s=String(text||''); const months={january:0,jan:0,february:1,feb:1,march:2,mar:2,april:3,apr:3,may:4,june:5,jun:5,july:6,jul:6,august:7,aug:7,september:8,sep:8,october:9,oct:9,november:10,nov:10,december:11,dec:11}; const m=s.match(/\b(January|Jan|February|Feb|March|Mar|April|Apr|May|June|Jun|July|Jul|August|Aug|September|Sep|October|Oct|November|Nov|December|Dec)\b[^\d]{0,16}(\d{1,2})/i); if(!m) return null; const now=new Date(), y=now.getFullYear(); let date=new Date(y,months[m[1].toLowerCase()],parseInt(m[2],10)); date.setHours(0,0,0,0); const today=new Date(now.getFullYear(),now.getMonth(),now.getDate()); if(date3) return null; const label=days===0?t('festival_today'):(days===1?t('festival_tomorrow'):t('festival_in_days').replace('{n}',days)); return {days,label}; } function startFestivalSlides(){ clearInterval(festivalSlideTimer); const frames=[...document.querySelectorAll('.festival-photo-frame[data-slideable="1"]')]; if(!frames.length) return; festivalSlideTimer=setInterval(()=>{ frames.forEach(frame=>{ const slides=[...frame.querySelectorAll('.festival-photo-slide')]; const dots=[...frame.querySelectorAll('.festival-slide-dots span')]; if(slides.length<3) return; const next=((parseInt(frame.dataset.idx||'0',10)+1)%slides.length); slides.forEach((s,i)=>s.classList.toggle('active',i===next)); dots.forEach((d,i)=>d.classList.toggle('active',i===next)); frame.dataset.idx=String(next); }); },4200); } function festivalPhotoHtml(f,accent,soon){ const imgs=festivalImages(f); if(!imgs.length){ return `
            ${soon?`
            ${h(soon.label)}
            `:''}
            ${f.icon||'??'}
            `; } const slideable=imgs.length>2; return `
            ${soon?`
            ${h(soon.label)}
            `:''} ${imgs.map((src,i)=>`
            ${h(f.name_en||'Festival')}
            `).join('')} ${slideable?`
            ${imgs.map((_,i)=>``).join('')}
            `:''}
            `; } async function openFestivals(){ om('fest-ov'); const grid=document.getElementById('fest-modal-grid'); clearInterval(festivalSlideTimer); grid.innerHTML=`
            ${h(t('loading_festivals'))}
            `; const accentColors=['#C8922A','#8B1A1A','#1A4A6B','#1B5E3B','#7B3A8B','#C06010']; const FESTIVAL_IDS=[266,267,268,269,270,271]; let festivals=[]; try{ const r=await fetch(`${API}?action=places`).then(x=>x.json()); const all=r.data||[]; festivals=all.filter(p=>FESTIVAL_IDS.includes(Number(p.id))||String(p.planner_tags||'').toLowerCase().includes('festival')||String(p.cat_slug||'').toLowerCase().includes('festival')); festivals.sort((a,b)=>{ const ai=FESTIVAL_IDS.indexOf(Number(a.id)), bi=FESTIVAL_IDS.indexOf(Number(b.id)); if(ai!==-1||bi!==-1) return (ai===-1?999:ai)-(bi===-1?999:bi); return (a.sort_order||0)-(b.sort_order||0); }); }catch(e){console.warn('Festival load error:',e);} if(!festivals.length){ festivals=FESTIVALS_DATA.map(f=>({ id:f.id,name_en:f.name_en,name_kh:f.name_kh,icon:f.icon, description_en:f.desc,description_kh:null, opening_hours:f.when,duration:f.duration, image_url:'',images:'', _accent:f.color,_provinces:f.provinces })); } if(!festivals.length){ grid.innerHTML=`
            ${h(t('no_festivals'))}
            `; return; } const upcoming=festivals.map(f=>({f,soon:festivalCountdown(f)})).filter(x=>x.soon); const alertHtml=upcoming.length?`
            ${h(t('festival_coming_title'))}
            ${h(upcoming.map(x=>`${lang==='kh'&&x.f.name_kh?x.f.name_kh:x.f.name_en} ${x.soon.label}`).join(' � '))}
            ${h(t('festival_coming_msg'))}
            `:''; grid.innerHTML=alertHtml+festivals.map((f,i)=>{ const accent=f._accent||f.cat_color||accentColors[i%accentColors.length]; const provinces=f._provinces||[]; const placeId=f.id||f.place_id||null; const soon=festivalCountdown(f); const click=placeId?`onclick="cm('fest-ov');setTimeout(()=>openDetail(${Number(placeId)}),240)"`:''; return `
            ${festivalPhotoHtml(f,accent,soon)}
            ${f.icon||'??'}
            ${h(lang==='kh'&&f.name_kh?f.name_kh:f.name_en)}
            ${h(lang==='kh'?f.name_en:(f.name_kh||''))}
            ${f.opening_hours?`?? ${h(f.opening_hours)}`:''} ${f.duration?`? ${h(f.duration)}`:''} ${f.entry_fee&&f.entry_fee!=='Free'?`?? ${h(f.entry_fee)}`:''}
            ${provinces.length?`
            ${h(t('celebrated_in'))}
            ${provinces.slice(0,5).map(p=>`${h(p.name)}`).join('')}
            `:''}
            ${h(t('view_details'))}
            `; }).join(''); startFestivalSlides(); } // ---------------------------------------------- // PHONE / TABLET APP SHELL // ---------------------------------------------- const dcAppState={ enabled:false, tab:'home', query:'', searchOpen:false, selectedProvince:null, font:localStorage.getItem('dc-app-font')||'normal' }; function dcEsc(v){ if(typeof h==='function')return h(v||''); return String(v||'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m])); } function dcTxt(en,kh){return lang==='kh'&&kh?kh:(en||kh||'');} function dcCopy(){ const kh=lang==='kh'; return { home:kh?'????????':'Home', provinces:kh?'?????':'Provinces', festivals:kh?'?????':'Festivals', services:kh?'????':'Services', chat:kh?'?????':'Chat', settings:kh?'?????':'Setting', search:kh?'??????????????? ??????????? ?????...':'Search places, restaurants, provinces...', popular:kh?'???????????????':'Popular Places', popularKicker:kh?'????????????????':'Recommended for you', provinceTitle:kh?'?????????????':'Choose Your Destination', provinceKicker:kh?'????? ??? ??????????? ??':'All 25 provinces & cities', provinceIntro:kh?'????????????????????????? ????????? ???????? ?????????':'Tap a province to explore places, weather, hotels, food, and travel details.', selected:kh?'????????????????':'Selected province', open:kh?'???':'Open', festivalTitle:kh?'?????????????':'Upcoming Festivals', festivalKicker:kh?'??????????????????':'Coming soon first', servicesTitle:kh?'???????????????':'Travel Services', servicesKicker:kh?'????? ??? ???????':'Book, apply, and contact', settingsTitle:kh?'?????????':'Settings', settingsKicker:kh?'???? ???? ?????????????':'Profile, language, and requests', info:kh?'???????':'Info', trip:kh?'?????':'Trip', map:kh?'?????':'Map', viewGuides:kh?'???????????????':'View Guides', becomeGuide:kh?'???????????????????????':'Become a Guide', bookTransport:kh?'???????????????':'Book Transport', partner:kh?'????????????':'Partner With Us', contact:kh?'?????????':'Contact Us', profile:kh?'???????????':'My Profile', requests:kh?'??????????':'My Requests', bookmarks:kh?'?????':'Bookmarks', myTrips:kh?'???????????':'My Trips', donate:kh?'????????':'Donate', language:kh?'????':'Language', theme:kh?'????? / ????':'Light / Dark', font:kh?'?????????':'Font Size', small:kh?'???':'Small', big:kh?'??':'Big', botTitle:'Discover Cambodia (DC)', botText:kh?'?????????????????????????????? ??????????????????????????????':'Algorithm travel helper. Choose what you want to explore.', nextSteps:kh?'??????????????? ????????????????????????????? My Requests ????????????????????????????':'Status and next steps appear in My Requests after you submit a booking or application.' }; } function dcAppIsCompact(){return window.matchMedia('(max-width:1280px)').matches;} function dcAppImages(item){ const raw=[item?.image_url||item?.cover_image||''].concat(String(item?.images||'').split('|')); return [...new Set(raw.map(s=>String(s||'').trim()).filter(Boolean))]; } function dcAppFirstImage(item){return dcAppImages(item)[0]||'';} function dcAppPrice(v){return dcEsc(v||'Free');} function dcAppGuard(fn,...args){if(typeof window[fn]==='function')return window[fn](...args);} function dcAppNeedLogin(){ if(me)return false; if(typeof openAuth==='function')openAuth('login'); return true; } function dcAppOpenDetail(id){if(typeof openDetail==='function')openDetail(Number(id));} function dcAppOpenTrip(id){ if(dcAppNeedLogin())return; const p=allPlaces.find(x=>Number(x.id)===Number(id)); if(typeof openAT==='function')openAT(Number(id),dcTxt(p?.name_en,p?.name_kh)); } function dcAppOpenMap(id){ const p=allPlaces.find(x=>Number(x.id)===Number(id)); const lat=Number(p?.latitude),lng=Number(p?.longitude); if(lat&&lng)window.open(`https://www.google.com/maps?q=${lat},${lng}`,'_blank'); else dcAppOpenDetail(id); } function dcAppFilteredPlaces(source){ const q=dcAppState.query.trim().toLowerCase(); const list=source&&source.length?source:allPlaces; if(!q)return list; return list.filter(p=>[ p.name_en,p.name_kh,p.description_en,p.description_kh,p.prov_en,p.prov_kh,p.cat_en,p.cat_kh ].some(v=>String(v||'').toLowerCase().includes(q))); } function dcAppPlaceCard(p){ const c=dcCopy(); const img=dcAppFirstImage(p); const cat=dcTxt(p.cat_en,p.cat_kh)||'Place'; const prov=dcTxt(p.prov_en,p.prov_kh)||'Cambodia'; const desc=dcTxt(p.description_en,p.description_kh); const color=p.cat_color||'#a15a13'; return `
            ${img?`${dcEsc(dcTxt(p.name_en,p.name_kh))}`:''} ${dcEsc(cat)} ${dcEsc(prov)}
            ${dcEsc(dcTxt(p.name_en,p.name_kh))}
            ${dcEsc(dcTxt(p.name_kh,p.name_en))}
            ${dcEsc(desc).slice(0,190)}${desc&&desc.length>190?'...':''}
            ${dcEsc(p.opening_hours||'Always open')} � ${dcAppPrice(p.entry_fee)}
            `; } function dcAppProvinceCard(p){ const img=p.cover_image||''; return ``; } function dcAppOpenProvince(slug){ dcAppState.selectedProvince=slug; dcAppState.tab='provinceDetail'; dcAppRender(); const view=document.getElementById('dc-app-view'); if(view&&typeof view.scrollTo==='function')view.scrollTo({top:0,behavior:'smooth'}); window.scrollTo({top:0,behavior:'smooth'}); } const dcProvinceMapPoints=[ {slug:'banteay-meanchey',x:14.5,y:20.5,s:28},{slug:'oddar-meanchey',x:30,y:9.5,s:28},{slug:'preah-vihear',x:49,y:16,s:28}, {slug:'stung-treng',x:71,y:23,s:28},{slug:'ratanakiri',x:88,y:21,s:28},{slug:'pailin',x:7.6,y:46.8,s:24}, {slug:'battambang',x:18,y:39,s:30},{slug:'siem-reap',x:31.5,y:25,s:30},{slug:'pursat',x:27,y:56,s:30}, {slug:'koh-kong',x:14.8,y:77,s:30},{slug:'kampong-speu',x:38,y:62,s:26},{slug:'kampong-chhnang',x:44,y:47,s:24}, {slug:'kampong-thom',x:51,y:38,s:36},{slug:'kratie',x:67,y:48,s:34},{slug:'mondulkiri',x:87,y:45,s:34}, {slug:'kampong-cham',x:60,y:62,s:28},{slug:'tboung-khmum',x:66,y:70,s:22},{slug:'phnom-penh',x:55,y:69,s:22}, {slug:'kandal',x:58,y:76,s:24},{slug:'prey-veng',x:66,y:76,s:24},{slug:'svay-rieng',x:78,y:83,s:20}, {slug:'takeo',x:53,y:89,s:22},{slug:'kampot',x:45,y:88,s:24},{slug:'kep',x:50,y:95,s:16},{slug:'preah-sihanouk',x:37,y:90,s:24} ]; function dcAppMapDots(activeSlug){ return dcProvinceMapPoints.map(p=>``).join(''); } function dcAppMapPick(ev){ if(ev.target.closest('.dc-map-dot'))return; const rect=ev.currentTarget.getBoundingClientRect(); const x=(ev.clientX-rect.left)/rect.width*100; const y=(ev.clientY-rect.top)/rect.height*100; let best=dcProvinceMapPoints[0],dist=Infinity; dcProvinceMapPoints.forEach(p=>{const d=(p.x-x)*(p.x-x)+(p.y-y)*(p.y-y);if(d
            `; document.body.insertBefore(shell,document.body.firstChild); } function initDcAppShell(){ dcAppEnsureShell(); document.getElementById('dc-app-brand')?.addEventListener('click',()=>{dcAppState.tab='home';dcAppRender();window.scrollTo({top:0,behavior:'smooth'});}); document.getElementById('dc-app-notify')?.addEventListener('click',()=>{if(dcAppNeedLogin())return;openRequests();}); document.getElementById('dc-app-search-toggle')?.addEventListener('click',()=>{ dcAppState.searchOpen=!dcAppState.searchOpen; dcAppRender(); if(dcAppState.searchOpen)setTimeout(()=>document.getElementById('dc-app-search-input')?.focus(),40); }); document.getElementById('dc-app-search-input')?.addEventListener('input',e=>{dcAppState.query=e.target.value;dcAppRender();}); document.getElementById('dc-bottom-nav')?.addEventListener('click',e=>{ const btn=e.target.closest('[data-dc-tab]'); if(!btn)return; dcAppState.tab=btn.dataset.dcTab; dcAppRender(); window.scrollTo({top:0,behavior:'smooth'}); }); window.addEventListener('resize',dcAppSyncMode); dcAppSyncMode(); } function dcAppSyncMode(){ dcAppEnsureShell(); const compact=dcAppIsCompact(); dcAppState.enabled=compact; document.body.classList.toggle('dc-app-mode',compact); document.body.classList.toggle('dc-font-big',dcAppState.font==='big'); Array.from(document.body.children).forEach(el=>{ const id=el.id||''; const keep=el.tagName==='SCRIPT'||el.tagName==='STYLE'||id==='dc-app-shell'||id==='cgt-bot-bubble'||id.endsWith('-ov')||el.classList.contains('modal')||el.classList.contains('overlay')||el.classList.contains('modal-overlay'); if(!keep)el.classList.toggle('dc-app-desktop-hidden',compact); }); dcAppRender(); } function dcAppNav(){ const c=dcCopy(); const items=[ ['home',c.home],['provinces',c.provinces],['festivals',c.festivals], ['services',c.services],['chat',c.chat],['settings',c.settings] ]; return items.map(([tab,label])=>{ const active=dcAppState.tab===tab||(tab==='provinces'&&dcAppState.tab==='provinceDetail'); return ``; }).join(''); }function dcAppHead(kicker,title,intro){ return `
            ${dcEsc(kicker)}

            ${title}

            ${intro?`

            ${dcEsc(intro)}

            `:''}
            `; } function dcAppRenderHome(){ const c=dcCopy(); let featured=allFeatured.length?allFeatured:allPlaces.filter(p=>String(p.is_featured||'0')==='1'); if(!featured.length)featured=allPlaces; const places=dcAppFilteredPlaces(featured).slice(0,40); return `${dcAppHead(c.popularKicker,`${dcEsc(c.popular)} Cambodia`, '')}
            ${places.map(dcAppPlaceCard).join('')||`

            No places found.

            `}
            `; } function dcAppRenderProvinces(){ const c=dcCopy(); const selected=allProvinces.find(p=>p.slug===dcAppState.selectedProvince)||allProvinces.find(p=>p.slug==='battambang')||allProvinces[0]||{}; const groups={}; allProvinces.forEach(p=>{(groups[p.region||'Cambodia']||(groups[p.region||'Cambodia']=[])).push(p);}); const rows=Object.entries(groups).map(([region,items])=>`

            ${dcEsc(region)}

            ${items.map(dcAppProvinceCard).join('')}
            `).join(''); return `${dcAppHead(c.provinceKicker,`${dcEsc(c.provinceTitle)} Cambodia`,c.provinceIntro)}
            Cambodia province map ${dcAppMapDots(selected.slug)}
            ${dcEsc(c.selected)}
            ${dcEsc(dcTxt(selected.name_en,selected.name_kh))}
            ${dcEsc(dcTxt(selected.name_kh,selected.name_en))}
            ${rows}
            `; } function dcAppProvincePlaces(prov){ const slug=String(prov?.slug||''); const name=String(prov?.name_en||''); const id=Number(prov?.id||0); return allPlaces.filter(p=>String(p.prov_slug||p.province_slug||'')===slug||String(p.prov_en||p.province_en||'')===name||(id&&Number(p.province_id||0)===id)); } function dcAppRenderProvinceDetail(){ const c=dcCopy(); const prov=allProvinces.find(p=>p.slug===dcAppState.selectedProvince)||allProvinces[0]||{}; const places=dcAppProvincePlaces(prov); const name=dcTxt(prov.name_en,prov.name_kh)||'Province'; const sub=dcTxt(prov.name_kh,prov.name_en)||''; const img=dcAppFirstImage(prov)||prov.main_image||prov.background_image||prov.hero_image||''; const cats=[...new Set(places.map(p=>dcTxt(p.cat_en,p.cat_kh)).filter(Boolean))]; const shown=places.slice(0,24); return `
            ${img?`
            ${dcEsc(name)}
            `:''}
            ${dcEsc(prov.region||'Cambodia')}

            ${dcEsc(name)}

            ${dcEsc(sub)}

            ${dcEsc(dcTxt(prov.description_en,prov.description_kh)||'Open places, hotels, food, weather, and travel details for this province.')}

            ${Number(prov.place_count)||places.length}${dcEsc(c.places)}
            ${cats.length||0}${dcEsc(lang==='kh'?'Categories':'Categories')}
            ${shown.length?shown.map(dcAppPlaceCard).join(''):`
            No places yet.
            `}
            `; }function dcAppRenderFestivals(){ const c=dcCopy(); const fallback=(typeof FESTIVAL_FALLBACK!=='undefined'?FESTIVAL_FALLBACK:[]); const source=allPlaces.filter(p=>String(p.name_en||'').toLowerCase().includes('festival')).concat(fallback); const seen=new Set(); const festivals=source.filter(f=>{const k=(f.name_en||f.title_en||'').toLowerCase();if(seen.has(k))return false;seen.add(k);return true;}) .sort((a,b)=>{ const ad=typeof festivalCountdown==='function'?festivalCountdown(a):null; const bd=typeof festivalCountdown==='function'?festivalCountdown(b):null; return (ad?.daysLeft??999)-(bd?.daysLeft??999); }); return `${dcAppHead(c.festivalKicker,`${dcEsc(c.festivalTitle)} Cambodia`,'')}
            ${festivals.map(f=>{ const imgs=typeof festivalImages==='function'?festivalImages(f):dcAppImages(f); const img=imgs[0]||dcAppFirstImage(f); const count=typeof festivalCountdown==='function'?festivalCountdown(f):null; const name=dcTxt(f.name_en||f.title_en,f.name_kh||f.title_kh); const desc=dcTxt(f.description_en,f.description_kh); return `
            ${img?`${dcEsc(name)}`:''}${count?dcEsc(count.label):'Festival'}
            ${dcEsc(name)}
            ${dcEsc(dcTxt(f.date_text_en,f.date_text_kh)||'Cambodia')}
            ${dcEsc(desc).slice(0,190)}${desc&&desc.length>190?'...':''}
            `; }).join('')}
            `; } function dcAppRenderServices(){ const c=dcCopy(); const cards=[ [c.viewGuides,'Find approved local guides and request one for your trip.','openGuides'], [c.becomeGuide,'Apply to become an approved guide with your profile and documents.','openGuideApply'], [c.bookTransport,'Book taxi, tuk-tuk, van, bus, or other transport.','openTransport'], [c.partner,'Hotels, restaurants, guides, and transport owners can contact us.','openPartnerContactFromNav'], [c.contact,'Need help with travel, partnership, or your account? Contact us.','openContactCard'] ]; return `${dcAppHead(c.servicesKicker,`${dcEsc(c.servicesTitle)} DC`,'')}
            ${cards.map(([title,body,fn])=>`

            ${dcEsc(title)}

            ${dcEsc(body)}

            `).join('')}
            `; } function dcAppRenderChat(){ const c=dcCopy(); return `${dcAppHead('DC Assistant',dcEsc(c.botTitle),c.botText)}

            ${dcEsc(c.chat)}

            ${dcEsc(c.botText)}

            `; } function dcAppToggleFont(){ dcAppState.font=dcAppState.font==='big'?'normal':'big'; localStorage.setItem('dc-app-font',dcAppState.font); document.body.classList.toggle('dc-font-big',dcAppState.font==='big'); dcAppRender(); } function dcAppOpenRequests(){ if(dcAppNeedLogin())return; if(typeof openRequests==='function')openRequests(); } function dcAppRenderSettings(){ const c=dcCopy(); const cards=[ [c.profile,me?me.name||me.email:'Login required','openProfile()'], [c.requests,c.nextSteps,'dcAppOpenRequests()'], [c.bookmarks,'Saved places you love.','openBookmarks()'], [c.myTrips,'Trip plans created from planner.','openTrips()'], [c.donate,'Support Discover Cambodia.','openDonate()'], [c.language,lang==='kh'?'?????':'English',`setLang(lang==='kh'?'en':'kh')`], [c.theme,document.body.classList.contains('theme-light')?'Light':'Dark','toggleTheme()'], [c.font,dcAppState.font==='big'?c.big:c.small,'dcAppToggleFont()'] ]; return `${dcAppHead(c.settingsKicker,`${dcEsc(c.settingsTitle)} DC`,'')}
            ${cards.map(([title,body,action])=>`

            ${dcEsc(title)}

            ${dcEsc(body)}

            `).join('')}
            `; } function dcAppRender(){ const shell=document.getElementById('dc-app-shell'); if(!shell)return; const c=dcCopy(); const nav=document.getElementById('dc-bottom-nav'); if(nav)nav.innerHTML=dcAppNav(); const panel=document.getElementById('dc-app-search-panel'); if(panel)panel.classList.toggle('open',dcAppState.searchOpen); const input=document.getElementById('dc-app-search-input'); if(input){input.placeholder=c.search;if(input.value!==dcAppState.query)input.value=dcAppState.query;} if(!dcAppState.enabled)return; const view=document.getElementById('dc-app-view'); if(!view)return; const renderers={home:dcAppRenderHome,provinces:dcAppRenderProvinces,provinceDetail:dcAppRenderProvinceDetail,festivals:dcAppRenderFestivals,services:dcAppRenderServices,chat:dcAppRenderChat,settings:dcAppRenderSettings}; view.innerHTML=(renderers[dcAppState.tab]||dcAppRenderHome)(); } // ---------------------------------------------- // BOOT // ---------------------------------------------- initTheme(); initCompactSearch(); initDcAppShell(); loadData();
            ??
            Discover Cambodia (DC)
            Algorithm travel helper, no AI cost