@whykusanagi/corrupted-theme 0.1.1 → 0.1.3
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/CHANGELOG.md +253 -0
- package/README.md +97 -7
- package/docs/CAPABILITIES.md +209 -0
- package/docs/CHARACTER_LEVEL_CORRUPTION.md +264 -0
- package/docs/COMPONENTS_REFERENCE.md +295 -8
- package/docs/CORRUPTION_PHRASES.md +529 -0
- package/docs/FUTURE_WORK.md +189 -0
- package/docs/IMPLEMENTATION_VALIDATION.md +401 -0
- package/docs/LLM_PROVIDERS.md +345 -0
- package/docs/PERSONALITY.md +128 -0
- package/docs/ROADMAP.md +266 -0
- package/docs/ROUTING.md +324 -0
- package/docs/STYLE_GUIDE.md +605 -0
- package/docs/brand/BRAND_OVERVIEW.md +413 -0
- package/docs/brand/COLOR_SYSTEM.md +583 -0
- package/docs/brand/DESIGN_TOKENS.md +1009 -0
- package/docs/brand/TRANSLATION_FAILURE_AESTHETIC.md +525 -0
- package/docs/brand/TYPOGRAPHY.md +624 -0
- package/docs/components/ANIMATION_GUIDELINES.md +901 -0
- package/docs/components/COMPONENT_LIBRARY.md +1061 -0
- package/docs/components/GLASSMORPHISM.md +602 -0
- package/docs/components/INTERACTIVE_STATES.md +766 -0
- package/docs/governance/CONTRIBUTION_GUIDELINES.md +593 -0
- package/docs/governance/DESIGN_SYSTEM_GOVERNANCE.md +451 -0
- package/docs/governance/VERSION_MANAGEMENT.md +447 -0
- package/docs/governance/VERSION_REFERENCES.md +229 -0
- package/docs/platforms/CLI_IMPLEMENTATION.md +1025 -0
- package/docs/platforms/COMPONENT_MAPPING.md +579 -0
- package/docs/platforms/NPM_PACKAGE.md +854 -0
- package/docs/platforms/WEB_IMPLEMENTATION.md +1221 -0
- package/docs/standards/ACCESSIBILITY.md +715 -0
- package/docs/standards/ANTI_PATTERNS.md +554 -0
- package/docs/standards/SPACING_SYSTEM.md +549 -0
- package/examples/assets/celeste-avatar.png +0 -0
- package/examples/button.html +22 -10
- package/examples/card.html +22 -9
- package/examples/extensions-showcase.html +716 -0
- package/examples/form.html +22 -9
- package/examples/index.html +619 -396
- package/examples/layout.html +22 -8
- package/examples/nikke-team-builder.html +23 -9
- package/examples/showcase-complete.html +884 -28
- package/examples/showcase.html +21 -8
- package/package.json +14 -5
- package/src/css/components.css +676 -0
- package/src/css/extensions.css +933 -0
- package/src/css/theme.css +6 -74
- package/src/css/typography.css +5 -0
- package/src/lib/character-corruption.js +563 -0
- package/src/lib/components.js +283 -0
- package/src/lib/countdown-widget.js +609 -0
- package/src/lib/gallery.js +481 -0
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* countdown-widget.js — Event Countdown Widget with Configurable Shapes
|
|
3
|
+
*
|
|
4
|
+
* A sophisticated countdown widget with JSON configuration support,
|
|
5
|
+
* multiple shape containers, and animated popup messages.
|
|
6
|
+
*
|
|
7
|
+
* @module countdown-widget
|
|
8
|
+
* @version 1.0.0
|
|
9
|
+
* @license MIT
|
|
10
|
+
*
|
|
11
|
+
* Features:
|
|
12
|
+
* - JSON-based configuration
|
|
13
|
+
* - Multiple shape containers (diamond, circle, heart, star, hexagon, octagon)
|
|
14
|
+
* - Character image with optional overlay
|
|
15
|
+
* - Animated popup messages
|
|
16
|
+
* - Real-time countdown timer
|
|
17
|
+
* - Completion state handling
|
|
18
|
+
* - Responsive design
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* ```html
|
|
22
|
+
* <div id="countdown-widget"></div>
|
|
23
|
+
*
|
|
24
|
+
* <script type="module">
|
|
25
|
+
* import { initCountdown } from '@whykusanagi/corrupted-theme/src/lib/countdown-widget.js';
|
|
26
|
+
*
|
|
27
|
+
* // Using URL parameter: ?event=kirara loads /data/countdown/kirara.json
|
|
28
|
+
* initCountdown();
|
|
29
|
+
*
|
|
30
|
+
* // Or with inline config:
|
|
31
|
+
* initCountdown({
|
|
32
|
+
* config: {
|
|
33
|
+
* title: 'Launch Countdown',
|
|
34
|
+
* eventDate: '2025-04-01T00:00:00-07:00',
|
|
35
|
+
* character: { image: 'character.png' }
|
|
36
|
+
* }
|
|
37
|
+
* });
|
|
38
|
+
* </script>
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
// ============================================================================
|
|
43
|
+
// CONFIGURATION SCHEMA
|
|
44
|
+
// ============================================================================
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @typedef {Object} CountdownConfig
|
|
48
|
+
* @property {string} title - Title displayed above countdown
|
|
49
|
+
* @property {string} eventDate - ISO 8601 date string for target time
|
|
50
|
+
* @property {string} [basicMessage] - Short description
|
|
51
|
+
* @property {string} [detailedMessage] - HTML message with links
|
|
52
|
+
* @property {string} [completedMessage] - Message shown when countdown ends
|
|
53
|
+
* @property {string} [style='compact'] - Widget style variant
|
|
54
|
+
* @property {CharacterConfig} [character] - Character image configuration
|
|
55
|
+
* @property {PopupConfig} [popup] - Popup message configuration
|
|
56
|
+
* @property {ColorConfig} [colors] - Color overrides
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @typedef {Object} CharacterConfig
|
|
61
|
+
* @property {string} image - Character image URL or path
|
|
62
|
+
* @property {number} [rotation=0] - Image rotation in degrees
|
|
63
|
+
* @property {string} [objectPosition] - CSS object-position value
|
|
64
|
+
* @property {BackgroundConfig} [background] - Shape background config
|
|
65
|
+
* @property {OverlayConfig} [overlay] - Overlay image config
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @typedef {Object} BackgroundConfig
|
|
70
|
+
* @property {string} [type='diamond'] - Shape type
|
|
71
|
+
* @property {string} [color] - CSS background value
|
|
72
|
+
* @property {string} [borderColor] - Hex color for border
|
|
73
|
+
* @property {boolean} [pattern=false] - Use pattern overlay
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @typedef {Object} OverlayConfig
|
|
78
|
+
* @property {string} [image] - Overlay image URL
|
|
79
|
+
* @property {string} [position='behind'] - 'behind' or 'front'
|
|
80
|
+
* @property {string} [animation] - Animation type ('float' or null)
|
|
81
|
+
* @property {number} [rotation] - Overlay rotation
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @typedef {Object} PopupConfig
|
|
86
|
+
* @property {string} message - HTML content for popup
|
|
87
|
+
* @property {number} [frequency=10000] - Ms between popups
|
|
88
|
+
* @property {number} [duration=5000] - Ms popup stays visible
|
|
89
|
+
* @property {Object} [colors] - Popup color overrides
|
|
90
|
+
*/
|
|
91
|
+
|
|
92
|
+
// ============================================================================
|
|
93
|
+
// DEFAULT CONFIGURATION
|
|
94
|
+
// ============================================================================
|
|
95
|
+
|
|
96
|
+
const DEFAULT_CONFIG = {
|
|
97
|
+
title: 'Event Countdown',
|
|
98
|
+
eventDate: null,
|
|
99
|
+
basicMessage: '',
|
|
100
|
+
detailedMessage: '',
|
|
101
|
+
completedMessage: 'Event is Live!',
|
|
102
|
+
style: 'compact',
|
|
103
|
+
character: {
|
|
104
|
+
image: null,
|
|
105
|
+
rotation: 0,
|
|
106
|
+
objectPosition: 'center',
|
|
107
|
+
background: {
|
|
108
|
+
type: 'diamond',
|
|
109
|
+
color: null,
|
|
110
|
+
borderColor: null,
|
|
111
|
+
pattern: false
|
|
112
|
+
},
|
|
113
|
+
overlay: null
|
|
114
|
+
},
|
|
115
|
+
popup: null,
|
|
116
|
+
colors: {
|
|
117
|
+
primary: null,
|
|
118
|
+
accent: null,
|
|
119
|
+
text: null
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const WIDGET_OPTIONS = {
|
|
124
|
+
containerId: 'countdown-widget',
|
|
125
|
+
configPath: 'data/countdown',
|
|
126
|
+
assetBasePath: ''
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// ============================================================================
|
|
130
|
+
// STATE
|
|
131
|
+
// ============================================================================
|
|
132
|
+
|
|
133
|
+
let state = {
|
|
134
|
+
config: null,
|
|
135
|
+
countdownInterval: null,
|
|
136
|
+
popupInterval: null,
|
|
137
|
+
isCompleted: false
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// ============================================================================
|
|
141
|
+
// ASSET HANDLING
|
|
142
|
+
// ============================================================================
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Converts relative asset paths to full URLs
|
|
146
|
+
* @private
|
|
147
|
+
* @param {string} path - Asset path
|
|
148
|
+
* @returns {string} Full URL
|
|
149
|
+
*/
|
|
150
|
+
function resolveAssetPath(path) {
|
|
151
|
+
if (!path) return '';
|
|
152
|
+
|
|
153
|
+
// Already a full URL
|
|
154
|
+
if (path.startsWith('http://') || path.startsWith('https://') || path.startsWith('data:')) {
|
|
155
|
+
return path;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Check for global AssetConfig (for environment-aware paths)
|
|
159
|
+
if (typeof window !== 'undefined' && window.AssetConfig?.convertUrl) {
|
|
160
|
+
return window.AssetConfig.convertUrl(path);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Use base path if configured
|
|
164
|
+
if (WIDGET_OPTIONS.assetBasePath) {
|
|
165
|
+
return `${WIDGET_OPTIONS.assetBasePath}/${path}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return path;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ============================================================================
|
|
172
|
+
// CONFIG LOADING
|
|
173
|
+
// ============================================================================
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Loads countdown configuration from JSON file
|
|
177
|
+
* @private
|
|
178
|
+
* @param {string} eventName - Event name (filename without .json)
|
|
179
|
+
* @returns {Promise<CountdownConfig>}
|
|
180
|
+
*/
|
|
181
|
+
async function loadConfigFromJson(eventName) {
|
|
182
|
+
const url = `${WIDGET_OPTIONS.configPath}/${eventName}.json`;
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const response = await fetch(url, { cache: 'no-store' });
|
|
186
|
+
|
|
187
|
+
if (!response.ok) {
|
|
188
|
+
throw new Error(`Failed to load config: ${response.status}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return await response.json();
|
|
192
|
+
} catch (error) {
|
|
193
|
+
console.error(`[CountdownWidget] Error loading config for "${eventName}":`, error);
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Gets URL parameter value
|
|
200
|
+
* @private
|
|
201
|
+
* @param {string} name - Parameter name
|
|
202
|
+
* @returns {string|null}
|
|
203
|
+
*/
|
|
204
|
+
function getUrlParam(name) {
|
|
205
|
+
if (typeof window === 'undefined') return null;
|
|
206
|
+
const params = new URLSearchParams(window.location.search);
|
|
207
|
+
return params.get(name);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Merges config with URL parameter overrides
|
|
212
|
+
* @private
|
|
213
|
+
* @param {CountdownConfig} config - Base config
|
|
214
|
+
* @returns {CountdownConfig}
|
|
215
|
+
*/
|
|
216
|
+
function applyUrlOverrides(config) {
|
|
217
|
+
const overrides = {};
|
|
218
|
+
|
|
219
|
+
const dateOverride = getUrlParam('date');
|
|
220
|
+
if (dateOverride) overrides.eventDate = dateOverride;
|
|
221
|
+
|
|
222
|
+
const titleOverride = getUrlParam('title');
|
|
223
|
+
if (titleOverride) overrides.title = decodeURIComponent(titleOverride);
|
|
224
|
+
|
|
225
|
+
return { ...config, ...overrides };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ============================================================================
|
|
229
|
+
// RENDERING
|
|
230
|
+
// ============================================================================
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Renders the countdown widget HTML
|
|
234
|
+
* @private
|
|
235
|
+
* @param {CountdownConfig} config
|
|
236
|
+
* @returns {Object} DOM element references
|
|
237
|
+
*/
|
|
238
|
+
function renderWidget(config) {
|
|
239
|
+
const container = document.getElementById(WIDGET_OPTIONS.containerId);
|
|
240
|
+
if (!container) {
|
|
241
|
+
throw new Error(`[CountdownWidget] Container #${WIDGET_OPTIONS.containerId} not found`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
container.innerHTML = '';
|
|
245
|
+
|
|
246
|
+
const wrapper = document.createElement('div');
|
|
247
|
+
wrapper.className = 'countdown-container';
|
|
248
|
+
|
|
249
|
+
// Shape container
|
|
250
|
+
const shapeType = config.character?.background?.type || 'diamond';
|
|
251
|
+
const shapeContainer = document.createElement('div');
|
|
252
|
+
shapeContainer.className = `shape-container ${shapeType}`;
|
|
253
|
+
|
|
254
|
+
// Apply custom colors
|
|
255
|
+
if (config.character?.background?.color) {
|
|
256
|
+
shapeContainer.style.background = config.character.background.color;
|
|
257
|
+
}
|
|
258
|
+
if (config.character?.background?.borderColor) {
|
|
259
|
+
shapeContainer.style.borderColor = config.character.background.borderColor;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const shapeContent = document.createElement('div');
|
|
263
|
+
shapeContent.className = 'shape-content';
|
|
264
|
+
|
|
265
|
+
// Overlay (behind character)
|
|
266
|
+
if (config.character?.overlay?.image && config.character.overlay.position !== 'front') {
|
|
267
|
+
const overlayWrapper = createOverlay(config.character.overlay);
|
|
268
|
+
shapeContent.appendChild(overlayWrapper);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Character image
|
|
272
|
+
if (config.character?.image) {
|
|
273
|
+
const characterImg = document.createElement('img');
|
|
274
|
+
characterImg.className = 'countdown-character';
|
|
275
|
+
characterImg.src = resolveAssetPath(config.character.image);
|
|
276
|
+
characterImg.alt = config.title || 'Event Character';
|
|
277
|
+
|
|
278
|
+
if (config.character.rotation) {
|
|
279
|
+
characterImg.style.transform = `translate(-50%, -50%) scale(0.9) rotate(${config.character.rotation}deg)`;
|
|
280
|
+
}
|
|
281
|
+
if (config.character.objectPosition) {
|
|
282
|
+
characterImg.style.objectPosition = config.character.objectPosition;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
shapeContent.appendChild(characterImg);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Overlay (front of character)
|
|
289
|
+
if (config.character?.overlay?.image && config.character.overlay.position === 'front') {
|
|
290
|
+
const overlayWrapper = createOverlay(config.character.overlay);
|
|
291
|
+
overlayWrapper.classList.add('front');
|
|
292
|
+
shapeContent.appendChild(overlayWrapper);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
shapeContainer.appendChild(shapeContent);
|
|
296
|
+
wrapper.appendChild(shapeContainer);
|
|
297
|
+
|
|
298
|
+
// Countdown box
|
|
299
|
+
const countdownBox = document.createElement('div');
|
|
300
|
+
countdownBox.className = 'countdown-box';
|
|
301
|
+
countdownBox.innerHTML = `
|
|
302
|
+
<div class="countdown-title">${escapeHtml(config.title)}</div>
|
|
303
|
+
<div class="countdown-timer">
|
|
304
|
+
<span class="unit days">--</span><span class="separator">D</span>
|
|
305
|
+
<span class="unit hours">--</span><span class="separator">H</span>
|
|
306
|
+
<span class="unit minutes">--</span><span class="separator">M</span>
|
|
307
|
+
<span class="unit seconds">--</span><span class="separator">S</span>
|
|
308
|
+
</div>
|
|
309
|
+
`;
|
|
310
|
+
wrapper.appendChild(countdownBox);
|
|
311
|
+
|
|
312
|
+
// Popup (optional)
|
|
313
|
+
let popup = null;
|
|
314
|
+
if (config.popup?.message) {
|
|
315
|
+
popup = document.createElement('div');
|
|
316
|
+
popup.className = 'countdown-popup';
|
|
317
|
+
popup.innerHTML = config.popup.message;
|
|
318
|
+
|
|
319
|
+
if (config.popup.colors) {
|
|
320
|
+
if (config.popup.colors.bg) popup.style.background = config.popup.colors.bg;
|
|
321
|
+
if (config.popup.colors.border) popup.style.borderColor = config.popup.colors.border;
|
|
322
|
+
if (config.popup.colors.text) popup.style.color = config.popup.colors.text;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
wrapper.appendChild(popup);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
container.appendChild(wrapper);
|
|
329
|
+
|
|
330
|
+
return { countdownBox, popup };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Creates overlay element
|
|
335
|
+
* @private
|
|
336
|
+
* @param {OverlayConfig} overlay
|
|
337
|
+
* @returns {HTMLElement}
|
|
338
|
+
*/
|
|
339
|
+
function createOverlay(overlay) {
|
|
340
|
+
const wrapper = document.createElement('div');
|
|
341
|
+
wrapper.className = 'countdown-overlay-wrapper';
|
|
342
|
+
|
|
343
|
+
const img = document.createElement('img');
|
|
344
|
+
img.className = 'countdown-overlay-img';
|
|
345
|
+
img.src = resolveAssetPath(overlay.image);
|
|
346
|
+
img.alt = '';
|
|
347
|
+
|
|
348
|
+
if (overlay.animation === 'float') {
|
|
349
|
+
img.classList.add('animate-float');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (overlay.rotation) {
|
|
353
|
+
img.style.transform = `rotate(${overlay.rotation}deg)`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
wrapper.appendChild(img);
|
|
357
|
+
return wrapper;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Escapes HTML to prevent XSS
|
|
362
|
+
* @private
|
|
363
|
+
* @param {string} text
|
|
364
|
+
* @returns {string}
|
|
365
|
+
*/
|
|
366
|
+
function escapeHtml(text) {
|
|
367
|
+
const div = document.createElement('div');
|
|
368
|
+
div.textContent = text;
|
|
369
|
+
return div.innerHTML;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ============================================================================
|
|
373
|
+
// COUNTDOWN LOGIC
|
|
374
|
+
// ============================================================================
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Updates the countdown timer display
|
|
378
|
+
* @private
|
|
379
|
+
* @param {Date} targetDate
|
|
380
|
+
* @param {HTMLElement} countdownBox
|
|
381
|
+
* @param {string} completedMessage
|
|
382
|
+
*/
|
|
383
|
+
function updateCountdown(targetDate, countdownBox, completedMessage) {
|
|
384
|
+
const now = new Date().getTime();
|
|
385
|
+
const distance = targetDate.getTime() - now;
|
|
386
|
+
|
|
387
|
+
if (distance <= 0) {
|
|
388
|
+
// Countdown completed
|
|
389
|
+
if (!state.isCompleted) {
|
|
390
|
+
state.isCompleted = true;
|
|
391
|
+
countdownBox.classList.add('completed');
|
|
392
|
+
countdownBox.querySelector('.countdown-timer').innerHTML = `
|
|
393
|
+
<span class="completed-message">${escapeHtml(completedMessage)}</span>
|
|
394
|
+
`;
|
|
395
|
+
|
|
396
|
+
// Stop intervals
|
|
397
|
+
if (state.countdownInterval) {
|
|
398
|
+
clearInterval(state.countdownInterval);
|
|
399
|
+
state.countdownInterval = null;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const days = Math.floor(distance / (1000 * 60 * 60 * 24));
|
|
406
|
+
const hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
|
407
|
+
const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
|
|
408
|
+
const seconds = Math.floor((distance % (1000 * 60)) / 1000);
|
|
409
|
+
|
|
410
|
+
const daysEl = countdownBox.querySelector('.days');
|
|
411
|
+
const hoursEl = countdownBox.querySelector('.hours');
|
|
412
|
+
const minutesEl = countdownBox.querySelector('.minutes');
|
|
413
|
+
const secondsEl = countdownBox.querySelector('.seconds');
|
|
414
|
+
|
|
415
|
+
if (daysEl) daysEl.textContent = String(days).padStart(2, '0');
|
|
416
|
+
if (hoursEl) hoursEl.textContent = String(hours).padStart(2, '0');
|
|
417
|
+
if (minutesEl) minutesEl.textContent = String(minutes).padStart(2, '0');
|
|
418
|
+
if (secondsEl) secondsEl.textContent = String(seconds).padStart(2, '0');
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Starts the countdown timer
|
|
423
|
+
* @private
|
|
424
|
+
* @param {string} eventDate
|
|
425
|
+
* @param {HTMLElement} countdownBox
|
|
426
|
+
* @param {string} completedMessage
|
|
427
|
+
*/
|
|
428
|
+
function startCountdown(eventDate, countdownBox, completedMessage) {
|
|
429
|
+
const targetDate = new Date(eventDate);
|
|
430
|
+
|
|
431
|
+
if (isNaN(targetDate.getTime())) {
|
|
432
|
+
console.error('[CountdownWidget] Invalid event date:', eventDate);
|
|
433
|
+
countdownBox.querySelector('.countdown-timer').textContent = 'Invalid Date';
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Initial update
|
|
438
|
+
updateCountdown(targetDate, countdownBox, completedMessage);
|
|
439
|
+
|
|
440
|
+
// Update every second
|
|
441
|
+
state.countdownInterval = setInterval(() => {
|
|
442
|
+
updateCountdown(targetDate, countdownBox, completedMessage);
|
|
443
|
+
}, 1000);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ============================================================================
|
|
447
|
+
// POPUP LOGIC
|
|
448
|
+
// ============================================================================
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Starts the popup cycle
|
|
452
|
+
* @private
|
|
453
|
+
* @param {PopupConfig} popupConfig
|
|
454
|
+
* @param {HTMLElement} popupElement
|
|
455
|
+
*/
|
|
456
|
+
function startPopup(popupConfig, popupElement) {
|
|
457
|
+
if (!popupConfig?.message || !popupElement) return;
|
|
458
|
+
|
|
459
|
+
const frequency = popupConfig.frequency || 10000;
|
|
460
|
+
const duration = popupConfig.duration || 5000;
|
|
461
|
+
|
|
462
|
+
// Show popup initially after a delay
|
|
463
|
+
setTimeout(() => {
|
|
464
|
+
showPopup(popupElement, duration);
|
|
465
|
+
}, 2000);
|
|
466
|
+
|
|
467
|
+
// Start cycle
|
|
468
|
+
state.popupInterval = setInterval(() => {
|
|
469
|
+
showPopup(popupElement, duration);
|
|
470
|
+
}, frequency);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Shows popup for specified duration
|
|
475
|
+
* @private
|
|
476
|
+
* @param {HTMLElement} popup
|
|
477
|
+
* @param {number} duration
|
|
478
|
+
*/
|
|
479
|
+
function showPopup(popup, duration) {
|
|
480
|
+
popup.classList.add('active');
|
|
481
|
+
|
|
482
|
+
setTimeout(() => {
|
|
483
|
+
popup.classList.remove('active');
|
|
484
|
+
}, duration);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// ============================================================================
|
|
488
|
+
// PUBLIC API
|
|
489
|
+
// ============================================================================
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Initializes the countdown widget
|
|
493
|
+
* @param {Object} options - Initialization options
|
|
494
|
+
* @param {string} [options.event] - Event name to load config from JSON
|
|
495
|
+
* @param {CountdownConfig} [options.config] - Inline configuration
|
|
496
|
+
* @param {string} [options.containerId] - Container element ID
|
|
497
|
+
* @param {string} [options.configPath] - Path to config JSON files
|
|
498
|
+
* @param {string} [options.assetBasePath] - Base path for assets
|
|
499
|
+
* @returns {Promise<Object>} Widget API
|
|
500
|
+
*/
|
|
501
|
+
export async function initCountdown(options = {}) {
|
|
502
|
+
// Apply options
|
|
503
|
+
if (options.containerId) WIDGET_OPTIONS.containerId = options.containerId;
|
|
504
|
+
if (options.configPath) WIDGET_OPTIONS.configPath = options.configPath;
|
|
505
|
+
if (options.assetBasePath) WIDGET_OPTIONS.assetBasePath = options.assetBasePath;
|
|
506
|
+
|
|
507
|
+
// Clean up previous instance
|
|
508
|
+
destroyCountdown();
|
|
509
|
+
|
|
510
|
+
let config;
|
|
511
|
+
|
|
512
|
+
try {
|
|
513
|
+
if (options.config) {
|
|
514
|
+
// Use inline config
|
|
515
|
+
config = { ...DEFAULT_CONFIG, ...options.config };
|
|
516
|
+
} else {
|
|
517
|
+
// Load from JSON
|
|
518
|
+
const eventName = options.event || getUrlParam('event');
|
|
519
|
+
|
|
520
|
+
if (!eventName) {
|
|
521
|
+
throw new Error('No event specified. Use ?event=<name> or provide config.');
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const loadedConfig = await loadConfigFromJson(eventName);
|
|
525
|
+
config = { ...DEFAULT_CONFIG, ...loadedConfig };
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Apply URL overrides
|
|
529
|
+
config = applyUrlOverrides(config);
|
|
530
|
+
state.config = config;
|
|
531
|
+
|
|
532
|
+
// Validate required fields
|
|
533
|
+
if (!config.eventDate) {
|
|
534
|
+
throw new Error('eventDate is required in configuration');
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Render widget
|
|
538
|
+
const { countdownBox, popup } = renderWidget(config);
|
|
539
|
+
|
|
540
|
+
// Start countdown
|
|
541
|
+
startCountdown(config.eventDate, countdownBox, config.completedMessage);
|
|
542
|
+
|
|
543
|
+
// Start popup cycle
|
|
544
|
+
if (popup && config.popup) {
|
|
545
|
+
startPopup(config.popup, popup);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Return API
|
|
549
|
+
return {
|
|
550
|
+
getConfig: () => ({ ...state.config }),
|
|
551
|
+
isCompleted: () => state.isCompleted,
|
|
552
|
+
destroy: destroyCountdown
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
} catch (error) {
|
|
556
|
+
console.error('[CountdownWidget] Initialization error:', error);
|
|
557
|
+
|
|
558
|
+
const container = document.getElementById(WIDGET_OPTIONS.containerId);
|
|
559
|
+
if (container) {
|
|
560
|
+
container.innerHTML = `
|
|
561
|
+
<div class="countdown-error" style="
|
|
562
|
+
padding: 2rem;
|
|
563
|
+
text-align: center;
|
|
564
|
+
color: var(--text-secondary, #888);
|
|
565
|
+
background: var(--glass, rgba(20, 12, 40, 0.7));
|
|
566
|
+
border-radius: var(--radius-lg, 12px);
|
|
567
|
+
border: 1px solid var(--border, #3a2555);
|
|
568
|
+
">
|
|
569
|
+
<p style="margin-bottom: 0.5rem; color: var(--accent, #d94f90);">⚠️ Countdown Error</p>
|
|
570
|
+
<p style="font-size: 0.9rem;">${escapeHtml(error.message)}</p>
|
|
571
|
+
</div>
|
|
572
|
+
`;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
throw error;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Destroys the countdown widget and cleans up resources
|
|
581
|
+
*/
|
|
582
|
+
export function destroyCountdown() {
|
|
583
|
+
if (state.countdownInterval) {
|
|
584
|
+
clearInterval(state.countdownInterval);
|
|
585
|
+
state.countdownInterval = null;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (state.popupInterval) {
|
|
589
|
+
clearInterval(state.popupInterval);
|
|
590
|
+
state.popupInterval = null;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
state.config = null;
|
|
594
|
+
state.isCompleted = false;
|
|
595
|
+
|
|
596
|
+
const container = document.getElementById(WIDGET_OPTIONS.containerId);
|
|
597
|
+
if (container) {
|
|
598
|
+
container.innerHTML = '';
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Export for global usage
|
|
603
|
+
if (typeof window !== 'undefined') {
|
|
604
|
+
window.CorruptedCountdown = {
|
|
605
|
+
init: initCountdown,
|
|
606
|
+
destroy: destroyCountdown
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|