PageSourceSearch

https://www.rattlebone.co.uk/assets/website-menu-style20261006-r2/WebsiteMenuSharing-20260913.js

js rattlebone.co.uk collected 2026-10-10 01:14:27 UTC 13,579 bytes, 83 lines download raw bytes

1import {usePublicContact as __rbUseContact} from './PublicContact20260921.js';
2import {a as interop,n as react} from './jsx-runtime-OQpaS_Dv.js';
3const R=interop(react(),1),h=R.createElement;
4const base='/api/public/menu-sharing/';
5const stateNames={queued:'Waiting to send',sending:'Submitting to mail service',accepted:'Accepted by mail service',uncertain:'Acceptance could not be confirmed — contact the pub before trying a new request',blocked:'Not sent — menu or mail preparation needs attention',cancelled:'Cancelled'};
6const storageKey='rattlebone-menu-invitation-draft-v1';
7async function api(action,body){
8  const response=await fetch(base+action,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body),credentials:'same-origin'});
9  let value;try{value=await response.json();}catch{throw new Error('We could not confirm the result. Retry this same request below.');}
10  if(!response.ok||value.success===false)throw Object.assign(new Error(value.message||'The request could not be completed.'),{status:response.status});return value;
11}
12function remember(value){try{if(value)sessionStorage.setItem(storageKey,JSON.stringify({at:Date.now(),...value}));else sessionStorage.removeItem(storageKey);return true;}catch{return false;}}
13function recovered(){try{const d=JSON.parse(sessionStorage.getItem(storageKey)||'null');if(d?.at>Date.now()-864e5&&d?.body?.requestId)return d;}catch{}return null;}
14const button=(label,onClick,props={})=>h('button',{type:'button',onClick,...props},label);
15function field(label,value,onChange,props={}){const id='rbmenu-'+label.toLowerCase().replace(/[^a-z0-9]+/g,'-');return h('label',{htmlFor:id},label,h('input',{id,value,onChange:e=>onChange(e.target.value),...props}));}
16function MenuJobStatus({data}){return h('div',null,
17  h('p',{className:'rbmenu-notice'},'Mail acceptance is not proof of inbox delivery. Check junk mail too.'),
18  h('ul',{className:'rbmenu-deliveries'},data.jobs.map(j=>h('li',{key:j.id},h('strong',null,j.to),h('span',null,(j.kind==='verify'?'Verification: ':'')+(stateNames[j.state]||j.state))))));}
19export function MenuShareFeature({menuId,menuName,menus}){
20  const [open,setOpen]=R.useState(false);
21  return h('section',{className:'rbmenu-feature'},h('div',null,h('p',{className:'rbmenu-eyebrow'},'Good food. Better together.'),h('h2',null,'Make a date at the Rattlebone'),h('p',null,'Why not share a menu with one or more friends and suggest a date and time to meet? We’ll send your invitations with the menus, and you can see who’s coming.')),
22    button('Invite friends & share menus',()=>setOpen(true)),open&&h(MenuDeliveryDialog,{mode:'share',menuId,menuName,menus,open,onClose:()=>setOpen(false)}));
23}
24export function EmailMenuDialog(props){return h(MenuDeliveryDialog,{...props,mode:'email'});}
25export function ShareMenuDialog(props){return h(MenuDeliveryDialog,{...props,mode:'share'});}
26export function MenuDeliveryDialog({mode='email',menuId,menuName,menus=[],open,onClose}){const __rbContact=__rbUseContact();
27  const recoveredDraft=R.useMemo(()=>recovered(),[]),[form,setForm]=R.useState({mode,menuIds:menuId?[menuId]:[],email:'',name:'',guests:[{name:'',email:''}],suggestedDate:'',suggestedTime:'13:00',message:'',consent:false}),
28    [pending,setPending]=R.useState(null),[data,setData]=R.useState(null),[review,setReview]=R.useState(false),[busy,setBusy]=R.useState(false),[error,setError]=R.useState(''),[code,setCode]=R.useState(''),[hasDraft,setHasDraft]=R.useState(Boolean(recoveredDraft)),[storageOK,setStorageOK]=R.useState(true);
29  const dialog=R.useRef(null),isShare=form.mode==='share',locked=Boolean(pending||data),options=menus.length?menus:[{id:menuId,name:menuName}].filter(m=>m.id);
30  R.useEffect(()=>{if(!open)return;const previous=document.activeElement;dialog.current?.querySelector('input,button')?.focus();return()=>previous?.focus?.();},[open]);
31  R.useEffect(()=>{if(!open)return;function key(e){if(e.key==='Escape'&&!busy)onClose();if(e.key!=='Tab')return;const nodes=[...dialog.current.querySelectorAll('input:not(:disabled),button:not(:disabled),textarea:not(:disabled),a[href],select:not(:disabled)')].filter(n=>n.offsetParent!==null);if(!nodes.length)return;const first=nodes[0],last=nodes.at(-1);if(e.shiftKey&&document.activeElement===first){e.preventDefault();last.focus();}else if(!e.shiftKey&&document.activeElement===last){e.preventDefault();first.focus();}}document.addEventListener('keydown',key);return()=>document.removeEventListener('keydown',key);},[open,busy,onClose]);
32  function save(body,result){setStorageOK(remember({body,result}));}
33  function change(key,value){if(!locked){setForm(v=>({...v,[key]:value}));setReview(false);}}
34  async function submit(){
35    const body=pending||{...form,requestId:crypto.randomUUID()};setPending(body);save(body,data);setBusy(true);setError('');
36    try{const result=await api('start',body);setData(result);save(body,result);}
37    catch(e){setError(e.message);if(e.status&&e.status<500&&e.status!==409){setPending(null);remember(null);}}
38    finally{setBusy(false);}
39  }
40  async function verify(){setBusy(true);setError('');try{const result=await api('verify',{requestId:data.requestId,manageToken:data.manageToken,code});setData(result);save(pending,result);}catch(e){setError(e.message);}finally{setBusy(false);}}
41  async function refresh(){setBusy(true);setError('');try{const result=await api('status',{requestId:data.requestId,manageToken:data.manageToken});setData(result);save(pending,result);}catch(e){setError(e.message);}finally{setBusy(false);}}
42  function restore(){setForm(recoveredDraft.body);setPending(recoveredDraft.body);setData(recoveredDraft.result||null);setReview(true);setHasDraft(false);}
43  const names=form.menuIds.map(id=>options.find(m=>m.id===id)?.name||data?.menus?.find(m=>m.id===id)?.name||id).join(', ');
44  if(!open)return null;
45  return h('div',{className:'rbmenu-overlay'},h('section',{className:'rbmenu-dialog',role:'dialog','aria-modal':true,'aria-labelledby':'rbmenu-title',ref:dialog},
46    h('header',null,h('h2',{id:'rbmenu-title'},isShare?'Invite friends to the Rattlebone':'Email me a menu'),button('Close',onClose,{disabled:busy,'aria-label':'Close menu form'})),
47    hasDraft&&!locked&&h('div',{className:'rbmenu-notice'},h('p',null,'There is a menu request from earlier in this browser session. Restore it to check its result before sending again.'),button('Restore request',restore),button('Discard saved copy',()=>{remember(null);setHasDraft(false);}),h('p',{className:'rbmenu-small'},'Discarding this saved copy does not cancel messages already requested.')),
48    !storageOK&&h('p',{role:'status'},'This browser cannot retain the request. Keep this window open until the result is confirmed.'),
49    error&&h('p',{role:'alert',className:'rbmenu-error'},error),
50    !data&&h('form',{onSubmit:e=>{e.preventDefault();setReview(true);setError('');}},
51      h('p',null,isShare?'Suggest a get-together, choose the menus and review your invitations. We’ll confirm your email before sending anything to your friends.':'We’ll send the current menu PDF to your email address.'),
52      h('fieldset',{disabled:locked||busy,hidden:review},h('legend',null,'Menus to include'),options.map(m=>h('label',{className:'rbmenu-check',key:m.id},h('input',{type:'checkbox',checked:form.menuIds.includes(m.id),onChange:e=>change('menuIds',e.target.checked?[...form.menuIds,m.id]:form.menuIds.filter(id=>id!==m.id)),disabled:!form.menuIds.includes(m.id)&&form.menuIds.length>=3}),m.name)),
53        h('div',{className:'rbmenu-grid'},field('Your name',form.name,v=>change('name',v),{required:isShare,maxLength:100,autoComplete:'name'}),field('Your email',form.email,v=>change('email',v),{required:true,type:'email',autoComplete:'email',maxLength:254})),
54        isShare&&h(R.Fragment,null,
55          h('div',{className:'rbmenu-grid'},field('Suggested date',form.suggestedDate,v=>change('suggestedDate',v),{type:'date',required:true}),field('Suggested time (UK)',form.suggestedTime,v=>change('suggestedTime',v),{type:'time',required:true})),
56          h('h3',null,'Your friends'),form.guests.map((g,i)=>h('div',{className:'rbmenu-friend',key:i},field('Friend '+(i+1)+' name',g.name,v=>change('guests',form.guests.map((x,k)=>k===i?{...x,name:v}:x)),{maxLength:100}),field('Friend '+(i+1)+' email',g.email,v=>change('guests',form.guests.map((x,k)=>k===i?{...x,email:v}:x)),{type:'email',required:true,maxLength:254}),form.guests.length>1&&button('Remove',()=>change('guests',form.guests.filter((_,k)=>k!==i)),{'aria-label':'Remove friend '+(i+1)}))),
57          form.guests.length<8&&button('Add another friend',()=>change('guests',[...form.guests,{name:'',email:''}])),
58          h('label',null,'A short message (optional)',h('textarea',{value:form.message,maxLength:600,rows:3,onChange:e=>change('message',e.target.value.replace(/[\r\n]+/g,' '))}))),
59        h('label',{className:'rbmenu-check'},h('input',{type:'checkbox',required:true,checked:form.consent,onChange:e=>change('consent',e.target.checked)}),isShare?'Send these personal invitations on my behalf. These friends expect to hear from me.':'Please email me the selected menu.'),
60        h('p',{className:'rbmenu-small'},'No one will be added to a mailing list. Invitations do not reserve a table.'),
61        !review&&h('button',{type:'submit',disabled:!form.menuIds.length},'Review '+(isShare?'invitations':'menu email'))),
62      review&&h('section',{className:'rbmenu-review','aria-label':'Review before sending'},h('h3',null,'Please check before sending'),h('p',null,names),h('p',null,'Your copy: '+form.email),isShare&&h(R.Fragment,null,h('p',null,form.suggestedDate+' at '+form.suggestedTime+' (UK time)'),h('ul',null,form.guests.map(g=>h('li',{key:g.email}
62,[g.name,g.email].filter(Boolean).join(' — ')))),form.message&&h('blockquote',null,form.message)),
63        button(busy?'Submitting…':pending?'Retry this same request':isShare?'Email me a verification code':'Send my menu',submit,{disabled:busy}),!pending&&button('Edit details',()=>setReview(false)))),
64    data&&h(R.Fragment,null,h('p',{role:'status'},data.message),h(MenuJobStatus,{data}),
65      data.phase==='awaiting_verification'&&h('form',{onSubmit:e=>{e.preventDefault();verify();}},field('Six-digit email code',code,v=>setCode(v.replace(/\D/g,'').slice(0,6)),{inputMode:'numeric',autoComplete:'one-time-code',pattern:'[0-9]{6}',required:true,maxLength:6}),h('button',{type:'submit',disabled:busy||code.length!==6},busy?'Confirming…':'Confirm & send invitations')),
66      data.phase==='active'&&data.statusUrl&&h('p',null,h('a',{href:data.statusUrl},'View private invitation status')),
67      button('Refresh send status',refresh,{disabled:busy}),data.phase==='active'&&h('p',null,h('a',{href:'/book-a-table'},'Check availability & book a table'))),
68    h('p',{className:'rbmenu-small'},__rbContact.text('Need help? Call 01666 840871.'))));
69}
70
71export function MenuInvitationPage(){
72  const [data,setData]=R.useState(null),[error,setError]=R.useState(''),[busy,setBusy]=R.useState(false);
73  const link=R.useMemo(()=>{const p=new URLSearchParams(location.hash.slice(1));for(const kind of ['manage','guest'])if(p.has(kind)){const parts=p.get(kind).split('.');return kind==='manage'?{kind,requestId:parts[0],manageToken:parts[1]}:{kind,requestId:parts[0],guestIndex:parts[1],token:parts[2]};}return null;},[]);
74  async function load(){if(!link){setError('This invitation link is incomplete. Please use the complete link in your email.');return;}setBusy(true);setError('');try{setData(await api(link.kind==='manage'?'status':'invitation',link));}catch(e){setError(e.message);}finally{setBusy(false);}}
75  R.useEffect(()=>{load();},[]);
76  async function rsvp(response){setBusy(true);setError('');try{setData(await api('rsvp',{...link,response}));}catch(e){setError(e.message);}finally{setBusy(false);}}
77  return h('section',{className:'rbmenu-invitation'},h('p',{className:'rbmenu-eyebrow'},'The Rattlebone Inn · Sherston'),h('h1',null,link?.kind==='manage'?'Your invitations':'You’re invited'),error&&h('p',{role:'alert',className:'rbmenu-error'},error),busy&&!data&&h('p',{role:'status'},'Loading your invitation…'),
78    data&&h(R.Fragment,null,h('p',null,link.kind==='guest'?data.organiserName+' would like you to join them for a meal.':'Keep this link private. It shows your invited friends and their replies.'),h('h2',null,new Date(data.suggestedDate+'T12:00:00Z').toLocaleDateString('en-GB',{weekday:'long',day:'numeric',month:'long',year:'numeric'})+' at '+data.suggestedTime),h('p',null,data.menus.map(m=>m.name).join(' · ')),data.message&&link.kind==='guest'&&h('blockquote',null,data.message),h('a',{href:'/menus'},'View current menus'),
79      link.kind==='manage'?h(R.Fragment,null,h(MenuJobStatus,{data}),h('h3',null,'Your friends’ replies'),h('ul',{className:'rbmenu-deliveries'},data.guests.map(g=>h('li',{key:g.id},h('strong',null,g.name||g.email),h('span',null,g.email+' · '+g.rsvp)))),button('Refresh replies',load,{disabled:busy})):
80        h('div',{className:'rbmenu-response'},h('p',{role:'status'},'Your reply: '+data.guest.rsvp),['yes','maybe','no'].map(answer=>button({yes:'Yes, I’d love to',maybe:'Maybe',no:'Sorry, I can’t'}[answer],()=>rsvp(answer),{key:answer,disabled:busy,'aria-pressed':data.guest.rsvp===answer}))),
81      h('p',{className:'rbmenu-notice'},'This is a proposed get-together, not a table reservation. Agree your plans with the organiser, then check availability and book.'),h('a',{href:'/book-a-table'},'Check availability & book a table')));
82}
83export default MenuInvitationPage;

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.