/* The itinerary as a document: the same branded layout behind "Download PDF" (a print window) and the public share page. Shows only what it is given; the share reading from the database already leaves out codes, costs and receipts. */ (function(){ const esc=s=>String(s==null?'':s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); const KIND={flight:['Flight','✈'],lodging:['Stay','⌂'],transport:['Transport','⇢'],activity:['Activity','◆'],dining:['Dining','✦'],note:['Note','·']}; const fmtD=s=>{if(!s)return'';const d=new Date(s+'T12:00:00');return isNaN(d)?s:d.toLocaleDateString('en-US',{month:'short',day:'numeric'});}; const fmtLong=s=>{if(!s)return'';const d=new Date(s+'T12:00:00');return isNaN(d)?s:d.toLocaleDateString('en-US',{weekday:'long',month:'long',day:'numeric'});}; const fmtYear=s=>s?s.slice(0,4):''; const fmtTime=t=>{if(!t)return'';const [h,m]=t.split(':').map(Number);return ((h%12)||12)+(m?':'+String(m).padStart(2,'0'):'')+' '+(h>=12?'pm':'am');}; const addDays=(s,n)=>{const d=new Date(s+'T12:00:00');d.setDate(d.getDate()+n);return d.toISOString().slice(0,10);}; const dayList=(a,b)=>{const out=[];let d=a;while(d<=b&&out.length<120){out.push(d);d=addDays(d,1);}return out;}; const where=i=>i.kind==='flight'||i.kind==='transport'?[i.from_place,i.to_place].filter(Boolean).join(' → '):(i.place||i.address||''); const span=i=>{ if(!i.start_date&&!i.start_time)return''; const a=(i.start_time?fmtTime(i.start_time):''); const sameDay=!i.end_date||i.end_date===i.start_date; const b=i.end_time?(sameDay?'':fmtD(i.end_date)+' · ')+fmtTime(i.end_time):(!sameDay&&i.kind==='lodging'?fmtD(i.end_date):''); if(i.kind==='lodging')return (a?'Check-in '+a:'')+(b?(a?' · ':'')+'Check-out '+(i.end_time?fmtTime(i.end_time):'')+(i.end_date&&i.end_date!==i.start_date?' '+fmtD(i.end_date):''):''); return b?a+' → '+b:a; }; const cityLine=(t,days)=>{ if(window.vedutaDayCities)return window.vedutaDayCities(t,days); const m={};days.forEach(d=>{m[d]=t.day_places&&t.day_places[d]?[t.day_places[d]]:[];});return m; }; const origin=()=>window.location.origin+window.location.pathname.replace(/[^/]*$/,''); function css(){ return ` :root{--navy:#1b2a4a;--gold:#c8a96a;--cream:#f7f4ee;--paper:#fffdf9;--muted:#6b7280;--rule:#e6e0d3} *{box-sizing:border-box} body{margin:0;background:var(--cream);color:var(--navy);font-family:"DM Sans","Helvetica Neue",Arial,sans-serif;-webkit-print-color-adjust:exact;print-color-adjust:exact} .doc{max-width:820px;margin:0 auto;background:var(--paper)} .head{background:var(--navy);color:#f5f1e8;padding:34px 44px;display:flex;justify-content:space-between;align-items:center;gap:20px} .head .lockup{display:flex;align-items:center;gap:14px} .head .lockup img{height:44px;width:auto;display:block} .head .lockup .w{font-family:"Cormorant Garamond",Georgia,serif;font-size:22px;letter-spacing:.22em;text-transform:uppercase;line-height:1;color:#f5f1e8} .head .lockup .s{font-size:10px;letter-spacing:.3em;text-transform:uppercase;line-height:1;color:var(--gold);margin-top:6px;font-weight:500} .head .k{font-size:11px;letter-spacing:.28em;text-transform:uppercase;color:var(--gold)} .hero{padding:36px 44px 22px;border-bottom:1px solid var(--gold)} .k{font-size:11px;letter-spacing:.28em;text-transform:uppercase;color:var(--gold);font-weight:500} h1{font-family:"Cormorant Garamond",Cormorant,Georgia,"Times New Roman",serif;font-weight:400;font-size:40px;margin:8px 0 6px;line-height:1.05} .sub{font-size:13px;color:var(--muted)} .notes{font-size:14px;line-height:1.6;margin-top:12px;white-space:pre-line} .day{padding:22px 44px 6px;page-break-inside:avoid} .day h2{font-family:"Cormorant Garamond",Cormorant,Georgia,serif;font-weight:400;font-size:24px;margin:0 0 4px;display:flex;gap:14px;align-items:baseline;flex-wrap:wrap} .day h2 .city{color:var(--gold);font-size:19px} .day .rule{border-top:1px solid var(--rule);margin:8px 0 14px} .stay{font-size:12px;color:var(--muted);margin:-4px 0 10px} .stay b{color:var(--gold);font-weight:400} .item{display:flex;gap:16px;padding:10px 0 12px;border-bottom:1px dotted var(--rule);page-break-inside:avoid} .item .t{flex:none;width:76px;font-size:12px;color:var(--muted);padding-top:4px} .item .body{flex:1;min-width:0} .item .title{font-family:"Cormorant Garamond",Cormorant,Georgia,serif;font-size:20px;line-height:1.2} .item .title .ic{color:var(--gold);margin-right:8px} .item .line{font-size:13px;margin-top:3px;color:#2b3446} .item .det{font-size:12px;color:var(--muted);margin-top:4px;line-height:1.6} .item .note{font-size:13px;line-height:1.55;margin-top:4px;white-space:pre-line} .open{font-size:13px;color:var(--muted);font-style:italic;padding:2px 0 10px} .foot{padding:26px 44px 34px;border-top:1px solid var(--gold);display:flex;justify-content:space-between;align-items:center;gap:20px;font-size:11px;color:var(--muted);letter-spacing:.06em} .foot img{height:34px;opacity:.9} .runner{position:fixed;bottom:10px;left:0;right:0;text-align:center;font-size:10px;letter-spacing:.24em;text-transform:uppercase;color:var(--gold);display:none} .upsell{margin:36px 44px 44px;background:var(--navy);color:#f5f1e8;border-radius:14px;padding:30px 34px;display:flex;justify-content:space-between;gap:24px;align-items:center;flex-wrap:wrap} .upsell h3{font-family:"Cormorant Garamond",Cormorant,Georgia,serif;font-weight:400;font-size:26px;margin:6px 0 8px} .upsell p{margin:0;font-size:14px;line-height:1.6;color:rgba(245,241,232,.78);max-width:520px} .upsell a.btn{display:inline-block;background:var(--gold);color:var(--navy);text-decoration:none;font-size:12px;letter-spacing:.18em;text-transform:uppercase;padding:13px 22px;border-radius:4px;font-weight:500;white-space:nowrap} @media print{ body{background:#fff} .doc{max-width:none} .runner{display:block} .upsell a.btn{border:1px solid var(--gold)} @page{margin:14mm 12mm 16mm} } @media(max-width:600px){ .head,.hero,.day,.foot{padding-left:20px;padding-right:20px} .upsell{margin:24px 20px} h1{font-size:30px} .item .t{width:60px} }`; } function body(t,opts){ opts=opts||{}; const assets=opts.assets||{}; opts=opts||{}; const base=origin(); const days=t.start_date&&t.end_date?dayList(t.start_date,t.end_date):t.start_date?[t.start_date]:[]; const cities=cityLine(t,days); const unscheduled=(t.items||[]).filter(i=>!i.start_date); const outside=(t.items||[]).filter(i=>i.start_date&&days.length&&(i.start_datedays[days.length-1])); const item=i=>{ const det=Object.entries(i.details||{}).filter(([k,v])=>v!=null&&v!==''&&!/^inbox_/.test(k)); const extra=[]; if(!opts.share&&i.confirmation)extra.push('Confirmation '+esc(i.confirmation)+''); if(i.address&&where(i)!==i.address)extra.push(esc(i.address)); det.forEach(([k,v])=>extra.push(esc(k.replace(/_/g,' '))+': '+esc(v))); return '
'+esc(i.start_time?fmtTime(i.start_time):(KIND[i.kind]||KIND.note)[0])+'
' +'
'+(KIND[i.kind]||KIND.note)[1]+''+esc(i.title)+'
' +'
'+esc([span(i),i.vendor,where(i)].filter(Boolean).join(' · '))+'
' +(extra.length?'
'+extra.join('  ·  ')+'
':'') +(i.note?'
'+esc(i.note)+'
':'')+'
'; }; const dayBlock=(d)=>{ const items=(t.items||[]).filter(i=>i.start_date===d); const stays=(t.items||[]).filter(i=>i.kind==='lodging'&&i.start_date&&i.end_date&&i.start_date

'+esc(fmtLong(d))+(cs.length?''+esc(cs.join(' → '))+'':'')+'

' +(stays.length?'
'+esc(stays.map(s=>s.title).join(' · '))+'
':'')+'
' +(items.length?items.map(item).join(''):'
Open
')+''; }; const who=(t.travelers&&t.travelers.length?t.travelers:(t.members||[]).map(m=>m.first_name).filter(Boolean)); const upsell=opts.share?'
Interested in joining the Veduta Travel Club?

Fifteen cities, personally vetted stays, and a concierge when you want the trip planned for you.

Every member was interviewed and invited. Members plan trips like this one on a single page, forward confirmations to a private address, and travel with people they have met through the club. If '+esc(t.owner_first_name||'a member')+' sent you this, say so when you ask.

Request an invitation
':''; return '
' +'
Veduta
Travel Club
'+(opts.share?'A member’s itinerary':'Member itinerary')+'
' +'
'+esc([t.start_date?fmtD(t.start_date)+(t.end_date&&t.end_date!==t.start_date?' – '+fmtD(t.end_date):'')+(fmtYear(t.start_date)?', '+fmtYear(t.start_date):''):'Dates to come',days.length>1?(days.length-1)+' nights':null].filter(Boolean).join(' · '))+'
' +'

'+esc(t.title)+'

'+esc([(t.cities||[]).join(' → '),who.length?'Travelling: '+who.join(', '):null,opts.viewer?'Prepared for '+opts.viewer:null].filter(Boolean).join(' · '))+'
' +(t.notes?'
'+esc(t.notes)+'
':'')+'
' +days.map(dayBlock).join('') +(outside.length?'

Around the trip

'+outside.map(item).join('')+'
':'') +(unscheduled.length?'

Not yet on a day

'+unscheduled.map(item).join('')+'
':'') +upsell +'
VEDUTA TRAVEL CLUB  ·  veduta.travel'+(opts.share?'':'  ·  Private to the travelers on this trip')+'  ·  '+esc(new Date().toLocaleDateString('en-US',{month:'long',day:'numeric',year:'numeric'}))+'
' +'
Veduta Travel Club · '+esc(t.title)+'
'; } function doc(t,opts){ return ''+esc(t.title)+' · Veduta Travel Club' +'' +''+body(t,opts)+''; } /* Opens the document in a new tab and offers the print dialog, where "Save as PDF" is one click. */ /* The club's marks, fetched once and carried into the document as data URIs, so the printed copy does not depend on a second load. */ let marks=null; function loadMarks(){ if(marks)return marks; const base=origin(); const one=file=>fetch(base+'assets/brand/'+file).then(r=>r.ok?r.blob():null).then(b=>b?new Promise(res=>{const fr=new FileReader();fr.onload=()=>res(fr.result);fr.onerror=()=>res(null);fr.readAsDataURL(b);}):null).catch(()=>null); marks=Promise.all([one('mark-gold-trim.png'),one('seal-navy.png')]).then(([mark,seal])=>({mark,seal})); return marks; } function download(t,opts){ opts=opts||{}; const w=window.open('','_blank'); // opened now, in the click, so the browser allows it if(!w)return false; w.document.open();w.document.write(''+esc(t.title)+'Preparing the itinerary…');w.document.close(); Promise.race([loadMarks(),new Promise(res=>setTimeout(()=>res({}),4000))]).then(assets=>{ w.document.open();w.document.write(doc(t,{...opts,assets:assets||{}}));w.document.close(); print(w); }); return true; } function print(w){ /* The print dialog renders what is on the page at that moment, so the club's marks and the type must be there before it opens. */ const go=()=>{try{w.focus();w.print();}catch(e){}}; const images=Array.from(w.document.images).map(i=>i.complete?Promise.resolve():new Promise(res=>{i.onload=i.onerror=res;})); const fonts=w.document.fonts&&w.document.fonts.ready?w.document.fonts.ready.catch(()=>{}):Promise.resolve(); Promise.race([Promise.all(images.concat([fonts])),new Promise(res=>setTimeout(res,6000))]).then(()=>setTimeout(go,300)); } window.VedutaItinerary={css,body,doc,download}; /* The public page behind a share link. */ function SharedTripPage({token}){ const [t,setT]=React.useState(undefined); React.useEffect(()=>{let live=true;window.vedutaData.sharedTrip(token).then(r=>{if(live)setT(r);});return()=>{live=false;};},[token]); const wrap={maxWidth:'900px',margin:'0 auto',padding:'120px 20px 80px'}; const band=
; if(t===undefined)return
{band}
Opening the itinerary…
; if(!t)return
{band}
This link has been withdrawn.

The member who shared this itinerary has taken the link back. Ask them for a new one.

; return
An itinerary shared by {t.owner_first_name||'a member'} of the Veduta Travel Club
; } window.SharedTripPage=SharedTripPage; })();