@mylikita/booking-widget 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -24
- package/dist/demo.html +26 -5
- package/dist/mylikita-booking-widget.cjs +47 -2
- package/dist/mylikita-booking-widget.esm.js +47 -2
- package/dist/mylikita-booking-widget.js +47 -2
- package/dist/mylikita-booking-widget.min.js +24 -3
- package/package.json +1 -1
- package/src/brand.js +40 -0
- package/src/index.js +2 -1
- package/src/styles.js +21 -0
- package/src/widget.js +8 -1
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# @mylikita/booking-widget
|
|
2
2
|
|
|
3
|
+
**Live on npm:** [`@mylikita/booking-widget`](https://www.npmjs.com/package/@mylikita/booking-widget) ·
|
|
4
|
+
[unpkg CDN](https://unpkg.com/@mylikita/booking-widget) ·
|
|
5
|
+
[React wrapper `@mylikita/booking-widget-react`](https://www.npmjs.com/package/@mylikita/booking-widget-react)
|
|
6
|
+
|
|
3
7
|
Drop-in appointment booking for hospital websites built on MyLikita. A
|
|
4
8
|
zero-dependency vanilla-JS widget (no framework required) that:
|
|
5
9
|
|
|
@@ -8,8 +12,9 @@ zero-dependency vanilla-JS widget (no framework required) that:
|
|
|
8
12
|
3. polls the booking (`GET /v1/bookings/:ref`) until the hospital confirms,
|
|
9
13
|
cancels, reschedules, marks no-show, or the request expires.
|
|
10
14
|
|
|
11
|
-
It implements the
|
|
12
|
-
|
|
15
|
+
It implements the **MyLikita Website Booking API (v1)** exactly — same auth,
|
|
16
|
+
same fields, same idempotency rules, same statuses — so the hospital side
|
|
17
|
+
needs no custom integration work.
|
|
13
18
|
|
|
14
19
|
---
|
|
15
20
|
|
|
@@ -22,7 +27,7 @@ exactly — same auth, same fields, same idempotency rules, same statuses.
|
|
|
22
27
|
<div id="booking"></div>
|
|
23
28
|
<script>
|
|
24
29
|
MyLikitaBookingWidget.createBookingWidget(document.getElementById('booking'), {
|
|
25
|
-
relayUrl: 'https://api.mylikita.
|
|
30
|
+
relayUrl: 'https://api.mylikita.com',
|
|
26
31
|
websiteKey: 'wk_9f2k…', // public client id — not a secret
|
|
27
32
|
facilityId: 'F1',
|
|
28
33
|
});
|
|
@@ -40,7 +45,7 @@ import { createBookingWidget } from '@mylikita/booking-widget';
|
|
|
40
45
|
createBookingWidget(document.getElementById('booking'), { relayUrl, websiteKey, facilityId });
|
|
41
46
|
```
|
|
42
47
|
|
|
43
|
-
**React sites:** use [`@mylikita/booking-widget-react`](/
|
|
48
|
+
**React sites:** use [`@mylikita/booking-widget-react`](https://www.npmjs.com/package/@mylikita/booking-widget-react) — the
|
|
44
49
|
same widget as a `<BookingWidget relayUrl=… websiteKey=… facilityId=… />`
|
|
45
50
|
component (all options are props, SSR-safe, imperative ref).
|
|
46
51
|
|
|
@@ -48,15 +53,16 @@ component (all options are props, SSR-safe, imperative ref).
|
|
|
48
53
|
|
|
49
54
|
| Option | Type | Default | Description |
|
|
50
55
|
|---|---|---|---|
|
|
51
|
-
| `relayUrl` | string | — | **required** — relay base URL, e.g. `https://api.mylikita.
|
|
56
|
+
| `relayUrl` | string | — | **required** — relay base URL, e.g. `https://api.mylikita.com` |
|
|
52
57
|
| `websiteKey` | string | — | **required** — your public client id (Bearer on every request) |
|
|
53
58
|
| `facilityId` | string | — | **required** — the hospital's public facility id |
|
|
54
59
|
| `providers` | array | `[]` | `[{ external_id, label }]` — shown as a "Preferred doctor" select; unmapped slugs simply arrive unassigned (never block on them). When provided, it wins and no fetch happens |
|
|
55
|
-
| `loadProviders` | bool | `false` |
|
|
60
|
+
| `loadProviders` | bool | `false` | fetch the facility's mapped providers from the relay (`GET /v1/providers`) on mount and populate the doctor dropdown automatically. Ignored when `providers` is non-empty. A fetch failure is non-fatal: the widget keeps "No preference" and still works |
|
|
56
61
|
| `services` | array | `[]` | `['General consultation', …]` — shown as a service select (free-text input otherwise) |
|
|
57
62
|
| `durationMins` | number | `30` | sent with the booking |
|
|
58
63
|
| `pollIntervalMs` | number | `5000` | how often to poll the booking status |
|
|
59
64
|
| `maxTries` | number | `12` | poll budget (~1 min at the default); the widget then shows "request received" |
|
|
65
|
+
| `showBrand` | bool | `true` | set `false` to white-label the widget — hides the MyLikita mark + wordmark header (for premium-tier hosts). Everything else (title, form, status view) is untouched |
|
|
60
66
|
| `theme` | object | defaults | see [Theming](#theming) |
|
|
61
67
|
| `text` | object | defaults | i18n overrides for every label/message (see `src/widget.js` `DEFAULT_TEXT`) |
|
|
62
68
|
| `externalRef` | fn | auto | override the idempotency-key generator |
|
|
@@ -140,25 +146,11 @@ createBookingWidget(el, {
|
|
|
140
146
|
});
|
|
141
147
|
```
|
|
142
148
|
|
|
143
|
-
##
|
|
144
|
-
|
|
145
|
-
`demo/demo.html` runs the widget **fully offline** against an in-page mock of
|
|
146
|
-
the relay — submit a booking and watch it flip to *confirmed* after two polls.
|
|
147
|
-
Open it in a browser directly:
|
|
148
|
-
|
|
149
|
-
```bash
|
|
150
|
-
open packages/booking-widget/demo/demo.html
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
## Development
|
|
154
|
-
|
|
155
|
-
```bash
|
|
156
|
-
npm run build # esbuild → dist/ (iife, iife.min, esm, cjs)
|
|
157
|
-
npm test # mocked-fetch suite for the core logic
|
|
158
|
-
```
|
|
149
|
+
## Support
|
|
159
150
|
|
|
160
|
-
|
|
161
|
-
|
|
151
|
+
- **Issues:** report through your MyLikita account manager or the MyLikita
|
|
152
|
+
support channel you were onboarded with.
|
|
153
|
+
- **The React wrapper:** [`@mylikita/booking-widget-react`](https://www.npmjs.com/package/@mylikita/booking-widget-react).
|
|
162
154
|
|
|
163
155
|
## Security notes
|
|
164
156
|
|
package/dist/demo.html
CHANGED
|
@@ -69,8 +69,8 @@
|
|
|
69
69
|
</div>
|
|
70
70
|
|
|
71
71
|
<script>
|
|
72
|
-
/*! @mylikita/booking-widget v0.1.
|
|
73
|
-
var MyLikitaBookingWidget=(()=>{var Y=Object.defineProperty;var
|
|
72
|
+
/*! @mylikita/booking-widget v0.1.1 | MIT */
|
|
73
|
+
var MyLikitaBookingWidget=(()=>{var Y=Object.defineProperty;var we=Object.getOwnPropertyDescriptor;var ye=Object.getOwnPropertyNames;var xe=Object.prototype.hasOwnProperty;var be=(i,e)=>{for(var t in e)Y(i,t,{get:e[t],enumerable:!0})},_e=(i,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let a of ye(e))!xe.call(i,a)&&a!==t&&Y(i,a,{get:()=>e[a],enumerable:!(r=we(e,a))||r.enumerable});return i};var ve=i=>_e(Y({},"__esModule",{value:!0}),i);var Ae={};be(Ae,{DEFAULT_THEME:()=>O,MYLIKITA_MARK_DATA_URI:()=>G,STATUS_COPY:()=>N,TERMINAL_STATUSES:()=>ae,createBooking:()=>I,createBookingWidget:()=>le,createBrandHeader:()=>M,fetchProviders:()=>S,fetchStatus:()=>E,newExternalRef:()=>A,pollStatus:()=>T,resolveTheme:()=>P,statusCopy:()=>C});async function I({relayUrl:i,websiteKey:e,payload:t,signal:r}){let a=await fetch(`${q(i)}/v1/bookings`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${e}`},body:JSON.stringify(t),signal:r}),o=await Z(a);if(a.status===409)return{ok:!0,duplicate:!0,booking_ref:o?.booking_ref,error:o?.message};if(!a.ok)throw K(a.status,o,"create");return{ok:!0,duplicate:!1,booking_ref:o?.booking_ref,status:o?.status||"pending_confirmation"}}async function S({relayUrl:i,websiteKey:e,signal:t}){let r=await fetch(`${q(i)}/v1/providers`,{headers:{Authorization:`Bearer ${e}`},signal:t}),a=await Z(r);if(!r.ok)throw K(r.status,a,"providers");return(Array.isArray(a?.providers)?a.providers:[]).map(d=>({external_id:d.external_id,name:d.name||d.external_id,specialty:d.specialty||null,module:d.module||"general"}))}async function E({relayUrl:i,websiteKey:e,bookingRef:t,signal:r}){let a=await fetch(`${q(i)}/v1/bookings/${encodeURIComponent(t)}`,{headers:{Authorization:`Bearer ${e}`},signal:r}),o=await Z(a);if(!a.ok)throw K(a.status,o,"status");return{booking_ref:o?.booking_ref,status:o?.status||"pending_confirmation",appt_ref:o?.appt_ref||null}}async function T(i,{intervalMs:e=5e3,maxTries:t=12,signal:r}={}){for(let a=0;a<t;a++){if(r?.aborted)return{status:"aborted",resolved:!1,data:null};let o=await i();if(o.status!=="pending_confirmation")return{status:o.status,resolved:!0,data:o};a<t-1&&await he(e,r)}return{status:"pending_confirmation",resolved:!1,data:null}}function A(){return`BK-${Date.now()}-${Math.random().toString(36).slice(2,6)}`}function q(i){return String(i||"").replace(/\/+$/,"")}function K(i,e,t){let r=new Error(e?.message||e?.error||`Relay ${t} failed (HTTP ${i})`);return r.code=e?.error||"http_error",r.status=i,r}async function Z(i){try{return await i.json()}catch{return null}}function he(i,e){return new Promise(t=>{if(e?.aborted)return t();let r=()=>{clearTimeout(a),t()},a=setTimeout(()=>{e?.removeEventListener("abort",r),t()},i);e?.addEventListener("abort",r,{once:!0})})}var N={pending_confirmation:{title:"Request received",message:"We've received your booking request \u2014 we'll confirm shortly.",kind:"info"},confirmed:{title:"Appointment confirmed",message:"Your appointment is confirmed. See you at the clinic!",kind:"success"},cancelled:{title:"Appointment cancelled",message:"This appointment was cancelled. Please contact the clinic if this was unexpected.",kind:"danger"},rescheduled:{title:"Appointment rescheduled",message:"This appointment was moved \u2014 the new time was sent to you.",kind:"info"},no_show:{title:"Missed appointment",message:"This appointment was marked as missed.",kind:"danger"},expired:{title:"Request expired",message:"This booking request expired \u2014 please call the clinic to book.",kind:"danger"},poll_error:{title:"Something went wrong",message:"We could not check your booking right now. Please try again shortly.",kind:"danger"}};function C(i){return N[i]||N.pending_confirmation}var ae=["confirmed","cancelled","rescheduled","no_show","expired"];var O={primary:"#0d6efd",primaryDark:"#0b5ed7",primaryText:"#ffffff",bg:"#ffffff",text:"#1e293b",muted:"#64748b",border:"#e2e8f0",danger:"#dc3545",success:"#15803d",radius:10,font:"system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif"};function P(i={}){let e={...O,...i||{}};return{"--mlw-primary":e.primary,"--mlw-primary-dark":e.primaryDark,"--mlw-primary-text":e.primaryText,"--mlw-bg":e.bg,"--mlw-text":e.text,"--mlw-muted":e.muted,"--mlw-border":e.border,"--mlw-danger":e.danger,"--mlw-success":e.success,"--mlw-radius":`${e.radius}px`,"--mlw-font":e.font}}var ne=`
|
|
74
74
|
.mylikita-widget {
|
|
75
75
|
--mlw-primary: #0d6efd;
|
|
76
76
|
--mlw-primary-dark: #0b5ed7;
|
|
@@ -97,6 +97,27 @@ var MyLikitaBookingWidget=(()=>{var Y=Object.defineProperty;var pe=Object.getOwn
|
|
|
97
97
|
.mylikita-widget *::before,
|
|
98
98
|
.mylikita-widget *::after { box-sizing: border-box; }
|
|
99
99
|
|
|
100
|
+
.mylikita-widget__brand {
|
|
101
|
+
display: flex;
|
|
102
|
+
align-items: center;
|
|
103
|
+
gap: 8px;
|
|
104
|
+
margin-bottom: 14px;
|
|
105
|
+
padding-bottom: 12px;
|
|
106
|
+
border-bottom: 1px solid var(--mlw-border);
|
|
107
|
+
}
|
|
108
|
+
.mylikita-widget__brand-mark {
|
|
109
|
+
display: block;
|
|
110
|
+
flex: 0 0 auto;
|
|
111
|
+
border-radius: 5px;
|
|
112
|
+
box-shadow: 0 1px 3px rgba(15, 23, 42, .12);
|
|
113
|
+
}
|
|
114
|
+
.mylikita-widget__brand-word {
|
|
115
|
+
font-size: 14px;
|
|
116
|
+
font-weight: 700;
|
|
117
|
+
letter-spacing: .3px;
|
|
118
|
+
color: var(--mlw-muted);
|
|
119
|
+
}
|
|
120
|
+
|
|
100
121
|
.mylikita-widget__title {
|
|
101
122
|
font-size: 18px;
|
|
102
123
|
font-weight: 700;
|
|
@@ -210,7 +231,7 @@ var MyLikitaBookingWidget=(()=>{var Y=Object.defineProperty;var pe=Object.getOwn
|
|
|
210
231
|
cursor: pointer;
|
|
211
232
|
}
|
|
212
233
|
.mylikita-widget__link-btn:hover { border-color: var(--mlw-primary); color: var(--mlw-primary); }
|
|
213
|
-
`;var
|
|
234
|
+
`;var G="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0ODMgODA0IiB3aWR0aD0iNDgzIiBoZWlnaHQ9IjgwNCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJNeUxpa2l0YSI+CiAgICA8cmVjdCB4PSIxNjEiIHk9IjAiIHdpZHRoPSIxNjEiIGhlaWdodD0iMTYxIiBmaWxsPSIjMDE2OURBIi8+CiAgICA8cmVjdCB4PSIwIiB5PSIxNjAiIHdpZHRoPSIxNjIiIGhlaWdodD0iMTYxIiBmaWxsPSIjMDE2OURBIi8+CiAgICA8cmVjdCB4PSIzMjEiIHk9IjE2MCIgd2lkdGg9IjE2MiIgaGVpZ2h0PSIxNjEiIGZpbGw9IiMwMTY5REEiLz4KICAgIDxyZWN0IHg9IjAiIHk9IjQ4MiIgd2lkdGg9IjE2MiIgaGVpZ2h0PSIxNjIiIGZpbGw9IiMwNDk4RkIiLz4KICAgIDxyZWN0IHg9IjMyMSIgeT0iNDgyIiB3aWR0aD0iMTYyIiBoZWlnaHQ9IjE2MiIgZmlsbD0iIzA0OThGQiIvPgogICAgPHJlY3QgeD0iMTYxIiB5PSI2NDMiIHdpZHRoPSIxNjEiIGhlaWdodD0iMTYxIiBmaWxsPSIjMDQ5OEZCIi8+Cjwvc3ZnPg==";function M(){let i=document.createElement("div");i.className="mylikita-widget__brand";let e=document.createElement("img");e.className="mylikita-widget__brand-mark",e.src=G,e.alt="MyLikita",e.width=24,e.height=24;let t=document.createElement("span");return t.className="mylikita-widget__brand-word",t.textContent="MyLikita",i.append(e,t),i}var ke={title:"Book an appointment",subtitle:"Request a slot and we will confirm shortly.",name:"Full name",phone:"Phone number",email:"Email address",provider:"Preferred doctor (optional)",noPreference:"No preference",service:"Service (optional)",datetime:"Preferred date & time",visitType:"Appointment type",visitPhysical:"In person",visitTelemedicine:"Video call",visitHome:"Home visit",notes:"Notes (optional)",submit:"Request appointment",submitting:"Submitting\u2026",bookAnother:"Book another appointment",requiredPhoneOrEmail:"Please provide a phone number or an email address.",requiredName:"Please enter your name.",requiredDatetime:"Please choose a date and time.",networkError:"Could not reach the booking service. Please try again.",rateLimited:"Too many requests \u2014 please wait a moment and try again."},oe="mylikita-widget-styles";function le(i,e={}){if(!i)throw new Error("createBookingWidget: a container element is required");let t={relayUrl:e.relayUrl,websiteKey:e.websiteKey,facilityId:e.facilityId,providers:e.providers||[],services:e.services||[],loadProviders:e.loadProviders===!0&&!(e.providers&&e.providers.length),pollIntervalMs:e.pollIntervalMs??5e3,maxTries:e.maxTries??12,text:{...ke,...e.text||{}},theme:e.theme||{},onStatus:typeof e.onStatus=="function"?e.onStatus:null,onError:typeof e.onError=="function"?e.onError:null,onBooking:typeof e.onBooking=="function"?e.onBooking:null,externalRef:typeof e.externalRef=="function"?e.externalRef:A};if(!t.relayUrl||!t.websiteKey||!t.facilityId)throw new Error("createBookingWidget: relayUrl, websiteKey and facilityId are required");Ee();let r=t.text,a=i;a.classList.add("mylikita-widget"),Te(a,t.theme);let o=!0,d=!1,u=null,f=Math.random().toString(36).slice(2,8),B=`mylikita_ref_${t.facilityId}_${f}`,de=M(),y=l("form",{className:"mylikita-widget__form"}),D=l("h3",{className:"mylikita-widget__title",text:r.title}),j=l("p",{className:"mylikita-widget__subtitle",text:r.subtitle}),R=l("div",{className:"mylikita-widget__error",attrs:{role:"alert"}}),H=x("name",r.name,{required:!0}),F=l("div",{className:"mylikita-widget__row"}),L=x("phone",r.phone,{type:"tel",inputmode:"tel"}),$=x("email",r.email,{type:"email"});F.append(L.wrap,$.wrap);let g=V("provider",r.provider,[{value:"",label:r.noPreference},...t.providers.map(n=>({value:n.external_id,label:n.label||n.name||n.external_id}))]);function me(n){let s=g.input.value;g.input.replaceChildren();let c=document.createElement("option");c.value="",c.textContent=r.noPreference,g.input.append(c);for(let m of n||[]){let p=document.createElement("option");p.value=m.external_id,p.textContent=m.label||m.name||m.external_id,g.input.append(p)}s&&(g.input.value=s)}let Q=t.services.length?V("service",r.service,[{value:"",label:"\u2014"},...t.services.map(n=>({value:n,label:n}))]):x("service",r.service),v=x("datetime",r.datetime,{type:"datetime-local",required:!0});v.input.min=se(new Date);let J=V("visitType",r.visitType,[{value:"physical",label:r.visitPhysical},{value:"telemedicine",label:r.visitTelemedicine},{value:"home_visit",label:r.visitHome}]),X=x("notes",r.notes,{type:"textarea",maxlength:500}),w=l("button",{className:"mylikita-widget__submit",type:"submit",text:r.submit}),ee=l("div");ee.append(w);let U=l("p",{className:"mylikita-widget__hint"});y.append(R,H.wrap,F,g.wrap,Q.wrap,v.wrap,J.wrap,X.wrap,ee,U);let b=l("div",{className:"mylikita-widget__status",attrs:{"aria-live":"polite"},hidden:!0});y.addEventListener("submit",n=>{n.preventDefault(),!d&&ce()});async function ce(){d=!0,z(null),w.disabled=!0,w.textContent=r.submitting;let n={facility_id:t.facilityId,patient_name:H.input.value.trim(),patient_phone:L.input.value.trim(),patient_email:$.input.value.trim(),provider_external_id:g.input.value||void 0,service_name:Q.input.value.trim()||void 0,appt_datetime:v.input.value,visit_type:J.input.value||"physical",duration_mins:t.durationMins||void 0,notes:X.input.value.trim()||void 0};if(!n.patient_name)return k(r.requiredName);if(!n.patient_phone&&!n.patient_email)return k(r.requiredPhoneOrEmail);if(!n.appt_datetime||Number.isNaN(Date.parse(n.appt_datetime)))return k(r.requiredDatetime);let s=Se(B);if(!s){s=t.externalRef();try{sessionStorage.setItem(B,s)}catch{}}n.external_ref=s,u=new AbortController;let c=null;try{let m=await I({relayUrl:t.relayUrl,websiteKey:t.websiteKey,payload:n,signal:u.signal});c=m.booking_ref,m.duplicate?(U.textContent="",te("pending_confirmation",c,"We found an existing booking request for this slot \u2014 checking it\u2026")):(t.onBooking&&_(t.onBooking,m,n),te("pending_confirmation",c,null)),ue(c)}catch(m){if(!o||m?.name==="AbortError")return;let p=m.status===429?r.rateLimited:m.message||r.networkError;t.onError&&_(t.onError,m),k(p)}}async function ue(n){let s;try{s=await T(()=>E({relayUrl:t.relayUrl,websiteKey:t.websiteKey,bookingRef:n,signal:u.signal}),{intervalMs:t.pollIntervalMs,maxTries:t.maxTries,signal:u.signal})}catch(c){if(!o||c?.name==="AbortError")return;t.onError&&_(t.onError,c),h("poll_error",n,c.message||r.networkError),d=!1;return}if(!(!o||s.status==="aborted")){if(t.onStatus&&s.data&&_(t.onStatus,s.data),s.resolved){try{sessionStorage.removeItem(B)}catch{}h(s.status,n)}else h("pending_confirmation",n);d=!1}}function te(n,s,c){y.hidden=!0,D.hidden=!0,j.hidden=!0,b.hidden=!1,h(n,s,c)}function h(n,s,c){let m=C(n),p=l("div",{className:`mylikita-widget__status-icon ${m.kind}`});p.textContent=Ie(m.kind);let pe=l("p",{className:"mylikita-widget__status-title",text:m.title}),fe=l("p",{className:"mylikita-widget__status-message",text:c||m.message}),ge=l("p",{className:"mylikita-widget__status-ref",text:s?`Booking ref: ${s}`:""}),re=l("button",{className:"mylikita-widget__link-btn",type:"button",text:r.bookAnother});re.addEventListener("click",()=>ie()),b.replaceChildren(p,pe,fe,ge,re)}function k(n){d=!1,w.disabled=!1,w.textContent=r.submit,z(n)}function z(n){R.textContent=n||"",R.classList.toggle("visible",!!n)}function ie(){try{sessionStorage.removeItem(`mylikita_ref_${t.facilityId}`)}catch{}y.reset(),z(null),b.replaceChildren(),b.hidden=!0,y.hidden=!1,D.hidden=!1,j.hidden=!1,U.textContent="",d=!1,w.disabled=!1,w.textContent=r.submit,v.input.min=se(new Date)}a.replaceChildren(de,D,j,y,b);let W=null;if(t.loadProviders){let n=new AbortController;(async()=>{try{let s=await S({relayUrl:t.relayUrl,websiteKey:t.websiteKey,signal:n.signal});o&&!n.signal.aborted&&me(s)}catch(s){if(!o||s?.name==="AbortError")return;t.onError&&_(t.onError,s)}})(),W=()=>n.abort()}return{destroy(){o=!1,u&&u.abort(),W&&W(),a.replaceChildren(),a.classList.remove("mylikita-widget")},reset:ie,getForm(){return{name:H.input.value,phone:L.input.value,email:$.input.value}}}}function x(i,e,{type:t="text",required:r=!1,maxlength:a,inputmode:o}={}){let d=l("div",{className:"mylikita-widget__field"}),u=l("label",{className:"mylikita-widget__label",attrs:{for:`mlw-${i}`}});u.append(document.createTextNode(e)),r&&u.append(l("span",{className:"req",text:" *"}));let f;return t==="textarea"?f=l("textarea",{className:"mylikita-widget__textarea",attrs:{id:`mlw-${i}`,rows:3,maxlength:a||""}}):f=l("input",{className:"mylikita-widget__input",attrs:{id:`mlw-${i}`,type:t,inputmode:o||""}}),r&&f.setAttribute("required",""),d.append(u,f),{wrap:d,input:f}}function V(i,e,t){let r=l("div",{className:"mylikita-widget__field"}),a=l("label",{className:"mylikita-widget__label",attrs:{for:`mlw-${i}`}});a.textContent=e;let o=l("select",{className:"mylikita-widget__select",attrs:{id:`mlw-${i}`}});for(let d of t){let u=l("option",{text:d.label});u.value=d.value,o.append(u)}return r.append(a,o),{wrap:r,input:o}}function l(i,{className:e,text:t,attrs:r={}}={}){let a=document.createElement(i);e&&(a.className=e),t!==void 0&&(a.textContent=t);for(let[o,d]of Object.entries(r))d===void 0||d===""||a.setAttribute(o,d);return a}function Ie(i){return i==="success"?"\u2713":i==="danger"?"!":"\u2026"}function Se(i){try{return sessionStorage.getItem(i)||null}catch{return null}}function _(i,...e){try{i(...e)}catch{}}function se(i){let e=t=>String(t).padStart(2,"0");return`${i.getFullYear()}-${e(i.getMonth()+1)}-${e(i.getDate())}T${e(i.getHours())}:${e(i.getMinutes())}`}function Ee(){if(document.getElementById(oe))return;let i=document.createElement("style");i.id=oe,i.textContent=ne,document.head.appendChild(i)}function Te(i,e){for(let[t,r]of Object.entries(P(e)))i.style.setProperty(t,r)}return ve(Ae);})();
|
|
214
235
|
|
|
215
236
|
</script>
|
|
216
237
|
<script>
|
|
@@ -290,7 +311,7 @@ var MyLikitaBookingWidget=(()=>{var Y=Object.defineProperty;var pe=Object.getOwn
|
|
|
290
311
|
MyLikitaBookingWidget.createBookingWidget(document.getElementById('booking'), {
|
|
291
312
|
// The in-page mock relay overrides fetch, so this URL is never actually
|
|
292
313
|
// contacted — the widget still requires a non-empty relay URL.
|
|
293
|
-
relayUrl: 'https://api.mylikita.
|
|
314
|
+
relayUrl: 'https://api.mylikita.com',
|
|
294
315
|
websiteKey: KEY,
|
|
295
316
|
facilityId: FACILITY,
|
|
296
317
|
providers: [{ external_id: 'dr-khalil', label: 'Dr. Khalil' }, { external_id: 'dr-amina', label: 'Dr. Amina' }],
|
|
@@ -302,7 +323,7 @@ var MyLikitaBookingWidget=(()=>{var Y=Object.defineProperty;var pe=Object.getOwn
|
|
|
302
323
|
});
|
|
303
324
|
|
|
304
325
|
const w2 = MyLikitaBookingWidget.createBookingWidget(document.getElementById('booking2'), {
|
|
305
|
-
relayUrl: 'https://api.mylikita.
|
|
326
|
+
relayUrl: 'https://api.mylikita.com',
|
|
306
327
|
websiteKey: KEY,
|
|
307
328
|
facilityId: FACILITY,
|
|
308
329
|
// Phase C2/C3: no static providers — load them from the relay's
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! @mylikita/booking-widget v0.1.
|
|
1
|
+
/*! @mylikita/booking-widget v0.1.2 | MIT */
|
|
2
2
|
var __defProp = Object.defineProperty;
|
|
3
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -21,10 +21,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
DEFAULT_THEME: () => DEFAULT_THEME,
|
|
24
|
+
MYLIKITA_MARK_DATA_URI: () => MYLIKITA_MARK_DATA_URI,
|
|
24
25
|
STATUS_COPY: () => STATUS_COPY,
|
|
25
26
|
TERMINAL_STATUSES: () => TERMINAL_STATUSES,
|
|
26
27
|
createBooking: () => createBooking,
|
|
27
28
|
createBookingWidget: () => createBookingWidget,
|
|
29
|
+
createBrandHeader: () => createBrandHeader,
|
|
28
30
|
fetchProviders: () => fetchProviders,
|
|
29
31
|
fetchStatus: () => fetchStatus,
|
|
30
32
|
newExternalRef: () => newExternalRef,
|
|
@@ -226,6 +228,27 @@ var STYLES = `
|
|
|
226
228
|
.mylikita-widget *::before,
|
|
227
229
|
.mylikita-widget *::after { box-sizing: border-box; }
|
|
228
230
|
|
|
231
|
+
.mylikita-widget__brand {
|
|
232
|
+
display: flex;
|
|
233
|
+
align-items: center;
|
|
234
|
+
gap: 8px;
|
|
235
|
+
margin-bottom: 14px;
|
|
236
|
+
padding-bottom: 12px;
|
|
237
|
+
border-bottom: 1px solid var(--mlw-border);
|
|
238
|
+
}
|
|
239
|
+
.mylikita-widget__brand-mark {
|
|
240
|
+
display: block;
|
|
241
|
+
flex: 0 0 auto;
|
|
242
|
+
border-radius: 5px;
|
|
243
|
+
box-shadow: 0 1px 3px rgba(15, 23, 42, .12);
|
|
244
|
+
}
|
|
245
|
+
.mylikita-widget__brand-word {
|
|
246
|
+
font-size: 14px;
|
|
247
|
+
font-weight: 700;
|
|
248
|
+
letter-spacing: .3px;
|
|
249
|
+
color: var(--mlw-muted);
|
|
250
|
+
}
|
|
251
|
+
|
|
229
252
|
.mylikita-widget__title {
|
|
230
253
|
font-size: 18px;
|
|
231
254
|
font-weight: 700;
|
|
@@ -341,6 +364,24 @@ var STYLES = `
|
|
|
341
364
|
.mylikita-widget__link-btn:hover { border-color: var(--mlw-primary); color: var(--mlw-primary); }
|
|
342
365
|
`;
|
|
343
366
|
|
|
367
|
+
// src/brand.js
|
|
368
|
+
var MYLIKITA_MARK_DATA_URI = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0ODMgODA0IiB3aWR0aD0iNDgzIiBoZWlnaHQ9IjgwNCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJNeUxpa2l0YSI+CiAgICA8cmVjdCB4PSIxNjEiIHk9IjAiIHdpZHRoPSIxNjEiIGhlaWdodD0iMTYxIiBmaWxsPSIjMDE2OURBIi8+CiAgICA8cmVjdCB4PSIwIiB5PSIxNjAiIHdpZHRoPSIxNjIiIGhlaWdodD0iMTYxIiBmaWxsPSIjMDE2OURBIi8+CiAgICA8cmVjdCB4PSIzMjEiIHk9IjE2MCIgd2lkdGg9IjE2MiIgaGVpZ2h0PSIxNjEiIGZpbGw9IiMwMTY5REEiLz4KICAgIDxyZWN0IHg9IjAiIHk9IjQ4MiIgd2lkdGg9IjE2MiIgaGVpZ2h0PSIxNjIiIGZpbGw9IiMwNDk4RkIiLz4KICAgIDxyZWN0IHg9IjMyMSIgeT0iNDgyIiB3aWR0aD0iMTYyIiBoZWlnaHQ9IjE2MiIgZmlsbD0iIzA0OThGQiIvPgogICAgPHJlY3QgeD0iMTYxIiB5PSI2NDMiIHdpZHRoPSIxNjEiIGhlaWdodD0iMTYxIiBmaWxsPSIjMDQ5OEZCIi8+Cjwvc3ZnPg==";
|
|
369
|
+
function createBrandHeader() {
|
|
370
|
+
const header = document.createElement("div");
|
|
371
|
+
header.className = "mylikita-widget__brand";
|
|
372
|
+
const img = document.createElement("img");
|
|
373
|
+
img.className = "mylikita-widget__brand-mark";
|
|
374
|
+
img.src = MYLIKITA_MARK_DATA_URI;
|
|
375
|
+
img.alt = "MyLikita";
|
|
376
|
+
img.width = 24;
|
|
377
|
+
img.height = 24;
|
|
378
|
+
const word = document.createElement("span");
|
|
379
|
+
word.className = "mylikita-widget__brand-word";
|
|
380
|
+
word.textContent = "MyLikita";
|
|
381
|
+
header.append(img, word);
|
|
382
|
+
return header;
|
|
383
|
+
}
|
|
384
|
+
|
|
344
385
|
// src/widget.js
|
|
345
386
|
var DEFAULT_TEXT = {
|
|
346
387
|
title: "Book an appointment",
|
|
@@ -379,6 +420,9 @@ function createBookingWidget(element, options = {}) {
|
|
|
379
420
|
// (GET /v1/providers) on mount and populate the doctor dropdown. The
|
|
380
421
|
// static `providers` option, when given, always wins and skips the fetch.
|
|
381
422
|
loadProviders: options.loadProviders === true && !(options.providers && options.providers.length),
|
|
423
|
+
// White-label flag: premium hosts can drop the MyLikita brand header.
|
|
424
|
+
// Defaults to true (brand shown); pass showBrand:false to hide it.
|
|
425
|
+
showBrand: options.showBrand !== false,
|
|
382
426
|
pollIntervalMs: options.pollIntervalMs ?? 5e3,
|
|
383
427
|
maxTries: options.maxTries ?? 12,
|
|
384
428
|
text: { ...DEFAULT_TEXT, ...options.text || {} },
|
|
@@ -401,6 +445,7 @@ function createBookingWidget(element, options = {}) {
|
|
|
401
445
|
let pollCtrl = null;
|
|
402
446
|
const instanceId = Math.random().toString(36).slice(2, 8);
|
|
403
447
|
const refKey = `mylikita_ref_${opts.facilityId}_${instanceId}`;
|
|
448
|
+
const brand = opts.showBrand ? createBrandHeader() : null;
|
|
404
449
|
const form = el("form", { className: "mylikita-widget__form" });
|
|
405
450
|
const title = el("h3", { className: "mylikita-widget__title", text: t.title });
|
|
406
451
|
const subtitle = el("p", { className: "mylikita-widget__subtitle", text: t.subtitle });
|
|
@@ -574,7 +619,7 @@ function createBookingWidget(element, options = {}) {
|
|
|
574
619
|
submitBtn.textContent = t.submit;
|
|
575
620
|
datetime.input.min = toLocalInputValue(/* @__PURE__ */ new Date());
|
|
576
621
|
}
|
|
577
|
-
root.replaceChildren(title, subtitle, form, statusView);
|
|
622
|
+
root.replaceChildren(brand, title, subtitle, form, statusView);
|
|
578
623
|
let destroyProvidersFetch = null;
|
|
579
624
|
if (opts.loadProviders) {
|
|
580
625
|
const provCtrl = new AbortController();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! @mylikita/booking-widget v0.1.
|
|
1
|
+
/*! @mylikita/booking-widget v0.1.2 | MIT */
|
|
2
2
|
|
|
3
3
|
// src/client.js
|
|
4
4
|
async function createBooking({ relayUrl, websiteKey, payload, signal }) {
|
|
@@ -192,6 +192,27 @@ var STYLES = `
|
|
|
192
192
|
.mylikita-widget *::before,
|
|
193
193
|
.mylikita-widget *::after { box-sizing: border-box; }
|
|
194
194
|
|
|
195
|
+
.mylikita-widget__brand {
|
|
196
|
+
display: flex;
|
|
197
|
+
align-items: center;
|
|
198
|
+
gap: 8px;
|
|
199
|
+
margin-bottom: 14px;
|
|
200
|
+
padding-bottom: 12px;
|
|
201
|
+
border-bottom: 1px solid var(--mlw-border);
|
|
202
|
+
}
|
|
203
|
+
.mylikita-widget__brand-mark {
|
|
204
|
+
display: block;
|
|
205
|
+
flex: 0 0 auto;
|
|
206
|
+
border-radius: 5px;
|
|
207
|
+
box-shadow: 0 1px 3px rgba(15, 23, 42, .12);
|
|
208
|
+
}
|
|
209
|
+
.mylikita-widget__brand-word {
|
|
210
|
+
font-size: 14px;
|
|
211
|
+
font-weight: 700;
|
|
212
|
+
letter-spacing: .3px;
|
|
213
|
+
color: var(--mlw-muted);
|
|
214
|
+
}
|
|
215
|
+
|
|
195
216
|
.mylikita-widget__title {
|
|
196
217
|
font-size: 18px;
|
|
197
218
|
font-weight: 700;
|
|
@@ -307,6 +328,24 @@ var STYLES = `
|
|
|
307
328
|
.mylikita-widget__link-btn:hover { border-color: var(--mlw-primary); color: var(--mlw-primary); }
|
|
308
329
|
`;
|
|
309
330
|
|
|
331
|
+
// src/brand.js
|
|
332
|
+
var MYLIKITA_MARK_DATA_URI = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0ODMgODA0IiB3aWR0aD0iNDgzIiBoZWlnaHQ9IjgwNCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJNeUxpa2l0YSI+CiAgICA8cmVjdCB4PSIxNjEiIHk9IjAiIHdpZHRoPSIxNjEiIGhlaWdodD0iMTYxIiBmaWxsPSIjMDE2OURBIi8+CiAgICA8cmVjdCB4PSIwIiB5PSIxNjAiIHdpZHRoPSIxNjIiIGhlaWdodD0iMTYxIiBmaWxsPSIjMDE2OURBIi8+CiAgICA8cmVjdCB4PSIzMjEiIHk9IjE2MCIgd2lkdGg9IjE2MiIgaGVpZ2h0PSIxNjEiIGZpbGw9IiMwMTY5REEiLz4KICAgIDxyZWN0IHg9IjAiIHk9IjQ4MiIgd2lkdGg9IjE2MiIgaGVpZ2h0PSIxNjIiIGZpbGw9IiMwNDk4RkIiLz4KICAgIDxyZWN0IHg9IjMyMSIgeT0iNDgyIiB3aWR0aD0iMTYyIiBoZWlnaHQ9IjE2MiIgZmlsbD0iIzA0OThGQiIvPgogICAgPHJlY3QgeD0iMTYxIiB5PSI2NDMiIHdpZHRoPSIxNjEiIGhlaWdodD0iMTYxIiBmaWxsPSIjMDQ5OEZCIi8+Cjwvc3ZnPg==";
|
|
333
|
+
function createBrandHeader() {
|
|
334
|
+
const header = document.createElement("div");
|
|
335
|
+
header.className = "mylikita-widget__brand";
|
|
336
|
+
const img = document.createElement("img");
|
|
337
|
+
img.className = "mylikita-widget__brand-mark";
|
|
338
|
+
img.src = MYLIKITA_MARK_DATA_URI;
|
|
339
|
+
img.alt = "MyLikita";
|
|
340
|
+
img.width = 24;
|
|
341
|
+
img.height = 24;
|
|
342
|
+
const word = document.createElement("span");
|
|
343
|
+
word.className = "mylikita-widget__brand-word";
|
|
344
|
+
word.textContent = "MyLikita";
|
|
345
|
+
header.append(img, word);
|
|
346
|
+
return header;
|
|
347
|
+
}
|
|
348
|
+
|
|
310
349
|
// src/widget.js
|
|
311
350
|
var DEFAULT_TEXT = {
|
|
312
351
|
title: "Book an appointment",
|
|
@@ -345,6 +384,9 @@ function createBookingWidget(element, options = {}) {
|
|
|
345
384
|
// (GET /v1/providers) on mount and populate the doctor dropdown. The
|
|
346
385
|
// static `providers` option, when given, always wins and skips the fetch.
|
|
347
386
|
loadProviders: options.loadProviders === true && !(options.providers && options.providers.length),
|
|
387
|
+
// White-label flag: premium hosts can drop the MyLikita brand header.
|
|
388
|
+
// Defaults to true (brand shown); pass showBrand:false to hide it.
|
|
389
|
+
showBrand: options.showBrand !== false,
|
|
348
390
|
pollIntervalMs: options.pollIntervalMs ?? 5e3,
|
|
349
391
|
maxTries: options.maxTries ?? 12,
|
|
350
392
|
text: { ...DEFAULT_TEXT, ...options.text || {} },
|
|
@@ -367,6 +409,7 @@ function createBookingWidget(element, options = {}) {
|
|
|
367
409
|
let pollCtrl = null;
|
|
368
410
|
const instanceId = Math.random().toString(36).slice(2, 8);
|
|
369
411
|
const refKey = `mylikita_ref_${opts.facilityId}_${instanceId}`;
|
|
412
|
+
const brand = opts.showBrand ? createBrandHeader() : null;
|
|
370
413
|
const form = el("form", { className: "mylikita-widget__form" });
|
|
371
414
|
const title = el("h3", { className: "mylikita-widget__title", text: t.title });
|
|
372
415
|
const subtitle = el("p", { className: "mylikita-widget__subtitle", text: t.subtitle });
|
|
@@ -540,7 +583,7 @@ function createBookingWidget(element, options = {}) {
|
|
|
540
583
|
submitBtn.textContent = t.submit;
|
|
541
584
|
datetime.input.min = toLocalInputValue(/* @__PURE__ */ new Date());
|
|
542
585
|
}
|
|
543
|
-
root.replaceChildren(title, subtitle, form, statusView);
|
|
586
|
+
root.replaceChildren(brand, title, subtitle, form, statusView);
|
|
544
587
|
let destroyProvidersFetch = null;
|
|
545
588
|
if (opts.loadProviders) {
|
|
546
589
|
const provCtrl = new AbortController();
|
|
@@ -645,10 +688,12 @@ function applyTheme(rootNode, theme) {
|
|
|
645
688
|
}
|
|
646
689
|
export {
|
|
647
690
|
DEFAULT_THEME,
|
|
691
|
+
MYLIKITA_MARK_DATA_URI,
|
|
648
692
|
STATUS_COPY,
|
|
649
693
|
TERMINAL_STATUSES,
|
|
650
694
|
createBooking,
|
|
651
695
|
createBookingWidget,
|
|
696
|
+
createBrandHeader,
|
|
652
697
|
fetchProviders,
|
|
653
698
|
fetchStatus,
|
|
654
699
|
newExternalRef,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! @mylikita/booking-widget v0.1.
|
|
1
|
+
/*! @mylikita/booking-widget v0.1.2 | MIT */
|
|
2
2
|
var MyLikitaBookingWidget = (() => {
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -22,10 +22,12 @@ var MyLikitaBookingWidget = (() => {
|
|
|
22
22
|
var index_exports = {};
|
|
23
23
|
__export(index_exports, {
|
|
24
24
|
DEFAULT_THEME: () => DEFAULT_THEME,
|
|
25
|
+
MYLIKITA_MARK_DATA_URI: () => MYLIKITA_MARK_DATA_URI,
|
|
25
26
|
STATUS_COPY: () => STATUS_COPY,
|
|
26
27
|
TERMINAL_STATUSES: () => TERMINAL_STATUSES,
|
|
27
28
|
createBooking: () => createBooking,
|
|
28
29
|
createBookingWidget: () => createBookingWidget,
|
|
30
|
+
createBrandHeader: () => createBrandHeader,
|
|
29
31
|
fetchProviders: () => fetchProviders,
|
|
30
32
|
fetchStatus: () => fetchStatus,
|
|
31
33
|
newExternalRef: () => newExternalRef,
|
|
@@ -226,6 +228,27 @@ var MyLikitaBookingWidget = (() => {
|
|
|
226
228
|
.mylikita-widget *::before,
|
|
227
229
|
.mylikita-widget *::after { box-sizing: border-box; }
|
|
228
230
|
|
|
231
|
+
.mylikita-widget__brand {
|
|
232
|
+
display: flex;
|
|
233
|
+
align-items: center;
|
|
234
|
+
gap: 8px;
|
|
235
|
+
margin-bottom: 14px;
|
|
236
|
+
padding-bottom: 12px;
|
|
237
|
+
border-bottom: 1px solid var(--mlw-border);
|
|
238
|
+
}
|
|
239
|
+
.mylikita-widget__brand-mark {
|
|
240
|
+
display: block;
|
|
241
|
+
flex: 0 0 auto;
|
|
242
|
+
border-radius: 5px;
|
|
243
|
+
box-shadow: 0 1px 3px rgba(15, 23, 42, .12);
|
|
244
|
+
}
|
|
245
|
+
.mylikita-widget__brand-word {
|
|
246
|
+
font-size: 14px;
|
|
247
|
+
font-weight: 700;
|
|
248
|
+
letter-spacing: .3px;
|
|
249
|
+
color: var(--mlw-muted);
|
|
250
|
+
}
|
|
251
|
+
|
|
229
252
|
.mylikita-widget__title {
|
|
230
253
|
font-size: 18px;
|
|
231
254
|
font-weight: 700;
|
|
@@ -341,6 +364,24 @@ var MyLikitaBookingWidget = (() => {
|
|
|
341
364
|
.mylikita-widget__link-btn:hover { border-color: var(--mlw-primary); color: var(--mlw-primary); }
|
|
342
365
|
`;
|
|
343
366
|
|
|
367
|
+
// src/brand.js
|
|
368
|
+
var MYLIKITA_MARK_DATA_URI = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0ODMgODA0IiB3aWR0aD0iNDgzIiBoZWlnaHQ9IjgwNCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJNeUxpa2l0YSI+CiAgICA8cmVjdCB4PSIxNjEiIHk9IjAiIHdpZHRoPSIxNjEiIGhlaWdodD0iMTYxIiBmaWxsPSIjMDE2OURBIi8+CiAgICA8cmVjdCB4PSIwIiB5PSIxNjAiIHdpZHRoPSIxNjIiIGhlaWdodD0iMTYxIiBmaWxsPSIjMDE2OURBIi8+CiAgICA8cmVjdCB4PSIzMjEiIHk9IjE2MCIgd2lkdGg9IjE2MiIgaGVpZ2h0PSIxNjEiIGZpbGw9IiMwMTY5REEiLz4KICAgIDxyZWN0IHg9IjAiIHk9IjQ4MiIgd2lkdGg9IjE2MiIgaGVpZ2h0PSIxNjIiIGZpbGw9IiMwNDk4RkIiLz4KICAgIDxyZWN0IHg9IjMyMSIgeT0iNDgyIiB3aWR0aD0iMTYyIiBoZWlnaHQ9IjE2MiIgZmlsbD0iIzA0OThGQiIvPgogICAgPHJlY3QgeD0iMTYxIiB5PSI2NDMiIHdpZHRoPSIxNjEiIGhlaWdodD0iMTYxIiBmaWxsPSIjMDQ5OEZCIi8+Cjwvc3ZnPg==";
|
|
369
|
+
function createBrandHeader() {
|
|
370
|
+
const header = document.createElement("div");
|
|
371
|
+
header.className = "mylikita-widget__brand";
|
|
372
|
+
const img = document.createElement("img");
|
|
373
|
+
img.className = "mylikita-widget__brand-mark";
|
|
374
|
+
img.src = MYLIKITA_MARK_DATA_URI;
|
|
375
|
+
img.alt = "MyLikita";
|
|
376
|
+
img.width = 24;
|
|
377
|
+
img.height = 24;
|
|
378
|
+
const word = document.createElement("span");
|
|
379
|
+
word.className = "mylikita-widget__brand-word";
|
|
380
|
+
word.textContent = "MyLikita";
|
|
381
|
+
header.append(img, word);
|
|
382
|
+
return header;
|
|
383
|
+
}
|
|
384
|
+
|
|
344
385
|
// src/widget.js
|
|
345
386
|
var DEFAULT_TEXT = {
|
|
346
387
|
title: "Book an appointment",
|
|
@@ -379,6 +420,9 @@ var MyLikitaBookingWidget = (() => {
|
|
|
379
420
|
// (GET /v1/providers) on mount and populate the doctor dropdown. The
|
|
380
421
|
// static `providers` option, when given, always wins and skips the fetch.
|
|
381
422
|
loadProviders: options.loadProviders === true && !(options.providers && options.providers.length),
|
|
423
|
+
// White-label flag: premium hosts can drop the MyLikita brand header.
|
|
424
|
+
// Defaults to true (brand shown); pass showBrand:false to hide it.
|
|
425
|
+
showBrand: options.showBrand !== false,
|
|
382
426
|
pollIntervalMs: options.pollIntervalMs ?? 5e3,
|
|
383
427
|
maxTries: options.maxTries ?? 12,
|
|
384
428
|
text: { ...DEFAULT_TEXT, ...options.text || {} },
|
|
@@ -401,6 +445,7 @@ var MyLikitaBookingWidget = (() => {
|
|
|
401
445
|
let pollCtrl = null;
|
|
402
446
|
const instanceId = Math.random().toString(36).slice(2, 8);
|
|
403
447
|
const refKey = `mylikita_ref_${opts.facilityId}_${instanceId}`;
|
|
448
|
+
const brand = opts.showBrand ? createBrandHeader() : null;
|
|
404
449
|
const form = el("form", { className: "mylikita-widget__form" });
|
|
405
450
|
const title = el("h3", { className: "mylikita-widget__title", text: t.title });
|
|
406
451
|
const subtitle = el("p", { className: "mylikita-widget__subtitle", text: t.subtitle });
|
|
@@ -574,7 +619,7 @@ var MyLikitaBookingWidget = (() => {
|
|
|
574
619
|
submitBtn.textContent = t.submit;
|
|
575
620
|
datetime.input.min = toLocalInputValue(/* @__PURE__ */ new Date());
|
|
576
621
|
}
|
|
577
|
-
root.replaceChildren(title, subtitle, form, statusView);
|
|
622
|
+
root.replaceChildren(brand, title, subtitle, form, statusView);
|
|
578
623
|
let destroyProvidersFetch = null;
|
|
579
624
|
if (opts.loadProviders) {
|
|
580
625
|
const provCtrl = new AbortController();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
/*! @mylikita/booking-widget v0.1.
|
|
2
|
-
var MyLikitaBookingWidget=(()=>{var Y=Object.defineProperty;var
|
|
1
|
+
/*! @mylikita/booking-widget v0.1.2 | MIT */
|
|
2
|
+
var MyLikitaBookingWidget=(()=>{var Y=Object.defineProperty;var we=Object.getOwnPropertyDescriptor;var ye=Object.getOwnPropertyNames;var xe=Object.prototype.hasOwnProperty;var be=(i,e)=>{for(var t in e)Y(i,t,{get:e[t],enumerable:!0})},_e=(i,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let a of ye(e))!xe.call(i,a)&&a!==t&&Y(i,a,{get:()=>e[a],enumerable:!(r=we(e,a))||r.enumerable});return i};var ve=i=>_e(Y({},"__esModule",{value:!0}),i);var Ae={};be(Ae,{DEFAULT_THEME:()=>O,MYLIKITA_MARK_DATA_URI:()=>G,STATUS_COPY:()=>N,TERMINAL_STATUSES:()=>ae,createBooking:()=>I,createBookingWidget:()=>le,createBrandHeader:()=>M,fetchProviders:()=>S,fetchStatus:()=>E,newExternalRef:()=>A,pollStatus:()=>T,resolveTheme:()=>P,statusCopy:()=>C});async function I({relayUrl:i,websiteKey:e,payload:t,signal:r}){let a=await fetch(`${q(i)}/v1/bookings`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${e}`},body:JSON.stringify(t),signal:r}),o=await Z(a);if(a.status===409)return{ok:!0,duplicate:!0,booking_ref:o?.booking_ref,error:o?.message};if(!a.ok)throw K(a.status,o,"create");return{ok:!0,duplicate:!1,booking_ref:o?.booking_ref,status:o?.status||"pending_confirmation"}}async function S({relayUrl:i,websiteKey:e,signal:t}){let r=await fetch(`${q(i)}/v1/providers`,{headers:{Authorization:`Bearer ${e}`},signal:t}),a=await Z(r);if(!r.ok)throw K(r.status,a,"providers");return(Array.isArray(a?.providers)?a.providers:[]).map(d=>({external_id:d.external_id,name:d.name||d.external_id,specialty:d.specialty||null,module:d.module||"general"}))}async function E({relayUrl:i,websiteKey:e,bookingRef:t,signal:r}){let a=await fetch(`${q(i)}/v1/bookings/${encodeURIComponent(t)}`,{headers:{Authorization:`Bearer ${e}`},signal:r}),o=await Z(a);if(!a.ok)throw K(a.status,o,"status");return{booking_ref:o?.booking_ref,status:o?.status||"pending_confirmation",appt_ref:o?.appt_ref||null}}async function T(i,{intervalMs:e=5e3,maxTries:t=12,signal:r}={}){for(let a=0;a<t;a++){if(r?.aborted)return{status:"aborted",resolved:!1,data:null};let o=await i();if(o.status!=="pending_confirmation")return{status:o.status,resolved:!0,data:o};a<t-1&&await he(e,r)}return{status:"pending_confirmation",resolved:!1,data:null}}function A(){return`BK-${Date.now()}-${Math.random().toString(36).slice(2,6)}`}function q(i){return String(i||"").replace(/\/+$/,"")}function K(i,e,t){let r=new Error(e?.message||e?.error||`Relay ${t} failed (HTTP ${i})`);return r.code=e?.error||"http_error",r.status=i,r}async function Z(i){try{return await i.json()}catch{return null}}function he(i,e){return new Promise(t=>{if(e?.aborted)return t();let r=()=>{clearTimeout(a),t()},a=setTimeout(()=>{e?.removeEventListener("abort",r),t()},i);e?.addEventListener("abort",r,{once:!0})})}var N={pending_confirmation:{title:"Request received",message:"We've received your booking request \u2014 we'll confirm shortly.",kind:"info"},confirmed:{title:"Appointment confirmed",message:"Your appointment is confirmed. See you at the clinic!",kind:"success"},cancelled:{title:"Appointment cancelled",message:"This appointment was cancelled. Please contact the clinic if this was unexpected.",kind:"danger"},rescheduled:{title:"Appointment rescheduled",message:"This appointment was moved \u2014 the new time was sent to you.",kind:"info"},no_show:{title:"Missed appointment",message:"This appointment was marked as missed.",kind:"danger"},expired:{title:"Request expired",message:"This booking request expired \u2014 please call the clinic to book.",kind:"danger"},poll_error:{title:"Something went wrong",message:"We could not check your booking right now. Please try again shortly.",kind:"danger"}};function C(i){return N[i]||N.pending_confirmation}var ae=["confirmed","cancelled","rescheduled","no_show","expired"];var O={primary:"#0d6efd",primaryDark:"#0b5ed7",primaryText:"#ffffff",bg:"#ffffff",text:"#1e293b",muted:"#64748b",border:"#e2e8f0",danger:"#dc3545",success:"#15803d",radius:10,font:"system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif"};function P(i={}){let e={...O,...i||{}};return{"--mlw-primary":e.primary,"--mlw-primary-dark":e.primaryDark,"--mlw-primary-text":e.primaryText,"--mlw-bg":e.bg,"--mlw-text":e.text,"--mlw-muted":e.muted,"--mlw-border":e.border,"--mlw-danger":e.danger,"--mlw-success":e.success,"--mlw-radius":`${e.radius}px`,"--mlw-font":e.font}}var ne=`
|
|
3
3
|
.mylikita-widget {
|
|
4
4
|
--mlw-primary: #0d6efd;
|
|
5
5
|
--mlw-primary-dark: #0b5ed7;
|
|
@@ -26,6 +26,27 @@ var MyLikitaBookingWidget=(()=>{var Y=Object.defineProperty;var pe=Object.getOwn
|
|
|
26
26
|
.mylikita-widget *::before,
|
|
27
27
|
.mylikita-widget *::after { box-sizing: border-box; }
|
|
28
28
|
|
|
29
|
+
.mylikita-widget__brand {
|
|
30
|
+
display: flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
gap: 8px;
|
|
33
|
+
margin-bottom: 14px;
|
|
34
|
+
padding-bottom: 12px;
|
|
35
|
+
border-bottom: 1px solid var(--mlw-border);
|
|
36
|
+
}
|
|
37
|
+
.mylikita-widget__brand-mark {
|
|
38
|
+
display: block;
|
|
39
|
+
flex: 0 0 auto;
|
|
40
|
+
border-radius: 5px;
|
|
41
|
+
box-shadow: 0 1px 3px rgba(15, 23, 42, .12);
|
|
42
|
+
}
|
|
43
|
+
.mylikita-widget__brand-word {
|
|
44
|
+
font-size: 14px;
|
|
45
|
+
font-weight: 700;
|
|
46
|
+
letter-spacing: .3px;
|
|
47
|
+
color: var(--mlw-muted);
|
|
48
|
+
}
|
|
49
|
+
|
|
29
50
|
.mylikita-widget__title {
|
|
30
51
|
font-size: 18px;
|
|
31
52
|
font-weight: 700;
|
|
@@ -139,4 +160,4 @@ var MyLikitaBookingWidget=(()=>{var Y=Object.defineProperty;var pe=Object.getOwn
|
|
|
139
160
|
cursor: pointer;
|
|
140
161
|
}
|
|
141
162
|
.mylikita-widget__link-btn:hover { border-color: var(--mlw-primary); color: var(--mlw-primary); }
|
|
142
|
-
`;var
|
|
163
|
+
`;var G="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0ODMgODA0IiB3aWR0aD0iNDgzIiBoZWlnaHQ9IjgwNCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJNeUxpa2l0YSI+CiAgICA8cmVjdCB4PSIxNjEiIHk9IjAiIHdpZHRoPSIxNjEiIGhlaWdodD0iMTYxIiBmaWxsPSIjMDE2OURBIi8+CiAgICA8cmVjdCB4PSIwIiB5PSIxNjAiIHdpZHRoPSIxNjIiIGhlaWdodD0iMTYxIiBmaWxsPSIjMDE2OURBIi8+CiAgICA8cmVjdCB4PSIzMjEiIHk9IjE2MCIgd2lkdGg9IjE2MiIgaGVpZ2h0PSIxNjEiIGZpbGw9IiMwMTY5REEiLz4KICAgIDxyZWN0IHg9IjAiIHk9IjQ4MiIgd2lkdGg9IjE2MiIgaGVpZ2h0PSIxNjIiIGZpbGw9IiMwNDk4RkIiLz4KICAgIDxyZWN0IHg9IjMyMSIgeT0iNDgyIiB3aWR0aD0iMTYyIiBoZWlnaHQ9IjE2MiIgZmlsbD0iIzA0OThGQiIvPgogICAgPHJlY3QgeD0iMTYxIiB5PSI2NDMiIHdpZHRoPSIxNjEiIGhlaWdodD0iMTYxIiBmaWxsPSIjMDQ5OEZCIi8+Cjwvc3ZnPg==";function M(){let i=document.createElement("div");i.className="mylikita-widget__brand";let e=document.createElement("img");e.className="mylikita-widget__brand-mark",e.src=G,e.alt="MyLikita",e.width=24,e.height=24;let t=document.createElement("span");return t.className="mylikita-widget__brand-word",t.textContent="MyLikita",i.append(e,t),i}var ke={title:"Book an appointment",subtitle:"Request a slot and we will confirm shortly.",name:"Full name",phone:"Phone number",email:"Email address",provider:"Preferred doctor (optional)",noPreference:"No preference",service:"Service (optional)",datetime:"Preferred date & time",visitType:"Appointment type",visitPhysical:"In person",visitTelemedicine:"Video call",visitHome:"Home visit",notes:"Notes (optional)",submit:"Request appointment",submitting:"Submitting\u2026",bookAnother:"Book another appointment",requiredPhoneOrEmail:"Please provide a phone number or an email address.",requiredName:"Please enter your name.",requiredDatetime:"Please choose a date and time.",networkError:"Could not reach the booking service. Please try again.",rateLimited:"Too many requests \u2014 please wait a moment and try again."},oe="mylikita-widget-styles";function le(i,e={}){if(!i)throw new Error("createBookingWidget: a container element is required");let t={relayUrl:e.relayUrl,websiteKey:e.websiteKey,facilityId:e.facilityId,providers:e.providers||[],services:e.services||[],loadProviders:e.loadProviders===!0&&!(e.providers&&e.providers.length),showBrand:e.showBrand!==!1,pollIntervalMs:e.pollIntervalMs??5e3,maxTries:e.maxTries??12,text:{...ke,...e.text||{}},theme:e.theme||{},onStatus:typeof e.onStatus=="function"?e.onStatus:null,onError:typeof e.onError=="function"?e.onError:null,onBooking:typeof e.onBooking=="function"?e.onBooking:null,externalRef:typeof e.externalRef=="function"?e.externalRef:A};if(!t.relayUrl||!t.websiteKey||!t.facilityId)throw new Error("createBookingWidget: relayUrl, websiteKey and facilityId are required");Ee();let r=t.text,a=i;a.classList.add("mylikita-widget"),Te(a,t.theme);let o=!0,d=!1,u=null,f=Math.random().toString(36).slice(2,8),B=`mylikita_ref_${t.facilityId}_${f}`,de=t.showBrand?M():null,y=l("form",{className:"mylikita-widget__form"}),D=l("h3",{className:"mylikita-widget__title",text:r.title}),j=l("p",{className:"mylikita-widget__subtitle",text:r.subtitle}),R=l("div",{className:"mylikita-widget__error",attrs:{role:"alert"}}),H=x("name",r.name,{required:!0}),F=l("div",{className:"mylikita-widget__row"}),L=x("phone",r.phone,{type:"tel",inputmode:"tel"}),$=x("email",r.email,{type:"email"});F.append(L.wrap,$.wrap);let g=V("provider",r.provider,[{value:"",label:r.noPreference},...t.providers.map(n=>({value:n.external_id,label:n.label||n.name||n.external_id}))]);function me(n){let s=g.input.value;g.input.replaceChildren();let c=document.createElement("option");c.value="",c.textContent=r.noPreference,g.input.append(c);for(let m of n||[]){let p=document.createElement("option");p.value=m.external_id,p.textContent=m.label||m.name||m.external_id,g.input.append(p)}s&&(g.input.value=s)}let Q=t.services.length?V("service",r.service,[{value:"",label:"\u2014"},...t.services.map(n=>({value:n,label:n}))]):x("service",r.service),v=x("datetime",r.datetime,{type:"datetime-local",required:!0});v.input.min=se(new Date);let J=V("visitType",r.visitType,[{value:"physical",label:r.visitPhysical},{value:"telemedicine",label:r.visitTelemedicine},{value:"home_visit",label:r.visitHome}]),X=x("notes",r.notes,{type:"textarea",maxlength:500}),w=l("button",{className:"mylikita-widget__submit",type:"submit",text:r.submit}),ee=l("div");ee.append(w);let U=l("p",{className:"mylikita-widget__hint"});y.append(R,H.wrap,F,g.wrap,Q.wrap,v.wrap,J.wrap,X.wrap,ee,U);let b=l("div",{className:"mylikita-widget__status",attrs:{"aria-live":"polite"},hidden:!0});y.addEventListener("submit",n=>{n.preventDefault(),!d&&ce()});async function ce(){d=!0,z(null),w.disabled=!0,w.textContent=r.submitting;let n={facility_id:t.facilityId,patient_name:H.input.value.trim(),patient_phone:L.input.value.trim(),patient_email:$.input.value.trim(),provider_external_id:g.input.value||void 0,service_name:Q.input.value.trim()||void 0,appt_datetime:v.input.value,visit_type:J.input.value||"physical",duration_mins:t.durationMins||void 0,notes:X.input.value.trim()||void 0};if(!n.patient_name)return k(r.requiredName);if(!n.patient_phone&&!n.patient_email)return k(r.requiredPhoneOrEmail);if(!n.appt_datetime||Number.isNaN(Date.parse(n.appt_datetime)))return k(r.requiredDatetime);let s=Se(B);if(!s){s=t.externalRef();try{sessionStorage.setItem(B,s)}catch{}}n.external_ref=s,u=new AbortController;let c=null;try{let m=await I({relayUrl:t.relayUrl,websiteKey:t.websiteKey,payload:n,signal:u.signal});c=m.booking_ref,m.duplicate?(U.textContent="",te("pending_confirmation",c,"We found an existing booking request for this slot \u2014 checking it\u2026")):(t.onBooking&&_(t.onBooking,m,n),te("pending_confirmation",c,null)),ue(c)}catch(m){if(!o||m?.name==="AbortError")return;let p=m.status===429?r.rateLimited:m.message||r.networkError;t.onError&&_(t.onError,m),k(p)}}async function ue(n){let s;try{s=await T(()=>E({relayUrl:t.relayUrl,websiteKey:t.websiteKey,bookingRef:n,signal:u.signal}),{intervalMs:t.pollIntervalMs,maxTries:t.maxTries,signal:u.signal})}catch(c){if(!o||c?.name==="AbortError")return;t.onError&&_(t.onError,c),h("poll_error",n,c.message||r.networkError),d=!1;return}if(!(!o||s.status==="aborted")){if(t.onStatus&&s.data&&_(t.onStatus,s.data),s.resolved){try{sessionStorage.removeItem(B)}catch{}h(s.status,n)}else h("pending_confirmation",n);d=!1}}function te(n,s,c){y.hidden=!0,D.hidden=!0,j.hidden=!0,b.hidden=!1,h(n,s,c)}function h(n,s,c){let m=C(n),p=l("div",{className:`mylikita-widget__status-icon ${m.kind}`});p.textContent=Ie(m.kind);let pe=l("p",{className:"mylikita-widget__status-title",text:m.title}),fe=l("p",{className:"mylikita-widget__status-message",text:c||m.message}),ge=l("p",{className:"mylikita-widget__status-ref",text:s?`Booking ref: ${s}`:""}),re=l("button",{className:"mylikita-widget__link-btn",type:"button",text:r.bookAnother});re.addEventListener("click",()=>ie()),b.replaceChildren(p,pe,fe,ge,re)}function k(n){d=!1,w.disabled=!1,w.textContent=r.submit,z(n)}function z(n){R.textContent=n||"",R.classList.toggle("visible",!!n)}function ie(){try{sessionStorage.removeItem(`mylikita_ref_${t.facilityId}`)}catch{}y.reset(),z(null),b.replaceChildren(),b.hidden=!0,y.hidden=!1,D.hidden=!1,j.hidden=!1,U.textContent="",d=!1,w.disabled=!1,w.textContent=r.submit,v.input.min=se(new Date)}a.replaceChildren(de,D,j,y,b);let W=null;if(t.loadProviders){let n=new AbortController;(async()=>{try{let s=await S({relayUrl:t.relayUrl,websiteKey:t.websiteKey,signal:n.signal});o&&!n.signal.aborted&&me(s)}catch(s){if(!o||s?.name==="AbortError")return;t.onError&&_(t.onError,s)}})(),W=()=>n.abort()}return{destroy(){o=!1,u&&u.abort(),W&&W(),a.replaceChildren(),a.classList.remove("mylikita-widget")},reset:ie,getForm(){return{name:H.input.value,phone:L.input.value,email:$.input.value}}}}function x(i,e,{type:t="text",required:r=!1,maxlength:a,inputmode:o}={}){let d=l("div",{className:"mylikita-widget__field"}),u=l("label",{className:"mylikita-widget__label",attrs:{for:`mlw-${i}`}});u.append(document.createTextNode(e)),r&&u.append(l("span",{className:"req",text:" *"}));let f;return t==="textarea"?f=l("textarea",{className:"mylikita-widget__textarea",attrs:{id:`mlw-${i}`,rows:3,maxlength:a||""}}):f=l("input",{className:"mylikita-widget__input",attrs:{id:`mlw-${i}`,type:t,inputmode:o||""}}),r&&f.setAttribute("required",""),d.append(u,f),{wrap:d,input:f}}function V(i,e,t){let r=l("div",{className:"mylikita-widget__field"}),a=l("label",{className:"mylikita-widget__label",attrs:{for:`mlw-${i}`}});a.textContent=e;let o=l("select",{className:"mylikita-widget__select",attrs:{id:`mlw-${i}`}});for(let d of t){let u=l("option",{text:d.label});u.value=d.value,o.append(u)}return r.append(a,o),{wrap:r,input:o}}function l(i,{className:e,text:t,attrs:r={}}={}){let a=document.createElement(i);e&&(a.className=e),t!==void 0&&(a.textContent=t);for(let[o,d]of Object.entries(r))d===void 0||d===""||a.setAttribute(o,d);return a}function Ie(i){return i==="success"?"\u2713":i==="danger"?"!":"\u2026"}function Se(i){try{return sessionStorage.getItem(i)||null}catch{return null}}function _(i,...e){try{i(...e)}catch{}}function se(i){let e=t=>String(t).padStart(2,"0");return`${i.getFullYear()}-${e(i.getMonth()+1)}-${e(i.getDate())}T${e(i.getHours())}:${e(i.getMinutes())}`}function Ee(){if(document.getElementById(oe))return;let i=document.createElement("style");i.id=oe,i.textContent=ne,document.head.appendChild(i)}function Te(i,e){for(let[t,r]of Object.entries(P(e)))i.style.setProperty(t,r)}return ve(Ae);})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mylikita/booking-widget",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Drop-in appointment booking widget for MyLikita hospital websites. Submits bookings through the MyLikita relay (POST /v1/bookings) and polls status (GET /v1/bookings/:ref). Themeable via CSS variables.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/mylikita-booking-widget.cjs",
|
package/src/brand.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brand assets for the widget.
|
|
3
|
+
*
|
|
4
|
+
* The MyLikita mark is embedded as a base64 data URI (not a network fetch) so
|
|
5
|
+
* the widget is fully self-contained — no extra HTTP request, works offline,
|
|
6
|
+
* and renders identically on every host page regardless of their origin.
|
|
7
|
+
* The vector mark stays razor-sharp at any size.
|
|
8
|
+
*
|
|
9
|
+
* The mark (frontend/public/icons/mylikita-mark.svg) is a 6-block pixel-art
|
|
10
|
+
* symbol: dark #0169DA top bar + upper arms, light #0498FB lower arms +
|
|
11
|
+
* bottom bar, with the hollow middle gap preserved.
|
|
12
|
+
*
|
|
13
|
+
* The data URI is base64 of that exact SVG (no URL-encoding pitfalls — the
|
|
14
|
+
* SVG contains `#` and `"` which would need escaping in a raw data URI).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Base64 data URI of the MyLikita mark — embed directly in an <img src>. */
|
|
18
|
+
export const MYLIKITA_MARK_DATA_URI =
|
|
19
|
+
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0ODMgODA0IiB3aWR0aD0iNDgzIiBoZWlnaHQ9IjgwNCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJNeUxpa2l0YSI+CiAgICA8cmVjdCB4PSIxNjEiIHk9IjAiIHdpZHRoPSIxNjEiIGhlaWdodD0iMTYxIiBmaWxsPSIjMDE2OURBIi8+CiAgICA8cmVjdCB4PSIwIiB5PSIxNjAiIHdpZHRoPSIxNjIiIGhlaWdodD0iMTYxIiBmaWxsPSIjMDE2OURBIi8+CiAgICA8cmVjdCB4PSIzMjEiIHk9IjE2MCIgd2lkdGg9IjE2MiIgaGVpZ2h0PSIxNjEiIGZpbGw9IiMwMTY5REEiLz4KICAgIDxyZWN0IHg9IjAiIHk9IjQ4MiIgd2lkdGg9IjE2MiIgaGVpZ2h0PSIxNjIiIGZpbGw9IiMwNDk4RkIiLz4KICAgIDxyZWN0IHg9IjMyMSIgeT0iNDgyIiB3aWR0aD0iMTYyIiBoZWlnaHQ9IjE2MiIgZmlsbD0iIzA0OThGQiIvPgogICAgPHJlY3QgeD0iMTYxIiB5PSI2NDMiIHdpZHRoPSIxNjEiIGhlaWdodD0iMTYxIiBmaWxsPSIjMDQ5OEZCIi8+Cjwvc3ZnPg==';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Build the widget header branding: the MyLikita mark + "MyLikita" wordmark.
|
|
23
|
+
* Returns a <div class="mylikita-widget__brand"> with the mark <img> and a
|
|
24
|
+
* wordmark <span> — styled by styles.js, themeable via the widget's CSS vars.
|
|
25
|
+
*/
|
|
26
|
+
export function createBrandHeader() {
|
|
27
|
+
const header = document.createElement('div');
|
|
28
|
+
header.className = 'mylikita-widget__brand';
|
|
29
|
+
const img = document.createElement('img');
|
|
30
|
+
img.className = 'mylikita-widget__brand-mark';
|
|
31
|
+
img.src = MYLIKITA_MARK_DATA_URI;
|
|
32
|
+
img.alt = 'MyLikita';
|
|
33
|
+
img.width = 24;
|
|
34
|
+
img.height = 24;
|
|
35
|
+
const word = document.createElement('span');
|
|
36
|
+
word.className = 'mylikita-widget__brand-word';
|
|
37
|
+
word.textContent = 'MyLikita';
|
|
38
|
+
header.append(img, word);
|
|
39
|
+
return header;
|
|
40
|
+
}
|
package/src/index.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* <script src="mylikita-booking-widget.min.js"></script>
|
|
14
14
|
* <script>
|
|
15
15
|
* MyLikitaBookingWidget.createBookingWidget(document.getElementById('booking'), {
|
|
16
|
-
* relayUrl: 'https://api.mylikita.
|
|
16
|
+
* relayUrl: 'https://api.mylikita.com',
|
|
17
17
|
* websiteKey: 'wk_...', // public client id — not a secret
|
|
18
18
|
* facilityId: 'F1',
|
|
19
19
|
* providers: [{ external_id: 'dr-khalil', label: 'Dr. Khalil' }],
|
|
@@ -28,3 +28,4 @@ export { createBookingWidget } from './widget.js';
|
|
|
28
28
|
export { createBooking, fetchStatus, pollStatus, newExternalRef, fetchProviders } from './client.js';
|
|
29
29
|
export { STATUS_COPY, statusCopy, TERMINAL_STATUSES } from './state.js';
|
|
30
30
|
export { DEFAULT_THEME, resolveTheme } from './theme.js';
|
|
31
|
+
export { MYLIKITA_MARK_DATA_URI, createBrandHeader } from './brand.js';
|
package/src/styles.js
CHANGED
|
@@ -31,6 +31,27 @@ export const STYLES = `
|
|
|
31
31
|
.mylikita-widget *::before,
|
|
32
32
|
.mylikita-widget *::after { box-sizing: border-box; }
|
|
33
33
|
|
|
34
|
+
.mylikita-widget__brand {
|
|
35
|
+
display: flex;
|
|
36
|
+
align-items: center;
|
|
37
|
+
gap: 8px;
|
|
38
|
+
margin-bottom: 14px;
|
|
39
|
+
padding-bottom: 12px;
|
|
40
|
+
border-bottom: 1px solid var(--mlw-border);
|
|
41
|
+
}
|
|
42
|
+
.mylikita-widget__brand-mark {
|
|
43
|
+
display: block;
|
|
44
|
+
flex: 0 0 auto;
|
|
45
|
+
border-radius: 5px;
|
|
46
|
+
box-shadow: 0 1px 3px rgba(15, 23, 42, .12);
|
|
47
|
+
}
|
|
48
|
+
.mylikita-widget__brand-word {
|
|
49
|
+
font-size: 14px;
|
|
50
|
+
font-weight: 700;
|
|
51
|
+
letter-spacing: .3px;
|
|
52
|
+
color: var(--mlw-muted);
|
|
53
|
+
}
|
|
54
|
+
|
|
34
55
|
.mylikita-widget__title {
|
|
35
56
|
font-size: 18px;
|
|
36
57
|
font-weight: 700;
|
package/src/widget.js
CHANGED
|
@@ -12,6 +12,7 @@ import { createBooking, fetchStatus, pollStatus, newExternalRef, fetchProviders
|
|
|
12
12
|
import { statusCopy, TERMINAL_STATUSES } from './state.js';
|
|
13
13
|
import { resolveTheme } from './theme.js';
|
|
14
14
|
import { STYLES as STYLES_CSS } from './styles.js';
|
|
15
|
+
import { createBrandHeader } from './brand.js';
|
|
15
16
|
|
|
16
17
|
const DEFAULT_TEXT = {
|
|
17
18
|
title: 'Book an appointment',
|
|
@@ -53,6 +54,9 @@ export function createBookingWidget(element, options = {}) {
|
|
|
53
54
|
// (GET /v1/providers) on mount and populate the doctor dropdown. The
|
|
54
55
|
// static `providers` option, when given, always wins and skips the fetch.
|
|
55
56
|
loadProviders: options.loadProviders === true && !(options.providers && options.providers.length),
|
|
57
|
+
// White-label flag: premium hosts can drop the MyLikita brand header.
|
|
58
|
+
// Defaults to true (brand shown); pass showBrand:false to hide it.
|
|
59
|
+
showBrand: options.showBrand !== false,
|
|
56
60
|
pollIntervalMs: options.pollIntervalMs ?? 5000,
|
|
57
61
|
maxTries: options.maxTries ?? 12,
|
|
58
62
|
text: { ...DEFAULT_TEXT, ...(options.text || {}) },
|
|
@@ -84,6 +88,9 @@ export function createBookingWidget(element, options = {}) {
|
|
|
84
88
|
const refKey = `mylikita_ref_${opts.facilityId}_${instanceId}`;
|
|
85
89
|
|
|
86
90
|
// ── DOM construction ────────────────────────────────────────────────────
|
|
91
|
+
// The brand header is optional (white-label hosts pass showBrand:false);
|
|
92
|
+
// replaceChildren() ignores null, so the rest of the tree is unchanged.
|
|
93
|
+
const brand = opts.showBrand ? createBrandHeader() : null;
|
|
87
94
|
const form = el('form', { className: 'mylikita-widget__form' });
|
|
88
95
|
|
|
89
96
|
const title = el('h3', { className: 'mylikita-widget__title', text: t.title });
|
|
@@ -289,7 +296,7 @@ export function createBookingWidget(element, options = {}) {
|
|
|
289
296
|
datetime.input.min = toLocalInputValue(new Date());
|
|
290
297
|
}
|
|
291
298
|
|
|
292
|
-
root.replaceChildren(title, subtitle, form, statusView);
|
|
299
|
+
root.replaceChildren(brand, title, subtitle, form, statusView);
|
|
293
300
|
|
|
294
301
|
// ── async provider load (Phase C2/C3) ────────────────────────────────────
|
|
295
302
|
// When loadProviders is on, fetch the facility's mapped doctors and fill the
|