1// Magic Feedback Component 2class MagicFeedback { 3 constructor() { 4 this.overlay = null; 5 this.modal = null; 6 this.selectedType = ''; 7 this.rating = 0; 8 this.init(); 9 } 10 11 init() { 12 this.createFeedbackModal(); 13 this.setupEventListeners(); 14 } 15 16 createFeedbackModal() { 17 this.overlay = document.createElement('div'); 18 this.overlay.className = 'magic-feedback-overlay'; 19 this.overlay.innerHTML = ` 20 <div class="magic-feedback-modal"> 21 <div class="magic-feedback-header"> 22 <button class="magic-feedback-close" id="feedbackClose"> 23 <i data-lucide="x" size="20"></i> 24 </button> 25 <h2 class="magic-feedback-title">Share Your Feedback</h2> 26 <p class="magic-feedback-subtitle">Help us improve Guess the Jumpshot! Your feedback is valuable to us.</p> 27 </div> 28 29 <div class="magic-feedback-body"> 30 <!-- Feedback Type Selection --> 31 <div class="magic-feedback-form-group"> 32 <label class="magic-feedback-label">What's your feedback about?</label> 33 <div class="magic-feedback-types"> 34 <div class="magic-feedback-type" data-type="bug"> 35 <div class="magic-feedback-type-icon">ð</div> 36 <div class="magic-feedback-type-label">Bug Report</div> 37 </div> 38 <div class="magic-feedback-type" data-type="feature"> 39 <div class="magic-feedback-type-icon">ð¡</div> 40 <div class="magic-feedback-type-label">Feature Request</div> 41 </div> 42 <div class="magic-feedback-type" data-type="improvement"> 43 <div class="magic-feedback-type-icon">â¨</div> 44 <div class="magic-feedback-type-label">Improvement</div> 45 </div> 46 <div class="magic-feedback-type" data-type="other"> 47 <div class="magic-feedback-type-icon">ð¬</div> 48 <div class="magic-feedback-type-label">Other</div> 49 </div> 50 </div> 51 </div> 52 53 <!-- Rating --> 54 <div class="magic-feedback-form-group magic-feedback-rating"> 55 <label class="magic-feedback-label">How would you rate your experience?</label> 56 <div class="magic-rating-stars"> 57 <span class="magic-rating-star" data-rating="1">â</span> 58 <span class="magic-rating-star" data-rating="2">â</span> 59 <span class="magic-rating-star" data-rating="3">â</span> 60 <span class="magic-rating-star" data-rating="4">â</span> 61 <span class="magic-rating-star" data-rating="5">â</span> 62 </div> 63 </div> 64 65 <!-- Email --> 66 <div class="magic-feedback-form-group"> 67 <label class="magic-feedback-label">Your Email (optional)</label> 68 <input 69 type="email" 70 id="feedbackEmail" 71 class="magic-feedback-input" 72 placeholder="[email protected]" 73 > 74 </div> 75 76 <!-- Message --> 77 <div class="magic-feedback-form-group"> 78 <label class="magic-feedback-label">Your Feedback</label> 79 <textarea 80 id="feedbackMessage" 81 class="magic-feedback-textarea" 82 placeholder="Tell us what you think..." 83 maxlength="1000" 84 ></textarea>
85 <span class="magic-feedback-char-limit"> 86 <span id="feedbackCharCount">0</span>/1000 87 </span> 88 </div> 89 </div> 90 91 <div class="magic-feedback-footer"> 92 <button class="magic-feedback-cancel" id="feedbackCancel"> 93 Cancel 94 </button> 95 <button class="magic-feedback-submit" id="feedbackSubmit"> 96 <i data-lucide="send" size="18"></i> 97 Send Feedback 98 </button> 99 </div> 100 </div> 101 `; 102 103 document.body.appendChild(this.overlay); 104 this.modal = this.overlay.querySelector('.magic-feedback-modal'); 105 106 // Initialize Lucide icons 107 if (window.lucide) { 108 window.lucide.createIcons(); 109 } 110 } 111 112 setupEventListeners() { 113 // Close modal 114 document.getElementById('feedbackClose').addEventListener('click', () => { 115 this.close(); 116 }); 117 118 document.getElementById('feedbackCancel').addEventListener('click', () => { 119 this.close(); 120 }); 121 122 // Click outside to close 123 this.overlay.addEventListener('click', (e) => { 124 if (e.target === this.overlay) { 125 this.close(); 126 } 127 }); 128 129 // ESC key to close 130 document.addEventListener('keydown', (e) => { 131 if (e.key === 'Escape' && this.overlay.classList.contains('active')) { 132 this.close(); 133 } 134 }); 135 136 // Feedback type selection 137 document.querySelectorAll('.magic-feedback-type').forEach(type => { 138 type.addEventListener('click', () => { 139 document.querySelectorAll('.magic-feedback-type').forEach(t => { 140 t.classList.remove('active'); 141 }); 142 type.classList.add('active'); 143 this.selectedType = type.dataset.type; 144 }); 145 }); 146 147 // Rating stars 148 document.querySelectorAll('.magic-rating-star').forEach(star => { 149 star.addEventListener('click', () => { 150 this.rating = parseInt(star.dataset.rating); 151 this.updateRatingStars(); 152 }); 153 154 star.addEventListener('mouseenter', () => { 155 const hoverRating = parseInt(star.dataset.rating); 156 document.querySelectorAll('.magic-rating-star').forEach((s, index) => { 157 if (index < hoverRating) { 158 s.classList.add('filled'); 159 } else { 160 s.classList.remove('filled'); 161 } 162 }); 163 }); 164 }); 165 166 // Reset stars on mouse leave 167 document.querySelector('.magic-rating-stars').addEventListener('mouseleave', () => { 168 this.updateRatingStars(); 169 }); 170 171 // Character counter 172 const textarea = document.getElementById('feedbackMessage'); 173 const charCount = document.getElementById('feedbackCharCount'); 174 textarea.addEventListener('input', () => { 175 charCount.textContent = textarea.value.length; 176 }); 177 178 // Submit feedback 179 document.getElementById('feedbackSubmit').addEventListener('click', () => { 180 this.submitFeedback(); 181 }); 182 } 183 184 updateRatingStars() { 185 document.querySelectorAll('.magic-rating-star').forEach((star, index) => { 186 if (index < this.rating) { 187 star.classList.add('filled'); 188 } else { 189 star.classList.remove('filled'); 190 } 191 }); 192 } 193 194 open() { 195 this.overlay.classList.add('active'); 196 // Reset form 197 this.reset(); 198 } 199 200 close() { 201 this.overlay.classList.remove('active'); 202 } 203 204 reset() { 205 this.selectedType = ''; 206 this.rating = 0; 207 document.querySelectorAll('.magic-feedback-type').forEach(type => { 208 type.classList.remove('active'); 209 }); 210 this.updateRatingStars(); 211 document.getElementById('feedbackEmail').value = ''; 212 document.getElementById('feedbackMessage').value = ''; 213 document.getElementById('feedbackCharCount').textContent = '0'; 214 } 215 216 async submitFeedback() { 217 const email = document.getElementById('feedbackEmail').value.trim(); 218 const message = document.getElementById('feedbackMessage').value.trim(); 219 const submitBtn = document.getElementById('feedbackSubmit'); 220 221 // Validation 222 if (!this.selectedType) { 223 this.showError('Please select a feedback type'); 224 return; 225 } 226 227 if (this.rating === 0) { 228 this.showError('Please rate your experience'); 229 return; 230 } 231 232 if (!message) { 233 this.showError('Please enter your feedback message'); 234 return; 235 } 236 237 // Disable submit button 238 submitBtn.disabled = true; 239 submitBtn.innerHTML = '<i data-lucide="loader" size="18"></i> Sending...'; 240 if (window.lucide) { 241 window.lucide.createIcons(); 242 } 243 244 try { 245 // Prepare feedback data 246 const feedbackData = { 247 type: this.selectedType, 248 rating: this.rating, 249 email: email || null, 250 message: message, 251 user_agent: navigator.userAgent, 252 screen_resolution: `${window.screen.width}x${window.screen.height}`, 253 created_at: new Date().toISOString() 254 }; 255 256 // Save to Supabase 257 if (window.GameDB) { 258 await window.GameDB.loadSupabase(); 259 const { data, error } = await window.supabase 260 .from('player_feedback') 261 .insert(feedbackData) 262 .select(); 263 264 if (error) throw error; 265 } 266 267 // Show success state 268 this.showSuccess(); 269 270 } catch (error) { 271 console.error('Error submitting feedback:', error); 272 alert('Failed to submit feedback. Please try again.'); 273 } finally { 274 submitBtn.disabled = false;
275 submitBtn.innerHTML = '<i data-lucide="send" size="18"></i> Send Feedback'; 276 if (window.lucide) { 277 window.lucide.createIcons(); 278 } 279 } 280 } 281 282 showError(message) { 283 // Create a temporary error message 284 const errorDiv = document.createElement('div'); 285 errorDiv.style.cssText = ` 286 position: fixed; 287 top: 20px; 288 left: 50%; 289 transform: translateX(-50%); 290 background: linear-gradient(135deg, #EF4444, #DC2626); 291 color: white; 292 padding: 1rem 2rem; 293 border-radius: 12px; 294 box-shadow: 0 10px 30px rgba(239, 68, 68, 0.3); 295 z-index: 10002; 296 animation: slideDown 0.3s ease; 297 `; 298 errorDiv.textContent = message; 299 300 document.body.appendChild(errorDiv); 301 302 setTimeout(() => { 303 errorDiv.remove(); 304 }, 3000); 305 } 306 307 showSuccess() { 308 const modalContent = this.modal.querySelector('.magic-feedback-body'); 309 const footer = this.modal.querySelector('.magic-feedback-footer'); 310 311 modalContent.innerHTML = ` 312 <div class="magic-feedback-success"> 313 <div class="magic-feedback-success-icon">â</div> 314 <h3 class="magic-feedback-success-title">Thank You!</h3> 315 <p class="magic-feedback-success-message"> 316 Your feedback has been received. We appreciate your input and will use it to improve the game! 317 </p> 318 </div> 319 `; 320 321 footer.innerHTML = ` 322 <button class="magic-feedback-submit" style="width: 100%;" onclick="magicFeedback.close()"> 323 Close 324 </button> 325 `; 326 327 // Auto close after 3 seconds 328 setTimeout(() => { 329 this.close(); 330 // Recreate the modal for next use 331 setTimeout(() => { 332 this.overlay.remove(); 333 this.createFeedbackModal(); 334 this.setupEventListeners(); 335 }, 300); 336 }, 3000); 337 } 338} 339 340// Add animation styles 341const feedbackStyles = document.createElement('style'); 342feedbackStyles.textContent = ` 343 @keyframes slideDown { 344 from { 345 opacity: 0; 346 transform: translateX(-50%) translateY(-20px); 347 } 348 to { 349 opacity: 1; 350 transform: translateX(-50%) translateY(0); 351 } 352 } 353`; 354document.head.appendChild(feedbackStyles); 355 356// Initialize feedback component 357window.magicFeedback = new MagicFeedback();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.