PotesTrip :root{--bg:#fbfaf5;--card:#fff;--ink:#14231c;--mute:#667a70;--g:#10b981;--gd:#0a8a60;--gl:#c9f2e3;--y:#fbbf24;--line:#e6e8e1;--chip:#f0f1ea;--bad:#c2410c;--nav:#fff;box-sizing:border-box;padding-top:env(safe-area-inset-top,0px)} @media (prefers-color-scheme:dark){:root:not([data-theme="light"]){--bg:#0d1a14;--card:#15271f;--ink:#eaf4ee;--mute:#9db3a8;--line:#254035;--chip:#1c3328;--gl:#124434;--nav:#12231b;--bad:#fb923c}} :root[data-theme="dark"]{--bg:#0d1a14;--card:#15271f;--ink:#eaf4ee;--mute:#9db3a8;--line:#254035;--chip:#1c3328;--gl:#124434;--nav:#12231b;--bad:#fb923c} *{box-sizing:border-box}html,body{margin:0} body{background:var(--bg);color:var(--ink);font:15px/1.45 "Plus Jakarta Sans",system-ui,sans-serif;padding-bottom:calc(84px + env(safe-area-inset-bottom,0px))} main{max-width:560px;margin:0 auto;padding:16px} h1,h2,h3,p{margin:0}h1{font-size:30px;line-height:1.1;font-weight:800}h2{font-size:21px;font-weight:800}h3{font-size:17px;font-weight:800} .mut{color:var(--mute);font-size:13px} .card{background:var(--card);border-radius:20px;padding:16px;margin-top:12px;border:1px solid var(--line)} .ban{border-radius:20px;padding:16px;margin-top:12px;display:flex;gap:12px;align-items:center;justify-content:space-between} .yel{background:var(--y);color:#2b1f00}.grn{background:var(--gl)} .btn{font:inherit;font-weight:800;border:0;border-radius:999px;padding:12px 18px;min-height:44px;background:var(--g);color:#04281c;cursor:pointer;width:100%} .btn.s{width:auto;padding:10px 16px}.btn.w{background:var(--chip);color:var(--ink)} .chip{font:inherit;font-weight:700;border:1px solid var(--line);background:var(--chip);color:var(--ink);border-radius:999px;padding:8px 14px;min-height:40px;cursor:pointer;white-space:nowrap} .chip.on{background:var(--gl);border-color:var(--g)} .chips{display:flex;gap:8px;overflow-x:auto;padding:12px 0 2px} input,select{width:100%;font:inherit;color:var(--ink);background:var(--chip);border:1px solid transparent;border-radius:16px;padding:13px 14px;min-height:46px} label{display:block;font-weight:700;font-size:13px;margin:12px 0 5px} .g2{display:grid;grid-template-columns:1fr 1fr;gap:12px} .big{font-size:28px;font-weight:800;line-height:1.1;margin:6px 0 2px} .band{display:flex;gap:14px;flex-wrap:wrap;margin-top:10px}.av{text-align:center;font-size:12px;font-weight:700} .av i{display:grid;place-items:center;width:46px;height:46px;border-radius:50%;background:var(--gl);font-style:normal;font-weight:800;font-size:18px;margin-bottom:3px} .hero{background:linear-gradient(160deg,#0f3d2e,#1f6f55 55%,#d9a441);color:#fff;border-radius:0 0 28px 28px;padding:36px 20px 70px;margin:-16px -16px 0} .pill{display:inline-block;background:var(--y);color:#2b1f00;font-weight:800;border-radius:999px;padding:4px 12px;font-size:13px;margin-bottom:10px} .hero p{margin-top:10px;opacity:.9} .sp{padding:0;overflow:hidden}.sh{padding:34px 16px 14px;color:#fff}.sh.fr{background:linear-gradient(135deg,#0f766e,#65a30d)}.sh.et{background:linear-gradient(135deg,#1d4ed8,#0ea5e9)} .sb{padding:14px 16px}.tag{display:inline-block;background:rgba(255,255,255,.25);border-radius:8px;padding:2px 9px;font-size:12px;font-weight:800;margin-right:6px} .price{background:var(--gl);border-radius:999px;padding:5px 12px;font-weight:800;font-size:13px}.price.bad{background:#fde6d6;color:var(--bad)} .row{display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap} .vote{display:flex;width:100%;justify-content:space-between;margin-top:10px;background:var(--chip);border:0;border-radius:14px;padding:12px 14px;font:inherit;font-weight:700;color:var(--ink);cursor:pointer;min-height:44px} .vote.on{background:var(--gl)} a.lnk{display:block;text-align:center;margin-top:8px;border:1px solid var(--line);border-radius:14px;padding:11px;color:var(--ink);font-weight:800;text-decoration:none} nav{position:fixed;left:0;right:0;bottom:0;background:var(--nav);border-top:1px solid var(--line);display:flex;justify-content:center;padding:6px 8px calc(6px + env(safe-area-inset-bottom,0px))} nav button{flex:1;max-width:180px;border:0;background:none;font:inherit;font-weight:800;font-size:12px;color:var(--mute);padding:8px;min-height:48px;cursor:pointer}nav button.on{color:var(--gd)}nav b{display:block;font-size:20px} button:focus-visible,input:focus-visible,select:focus-visible,a:focus-visible{outline:3px solid var(--y);outline-offset:2px}
const EX=[ ["fr","Lac de Saint-Ferréol","Montagne Noire, Tarn","1 h",["Lac","Forêt","Isolé"],180,"Revel, Tarn"], ["fr","Gorges de l’Aveyron","Saint-Antonin-Noble-Val","1 h 30",["Rivière","Falaises"],185,"Saint-Antonin-Noble-Val"], ["fr","Lac de Montbel","Ariège","1 h 30",["Lac","Baignade"],170,"Lac de Montbel, Ariège"], ["fr","Vallée du Salat","Couserans, Ariège","1 h 45",["Ruisseau","Montagne","Isolé"],160,"Seix, Ariège"], ["fr","Gers autour de Lectoure","Gers","1 h 15",["Piscine","Calme"],200,"Lectoure, Gers"], ["et","Costa Brava","Catalogne, Espagne","3 h 30 de route",["Mer","Criques"],225,"Costa Brava, Espagne"], ["et","Maison à Porto et plages de l’Atlantique","Porto, Portugal","1 h 40 de vol",["Vol direct","Surf","Vie nocturne"],550,"Porto, Portugal"], ["et","Algarve","Portugal","2 h de vol",["Plages","Piscine"],350,"Lagos, Portugal"], ["et","Dalmatie","Split, Croatie","2 h de vol",["Îles","Mer turquoise"],410,"Split, Croatie"] ].map((x,i)=>["ex"+i,{kind:x[0],name:x[1],place:x[2],time:x[3],tags:x[4],pp:x[5],q:x[6],ex:1}]); const D={participants:{},spots:{},votes:{},trip:{}}; let db=null,me=null,tab="home",flt="all",busy="",msg="",sample=null,form=false; const $=s=>document.querySelector(s),app=$("#app"); const esc=s=>String(s??"").replace(/[&"]/g,c=>({"&":"&","":">",'"':"""}[c])); const fd=d=>new Date(d+"T00:00").toLocaleDateString("fr-FR",{day:"numeric",month:"short"}); async function put(c,id,data){D[c][id]=data;if(!db)saveLocal();render();if(db)try{await db.doc(c+"/"+id).set(data)}catch(e){msg="Enregistrement refusé : demande un accès contributeur."}render()} function spots(){const m=Object.fromEntries(EX);Object.entries(D.spots).forEach(([k,v])=>m[k]=v);return Object.entries(m).filter(([k,v])=>!v.deleted)} function stats(){const all=Object.values(D.participants),a=all.filter(p=>p.d1&&p.d2); if(!a.length)return{n:all.length,a:0,pf:{}}; const s=a.map(p=>p.d1).sort().pop(),e=a.map(p=>p.d2).sort()[0],pf={};a.forEach(p=>pf[p.pref||"egal"]=(pf[p.pref||"egal"]||0)+1); return{n:all.length,a:a.length,s,e,ok:s+p.bud||0)),pf}} const votes=id=>Object.values(D.votes).filter(v=>(v.ids||[]).includes(id)).length; function link(sp,st){const q=encodeURIComponent(sp.q||sp.place||sp.name);let u=`https://www.airbnb.fr/s/${q}/homes?adults=${Math.max(st.n,2)}`;if(st.ok)u+=`&checkin=${st.s}&checkout=${new Date(new Date(st.s).getTime()+4*864e5).toISOString().slice(0,10)}`;return u} function render(){ const st=stats(),mine=D.participants[me]; $("#nav").hidden=!mine; document.querySelectorAll("nav button").forEach(b=>b.className=b.dataset.t===tab?"on":""); if(!mine){app.innerHTML=`
PotesTrip

Les vacances entre potes, sans prise de tête.

Dispos, budgets, envies : on trouve le spot parfait ensemble.

${msg?`

${esc(msg)}

`:""}
`; $("#go").onclick=async()=>{const n=$("#pn").value.trim();if(!n)return;if(!D.trip.info)await put("trip","info",{name:$("#tn").value.trim()||"Séjour entre potes"});await put("participants",me,{nom:n})};return} if(tab==="home"){ const top=spots().map(([id,s])=>[id,s,votes(id)]).filter(x=>x[2]).sort((a,b)=>b[2]-a[2]).slice(0,3); app.innerHTML=`

${db?"Partagé en direct":"Mode local"}

${esc(D.trip.info?.name)}

Salut ${esc(mine.nom)} !

${mine.d1?"":`

Remplis tes dispos

Dates, budget et envies : 1 minute chrono.
`}
Dates communes
${st.a?(st.ok?fd(st.s)+" → "+fd(st.e):"Aucune fenêtre commune"):"Pas encore"}
${st.a?"Calculé sur "+st.a+" réponse"+(st.a>1?"s":""):"Chacun doit cocher ses dates"}
Budget max commun
${st.a?st.bud+" €":"—"}
par pers., transport inclus
Ont répondu
${st.a}/${st.n}
France ${st.pf?.fr||0} · Étranger ${st.pf?.et||0} · Égal ${st.pf?.egal||0}

La bande

${Object.values(D.participants).map(p=>`
${esc(p.nom[0]?.toUpperCase())}${esc(p.nom)}
`).join("")}

${db?"Envoie le lien de cette page aux potes : dispos et votes se synchronisent en direct.":"Le lien contient le séjour. Chaque pote l’ouvre, ajoute ses dispos et te renvoie son lien."}

Top des votes

${top.length?top.map(([id,s,v])=>`

${esc(s.name)}

${esc(s.place)}
${v}/${st.n} ❤️
`).join(""):'

Aucun vote pour l’instant.

'}`} if(tab==="dispos"){ const k=(v,l)=>``; const envies=["Lac","Rivière","Piscine","Mer","Randonnée","Fête","Isolé"]; app.innerHTML=`

Mes dispos

${k("fr","France")}${k("et","Étranger")}${k("egal","Égal")}
${envies.map(e=>``).join("")}

${msg?`

${esc(msg)}

`:""}
`; const cur=()=>({...D.participants[me],d1:$("#d1").value,d2:$("#d2").value,bud:+$("#bud").value||0}); app.querySelectorAll("[data-pref]").forEach(b=>b.onclick=()=>put("participants",me,{...cur(),pref:b.dataset.pref})); app.querySelectorAll("[data-env]").forEach(b=>b.onclick=()=>{const c=cur(),s=new Set(c.envies||[]);s.has(b.dataset.env)?s.delete(b.dataset.env):s.add(b.dataset.env);put("participants",me,{...c,envies:[...s]})}); $("#sv").onclick=()=>{const c=cur();if(!c.d1||c.d2{tab="home";render()})}} if(tab==="spots"){ const list=spots().filter(([id,s])=>flt==="all"||flt==="top"||s.kind===flt).sort((a,b)=>flt==="top"?votes(b[0])-votes(a[0]):0).filter(([id])=>flt!=="top"||votes(id)); const f=(v,l)=>``; const myv=D.votes[me]?.ids||[]; app.innerHTML=`

Les spots

${f("all","Tous")}${f("fr","France")}${f("et","Étranger")}${f("top","Top")}

✨ Trouver des pépites avec l’IA

Basé sur vos dates et vos envies.

${msg?`

${esc(msg)}

`:""}
${list.length?list.map(([id,s])=>{const v=votes(id),mv=myv.includes(id),ok=st.a&&s.pp<=st.bud;return`
${s.kind==="fr"?"France":"Étranger"}${s.ex?'Exemple':""}

${esc(s.name)}

${esc(s.place)}
${esc(s.time)}~${+s.pp||0} € / pers.
${s.desc?`

${esc(s.desc)}

`:""}

${(s.tags||[]).map(esc).join(" · ")}

Voir les logements sur Airbnb

`}).join(""):'

Rien ici pour l’instant.

'} ${form?`
FranceÉtranger

`:`

`}`; app.querySelectorAll("[data-f]").forEach(b=>b.onclick=()=>{flt=b.dataset.f;render()}); app.querySelectorAll("[data-v]").forEach(b=>b.onclick=()=>{const s=new Set(myv);s.has(b.dataset.v)?s.delete(b.dataset.v):s.add(b.dataset.v);put("votes",me,{ids:[...s]})}); app.querySelectorAll("[data-rm]").forEach(b=>b.onclick=()=>put("spots",b.dataset.rm,{deleted:1})); app.querySelectorAll("[data-ai]").forEach(b=>b.onclick=()=>suggest(b.dataset.ai)); if($("#add"))$("#add").onclick=()=>{form=true;render()}; if($("#fa"))$("#fa").onclick=()=>{const n=$("#fn").value.trim();if(!n)return;form=false;put("spots","u"+Date.now(),{kind:$("#fk").value,name:n,place:$("#fp").value.trim(),time:"",tags:[],pp:+$("#fc").value||0,q:$("#fp").value.trim()||n})}} app.querySelectorAll("[data-go]").forEach(b=>b.onclick=()=>{tab=b.dataset.go;render()});if($("#sh"))$("#sh").onclick=share; } function saveLocal(){try{localStorage.setItem("pt",JSON.stringify({me,D}))}catch(e){}} function loadLocal(){try{let s={};try{s=JSON.parse(localStorage.getItem("pt")||"{}")}catch(e){} if(s.me)me=s.me;const m=location.hash.match(/^#s=(.+)$/); if(m){const j=JSON.parse(decodeURIComponent(escape(atob(m[1]))));Object.keys(D).forEach(c=>Object.assign(D[c],j[c]||{}))} Object.keys(D).forEach(c=>{const o=s.D?.[c]||{};Object.keys(o).forEach(k=>{if(!(k in D[c])||k===me||c==='spots')D[c][k]=o[k]})});saveLocal()}catch(e){}render()} async function share(){if(db){$("#shm").textContent="Envoie le lien de cette page aux potes : dispos et votes se synchronisent en direct.";return} const u=location.href.split("#")[0]+"#s="+btoa(unescape(encodeURIComponent(JSON.stringify(D)))); try{if(navigator.share){await navigator.share({title:"PotesTrip",text:"Rejoins notre séjour !",url:u});return}}catch(e){return} try{await navigator.clipboard.writeText(u);$("#shm").textContent="Lien copié. Colle-le dans le groupe WhatsApp."}catch(e){prompt("Copie ce lien :",u)}} async function suggest(kind){ if(!sample){msg="Suggestions IA indisponibles ici : ajoute des spots à la main.";render();return} busy=kind;msg="";render();const st=stats(),all=Object.values(D.participants); const p=`Un groupe de ${Math.max(st.n,2)} amis habite à Toulouse et veut un séjour d’environ 4 nuits. Fenêtre de dates : ${st.ok?st.s+" au "+st.e:"été 2027"}. Budget max par personne, transport inclus : ${st.a?st.bud:300} €. Envies : ${[...new Set(all.flatMap(x=>x.envies||[]))].join(", ")||"baignade, isolement"}. ${kind==="fr"?"Propose 4 lieux en France à moins de 2 h de Toulouse, isolés, avec lac, ruisseau ou piscine pour se baigner.":"Propose 4 destinations à l’étranger accessibles depuis Toulouse."} Réponds uniquement par un tableau JSON d’objets {name, place, time (durée depuis Toulouse), tags (3 mots-clés), desc (1 phrase), pp (estimation entière en € par personne), q (requête de recherche Airbnb)}. Ne reprends pas : ${spots().map(x=>x[1].name).join(", ")}.`; try{const r=await sample.json(p);for(const x of r.slice(0,4))await put("spots","ai"+Date.now()+Math.random().toString(36).slice(2,5),{kind,name:String(x.name),place:String(x.place),time:String(x.time||""),tags:(x.tags||[]).slice(0,3).map(String),desc:String(x.desc||""),pp:+x.pp||0,q:String(x.q||x.place)})}catch(e){msg="L’IA n’a pas répondu. Réessaie dans un instant."} busy="";render()} document.querySelectorAll("nav button").forEach(b=>b.onclick=()=>{tab=b.dataset.t;msg="";render()}); me="local-"+Math.random().toString(36).slice(2,8);render(); (async()=>{try{ const u=await claude.use("user"),d=await claude.use("db");sample=await claude.use("sample"); if(!u||!d){loadLocal();return}me=await u.id();db=d; for(const c of Object.keys(D))d.collection(c).onSnapshot(s=>{D[c]=Object.fromEntries(s.docs.map(x=>[x.id,x.data()]));render()},()=>{}); }catch(e){loadLocal()}})();

J’aime ça :

J’aime Chargement…