@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,32 +1,326 @@
1
+ import { DEFAULT_WIDGET_SURFACE, mixPrimary, resolveWidgetChrome } from "./chrome.js";
2
+ import { showCouponSuccess } from "./coupon-success.js";
3
+ import { textEscape } from "./types.js";
1
4
  export function renderMysteryBoxMarkup(rewards = [], theme) {
2
- const primary = theme?.brandPrimaryColor || "#7c3aed";
5
+ const topReward = rewards[0]?.name || "ของรางวัลพิเศษ";
6
+ const { primary, surface, border } = resolveWidgetChrome(theme);
7
+ const boxColor = theme?.hubBackgroundColor?.trim() || DEFAULT_WIDGET_SURFACE;
8
+ const lidColor = boxColor;
9
+ const ribbonColor = primary;
10
+ const stripeColor = theme?.textColor?.trim() || primary;
11
+ const logoFill = theme?.promptColor?.trim() || "#181817";
3
12
  return `
4
- <div class="pcampus-mystery-box-wrap" style="position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;width:12rem;height:12rem;user-select:none;">
5
- <div style="position:absolute;inset:1rem;border-radius:1.5rem;background:${primary};opacity:0.35;filter:blur(20px);"></div>
13
+ <style>
14
+ .pcampus-mystery-box-wrapper {
15
+ perspective: 1200px;
16
+ transform-style: preserve-3d;
17
+ width: 100%;
18
+ max-width: 22rem;
19
+ height: clamp(16rem, 50vw, 22rem);
20
+ display: flex;
21
+ justify-content: center;
22
+ align-items: flex-end;
23
+ padding: 3.25rem 0 1.75rem;
24
+ box-sizing: border-box;
25
+ position: relative;
26
+ user-select: none;
27
+ overflow: visible;
28
+ }
29
+ .pcampus-gift-box {
30
+ position: relative;
31
+ width: 150px;
32
+ height: 150px;
33
+ transform-style: preserve-3d;
34
+ transform: rotateX(-18deg) rotateY(22deg);
35
+ cursor: pointer;
36
+ }
37
+ .pcampus-gift-box:hover:not(.opened):not(.wiggling) {
38
+ transform: rotateX(-18deg) rotateY(22deg) scale(1.04);
39
+ }
40
+ .pcampus-gift-box.opened {
41
+ transform: rotateX(-18deg) rotateY(22deg);
42
+ cursor: default;
43
+ }
44
+ @keyframes pcampus-box-jiggle {
45
+ 0%, 100% { transform: rotateX(-18deg) rotateY(22deg) rotateZ(0deg); }
46
+ 16% { transform: rotateX(-18deg) rotateY(22deg) rotateZ(-8deg); }
47
+ 32% { transform: rotateX(-18deg) rotateY(22deg) rotateZ(8deg); }
48
+ 48% { transform: rotateX(-18deg) rotateY(22deg) rotateZ(-6deg); }
49
+ 64% { transform: rotateX(-18deg) rotateY(22deg) rotateZ(5deg); }
50
+ 80% { transform: rotateX(-18deg) rotateY(22deg) rotateZ(-3deg); }
51
+ }
52
+ .pcampus-gift-box.wiggling {
53
+ animation: pcampus-box-jiggle 0.48s ease-in-out;
54
+ }
55
+ .pcampus-gift-box-body {
56
+ position: absolute;
57
+ width: 150px;
58
+ height: 150px;
59
+ transform-style: preserve-3d;
60
+ }
61
+ .pcampus-body-face {
62
+ position: absolute;
63
+ top: 0;
64
+ left: 0;
65
+ width: 150px;
66
+ height: 150px;
67
+ background-color: ${boxColor};
68
+ border: 1.5px solid ${border};
69
+ box-sizing: border-box;
70
+ }
71
+ .pcampus-body-face::after {
72
+ content: '';
73
+ position: absolute;
74
+ background-color: ${stripeColor};
75
+ box-shadow: 0 0 8px ${mixPrimary(stripeColor, 30)};
76
+ }
77
+ .pcampus-body-front::after, .pcampus-body-back::after,
78
+ .pcampus-body-left::after, .pcampus-body-right::after {
79
+ width: 30px;
80
+ height: 100%;
81
+ left: 60px;
82
+ top: 0;
83
+ }
84
+ .pcampus-body-front { transform: rotateY(0deg) translateZ(75px); }
85
+ .pcampus-body-back { transform: rotateY(180deg) translateZ(75px); }
86
+ .pcampus-body-left { transform: rotateY(-90deg) translateZ(75px); }
87
+ .pcampus-body-right { transform: rotateY(90deg) translateZ(75px); }
88
+ .pcampus-body-bottom { transform: rotateX(-90deg) translateZ(75px); background-color: #141413; }
6
89
 
7
- <div class="pcampus-mystery-box" style="position:relative;width:8rem;height:8rem;display:flex;flex-direction:column;align-items:center;justify-content:center;border-radius:1rem;border:3px solid rgba(252,211,77,0.8);background:#0f172a;box-shadow:0 20px 40px rgba(0,0,0,0.6);cursor:pointer;transition:transform 200ms ease;">
8
- <div style="position:absolute;top:4px;left:4px;width:10px;height:10px;border-top:2px solid #fde68a;border-left:2px solid #fde68a;"></div>
9
- <div style="position:absolute;top:4px;right:4px;width:10px;height:10px;border-top:2px solid #fde68a;border-right:2px solid #fde68a;"></div>
10
- <div style="position:absolute;bottom:4px;left:4px;width:10px;height:10px;border-bottom:2px solid #fde68a;border-left:2px solid #fde68a;"></div>
11
- <div style="position:absolute;bottom:4px;right:4px;width:10px;height:10px;border-bottom:2px solid #fde68a;border-right:2px solid #fde68a;"></div>
90
+ .pcampus-box-front-seal {
91
+ position: absolute;
92
+ top: 50%;
93
+ left: 50%;
94
+ transform: translate(-50%, -50%);
95
+ width: 64px;
96
+ height: 64px;
97
+ border-radius: 10px;
98
+ background: ${logoFill};
99
+ border: 1.5px solid ${ribbonColor};
100
+ display: flex;
101
+ align-items: center;
102
+ justify-content: center;
103
+ z-index: 5;
104
+ overflow: hidden;
105
+ box-shadow: 0 4px 10px rgba(0,0,0,0.5);
106
+ padding: 4px;
107
+ box-sizing: border-box;
108
+ }
12
109
 
13
- <span style="font-size:2.5rem;font-weight:900;color:#fcd34d;filter:drop-shadow(0 0 10px rgba(252,211,77,0.8));">?</span>
14
- <span style="margin-top:4px;font-size:9px;font-weight:800;letter-spacing:0.1em;color:#fde68a;text-transform:uppercase;">MYSTERY CHEST</span>
110
+ .pcampus-gift-box-lid {
111
+ position: absolute;
112
+ width: 160px;
113
+ height: 38px;
114
+ top: -14px;
115
+ left: -5px;
116
+ transform-style: preserve-3d;
117
+ transform-origin: 50% 100% -80px;
118
+ transform: rotateX(0deg);
119
+ transition: transform 1.55s cubic-bezier(0.22, 1, 0.36, 1);
120
+ z-index: 10;
121
+ }
122
+ .pcampus-gift-box.opened .pcampus-gift-box-lid {
123
+ transform: rotateX(118deg);
124
+ }
125
+ .pcampus-lid-face {
126
+ position: absolute;
127
+ top: 0;
128
+ left: 0;
129
+ background-color: ${lidColor};
130
+ box-sizing: border-box;
131
+ }
132
+ .pcampus-lid-front,
133
+ .pcampus-lid-back,
134
+ .pcampus-lid-left,
135
+ .pcampus-lid-right {
136
+ border: 1.5px solid ${border};
137
+ }
138
+ .pcampus-lid-top {
139
+ width: 160px;
140
+ height: 160px;
141
+ transform: rotateX(90deg) translateZ(80px);
142
+ display: flex;
143
+ align-items: center;
144
+ justify-content: center;
145
+ position: relative;
146
+ transform-style: preserve-3d;
147
+ overflow: visible;
148
+ border: none;
149
+ }
150
+ .pcampus-lid-top::before {
151
+ content: '';
152
+ position: absolute;
153
+ width: 30px;
154
+ height: 100%;
155
+ left: 65px;
156
+ top: 0;
157
+ background-color: ${stripeColor};
158
+ }
159
+ .pcampus-lid-top::after {
160
+ content: '';
161
+ position: absolute;
162
+ width: 100%;
163
+ height: 30px;
164
+ left: 0;
165
+ top: 65px;
166
+ background-color: ${stripeColor};
167
+ }
168
+ .pcampus-lid-front { width: 160px; height: 38px; transform: rotateY(0deg) translateZ(80px); }
169
+ .pcampus-lid-front::after { content: ''; position: absolute; width: 30px; height: 100%; left: 65px; background-color: ${stripeColor}; }
170
+ .pcampus-lid-back { width: 160px; height: 38px; transform: rotateY(180deg) translateZ(80px); }
171
+ .pcampus-lid-back::after { content: ''; position: absolute; width: 30px; height: 100%; left: 65px; background-color: ${stripeColor}; }
172
+ .pcampus-lid-left { width: 160px; height: 38px; transform: rotateY(-90deg) translateZ(80px); }
173
+ .pcampus-lid-left::after { content: ''; position: absolute; width: 30px; height: 100%; left: 65px; background-color: ${stripeColor}; }
174
+ .pcampus-lid-right { width: 160px; height: 38px; transform: rotateY(90deg) translateZ(80px); }
175
+ .pcampus-lid-right::after { content: ''; position: absolute; width: 30px; height: 100%; left: 65px; background-color: ${stripeColor}; }
176
+
177
+ .pcampus-box-glow {
178
+ position: absolute;
179
+ top: 50%;
180
+ left: 50%;
181
+ transform: translate(-50%, -50%) scale(0.1);
182
+ width: 14rem;
183
+ height: 14rem;
184
+ background: radial-gradient(circle, ${mixPrimary(primary, 50)} 0%, ${mixPrimary(primary, 0)} 70%);
185
+ z-index: 1;
186
+ pointer-events: none;
187
+ opacity: 0;
188
+ transition: transform 0.8s ease-out, opacity 0.8s;
189
+ }
190
+ .pcampus-mystery-box-wrapper.opened .pcampus-box-glow {
191
+ transform: translate(-50%, -50%) scale(1.5);
192
+ opacity: 1;
193
+ }
194
+
195
+ .pcampus-prize-popup {
196
+ position: absolute;
197
+ left: 50%;
198
+ bottom: 11.5rem;
199
+ transform: translateX(-50%) translateY(0.5rem) scale(0.85);
200
+ opacity: 0;
201
+ z-index: 20;
202
+ transition: transform 0.55s cubic-bezier(0.22, 1, 0.36, 1) 0.7s, opacity 0.55s ease 0.7s;
203
+ pointer-events: none;
204
+ background: rgba(28, 28, 27, 0.95);
205
+ border: 2px solid ${ribbonColor};
206
+ padding: 0.85rem 1.4rem;
207
+ border-radius: 16px;
208
+ box-shadow: 0 15px 35px rgba(0, 0, 0, 0.5);
209
+ white-space: nowrap;
210
+ text-align: center;
211
+ display: flex;
212
+ flex-direction: column;
213
+ align-items: center;
214
+ }
215
+ .pcampus-mystery-box-wrapper.opened .pcampus-prize-popup {
216
+ transform: translateX(-50%) translateY(0) scale(1);
217
+ opacity: 1;
218
+ }
219
+ </style>
220
+
221
+ <div class="pcampus-mystery-box-wrap" style="position:relative;display:flex;flex-direction:column;align-items:center;width:100%;box-sizing:border-box;background:${surface};">
222
+
223
+ <div class="pcampus-mystery-box-wrapper">
224
+ <div class="pcampus-mystery-box pcampus-gift-box">
225
+
226
+ <div class="pcampus-gift-box-body">
227
+ <div class="pcampus-body-face pcampus-body-front">
228
+ <div class="pcampus-box-front-seal">
229
+ ${theme?.brandLogoUrl ? `
230
+ <img src="${textEscape(theme.brandLogoUrl)}" alt="${textEscape(theme?.brandName || 'Brand')}" style="max-width:92%;max-height:92%;object-fit:contain;" />
231
+ ` : `
232
+ <i class="fa-solid fa-lock" style="font-size:0.75rem;color:${ribbonColor};"></i>
233
+ `}
234
+ </div>
235
+ </div>
236
+ <div class="pcampus-body-face pcampus-body-back"></div>
237
+ <div class="pcampus-body-face pcampus-body-left"></div>
238
+ <div class="pcampus-body-face pcampus-body-right"></div>
239
+ <div class="pcampus-body-face pcampus-body-bottom"></div>
240
+ </div>
241
+
242
+ <div class="pcampus-gift-box-lid">
243
+ <div class="pcampus-lid-face pcampus-lid-top"></div>
244
+ <div class="pcampus-lid-face pcampus-lid-front"></div>
245
+ <div class="pcampus-lid-face pcampus-lid-back"></div>
246
+ <div class="pcampus-lid-face pcampus-lid-left"></div>
247
+ <div class="pcampus-lid-face pcampus-lid-right"></div>
248
+ </div>
249
+
250
+ </div>
251
+
252
+ <div class="pcampus-prize-popup">
253
+ <span style="font-size:0.7rem;font-weight:700;color:${primary};text-transform:uppercase;letter-spacing:0.1em;margin-bottom:0.25rem;">
254
+ ${textEscape(theme?.brandName ? `${theme.brandName} REWARD` : "CONGRATULATIONS")}
255
+ </span>
256
+ <span style="font-size:1.15rem;font-weight:900;color:#ffffff;">${textEscape(topReward)}</span>
257
+ </div>
258
+ <div class="pcampus-box-glow"></div>
15
259
  </div>
260
+
261
+ <div class="pcampus-mystery-outcome" style="margin-top:1rem;font-size:0.92rem;font-weight:600;color:#f0efed;text-align:center;padding:0.45rem 1.25rem;border-radius:9999px;border:1px solid rgba(240, 239, 237, 0.08);background:rgba(240, 239, 237, 0.03);">
262
+ แตะที่กล่องของขวัญ MYSTERY CHEST เพื่อเปิดดูรางวัล
263
+ </div>
264
+
16
265
  </div>
17
266
  `;
18
267
  }
268
+ function ensureFontAwesomeLoaded() {
269
+ if (typeof document === "undefined")
270
+ return;
271
+ const id = "pcampus-fa-cdn";
272
+ if (!document.getElementById(id)) {
273
+ const link = document.createElement("link");
274
+ link.id = id;
275
+ link.rel = "stylesheet";
276
+ link.href = "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css";
277
+ document.head.appendChild(link);
278
+ }
279
+ }
19
280
  export function mountMysteryBox(target, rewards = [], theme, onAction) {
281
+ ensureFontAwesomeLoaded();
20
282
  target.innerHTML = renderMysteryBoxMarkup(rewards, theme);
21
- const box = target.querySelector(".pcampus-mystery-box");
283
+ const wrapper = target.querySelector(".pcampus-mystery-box-wrapper");
284
+ const box = target.querySelector(".pcampus-gift-box");
285
+ const outcome = target.querySelector(".pcampus-mystery-outcome");
286
+ let isOpened = false;
287
+ const rewardItem = rewards[0] || { name: "ของขวัญพิเศษ" };
288
+ const { primary } = resolveWidgetChrome(theme);
22
289
  if (box) {
23
- box.addEventListener("click", () => {
24
- box.style.transform = "scale(1.1) rotate(-3deg)";
290
+ let lidStarted = false;
291
+ const openLid = () => {
292
+ if (lidStarted)
293
+ return;
294
+ lidStarted = true;
295
+ box.classList.remove("wiggling");
296
+ box.classList.add("opened");
297
+ wrapper?.classList.add("opened");
298
+ if (outcome) {
299
+ outcome.innerHTML = `ยินดีด้วย! คุณได้รับ: <strong style="color:${primary};">${textEscape(rewardItem.name)}</strong>`;
300
+ }
301
+ showCouponSuccess({
302
+ rewardName: rewardItem.name,
303
+ couponCode: rewardItem.couponCode,
304
+ });
25
305
  setTimeout(() => {
26
- box.style.transform = "scale(1)";
27
306
  if (onAction)
28
- onAction(rewards[0]);
29
- }, 300);
307
+ onAction(rewardItem);
308
+ }, 1600);
309
+ };
310
+ box.addEventListener("click", () => {
311
+ if (isOpened)
312
+ return;
313
+ isOpened = true;
314
+ box.classList.add("wiggling");
315
+ const fallback = window.setTimeout(openLid, 520);
316
+ const onJiggleEnd = (event) => {
317
+ if (event.animationName !== "pcampus-box-jiggle")
318
+ return;
319
+ window.clearTimeout(fallback);
320
+ box.removeEventListener("animationend", onJiggleEnd);
321
+ openLid();
322
+ };
323
+ box.addEventListener("animationend", onJiggleEnd);
30
324
  });
31
325
  }
32
326
  }
@@ -1,4 +1,7 @@
1
1
  import type { RewardWidgetTheme } from "../index.js";
2
2
  import { type WidgetRewardItem } from "./types.js";
3
+ export declare const PICK_CARD_VISIBLE_MAX = 6;
4
+ export declare function pickCardVisibleItems(rewards?: WidgetRewardItem[]): WidgetRewardItem[];
5
+ export declare function pickWeightedReward(rewards: WidgetRewardItem[], random?: () => number): WidgetRewardItem;
3
6
  export declare function renderPickCardMarkup(rewards?: WidgetRewardItem[], theme?: RewardWidgetTheme): string;
4
7
  export declare function mountPickCard(target: Element, rewards?: WidgetRewardItem[], theme?: RewardWidgetTheme, onAction?: (reward?: WidgetRewardItem) => void): void;
@@ -1,51 +1,213 @@
1
+ import { DEFAULT_WIDGET_SURFACE, resolveWidgetChrome } from "./chrome.js";
2
+ import { showCouponSuccess } from "./coupon-success.js";
3
+ import { textEscape } from "./types.js";
4
+ export const PICK_CARD_VISIBLE_MAX = 6;
5
+ const PLACEHOLDER_CARDS = [
6
+ { name: "ส่วนลด 100 บาท" },
7
+ { name: "รับโบนัส 500 คะแนน" },
8
+ { name: "คูปองส่งฟรี VIP" },
9
+ ];
10
+ export function pickCardVisibleItems(rewards = []) {
11
+ const source = rewards.length > 0 ? rewards : PLACEHOLDER_CARDS;
12
+ return source.slice(0, PICK_CARD_VISIBLE_MAX);
13
+ }
14
+ export function pickWeightedReward(rewards, random = Math.random) {
15
+ if (rewards.length === 0) {
16
+ return { name: "ส่วนลดพิเศษ" };
17
+ }
18
+ const weights = rewards.map((reward) => {
19
+ const weight = Number(reward.weight);
20
+ return Number.isFinite(weight) && weight > 0 ? weight : 0;
21
+ });
22
+ const total = weights.reduce((sum, weight) => sum + weight, 0);
23
+ if (total <= 0) {
24
+ const index = Math.min(rewards.length - 1, Math.floor(random() * rewards.length));
25
+ return rewards[index];
26
+ }
27
+ let ticket = random() * total;
28
+ for (let index = 0; index < rewards.length; index += 1) {
29
+ ticket -= weights[index];
30
+ if (ticket <= 0) {
31
+ return rewards[index];
32
+ }
33
+ }
34
+ return rewards[rewards.length - 1];
35
+ }
1
36
  export function renderPickCardMarkup(rewards = [], theme) {
2
- const primary = theme?.brandPrimaryColor || "#1e40af";
3
- const items = rewards.length >= 3
4
- ? rewards.slice(0, 3)
5
- : [
6
- rewards[0] || { name: "1st Prize" },
7
- rewards[1] || { name: "2nd Prize" },
8
- rewards[2] || { name: "3rd Prize" },
9
- ];
37
+ const { primary, surface, border } = resolveWidgetChrome(theme);
38
+ const cardFill = theme?.hubBackgroundColor?.trim() || DEFAULT_WIDGET_SURFACE;
39
+ const ink = theme?.textColor?.trim() || "#f0efed";
40
+ const prompt = theme?.promptColor?.trim() || "#f0efed";
41
+ const items = pickCardVisibleItems(rewards);
42
+ const weightedPool = rewards.length > PICK_CARD_VISIBLE_MAX;
10
43
  const cardsHtml = items
11
44
  .map((item, idx) => `
12
- <div class="pcampus-pick-card" data-card-index="${idx}" style="position:relative;width:5rem;height:8.5rem;display:flex;flex-direction:column;align-items:center;justify-content:space-between;padding:0.5rem;border-radius:0.75rem;border:2px solid rgba(252,211,77,0.6);background:${primary};box-shadow:0 12px 24px rgba(0,0,0,0.4);cursor:pointer;user-select:none;transition:transform 200ms ease, box-shadow 200ms ease;">
13
- <div style="display:flex;width:100%;justify-content:space-between;color:#fde68a;">
14
- <svg style="width:8px;height:8px;fill:currentColor;" viewBox="0 0 24 24"><path d="M12 2l2.5 7.5L22 12l-7.5 2.5L12 22l-2.5-7.5L2 12l7.5-2.5z"/></svg>
15
- <svg style="width:8px;height:8px;fill:currentColor;" viewBox="0 0 24 24"><path d="M12 2l2.5 7.5L22 12l-7.5 2.5L12 22l-2.5-7.5L2 12l7.5-2.5z"/></svg>
45
+ <div class="pcampus-pick-card pcampus-card-item" data-card-index="${idx}" style="--i:${idx};--n:${items.length}">
46
+ <div class="pcampus-card-face pcampus-card-front">
47
+ ${theme?.brandLogoUrl
48
+ ? `<img src="${textEscape(theme.brandLogoUrl)}" alt="" class="pcampus-card-mark" />`
49
+ : `<span class="pcampus-card-mark pcampus-card-mark-dot" aria-hidden="true"></span>`}
16
50
  </div>
17
-
18
- <div class="pcampus-card-center" style="display:flex;flex-direction:column;align-items:center;gap:2px;">
19
- <svg style="width:24px;height:24px;color:#fde68a;" fill="none" viewBox="0 0 24 24" stroke="currentColor">
20
- <rect x="5" y="3" width="14" height="18" rx="2" stroke-width="1.5" fill="rgba(255,255,255,0.15)"/>
21
- <path d="M12 8l1.5 2.5L16 12l-2.5 1.5L12 16l-1.5-2.5L8 12l2.5-1.5z" fill="currentColor"/>
22
- </svg>
23
- <span style="font-size:9px;font-weight:900;letter-spacing:0.05em;color:#ffffff;text-transform:uppercase;">PICK</span>
24
- </div>
25
-
26
- <div style="display:flex;width:100%;justify-content:space-between;color:#fde68a;">
27
- <svg style="width:8px;height:8px;fill:currentColor;" viewBox="0 0 24 24"><path d="M12 2l2.5 7.5L22 12l-7.5 2.5L12 22l-2.5-7.5L2 12l7.5-2.5z"/></svg>
28
- <svg style="width:8px;height:8px;fill:currentColor;" viewBox="0 0 24 24"><path d="M12 2l2.5 7.5L22 12l-7.5 2.5L12 22l-2.5-7.5L2 12l7.5-2.5z"/></svg>
51
+ <div class="pcampus-card-face pcampus-card-back">
52
+ <div class="pcampus-card-back-prize">${weightedPool ? "" : textEscape(item.name)}</div>
29
53
  </div>
30
54
  </div>
31
55
  `)
32
56
  .join("");
33
57
  return `
34
- <div class="pcampus-pick-card-wrap" style="display:flex;align-items:center;justify-content:center;gap:0.6rem;padding:0.5rem;">
35
- ${cardsHtml}
58
+ <style>
59
+ .pcampus-cards-container {
60
+ display: flex;
61
+ flex-wrap: wrap;
62
+ justify-content: center;
63
+ align-items: center;
64
+ gap: clamp(0.5rem, 2.2vw, 1.1rem);
65
+ width: 100%;
66
+ max-width: 42rem;
67
+ perspective: 1200px;
68
+ padding: 1.5rem 0.25rem;
69
+ box-sizing: border-box;
70
+ }
71
+ .pcampus-card-item {
72
+ width: min(24vw, 7.6rem);
73
+ height: min(37vw, 11.6rem);
74
+ position: relative;
75
+ transform-style: preserve-3d;
76
+ transition: transform 0.24s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.24s ease, opacity 0.3s ease;
77
+ cursor: pointer;
78
+ user-select: none;
79
+ touch-action: manipulation;
80
+ }
81
+ .pcampus-card-item:hover:not(.flipped):not(.dimmed) {
82
+ transform: translateY(-10px);
83
+ }
84
+ .pcampus-card-item.flipped {
85
+ transform: rotateY(180deg) scale(1.04) !important;
86
+ transition: transform 0.65s cubic-bezier(0.34, 1.56, 0.64, 1);
87
+ cursor: default;
88
+ }
89
+ .pcampus-card-item.dimmed {
90
+ opacity: 0.35;
91
+ transform: scale(0.92);
92
+ cursor: default;
93
+ pointer-events: none;
94
+ }
95
+ .pcampus-cards-container.is-stacked {
96
+ display: grid;
97
+ place-items: center;
98
+ height: min(46vw, 14.5rem);
99
+ max-width: 26rem;
100
+ }
101
+ .pcampus-cards-container.is-stacked .pcampus-card-item {
102
+ grid-area: 1 / 1;
103
+ --mid: calc((var(--n) - 1) / 2);
104
+ --dx: calc((var(--i) - var(--mid)) * min(1.45rem, 12rem / var(--n)));
105
+ --rot: calc((var(--i) - var(--mid)) * 3.4deg);
106
+ transform: translateX(var(--dx)) rotate(var(--rot));
107
+ z-index: calc(var(--i) + 1);
108
+ }
109
+ .pcampus-cards-container.is-stacked .pcampus-card-item:hover:not(.flipped):not(.dimmed) {
110
+ transform: translateX(var(--dx)) translateY(-16px) rotate(var(--rot));
111
+ }
112
+ .pcampus-cards-container.is-stacked .pcampus-card-item.flipped {
113
+ transform: translateX(var(--dx)) rotateY(180deg) scale(1.08) !important;
114
+ z-index: 50;
115
+ }
116
+ .pcampus-cards-container.is-stacked .pcampus-card-item.dimmed {
117
+ opacity: 0.28;
118
+ transform: translateX(var(--dx)) rotate(var(--rot)) scale(0.9);
119
+ }
120
+ .pcampus-card-face {
121
+ position: absolute;
122
+ inset: 0;
123
+ width: 100%;
124
+ height: 100%;
125
+ backface-visibility: hidden;
126
+ border-radius: clamp(12px, 2.5vw, 16px);
127
+ display: flex;
128
+ align-items: center;
129
+ justify-content: center;
130
+ padding: 0.55rem;
131
+ box-sizing: border-box;
132
+ text-align: center;
133
+ box-shadow: 0 12px 28px rgba(0, 0, 0, 0.4);
134
+ overflow: hidden;
135
+ }
136
+ .pcampus-card-front {
137
+ background: ${cardFill};
138
+ border: 2px solid ${border};
139
+ }
140
+ .pcampus-card-mark {
141
+ width: 78%;
142
+ height: 78%;
143
+ object-fit: contain;
144
+ }
145
+ .pcampus-card-mark-dot {
146
+ display: block;
147
+ width: 0.7rem;
148
+ height: 0.7rem;
149
+ border-radius: 50%;
150
+ background: ${primary};
151
+ opacity: 0.85;
152
+ }
153
+ .pcampus-card-back {
154
+ background: ${cardFill};
155
+ border: 2px solid ${border};
156
+ transform: rotateY(180deg);
157
+ }
158
+ .pcampus-card-back-prize {
159
+ font-weight: 700;
160
+ font-size: clamp(0.85rem, 2.4vw, 1.05rem);
161
+ color: ${ink};
162
+ line-height: 1.3;
163
+ }
164
+ </style>
165
+
166
+ <div class="pcampus-pick-card-wrap" style="position:relative;display:flex;flex-direction:column;align-items:center;width:100%;box-sizing:border-box;background:${surface};">
167
+ <div class="pcampus-cards-container${items.length >= 5 ? " is-stacked" : ""}" data-card-count="${items.length}">
168
+ ${cardsHtml}
169
+ </div>
170
+ <div class="pcampus-pick-card-outcome" style="margin-top:1.25rem;font-size:0.92rem;font-weight:600;color:${prompt};text-align:center;">
171
+ เลือกการ์ด 1 ใบเพื่อเปิดดูรางวัล
172
+ </div>
36
173
  </div>
37
174
  `;
38
175
  }
39
176
  export function mountPickCard(target, rewards = [], theme, onAction) {
40
177
  target.innerHTML = renderPickCardMarkup(rewards, theme);
41
- const cards = target.querySelectorAll(".pcampus-pick-card");
42
- cards.forEach((card, idx) => {
178
+ const cardElements = target.querySelectorAll(".pcampus-card-item");
179
+ const outcome = target.querySelector(".pcampus-pick-card-outcome");
180
+ let isPicked = false;
181
+ cardElements.forEach((card, idx) => {
43
182
  card.addEventListener("click", () => {
44
- card.style.transform = "translateY(-10px) scale(1.05)";
45
- card.style.borderColor = "#fcd34d";
46
- const chosen = rewards[idx] || rewards[0];
47
- if (onAction)
48
- onAction(chosen);
183
+ if (isPicked)
184
+ return;
185
+ isPicked = true;
186
+ const pool = rewards.length > 0 ? rewards : PLACEHOLDER_CARDS;
187
+ const chosenReward = pool.length > PICK_CARD_VISIBLE_MAX
188
+ ? pickWeightedReward(pool)
189
+ : pool[idx] || pool[0] || { name: "ส่วนลดพิเศษ" };
190
+ card.classList.add("flipped");
191
+ const prizeLabel = card.querySelector(".pcampus-card-back-prize");
192
+ if (prizeLabel) {
193
+ prizeLabel.textContent = chosenReward.name;
194
+ }
195
+ cardElements.forEach((other, otherIdx) => {
196
+ if (otherIdx !== idx) {
197
+ other.classList.add("dimmed");
198
+ }
199
+ });
200
+ if (outcome) {
201
+ outcome.innerHTML = `ยินดีด้วย! คุณได้รับ: <strong>${textEscape(chosenReward.name)}</strong>`;
202
+ }
203
+ showCouponSuccess({
204
+ rewardName: chosenReward.name,
205
+ couponCode: chosenReward.couponCode,
206
+ });
207
+ setTimeout(() => {
208
+ if (onAction)
209
+ onAction(chosenReward);
210
+ }, 500);
49
211
  });
50
212
  });
51
213
  }
@@ -1,4 +1,14 @@
1
1
  import type { RewardWidgetTheme } from "../index.js";
2
- import { type WidgetRewardItem } from "./types.js";
3
- export declare function renderQuizMarkup(rewards?: WidgetRewardItem[], theme?: RewardWidgetTheme): string;
4
- export declare function mountQuiz(target: Element, rewards?: WidgetRewardItem[], theme?: RewardWidgetTheme, onAction?: (reward?: WidgetRewardItem) => void): void;
2
+ import { type QuizAnswerIndex, type QuizContent, type WidgetRewardItem } from "./types.js";
3
+ type ResolvedQuiz = {
4
+ heading: string;
5
+ items: Array<{
6
+ question: string;
7
+ options: string[];
8
+ }>;
9
+ };
10
+ export declare function resolveQuizContent(content?: QuizContent, brandName?: string): ResolvedQuiz;
11
+ export declare function renderQuizMarkup(rewards?: WidgetRewardItem[], theme?: RewardWidgetTheme, quiz?: QuizContent): string;
12
+ export declare function collectQuizAnswer(answers: QuizAnswerIndex[], questionIndex: number, optionIndex: number): QuizAnswerIndex[];
13
+ export declare function mountQuiz(target: Element, rewards?: WidgetRewardItem[], theme?: RewardWidgetTheme, onAction?: (reward?: WidgetRewardItem) => void, quiz?: QuizContent, onQuizComplete?: (answers: QuizAnswerIndex[]) => void): void;
14
+ export {};