@pcampus/reward-widget 0.1.0 → 0.2.0

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.
@@ -1,61 +1,410 @@
1
- export function renderQuizMarkup(rewards = [], theme) {
2
- const primary = theme?.brandPrimaryColor || "#0284c7";
1
+ import { DEFAULT_WIDGET_SURFACE, mixPrimary, resolveWidgetChrome } from "./chrome.js";
2
+ import { showCouponSuccess } from "./coupon-success.js";
3
+ import { textEscape } from "./types.js";
4
+ const DEFAULT_HEADING = "คำถามลุ้นรับสิทธิ์";
5
+ const DEFAULT_OPTIONS = [
6
+ "พึงพอใจมากที่สุด (Excellent)",
7
+ "พึงพอใจดี (Good)",
8
+ "ควรปรับปรุง (Needs Improvement)",
9
+ ];
10
+ export function resolveQuizContent(content, brandName) {
11
+ const heading = content?.heading?.trim() || DEFAULT_HEADING;
12
+ const brand = brandName?.trim() || "บริการ";
13
+ const defaultQuestion = `คุณพึงพอใจกับประสบการณ์การใช้งานของ ${brand} ในระดับใด?`;
14
+ const fromItems = (content?.items ?? [])
15
+ .map((item) => ({
16
+ question: item.question?.trim() ?? "",
17
+ options: (item.options ?? []).map((option) => option.trim()).filter(Boolean).slice(0, 8),
18
+ }))
19
+ .filter((item) => item.question.length > 0 || item.options.length > 0)
20
+ .slice(0, 8);
21
+ if (fromItems.length > 0) {
22
+ return {
23
+ heading,
24
+ items: fromItems.map((item) => ({
25
+ question: item.question || defaultQuestion,
26
+ options: item.options.length > 0 ? item.options : DEFAULT_OPTIONS,
27
+ })),
28
+ };
29
+ }
30
+ const options = (content?.options ?? [])
31
+ .map((option) => option.trim())
32
+ .filter((option) => option.length > 0)
33
+ .slice(0, 8);
34
+ const question = content?.question?.trim() || defaultQuestion;
35
+ return {
36
+ heading,
37
+ items: [
38
+ {
39
+ question,
40
+ options: options.length >= 2 ? options : DEFAULT_OPTIONS,
41
+ },
42
+ ],
43
+ };
44
+ }
45
+ function renderQuizOptionButtons(options, optionBorder) {
46
+ return options
47
+ .map((label, index) => {
48
+ const letter = String.fromCharCode(65 + index);
49
+ return `
50
+ <button type="button" class="pcampus-quiz-option-btn" data-opt-index="${index}">
51
+ <div style="display:flex;align-items:center;">
52
+ <div class="pcampus-option-circle">${letter}</div>
53
+ <span>${textEscape(label)}</span>
54
+ </div>
55
+ <span class="pcampus-opt-radio" style="width:18px;height:18px;border-radius:50%;border:1.5px solid ${optionBorder};display:flex;align-items:center;justify-content:center;"></span>
56
+ </button>`;
57
+ })
58
+ .join("");
59
+ }
60
+ function quizSubmitLabel(isLast) {
61
+ return isLast ? "ส่งคำตอบเพื่อรับรางวัล" : "ถัดไป";
62
+ }
63
+ function clampQuizStep(requested, total) {
64
+ if (total <= 0)
65
+ return 0;
66
+ if (typeof requested !== "number" || !Number.isFinite(requested))
67
+ return 0;
68
+ return Math.min(Math.max(0, Math.trunc(requested)), total - 1);
69
+ }
70
+ export function renderQuizMarkup(rewards = [], theme, quiz) {
71
+ const topReward = rewards[0]?.name || "ส่วนลด 100 บาท";
72
+ const { primary, surface, border } = resolveWidgetChrome(theme);
73
+ const cardFill = theme?.hubBackgroundColor?.trim() || DEFAULT_WIDGET_SURFACE;
74
+ const headingColor = theme?.promptColor?.trim() || "#a8a8a5";
75
+ const bodyText = theme?.textColor?.trim() || "#f0efed";
76
+ const optionBorder = theme?.wheelBorderColor?.trim() || border;
77
+ const buttonText = theme?.buttonTextColor?.trim() || "#ffffff";
78
+ const copy = resolveQuizContent(quiz, theme?.brandName);
79
+ const total = copy.items.length;
80
+ const start = clampQuizStep(quiz?.initialStep, total);
81
+ const current = copy.items[start];
82
+ const optionButtons = renderQuizOptionButtons(current.options, optionBorder);
3
83
  return `
4
- <div class="pcampus-quiz-widget-wrap" style="position:relative;display:flex;flex-direction:column;align-items:center;width:100%;max-width:270px;padding:1rem;border-radius:1rem;border:1px solid rgba(255,255,255,0.15);background:#ffffff;color:#0f172a;box-shadow:0 20px 40px rgba(0,0,0,0.5);">
5
- <div style="display:inline-flex;align-items:center;gap:6px;padding:2px 8px;border-radius:9999px;background:#f1f5f9;font-size:10px;font-weight:700;color:#475569;margin-bottom:0.6rem;">
6
- <svg style="width:12px;height:12px;color:#64748b;" fill="none" viewBox="0 0 24 24" stroke="currentColor">
7
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
8
- </svg>
9
- <span>แบบสอบถามเพื่อรับรางวัล</span>
10
- </div>
84
+ <style>
85
+ .pcampus-quiz-widget-wrap {
86
+ background: ${surface};
87
+ }
88
+ .pcampus-quiz-box {
89
+ background: ${cardFill};
90
+ color: ${bodyText};
91
+ border-radius: 18px;
92
+ border: 1px solid ${optionBorder};
93
+ border-top: 24px solid ${optionBorder};
94
+ padding: 2.25rem 2rem 2.5rem;
95
+ width: 100%;
96
+ max-width: min(92vw, 32rem);
97
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
98
+ position: relative;
99
+ overflow: hidden;
100
+ box-sizing: border-box;
101
+ }
102
+ .pcampus-quiz-box::after {
103
+ content: '';
104
+ position: absolute;
105
+ top: -24px;
106
+ left: 50%;
107
+ transform: translateX(-50%);
108
+ width: 6.5rem;
109
+ height: 14px;
110
+ background: ${primary};
111
+ border-radius: 0 0 8px 8px;
112
+ box-shadow: 0 3px 6px rgba(0,0,0,0.4);
113
+ }
114
+ .pcampus-quiz-badge {
115
+ display: inline-flex;
116
+ align-items: center;
117
+ gap: 0.45rem;
118
+ background: ${mixPrimary(primary, 12)};
119
+ border: 1px solid ${mixPrimary(primary, 28)};
120
+ color: ${primary};
121
+ font-size: 0.75rem;
122
+ font-weight: 700;
123
+ padding: 0.25rem 0.65rem;
124
+ border-radius: 9999px;
125
+ margin-bottom: 0.75rem;
126
+ width: fit-content;
127
+ }
128
+ .pcampus-quiz-header {
129
+ display: flex;
130
+ flex-direction: column;
131
+ gap: 0.5rem;
132
+ margin-bottom: 1.5rem;
133
+ width: 100%;
134
+ }
135
+ .pcampus-quiz-step-row {
136
+ display: flex;
137
+ align-items: center;
138
+ justify-content: space-between;
139
+ }
140
+ .pcampus-quiz-step-label {
141
+ font-size: 0.78rem;
142
+ font-weight: 800;
143
+ text-transform: uppercase;
144
+ color: ${headingColor};
145
+ letter-spacing: 0.1em;
146
+ }
147
+ .pcampus-quiz-step-count {
148
+ font-size: 0.78rem;
149
+ font-weight: 800;
150
+ color: ${headingColor};
151
+ font-family: monospace;
152
+ }
153
+ .pcampus-quiz-progress-bar {
154
+ background: rgba(240, 239, 237, 0.08);
155
+ border-radius: 9999px;
156
+ height: 6px;
157
+ width: 100%;
158
+ overflow: hidden;
159
+ }
160
+ .pcampus-quiz-progress {
161
+ background: ${primary};
162
+ height: 100%;
163
+ width: ${((start + 1) / total) * 100}%;
164
+ border-radius: 9999px;
165
+ }
166
+ .pcampus-quiz-question {
167
+ font-size: 1.15rem;
168
+ font-weight: 800;
169
+ margin-bottom: 1.25rem;
170
+ color: ${bodyText};
171
+ text-align: left;
172
+ line-height: 1.45;
173
+ letter-spacing: -0.01em;
174
+ }
175
+ .pcampus-quiz-options {
176
+ display: flex;
177
+ flex-direction: column;
178
+ gap: 0.75rem;
179
+ width: 100%;
180
+ }
181
+ .pcampus-quiz-option-btn {
182
+ width: 100%;
183
+ padding: 0.9rem 1.15rem;
184
+ background: ${cardFill};
185
+ border: 1.5px solid ${optionBorder};
186
+ border-radius: 12px;
187
+ cursor: pointer;
188
+ display: flex;
189
+ align-items: center;
190
+ justify-content: space-between;
191
+ color: ${bodyText};
192
+ font-size: 0.9rem;
193
+ font-weight: 600;
194
+ text-align: left;
195
+ transition: all 160ms ease;
196
+ outline: none;
197
+ }
198
+ .pcampus-quiz-option-btn:hover {
199
+ border-color: ${primary};
200
+ background: ${mixPrimary(primary, 10)};
201
+ }
202
+ .pcampus-quiz-option-btn.selected {
203
+ border-color: ${primary};
204
+ background: ${mixPrimary(primary, 16)};
205
+ }
206
+ .pcampus-option-circle {
207
+ width: 22px;
208
+ height: 22px;
209
+ border-radius: 6px;
210
+ background: rgba(240, 239, 237, 0.08);
211
+ display: flex;
212
+ align-items: center;
213
+ justify-content: center;
214
+ font-weight: 800;
215
+ font-size: 0.72rem;
216
+ color: ${bodyText};
217
+ margin-right: 0.75rem;
218
+ }
219
+ .pcampus-quiz-option-btn.selected .pcampus-option-circle {
220
+ background: ${primary};
221
+ color: ${buttonText};
222
+ }
223
+ .pcampus-quiz-submit-btn {
224
+ width: 100%;
225
+ margin-top: 1.5rem;
226
+ padding: 0.95rem;
227
+ border-radius: 12px;
228
+ background: ${primary};
229
+ color: ${buttonText};
230
+ border: none;
231
+ font-size: 0.95rem;
232
+ font-weight: 800;
233
+ cursor: not-allowed;
234
+ opacity: 0.5;
235
+ display: flex;
236
+ align-items: center;
237
+ justify-content: center;
238
+ gap: 0.5rem;
239
+ transition: all 150ms ease;
240
+ }
241
+ </style>
11
242
 
12
- <div style="width:100%;display:flex;justify-content:space-between;font-size:10px;font-weight:700;color:#64748b;margin-bottom:4px;">
13
- <span>คำถามที่ 1</span>
14
- <span style="color:${primary};">1 / 3</span>
15
- </div>
16
- <div style="width:100%;height:6px;border-radius:9999px;background:#f1f5f9;border:1px solid #e2e8f0;overflow:hidden;margin-bottom:0.75rem;">
17
- <div style="width:33%;height:100%;background:${primary};border-radius:9999px;"></div>
18
- </div>
243
+ <div class="pcampus-quiz-widget-wrap" style="position:relative;display:flex;flex-direction:column;align-items:center;width:100%;box-sizing:border-box;">
244
+
245
+ <div class="pcampus-quiz-box">
246
+ <div class="pcampus-quiz-badge">
247
+ ${theme?.brandLogoUrl ? `
248
+ <div style="height:14px;max-width:32px;display:flex;align-items:center;background:#ffffff;padding:1px 3px;border-radius:2px;">
249
+ <img src="${textEscape(theme.brandLogoUrl)}" alt="Logo" style="height:100%;max-width:100%;object-fit:contain;" />
250
+ </div>
251
+ ` : `
252
+ <i class="fa-solid fa-circle-question"></i>
253
+ `}
254
+ <span>${textEscape(theme?.brandName ? `${theme.brandName} • แบบสอบถาม` : "แบบสอบถามเพื่อรับรางวัล")}</span>
255
+ </div>
19
256
 
20
- <p style="width:100%;text-align:left;font-size:12px;font-weight:800;color:#0f172a;margin:0 0 0.6rem 0;">
21
- คุณพึงพอใจกับบริการในระดับใด?
22
- </p>
23
-
24
- <div style="width:100%;display:flex;flex-direction:column;gap:6px;">
25
- <button type="button" class="pcampus-quiz-opt" style="width:100%;display:flex;align-items:center;justify-content:space-between;padding:6px 10px;border-radius:0.75rem;background:${primary};color:#ffffff;border:none;font-size:11px;font-weight:600;cursor:pointer;">
26
- <div style="display:flex;align-items:center;gap:6px;">
27
- <span style="display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;border-radius:4px;background:rgba(255,255,255,0.2);font-size:10px;font-weight:700;">A</span>
28
- <span>พึงพอใจมากที่สุด</span>
257
+ <div class="pcampus-quiz-header">
258
+ <div class="pcampus-quiz-step-row">
259
+ <span class="pcampus-quiz-step-label">${textEscape(copy.heading)}</span>
260
+ <span class="pcampus-quiz-step-count">${start + 1} / ${total}</span>
29
261
  </div>
30
- <svg style="width:14px;height:14px;" fill="none" viewBox="0 0 24 24" stroke="currentColor">
31
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"/>
32
- </svg>
33
- </button>
34
-
35
- <button type="button" class="pcampus-quiz-opt" style="width:100%;display:flex;align-items:center;justify-content:space-between;padding:6px 10px;border-radius:0.75rem;background:#f8fafc;color:#334155;border:1px solid #e2e8f0;font-size:11px;font-weight:500;cursor:pointer;">
36
- <div style="display:flex;align-items:center;gap:6px;">
37
- <span style="display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;border-radius:4px;background:#ffffff;border:1px solid #cbd5e1;font-size:10px;font-weight:700;color:#64748b;">B</span>
38
- <span>ปานกลาง</span>
262
+ <div class="pcampus-quiz-progress-bar">
263
+ <div class="pcampus-quiz-progress"></div>
39
264
  </div>
265
+ </div>
266
+
267
+ <div class="pcampus-quiz-question">
268
+ ${textEscape(current.question)}
269
+ </div>
270
+
271
+ <div class="pcampus-quiz-options">${optionButtons}
272
+ </div>
273
+
274
+ <button type="button" class="pcampus-quiz-submit-btn" disabled>
275
+ <span>${quizSubmitLabel(start === total - 1)}</span>
276
+ <i class="fa-solid fa-arrow-right"></i>
40
277
  </button>
41
278
  </div>
42
279
 
43
- <div style="margin-top:0.75rem;display:flex;align-items:center;justify-content:center;gap:6px;border-top:1px dashed #e2e8f0;padding-top:6px;font-size:10px;font-weight:600;color:#d97706;">
44
- <svg style="width:12px;height:12px;" fill="none" viewBox="0 0 24 24" stroke="currentColor">
45
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v13m0-13V6a2 2 0 112 2h-2zm0 0V6a2 2 0 10-2 2h2zm-7 5h14v7a2 2 0 01-2 2H7a2 2 0 01-2-2v-7zM3 10h18v3H3v-3z"/>
46
- </svg>
47
- <span>ตอบครบ 3 ข้อรับรางวัลทันที</span>
280
+ <div class="pcampus-quiz-outcome" style="margin-top:1.25rem;font-size:0.85rem;font-weight:600;color:${headingColor};text-align:center;display:flex;align-items:center;gap:0.4rem;">
281
+ <span style="color:${primary};"><i class="fa-solid fa-gift"></i></span>
282
+ <span>ตอบแบบสอบถามเพื่อรับ: <strong style="color:${bodyText};">${textEscape(topReward)}</strong></span>
48
283
  </div>
284
+
49
285
  </div>
50
286
  `;
51
287
  }
52
- export function mountQuiz(target, rewards = [], theme, onAction) {
53
- target.innerHTML = renderQuizMarkup(rewards, theme);
54
- const opts = target.querySelectorAll(".pcampus-quiz-opt");
55
- opts.forEach((btn) => {
56
- btn.addEventListener("click", () => {
57
- if (onAction)
58
- onAction(rewards[0]);
288
+ function ensureFontAwesomeLoaded() {
289
+ if (typeof document === "undefined")
290
+ return;
291
+ const id = "pcampus-fa-cdn";
292
+ if (!document.getElementById(id)) {
293
+ const link = document.createElement("link");
294
+ link.id = id;
295
+ link.rel = "stylesheet";
296
+ link.href = "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css";
297
+ document.head.appendChild(link);
298
+ }
299
+ }
300
+ export function collectQuizAnswer(answers, questionIndex, optionIndex) {
301
+ if (!Number.isInteger(questionIndex) ||
302
+ !Number.isInteger(optionIndex) ||
303
+ questionIndex < 0 ||
304
+ optionIndex < 0) {
305
+ return answers;
306
+ }
307
+ return [...answers, { questionIndex, optionIndex }];
308
+ }
309
+ export function mountQuiz(target, rewards = [], theme, onAction, quiz, onQuizComplete) {
310
+ ensureFontAwesomeLoaded();
311
+ target.innerHTML = renderQuizMarkup(rewards, theme, quiz);
312
+ const copy = resolveQuizContent(quiz, theme?.brandName);
313
+ const questionEl = target.querySelector(".pcampus-quiz-question");
314
+ const optionsEl = target.querySelector(".pcampus-quiz-options");
315
+ const stepCountEl = target.querySelector(".pcampus-quiz-step-count");
316
+ const progressEl = target.querySelector(".pcampus-quiz-progress");
317
+ const submitBtn = target.querySelector(".pcampus-quiz-submit-btn");
318
+ const outcome = target.querySelector(".pcampus-quiz-outcome");
319
+ let currentStep = clampQuizStep(quiz?.initialStep, copy.items.length);
320
+ let selectedIndex = -1;
321
+ let answers = [];
322
+ const rewardItem = rewards[0] || { name: "ของรางวัลพิเศษ" };
323
+ const { primary, border } = resolveWidgetChrome(theme);
324
+ const optionBorder = theme?.wheelBorderColor?.trim() || border;
325
+ const buttonText = theme?.buttonTextColor?.trim() || "#ffffff";
326
+ const total = copy.items.length;
327
+ const bindOptions = () => {
328
+ const options = target.querySelectorAll(".pcampus-quiz-option-btn");
329
+ options.forEach((opt, idx) => {
330
+ opt.addEventListener("click", () => {
331
+ selectedIndex = idx;
332
+ options.forEach((other, otherIdx) => {
333
+ const radio = other.querySelector(".pcampus-opt-radio");
334
+ if (otherIdx === selectedIndex) {
335
+ other.classList.add("selected");
336
+ if (radio) {
337
+ radio.style.borderColor = primary;
338
+ radio.style.background = primary;
339
+ radio.innerHTML = `<i class="fa-solid fa-check" style="font-size:10px;color:${buttonText};"></i>`;
340
+ }
341
+ }
342
+ else {
343
+ other.classList.remove("selected");
344
+ if (radio) {
345
+ radio.style.borderColor = optionBorder;
346
+ radio.style.background = "transparent";
347
+ radio.innerHTML = "";
348
+ }
349
+ }
350
+ });
351
+ if (submitBtn) {
352
+ submitBtn.disabled = false;
353
+ submitBtn.style.opacity = "1";
354
+ submitBtn.style.cursor = "pointer";
355
+ }
356
+ });
357
+ });
358
+ };
359
+ const paintStep = (step) => {
360
+ const item = copy.items[step];
361
+ if (!item)
362
+ return;
363
+ selectedIndex = -1;
364
+ if (questionEl)
365
+ questionEl.textContent = item.question;
366
+ if (stepCountEl)
367
+ stepCountEl.textContent = `${step + 1} / ${total}`;
368
+ if (progressEl)
369
+ progressEl.style.width = `${((step + 1) / total) * 100}%`;
370
+ if (optionsEl)
371
+ optionsEl.innerHTML = renderQuizOptionButtons(item.options, optionBorder);
372
+ if (submitBtn) {
373
+ submitBtn.disabled = true;
374
+ submitBtn.style.opacity = "0.5";
375
+ submitBtn.style.cursor = "not-allowed";
376
+ submitBtn.innerHTML = `<span>${quizSubmitLabel(step === total - 1)}</span><i class="fa-solid fa-arrow-right"></i>`;
377
+ }
378
+ bindOptions();
379
+ };
380
+ bindOptions();
381
+ if (submitBtn) {
382
+ submitBtn.addEventListener("click", () => {
383
+ if (selectedIndex < 0)
384
+ return;
385
+ answers = collectQuizAnswer(answers, currentStep, selectedIndex);
386
+ const isLast = currentStep >= total - 1;
387
+ if (!isLast) {
388
+ currentStep += 1;
389
+ paintStep(currentStep);
390
+ return;
391
+ }
392
+ submitBtn.disabled = true;
393
+ submitBtn.innerHTML = `<i class="fa-solid fa-spinner fa-spin"></i> <span>กำลังบันทึกคำตอบ…</span>`;
394
+ onQuizComplete?.(answers.slice());
395
+ setTimeout(() => {
396
+ submitBtn.style.background = "#22c55e";
397
+ submitBtn.innerHTML = `<i class="fa-solid fa-circle-check"></i> <span>รับรางวัลสำเร็จ!</span>`;
398
+ if (outcome) {
399
+ outcome.innerHTML = `ยินดีด้วย! คุณได้รับ: <strong style="color:${primary};">${textEscape(rewardItem.name)}</strong>`;
400
+ }
401
+ showCouponSuccess({
402
+ rewardName: rewardItem.name,
403
+ couponCode: rewardItem.couponCode,
404
+ });
405
+ if (onAction)
406
+ onAction(rewardItem);
407
+ }, 500);
59
408
  });
60
- });
409
+ }
61
410
  }
@@ -1,4 +1,7 @@
1
1
  import type { RewardWidgetTheme } from "../index.js";
2
2
  import { type WidgetRewardItem } from "./types.js";
3
+ /** Visible scratch face: Ticket color, or Foil color when ticket is unset. */
4
+ export declare function resolveScratchFoilTint(theme?: RewardWidgetTheme): string;
5
+ export declare function resolveScratchTextColor(theme?: RewardWidgetTheme): string;
3
6
  export declare function renderScratchCardMarkup(rewards?: WidgetRewardItem[], theme?: RewardWidgetTheme): string;
4
7
  export declare function mountScratchCard(target: Element, rewards?: WidgetRewardItem[], theme?: RewardWidgetTheme, onAction?: (reward?: WidgetRewardItem) => void): void;