@queuezero/embed 0.1.8 → 0.1.10

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 CHANGED
@@ -38,9 +38,34 @@ Mounts a complete waitlist widget (form + status + referral share) to the specif
38
38
 
39
39
  | Parameter | Type | Description |
40
40
  |-----------|------|-------------|
41
- | `elementId` | `string` | ID of the container element |
41
+ | `elementId` | `string \| null` | ID of the container element, or `null` for headless mode |
42
42
  | `campaignSlug` | `string` | Your campaign identifier |
43
43
  | `options.apiUrl` | `string` | API endpoint URL |
44
+ | `options.displayMode` | `'inline' \| 'modal'` | Display as inline form or modal (default: `'inline'`) |
45
+ | `options.modalOptions` | `object` | Modal configuration (only when `displayMode: 'modal'`) |
46
+ | `options.modalOptions.triggerText` | `string` | Button text (default: `'Join Waitlist'`) |
47
+ | `options.modalOptions.triggerText` | `string` | Button text (default: `'Join Waitlist'`) |
48
+ | `options.modalOptions.attachToText` | `string` | Automatically attach modal to buttons with this text |
49
+ | `options.modalOptions.size` | `'sm' \| 'md' \| 'lg'` | Modal width (default: `'md'`) |
50
+
51
+ ### Modal Mode Example
52
+
53
+ ```html
54
+ <script src="https://cdn.jsdelivr.net/npm/@queuezero/embed@latest/dist/index.global.js"></script>
55
+ <script>
56
+ // Headless mode: Pass null as container ID and use attachToText
57
+ QueueZero.init(null, 'your-campaign-slug', {
58
+ apiUrl: 'https://api.queuezero.io',
59
+ displayMode: 'modal',
60
+ modalOptions: {
61
+ attachToText: 'Join Waitlist', // Will attach to any button with this text
62
+ size: 'md'
63
+ }
64
+ });
65
+ </script>
66
+ ```
67
+
68
+ The modal automatically uses your campaign's branding (logo, cover image, theme color) from the API.
44
69
 
45
70
  ## Styling
46
71
 
@@ -1,4 +1,4 @@
1
- var QueueZero=(()=>{var h=Object.defineProperty;var b=Object.getOwnPropertyDescriptor;var y=Object.getOwnPropertyNames;var v=Object.prototype.hasOwnProperty;var z=(a,e,t)=>e in a?h(a,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):a[e]=t;var q=(a,e)=>{for(var t in e)h(a,t,{get:e[t],enumerable:!0})},x=(a,e,t,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of y(e))!v.call(a,i)&&i!==t&&h(a,i,{get:()=>e[i],enumerable:!(o=b(e,i))||o.enumerable});return a};var $=a=>x(h({},"__esModule",{value:!0}),a);var l=(a,e,t)=>z(a,typeof e!="symbol"?e+"":e,t);var C={};q(C,{init:()=>k});var w=`
1
+ var QueueZero=(()=>{var h=Object.defineProperty;var y=Object.getOwnPropertyDescriptor;var b=Object.getOwnPropertyNames;var v=Object.prototype.hasOwnProperty;var q=(n,e,t)=>e in n?h(n,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):n[e]=t;var z=(n,e)=>{for(var t in e)h(n,t,{get:e[t],enumerable:!0})},x=(n,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of b(e))!v.call(n,o)&&o!==t&&h(n,o,{get:()=>e[o],enumerable:!(r=y(e,o))||r.enumerable});return n};var $=n=>x(h({},"__esModule",{value:!0}),n);var d=(n,e,t)=>q(n,typeof e!="symbol"?e+"":e,t);var E={};z(E,{init:()=>k});var w=`
2
2
  /* Base Form Styles */
3
3
  .qz-form {
4
4
  font-family: var(--qz-font-family, system-ui, -apple-system, sans-serif);
@@ -217,90 +217,90 @@ var QueueZero=(()=>{var h=Object.defineProperty;var b=Object.getOwnPropertyDescr
217
217
  margin: 0 0 20px 0;
218
218
  text-align: center;
219
219
  }
220
- `,u=class{constructor(e,t,o){l(this,"container");l(this,"campaign","");l(this,"options",{apiUrl:""});l(this,"configData",null);l(this,"submitted",!1);l(this,"submittedData",null);l(this,"modalElement",null);l(this,"modalOverlay",null);let i=document.getElementById(e);if(!i){console.error(`QueueZero: Element with ID "${e}" not found.`),this.container=document.createElement("div");return}this.container=i,this.campaign=t,this.options=o,this.injectStyles(),this.render(),this.loadCampaign()}injectStyles(){if(document.getElementById("qz-styles"))return;let e=document.createElement("style");e.id="qz-styles",e.textContent=w,document.head.appendChild(e)}render(){this.container.innerHTML='<div class="qz-loading">Loading waitlist...</div>'}async loadCampaign(){try{let e=await fetch(`${this.options.apiUrl}/v1/config/${this.campaign}`);if(!e.ok)throw new Error(`Waitlist not found (${e.status})`);this.configData=await e.json(),this.applyBranding(),this.options.displayMode==="modal"?this.renderTrigger():this.renderInlineForm()}catch(e){this.container.innerHTML=`<div class="qz-error">Failed to load waitlist: ${e.message}</div>`}}applyBranding(){if(!this.configData)return;let e=this.configData.branding||{},t=document.documentElement;e.themeColor&&(t.style.setProperty("--qz-theme-color",e.themeColor),this.container.style.setProperty("--qz-theme-color",e.themeColor)),e.backgroundColor&&t.style.setProperty("--qz-bg-color",e.backgroundColor),e.fontFamily&&t.style.setProperty("--qz-font-family",e.fontFamily)}renderTrigger(){if(!this.configData)return;let e=this.options.modalOptions||{},t=e.triggerText||"Join Waitlist",o=e.triggerClass||"";this.container.innerHTML=`
221
- <button class="qz-trigger ${o}" id="qz-trigger-${this.campaign}">
220
+ `,u=class{constructor(e,t,r){d(this,"container");d(this,"campaign","");d(this,"options",{apiUrl:""});d(this,"configData",null);d(this,"submitted",!1);d(this,"submittedData",null);d(this,"modalElement",null);d(this,"modalOverlay",null);let o=e?document.getElementById(e):null;if(!o)if(r.displayMode==="modal")o=document.createElement("div");else{console.error(`QueueZero: Element with ID "${e}" not found.`),this.container=document.createElement("div");return}this.container=o,this.campaign=t,this.options=r,this.injectStyles(),this.render(),this.loadCampaign()}injectStyles(){if(document.getElementById("qz-styles"))return;let e=document.createElement("style");e.id="qz-styles",e.textContent=w,document.head.appendChild(e)}render(){this.container.innerHTML='<div class="qz-loading">Loading waitlist...</div>'}async loadCampaign(){try{let e=await fetch(`${this.options.apiUrl}/v1/config/${this.campaign}`);if(!e.ok)throw new Error(`Waitlist not found (${e.status})`);if(this.configData=await e.json(),this.applyBranding(),this.options.displayMode==="modal"){let{triggerSelector:t,attachToText:r}=this.options.modalOptions||{};if(t||r){if(this.createModalDOM(),t&&document.querySelectorAll(t).forEach(o=>{o.addEventListener("click",a=>{a.preventDefault(),this.openModal()})}),r){let o=r.toLowerCase();document.querySelectorAll("a, button").forEach(a=>{(a.textContent||"").toLowerCase().includes(o)&&a.addEventListener("click",i=>{i.preventDefault(),this.openModal()})})}}else this.renderTrigger()}else this.renderInlineForm()}catch(e){this.container.innerHTML=`<div class="qz-error">Failed to load waitlist: ${e.message}</div>`}}applyBranding(){if(!this.configData)return;let e=this.configData.branding||{},t=document.documentElement;e.themeColor&&(t.style.setProperty("--qz-theme-color",e.themeColor),this.container.style.setProperty("--qz-theme-color",e.themeColor)),e.backgroundColor&&t.style.setProperty("--qz-bg-color",e.backgroundColor),e.fontFamily&&t.style.setProperty("--qz-font-family",e.fontFamily)}renderTrigger(){if(!this.configData)return;let e=this.options.modalOptions||{},t=e.triggerText||"Join Waitlist",r=e.triggerClass||"";this.container.innerHTML=`
221
+ <button class="qz-trigger ${r}" id="qz-trigger-${this.campaign}">
222
222
  ${t}
223
223
  </button>
224
- `,this.container.querySelector(".qz-trigger")?.addEventListener("click",()=>this.openModal()),this.createModalDOM()}createModalDOM(){if(!this.configData)return;let e=this.configData.branding||{},o=`qz-modal-${(this.options.modalOptions||{}).size||"md"}`;this.modalOverlay=document.createElement("div"),this.modalOverlay.className="qz-modal-overlay",this.modalOverlay.id=`qz-modal-overlay-${this.campaign}`;let i="";e.logoUrl?i+=`<img src="${e.logoUrl}" alt="Logo" class="qz-modal-logo" />`:i+="<span></span>",i+=`
224
+ `,this.container.querySelector(".qz-trigger")?.addEventListener("click",()=>this.openModal()),this.createModalDOM()}createModalDOM(){if(!this.configData)return;let e=this.configData.branding||{},r=`qz-modal-${(this.options.modalOptions||{}).size||"md"}`;this.modalOverlay=document.createElement("div"),this.modalOverlay.className="qz-modal-overlay",this.modalOverlay.id=`qz-modal-overlay-${this.campaign}`;let o="";e.logoUrl?o+=`<img src="${e.logoUrl}" alt="Logo" class="qz-modal-logo" />`:o+="<span></span>",o+=`
225
225
  <button class="qz-modal-close" aria-label="Close">
226
226
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
227
227
  <path d="M18 6L6 18M6 6l12 12" />
228
228
  </svg>
229
229
  </button>
230
- `;let d="";e.coverImageUrl&&(d=`<img src="${e.coverImageUrl}" alt="" class="qz-modal-cover" />`),this.modalOverlay.innerHTML=`
231
- <div class="qz-modal ${o}">
230
+ `;let a="";e.coverImageUrl&&(a=`<img src="${e.coverImageUrl}" alt="" class="qz-modal-cover" />`),this.modalOverlay.innerHTML=`
231
+ <div class="qz-modal ${r}">
232
232
  <div class="qz-modal-header">
233
- ${i}
233
+ ${o}
234
234
  </div>
235
- ${d}
235
+ ${a}
236
236
  <div class="qz-modal-body">
237
237
  <h2 class="qz-modal-title">${this.configData.name}</h2>
238
238
  <div id="qz-modal-form-container-${this.campaign}"></div>
239
239
  </div>
240
240
  </div>
241
- `,document.body.appendChild(this.modalOverlay),this.modalOverlay.querySelector(".qz-modal-close")?.addEventListener("click",()=>this.closeModal()),this.modalOverlay.addEventListener("click",r=>{r.target===this.modalOverlay&&this.closeModal()}),document.addEventListener("keydown",r=>{r.key==="Escape"&&this.modalOverlay?.classList.contains("qz-modal-open")&&this.closeModal()})}openModal(){if(!this.modalOverlay)return;let e=document.getElementById(`qz-modal-form-container-${this.campaign}`);e&&!e.hasChildNodes()&&this.renderFormInto(e),this.modalOverlay.classList.add("qz-modal-open"),document.body.style.overflow="hidden"}closeModal(){this.modalOverlay?.classList.remove("qz-modal-open"),document.body.style.overflow=""}renderInlineForm(){this.renderFormInto(this.container)}renderFormInto(e){if(!this.configData)return;let t=this.configData.branding?.themeColor||"#10B981";e.style.setProperty("--qz-theme-color",t);let o=(this.configData.formFields||[]).filter(r=>r.enabled);o.length===0&&o.push({key:"email",label:"Email Address",type:"email",enabled:!0,required:!0});let i=r=>{let c=`qz-${r.key}`,p=`<label class="qz-label" for="${c}">${r.label}${r.required?" *":""}</label>`,n=r.required?"required":"",s=r.placeholder||"";switch(r.type){case"select":let m=(r.options||[]).map(f=>`<option value="${f}">${f}</option>`).join("");return`
241
+ `,document.body.appendChild(this.modalOverlay),this.modalOverlay.querySelector(".qz-modal-close")?.addEventListener("click",()=>this.closeModal()),this.modalOverlay.addEventListener("click",i=>{i.target===this.modalOverlay&&this.closeModal()}),document.addEventListener("keydown",i=>{i.key==="Escape"&&this.modalOverlay?.classList.contains("qz-modal-open")&&this.closeModal()})}openModal(){if(!this.modalOverlay)return;let e=document.getElementById(`qz-modal-form-container-${this.campaign}`);e&&!e.hasChildNodes()&&this.renderFormInto(e),this.modalOverlay.classList.add("qz-modal-open"),document.body.style.overflow="hidden"}closeModal(){this.modalOverlay?.classList.remove("qz-modal-open"),document.body.style.overflow=""}renderInlineForm(){this.renderFormInto(this.container)}renderFormInto(e){if(!this.configData)return;let t=this.configData.branding?.themeColor||"#10B981";e.style.setProperty("--qz-theme-color",t);let r=(this.configData.formFields||[]).filter(i=>i.enabled);r.length===0&&r.push({key:"email",label:"Email Address",type:"email",enabled:!0,required:!0});let o=i=>{let m=`qz-${i.key}`,g=`<label class="qz-label" for="${m}">${i.label}${i.required?" *":""}</label>`,s=i.required?"required":"",c=i.placeholder||"";switch(i.type){case"select":let p=(i.options||[]).map(f=>`<option value="${f}">${f}</option>`).join("");return`
242
242
  <div class="qz-form-group">
243
- ${p}
244
- <select class="qz-input qz-select" id="${c}" name="${r.key}" ${n}>
245
- <option value="">${s||`Select ${r.label}`}</option>
246
- ${m}
243
+ ${g}
244
+ <select class="qz-input qz-select" id="${m}" name="${i.key}" ${s}>
245
+ <option value="">Select an option</option>
246
+ ${p}
247
247
  </select>
248
248
  </div>
249
249
  `;case"checkbox":return`
250
250
  <div class="qz-form-group qz-checkbox-group">
251
251
  <label class="qz-checkbox-label">
252
- <input type="checkbox" class="qz-checkbox" id="${c}" name="${r.key}" ${n} />
253
- <span>${r.label}${r.required?" *":""}</span>
252
+ <input type="checkbox" class="qz-checkbox" id="${m}" name="${i.key}" ${s} />
253
+ <span>${i.label}${i.required?" *":""}</span>
254
254
  </label>
255
255
  </div>
256
256
  `;case"textarea":return`
257
257
  <div class="qz-form-group">
258
- ${p}
258
+ ${g}
259
259
  <textarea
260
260
  class="qz-input qz-textarea"
261
- id="${c}"
262
- name="${r.key}"
263
- placeholder="${s}"
261
+ id="${m}"
262
+ name="${i.key}"
263
+ placeholder="${c}"
264
264
  rows="3"
265
- ${n}
265
+ ${s}
266
266
  ></textarea>
267
267
  </div>
268
268
  `;case"number":return`
269
269
  <div class="qz-form-group">
270
- ${p}
270
+ ${g}
271
271
  <input
272
272
  class="qz-input"
273
- id="${c}"
274
- name="${r.key}"
273
+ id="${m}"
274
+ name="${i.key}"
275
275
  type="number"
276
- placeholder="${s}"
277
- ${n}
276
+ placeholder="${c}"
277
+ ${s}
278
278
  />
279
279
  </div>
280
280
  `;default:return`
281
281
  <div class="qz-form-group">
282
- ${p}
282
+ ${g}
283
283
  <input
284
284
  class="qz-input"
285
- id="${c}"
286
- name="${r.key}"
287
- type="${r.type==="email"?"email":"text"}"
288
- placeholder="${s}"
289
- ${n}
285
+ id="${m}"
286
+ name="${i.key}"
287
+ type="${i.type==="email"?"email":"text"}"
288
+ placeholder="${c}"
289
+ ${s}
290
290
  />
291
291
  </div>
292
- `}},d=o.map(i).join("");e.innerHTML=`
292
+ `}},a=r.map(o).join("");e.innerHTML=`
293
293
  <form class="qz-form" id="qz-form-${this.campaign}">
294
- ${d}
294
+ ${a}
295
295
  <button type="submit" class="qz-button">Join Waitlist</button>
296
296
  </form>
297
- `,e.querySelector("form")?.addEventListener("submit",r=>this.handleSubmit(r))}async handleSubmit(e){e.preventDefault();let t=e.target,o=t.querySelector("button"),i=new FormData(t),d={},g="";if(i.forEach((n,s)=>{let m=n.toString();s==="email"&&(g=m),d[s]=m}),!g){this.showError(t,"Email is required");return}let c=new URLSearchParams(window.location.search).get("ref"),p={campaign:this.campaign,email:g,metadata:d,referrer_code:c||null};o&&(o.disabled=!0,o.textContent="Joining...");try{let n=await fetch(`${this.options.apiUrl}/v1/submit`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(p)});if(!n.ok){let m=await n.json();throw new Error(m.message||m.error||"Failed to join")}let s=await n.json();this.submitted=!0,this.submittedData=s,this.renderSuccess(),this.options.onSuccess?.(s)}catch(n){this.showError(t,n.message||"Something went wrong"),o&&(o.disabled=!1,o.textContent="Join Waitlist"),this.options.onError?.(n)}}showError(e,t){let o=e.querySelector(".qz-error");o&&o.remove();let i=document.createElement("div");i.className="qz-error",i.textContent=t,e.insertBefore(i,e.firstChild)}renderSuccess(){let e=this.submittedData?.position||"?",t=this.submittedData?.referralLink,o=`
297
+ `,e.querySelector("form")?.addEventListener("submit",i=>this.handleSubmit(i))}async handleSubmit(e){e.preventDefault();let t=e.target,r=t.querySelector("button"),o=new FormData(t),a={},l="";if(o.forEach((s,c)=>{let p=s.toString();c==="email"&&(l=p),a[c]=p}),!l){this.showError(t,"Email is required");return}let m=new URLSearchParams(window.location.search).get("ref"),g={campaign:this.campaign,email:l,metadata:a,referrer_code:m||null};r&&(r.disabled=!0,r.textContent="Joining...");try{let s=await fetch(`${this.options.apiUrl}/v1/submit`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(g)});if(!s.ok){let p=await s.json();throw new Error(p.message||p.error||"Failed to join")}let c=await s.json();this.submitted=!0,this.submittedData=c,this.renderSuccess(),this.options.onSuccess?.(c)}catch(s){this.showError(t,s.message||"Something went wrong"),r&&(r.disabled=!1,r.textContent="Join Waitlist"),this.options.onError?.(s)}}showError(e,t){let r=e.querySelector(".qz-error");r&&r.remove();let o=document.createElement("div");o.className="qz-error",o.textContent=t,e.insertBefore(o,e.firstChild)}renderSuccess(){let e=this.submittedData?.position||"?",t=this.submittedData?.referralCode,r=window.location.origin+window.location.pathname,o=t?`${r}?ref=${t}`:null,a=`
298
298
  <div class="qz-success">
299
299
  <h3>You're on the list!</h3>
300
300
  <p>You're #${e} in line.</p>
301
- `;t&&(o+=`
301
+ `;if(o&&(a+=`
302
302
  <div style="margin-top: 20px;">
303
303
  <p style="margin-bottom: 8px; color: #a6e3a1;">Refer friends to move up:</p>
304
- <input class="qz-input" readonly value="${t}" onclick="this.select()" style="text-align: center; cursor: pointer;">
304
+ <input class="qz-input" readonly value="${o}" onclick="this.select()" style="text-align: center; cursor: pointer;">
305
305
  </div>
306
- `),o+="</div>",this.container.innerHTML=o}};function k(a,e,t){return new u(a,e,t)}return $(C);})();
306
+ `),a+="</div>",this.options.displayMode==="modal"){let l=document.getElementById(`qz-modal-form-container-${this.campaign}`);if(l){l.innerHTML=a;return}}this.container.innerHTML=a}};function k(n,e,t){return new u(n,e,t)}return $(E);})();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@queuezero/embed",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Embeddable vanilla JS widget for QueueZero viral waitlists",
5
5
  "main": "./dist/index.global.js",
6
6
  "browser": "./dist/index.global.js",