1// K-Chicken Sommelier - App Logic (Updated) 2console.log("Start of app.js execution"); 3 4// ========== STATE MANAGEMENT ========== 5const state = { 6 currentPage: 'home', 7 currentQuestion: 0, 8 answers: {}, 9 userProfile: null, 10 matches: [], 11 isPremium: false, 12 history: [], 13 brandDirectory: [], 14 brandSheetMeta: null, 15 homeStats: null, 16 nearbyStores: [], 17 nearbyLocation: null, 18 nearbyMap: null, 19 nearbyMarkers: [] 20}; 21 22const BRAND_SHEET_ID = '1CGhr6ETMKV3VTC_62Y-8hRgqt9KQQ-VXSnBQsdTkTnM'; 23const BRAND_SHEET_JSON_URL = `https://opensheet.elk.sh/${BRAND_SHEET_ID}/1`; 24const BRAND_SHEET_CSV_URL = `https://docs.google.com/spreadsheets/d/${BRAND_SHEET_ID}/gviz/tq?tqx=out:csv`; 25const BRAND_SHEET_CACHE_KEY = 'kcs-brand-sheet-cache-v1'; 26const NEARBY_RADIUS_METERS = 10000; 27const CHICKEN_NAME_REGEX = 'ì¹í¨|Chicken|Kyochon|BHC|BBQ|êµì´|êµ½ë¤|ë¤ë¤|í¸ë¼ë|ì²ê°ì§|í리카ë|ë©ìì¹´ë|ì§ì½ë°|í¸ìì´|60ê³|ë ¸ëíµë|ìë´|ëëì¤ë|KFC'; 28 29const BRAND_MATCH_ALIASES = { 30 BHC: ['ë¹ìì´ì¹ì¨(BHC)', 'ë¹ìì´ì¹ì¨'], 31 BBQ: ['BBQì¹í¨', 'BBQ ì¹í¨ì¤ë¹ì´'], 32 '60ê³ì¹í¨': ['60ê³'], 33 í¸ë¼ë: ['í¸ë¼ëì¹í¨'], 34 'í¸ìì´ëë§ë¦¬ì¹í¨': ['í¸ìì´ ëë§ë¦¬ì¹í¨'] 35}; 36 37// ========== INITIALIZATION ========== 38document.addEventListener('DOMContentLoaded', () => { 39 loadState(); 40 initNavigation(); 41 initQuiz(); 42 initPremium(); 43 initSettings(); 44 initFAQ(); 45 initLanguages(); 46 initShare(); 47 initNearbyStores(); 48 initStats(); // New 49 initBrandDirectory(); // New 50 initGoogleLogin(); // New 51 showPage('home'); 52}); 53 54// ========== NEW FEATURES ========== 55function initStats() { 56 const fallbackMenus = MENU_ITEMS.length; 57 const fallbackBrands = new Set(MENU_ITEMS.map(item => item.brand)).size; 58 const totalMenus = Number.isFinite(state.homeStats?.totalMenus) ? state.homeStats.totalMenus : fallbackMenus; 59 const uniqueBrands = Number.isFinite(state.homeStats?.brandCount) ? state.homeStats.brandCount : fallbackBrands; 60 const sourceDate = state.brandSheetMeta?.fetchedAt 61 ? new Date(state.brandSheetMeta.fetchedAt).toISOString().split('T')[0] 62 : new Date().toISOString().split('T')[0]; 63 const sourceLabel = state.homeStats?.fromSheet ? { 64 ko: 'êµ¬ê¸ ìí¸', 65 en: 'Google Sheet', 66 zh: 'Google Sheets', 67 ja: 'Google Sheets' 68 } : { 69 ko: 'ë¡ì»¬ ë°ì´í°', 70 en: 'Local data', 71 zh: 'æ¬å°æ°æ®', 72 ja: 'ãã¼ã«ã«ãã¼ã¿' 73 }; 74 const statsEl = document.getElementById('data-stats'); 75 if (statsEl) { 76 const lang = state.language || 'en'; 77 78 const stats = { 79 ko: `íì¬ <strong>${uniqueBrands}ê° ë¸ëë</strong>, <strong>${totalMenus}ê° ë©ë´</strong> ë¶ì ìë£ ð <span style="font-size: 11px; color: #999; margin-left: 5px;">(${sourceLabel.ko}: ${sourceDate})</span>`, 80 en: `Analyzing <strong>${uniqueBrands} Brands</strong> & <strong>${totalMenus} Menus</strong> ð <span style="font-size: 11px; color: #999; margin-left: 5px;">(${sourceLabel.en}: ${sourceDate})</span>`, 81 zh: `æ£å¨åæ <strong>${uniqueBrands} 个åç</strong> å <strong>${totalMenus} 个èå</strong> ð <span style="font-size: 11px; color: #999; margin-left: 5px;">(${sourceLabel.zh}: ${sourceDate})</span>`, 82 ja: `ç¾å¨ <strong>${uniqueBrands}ã¤ã®ãã©ã³ã</strong>ã<strong>${totalMenus}ã¤ã®ã¡ãã¥ã¼</strong>ãåæä¸ ð <span style="font-size: 11px; color: #999; margin-left: 5px;">(${sourceLabel.ja}: ${sourceDate})</span>` 83 }; 84 85 statsEl.innerHTML = stats[lang] || stats.en; 86 } 87} 88 89function normalizeBrandKey(value) { 90 return String(value || '') 91 .toLowerCase() 92 .replace(/[()]/g, '') 93 .replace(/\s+/g, '') 94 .replace(/[^a-z0-9ê°-í£]/g, ''); 95} 96 97function parseStoreCount(value) { 98 const digits = String(value || '').replace(/[^0-9]/g, ''); 99 if (!digits) return null; 100 const parsed = parseInt(digits, 10); 101 return Number.isFinite(parsed) ? parsed : null; 102} 103 104function parseMenuCount(value) { 105 const digits = String(value || '').replace(/[^0-9]/g, ''); 106 if (!digits) return null; 107 const parsed = parseInt(digits, 10); 108 return Number.isFinite(parsed) ? parsed : null; 109} 110 111function parseCsvRows(text) { 112 const rows = []; 113 let row = [];
114 let current = ''; 115 let inQuotes = false; 116 117 for (let i = 0; i < text.length; i++) { 118 const char = text[i]; 119 120 if (char === '"') { 121 if (inQuotes && text[i + 1] === '"') { 122 current += '"'; 123 i++; 124 } else { 125 inQuotes = !inQuotes; 126 } 127 continue; 128 } 129 130 if (char === ',' && !inQuotes) { 131 row.push(current); 132 current = ''; 133 continue; 134 } 135 136 if ((char === '\n' || char === '\r') && !inQuotes) { 137 if (char === '\r' && text[i + 1] === '\n') i++; 138 row.push(current); 139 rows.push(row); 140 row = []; 141 current = ''; 142 continue; 143 } 144 145 current += char; 146 } 147 148 if (current.length || row.length) { 149 row.push(current); 150 rows.push(row); 151 } 152 153 return rows.filter(r => r.some(cell => String(cell).trim().length > 0)); 154} 155 156function mapSheetRows(rawRows) { 157 return rawRows 158 .map(row => { 159 const brandName = String(row.brandName || '').trim(); 160 const category = String(row.category || '').trim(); 161 return { 162 brandName, 163 category, 164 storeCount: parseStoreCount(row.storeCount), 165 menuCount: parseMenuCount(row.menuCount), 166 normalized: normalizeBrandKey(brandName) 167 }; 168 }) 169 .filter(row => row.brandName); 170} 171 172async function fetchBrandSheetFromJson() { 173 const response = await fetch(`${BRAND_SHEET_JSON_URL}?_ts=${Date.now()}`, { 174 cache: 'no-store' 175 }); 176 if (!response.ok) throw new Error(`Brand sheet JSON fetch failed: ${response.status}`); 177 178 const jsonRows = await response.json(); 179 const mappedRows = mapSheetRows( 180 jsonRows.map(row => ({ 181 brandName: row['ë¸ëëëª '], 182 category: row['ë¶ë¥'], 183 storeCount: row['ë§¤ì¥ ì (ì¶ì )'], 184 menuCount: row['ë©ë´ ì'] || row['ë©ë´ì'] || row['ì´ ë©ë´ ì'] || row['ì´ ë©ë´ì'] 185 })) 186 ); 187 188 return { rows: mappedRows, source: 'json' }; 189} 190 191async function fetchBrandSheetFromCsv() { 192 const response = await fetch(`${BRAND_SHEET_CSV_URL}&_ts=${Date.now()}`, { 193 cache: 'no-store' 194 }); 195 if (!response.ok) throw new Error(`Brand sheet CSV fetch failed: ${response.status}`); 196 197 const csvText = await response.text(); 198 const rows = parseCsvRows(csvText); 199 if (!rows.length) throw new Error('Brand sheet CSV is empty.'); 200 201 const headers = rows[0].map(header => String(header || '').replace(/^\uFEFF/, '').trim()); 202 const brandIndex = headers.findIndex(header => header.includes('ë¸ëëëª ')); 203 const categoryIndex = headers.findIndex(header => header.includes('ë¶ë¥')); 204 const storeIndex = headers.findIndex(header => header.includes('매ì¥')); 205 const menuIndex = headers.findIndex(header => header.includes('ë©ë´')); 206 207 if (brandIndex === -1 || categoryIndex === -1 || storeIndex === -1) { 208 throw new Error('Brand sheet CSV header mismatch.'); 209 } 210 211 const mappedRows = mapSheetRows(rows.slice(1).map(row => ({ 212 brandName: row[brandIndex], 213 category: row[categoryIndex], 214 storeCount: row[storeIndex], 215 menuCount: menuIndex === -1 ? null : row[menuIndex] 216 }))); 217 218 return { rows: mappedRows, source: 'csv' }; 219} 220 221function loadBrandSheetCache() { 222 try { 223 const raw = localStorage.getItem(BRAND_SHEET_CACHE_KEY); 224 if (!raw) return null; 225 226 const parsed = JSON.parse(raw); 227 if (!parsed || !Array.isArray(parsed.rows) || !parsed.fetchedAt) return null; 228 229 return parsed; 230 } catch (error) { 231 console.warn('Failed to parse brand sheet cache:', error); 232 return null; 233 } 234} 235 236function saveBrandSheetCache(rows, source) { 237 try { 238 localStorage.setItem(BRAND_SHEET_CACHE_KEY, JSON.stringify({ 239 rows, 240 source, 241 fetchedAt: Date.now() 242 })); 243 } catch (error) { 244 console.warn('Failed to save brand sheet cache:', error); 245 } 246} 247 248async function loadBrandSheetRows() { 249 const cached = loadBrandSheetCache(); 250 251 try { 252 const jsonResult = await fetchBrandSheetFromJson(); 253 saveBrandSheetCache(jsonResult.rows, jsonResult.source); 254 return { rows: jsonResult.rows, source: jsonResult.source, fetchedAt: Date.now() }; 255 } catch (jsonError) { 256 console.warn('Brand sheet JSON load failed, trying CSV:', jsonError); 257 } 258 259 try { 260 const csvResult = await fetchBrandSheetFromCsv(); 261 saveBrandSheetCache(csvResult.rows, csvResult.source); 262 return { rows: csvResult.rows, source: csvResult.source, fetchedAt: Date.now() }; 263 } catch (csvError) { 264 console.warn('Brand sheet CSV load failed:', csvError); 265 } 266 267 if (cached) { 268 return { rows: cached.rows, source: 'stale-cache', fetchedAt: cached.fetchedAt }; 269 } 270 271 throw new Error('Failed to load brand sheet data.'); 272} 273 274function collectServiceBrands() { 275 const byBrand = new Map(); 276 277 MENU_ITEMS.forEach(item => { 278 if (!byBrand.has(item.brand)) { 279 byBrand.set(item.brand, { 280 brand: item.brand, 281 menuCount: 0, 282 brandImage: item.brandImage || '', 283 website: item.website || '#' 284 }); 285 } 286 287 byBrand.get(item.brand).menuCount += 1; 288 }); 289 290 return Array.from(byBrand.values()); 291} 292 293function findSheetRowForBrand(brandName, sheetRows) { 294 const primaryKey = normalizeBrandKey(brandName); 295 const aliasKeys = (BRAND_MATCH_ALIASES[brandName] || []).map(normalizeBrandKey);
296 const keysToMatch = [primaryKey, ...aliasKeys].filter(Boolean); 297 298 const candidates = sheetRows.filter(row => { 299 const rowKey = row.normalized; 300 if (!rowKey || rowKey.length < 3) return false; 301 302 return keysToMatch.some(key => { 303 if (!key || key.length < 3) return false; 304 return rowKey === key || rowKey.includes(key) || key.includes(rowKey); 305 }); 306 }); 307 308 if (!candidates.length) return null; 309 310 return candidates.sort((a, b) => (b.storeCount || -1) - (a.storeCount || -1))[0]; 311} 312 313function buildBrandDirectoryRecords(sheetRows) { 314 return collectServiceBrands() 315 .map(serviceBrand => { 316 const matched = findSheetRowForBrand(serviceBrand.brand, sheetRows); 317 return { 318 ...serviceBrand, 319 category: matched?.category || null, 320 storeCount: matched?.storeCount ?? null, 321 sourceBrandName: matched?.brandName || null 322 }; 323 }) 324 .sort((a, b) => (b.storeCount || -1) - (a.storeCount || -1) || a.brand.localeCompare(b.brand, 'ko')); 325} 326 327function computeHomeStatsFromSheet(sheetRows) { 328 const uniqueSheetBrandKeys = new Set( 329 sheetRows.map(row => row.normalized).filter(Boolean) 330 ); 331 const serviceBrands = collectServiceBrands(); 332 const matchedServiceBrands = serviceBrands.filter(serviceBrand => ( 333 Boolean(findSheetRowForBrand(serviceBrand.brand, sheetRows)) 334 )); 335 336 const sheetBrandCount = uniqueSheetBrandKeys.size; 337 const menusFromMatchedBrands = matchedServiceBrands.reduce((sum, record) => sum + record.menuCount, 0); 338 const hasExplicitMenuColumn = sheetRows.some(row => Number.isFinite(row.menuCount)); 339 const menusFromSheetColumn = sheetRows.reduce((sum, row) => ( 340 sum + (Number.isFinite(row.menuCount) ? row.menuCount : 0) 341 ), 0); 342 343 return { 344 brandCount: sheetBrandCount || matchedServiceBrands.length, 345 totalMenus: hasExplicitMenuColumn ? menusFromSheetColumn : menusFromMatchedBrands, 346 fromSheet: true 347 }; 348} 349 350function translateSheetCategory(category, lang) { 351 const labels = { 352 ëí: { en: 'Major', ko: 'ëí', zh: '大å', ja: '大æ' }, 353 ì¤í: { en: 'Mid-size', ko: 'ì¤í', zh: 'ä¸å', ja: 'ä¸è¦æ¨¡' }, 354 ìí: { en: 'Small', ko: 'ìí', zh: 'å°å', ja: 'å°è¦æ¨¡' }, 355 ì§ì구: { en: 'Regional', ko: 'ì§ì구', zh: 'å°åºå', ja: 'å°åå¯ç' } 356 }; 357 358 if (!category) return null; 359 return labels[category]?.[lang] || category; 360} 361 362function formatStoreCount(storeCount, lang) { 363 if (!Number.isFinite(storeCount)) { 364 return (lang === 'ko') ? 'ì ë³´ ìì' : 365 (lang === 'zh') ? 'ææ ' : 366 (lang === 'ja') ? 'ãã¼ã¿ãªã' : 'N/A'; 367 } 368 369 const formatted = storeCount.toLocaleString(); 370 return (lang === 'ko') ? `${formatted}ê°` : 371 (lang === 'zh') ? `${formatted}å®¶` : 372 (lang === 'ja') ? `${formatted}åº` : formatted; 373} 374 375function renderBrandDirectory() { 376 const container = document.getElementById('brand-directory-list'); 377 const sourceEl = document.getElementById('brand-directory-source'); 378 if (!container) return; 379 380 const strings = getStrings(); 381 const lang = state.language || 'en'; 382 383 if (!state.brandDirectory.length) { 384 container.innerHTML = `<div class="brand-directory-placeholder">${strings.brandLoading || 'Loading Google Sheets data...'}</div>`; 385 } else { 386 container.innerHTML = state.brandDirectory.map(record => { 387 const categoryText = translateSheetCategory(record.category, lang) || (strings.brandNotFound || 'No matched data'); 388 const storeCountText = formatStoreCount(record.storeCount, lang); 389 390 return ` 391 <article class="brand-directory-card"> 392 <div class="brand-directory-top"> 393 ${record.brandImage ? `<img class="brand-directory-logo" src="${record.brandImage}" alt="${record.brand} logo" loading="lazy" decoding="async" referrerpolicy="no-referrer">` : ''} 394 <div class="brand-directory-name">${record.brand}</div> 395 </div> 396 <div class="brand-directory-meta"><span>${strings.brandCategoryLabel || 'Category'}</span><strong>${categoryText}</strong></div> 397 <div class="brand-directory-meta"><span>${strings.brandStoreLabel || 'Stores'}</span><strong>${storeCountText}</strong></div> 398 <div class="brand-directory-meta"><span>${strings.brandMenuLabel || 'Menus'}</span><strong>${record.menuCount}</strong></div> 399 </article> 400 `; 401 }).join(''); 402 } 403 404 if (sourceEl) { 405 if (!state.brandSheetMeta?.fetchedAt) { 406 sourceEl.textContent = ''; 407 } else { 408 const dateText = new Date(state.brandSheetMeta.fetchedAt).toLocaleDateString(lang === 'ko' ? 'ko-KR' : 'en-US'); 409 sourceEl.textContent = `${strings.brandSourcePrefix || 'Source'}: ${strings.brandSourceGoogleSheet || 'Google Sheets'} · ${dateText}`; 410 } 411 } 412} 413 414function initBrandDirectory() { 415 renderBrandDirectory(); 416 417 loadBrandSheetRows() 418 .then(result => { 419 state.brandDirectory = buildBrandDirectoryRecords(result.rows); 420 state.homeStats = computeHomeStatsFromSheet(result.rows); 421 state.brandSheetMeta = { 422 source: result.source, 423 fetchedAt: result.fetchedAt 424 }; 425 renderBrandDirectory(); 426 initStats(); 427 }) 428 .catch(error => { 429 console.error('Failed to initialize brand directory:', error); 430 state.brandDirectory = buildBrandDirectoryRecords([]); 431 state.brandSheetMeta = null; 432 state.homeStats = null; 433 renderBrandDirectory(); 434 initStats(); 435 }); 436} 437 438function initGoogleLogin() { 439 const btn = document.getElementById('google-login-btn'); 440 if (btn) { 441 btn.addEventListener('click', () => { 442 // Mock login 443 const isKo = state.language === 'ko'; 444 alert(isKo 445 ? "êµ¬ê¸ ë¡ê·¸ì¸ ì±ê³µ! ë ë§ì ì 보를 무ë£ë¡ íì¸íì¸ì." 446 : "Logged in with Google! Enjoy free access to extended insights." 447 );
448 btn.innerHTML = isKo ? "ë¡ê·¸ì¸ë¨ (Google)" : "Logged In (Google)"; 449 btn.disabled = true; 450 btn.style.opacity = "0.7"; 451 }); 452 } 453} 454 455// ========== NAVIGATION ========== 456function showPage(pageName) { 457 // Hide all pages 458 document.querySelectorAll('.page').forEach(page => { 459 page.classList.remove('active'); 460 }); 461 462 // Show target page 463 const targetPage = document.getElementById(`page-${pageName}`); 464 if (targetPage) { 465 targetPage.classList.add('active'); 466 state.currentPage = pageName; 467 468 // Scroll to top 469 window.scrollTo(0, 0); 470 471 // Page-specific initialization 472 if (pageName === 'quiz') { 473 renderQuizQuestion(); 474 } else if (pageName === 'results') { 475 renderResults(); 476 } else if (pageName === 'settings') { 477 renderSettings(); 478 } 479 } 480} 481 482function initNavigation() { 483 // Navigation buttons 484 document.querySelectorAll('[data-navigate]').forEach(btn => { 485 btn.addEventListener('click', () => { 486 const target = btn.dataset.navigate; 487 showPage(target); 488 }); 489 }); 490 491 // Start quiz button 492 document.querySelectorAll('[data-action="start-quiz"]').forEach(btn => { 493 btn.addEventListener('click', () => { 494 state.currentQuestion = 0; 495 state.answers = {}; 496 showPage('quiz'); 497 }); 498 }); 499} 500 501// ========== QUIZ LOGIC ========== 502function initQuiz() { 503 // Next button 504 const nextBtn = document.getElementById('quiz-next'); 505 if (nextBtn) { 506 nextBtn.addEventListener('click', handleQuizNext); 507 } 508 509 // Skip button 510 const skipBtn = document.getElementById('quiz-skip'); 511 if (skipBtn) { 512 skipBtn.addEventListener('click', handleQuizSkip); 513 } 514 515 // Back button 516 const backBtn = document.getElementById('quiz-back'); 517 if (backBtn) { 518 backBtn.addEventListener('click', handleQuizBack); 519 } 520} 521 522function getAnsweredQuestionIds(answers) { 523 return QUIZ_QUESTIONS 524 .map(question => question.id) 525 .filter(id => answers[id] !== undefined && answers[id] !== null && answers[id] !== ''); 526} 527 528function getProgressText(questionIndex, totalQuestions, lang) { 529 const current = questionIndex + 1; 530 if (lang === 'ko') return `${current} / ${totalQuestions} 문í`; 531 if (lang === 'zh') return `第 ${current} / ${totalQuestions} é¢`; 532 if (lang === 'ja') return `${current} / ${totalQuestions} å`; 533 return `Question ${current} of ${totalQuestions}`; 534} 535 536function renderQuizQuestion() { 537 const question = QUIZ_QUESTIONS[state.currentQuestion]; 538 if (!question) return; 539 540 const progressFill = document.querySelector('.progress-fill'); 541 const progressText = document.querySelector('.progress-text'); 542 const lang = state.language || 'en'; 543 544 // Update progress 545 if (progressText) { 546 progressText.textContent = getProgressText(state.currentQuestion, QUIZ_QUESTIONS.length, lang); 547 } 548 if (progressFill) { 549 const progress = ((state.currentQuestion + 1) / QUIZ_QUESTIONS.length) * 100; 550 progressFill.style.width = `${progress}%`; 551 } 552 553 // Get translations 554 const qTrans = QUIZ_TRANSLATIONS[question.id]; 555 const titleText = qTrans?.title[lang] || question.title; 556 557 // Update question title 558 const titleEl = document.getElementById('quiz-title'); 559 if (titleEl) { 560 titleEl.textContent = titleText; 561 } 562 563 // Render options 564 const optionsContainer = document.getElementById('quiz-options'); 565 if (optionsContainer) { 566 // Different rendering based on type 567 if (question.type === 'scale' || question.type === 'filter') { 568 renderSingleSelectOptions(optionsContainer, question); 569 } else if (question.type === 'tags') { 570 renderMultiSelectOptions(optionsContainer, question); 571 } 572 } 573 574 // Update button states 575 updateQuizButtons(); 576 renderLiveRecommendation(); 577} 578
579function renderSingleSelectOptions(container, question) { 580 const lang = state.language || 'en'; 581 const qTrans = QUIZ_TRANSLATIONS[question.id]; 582 583 container.innerHTML = question.options.map((option, index) => { 584 const isSelected = state.answers[question.id] === option.value; 585 // Try to get translation, fallback to option.label 586 const translatedLabel = qTrans?.options?.[option.value]?.[lang] || option.label; 587 588 return ` 589 <button class="quiz-option ${isSelected ? 'selected' : ''}" 590 data-type="single" 591 data-question-id="${question.id}" 592 data-value="${option.value}"> 593 ${translatedLabel} 594 </button> 595 `; 596 }).join(''); 597 598 attachOptionListeners(container); 599} 600 601function renderMultiSelectOptions(container, question) { 602 const lang = state.language || 'en'; 603 const qTrans = QUIZ_TRANSLATIONS[question.id]; 604 605 container.innerHTML = question.options.map((option, index) => { 606 const currentAnswers = state.answers[question.id] || []; 607 const isSelected = currentAnswers.includes(option.value); 608 // Try to get translation, fallback to option.label 609 const translatedLabel = qTrans?.options?.[option.value]?.[lang] || option.label; 610 611 return ` 612 <button class="quiz-option ${isSelected ? 'selected' : ''}" 613 data-type="multi" 614 data-question-id="${question.id}" 615 data-value="${option.value}"> 616 ${translatedLabel} 617 </button> 618 `; 619 }).join(''); 620 621 attachOptionListeners(container); 622} 623 624function attachOptionListeners(container) { 625 container.querySelectorAll('.quiz-option').forEach(btn => { 626 btn.addEventListener('click', () => { 627 const qId = btn.dataset.questionId; 628 const val = btn.dataset.value; 629 const type = btn.dataset.type; 630 631 if (type === 'single') { 632 // Parse int if it looks like a number (for 1-5 scales) 633 const parsedVal = isNaN(val) ? val : parseInt(val, 10); 634 handleOptionSelect(qId, parsedVal); 635 } else { 636 handleMultiSelect(qId, val); 637 } 638 }); 639 }); 640} 641 642function handleOptionSelect(questionId, value) { 643 state.answers[questionId] = value; 644 // Re-render to update UI selection state 645 renderQuizQuestion(); 646} 647 648function handleMultiSelect(questionId, value) { 649 const current = state.answers[questionId] || []; 650 const idx = current.indexOf(value); 651 652 if (idx >= 0) { 653 // Remove 654 current.splice(idx, 1); 655 } else { 656 // Add 657 current.push(value); 658 } 659 state.answers[questionId] = current; 660 renderQuizQuestion(); 661} 662 663function updateQuizButtons() { 664 const question = QUIZ_QUESTIONS[state.currentQuestion]; 665 const nextBtn = document.getElementById('quiz-next'); 666 667 let hasAnswer = false; 668 if (question.type === 'tags') { 669 // For tags, maybe allow empty? Or require at least one? Let's allow empty or require 1. 670 // Let's require at least one for better results. 671 hasAnswer = state.answers[question.id] && state.answers[question.id].length > 0; 672 } else { 673 hasAnswer = state.answers[question.id] !== undefined; 674 } 675 676 if (nextBtn) { 677 nextBtn.disabled = !hasAnswer; 678 679 // Update button text for last question 680 const lang = state.language || 'en'; 681 const strings = UI_STRINGS[lang] || UI_STRINGS['en']; 682 683 if (state.currentQuestion === QUIZ_QUESTIONS.length - 1) { 684 nextBtn.textContent = (lang === 'ko' ? 'ê²°ê³¼ 보기' : (lang === 'zh' ? 'æ¥çç»æ' : (lang === 'ja' ? 'çµæãè¦ã' : 'Get Matches'))); 685 } else { 686 nextBtn.textContent = strings.nextBtn; 687 } 688 } 689} 690 691function handleQuizNext() { 692 if (state.currentQuestion < QUIZ_QUESTIONS.length - 1) { 693 state.currentQuestion++; 694 renderQuizQuestion(); 695 } else { 696 // Calculate results and show 697 calculateResults(); 698 showPage('results'); 699 } 700} 701 702function handleQuizSkip() { 703 if (state.currentQuestion < QUIZ_QUESTIONS.length - 1) { 704 state.currentQuestion++; 705 renderQuizQuestion(); 706 } else { 707 calculateResults(); 708 showPage('results'); 709 } 710} 711 712function handleQuizBack() { 713 if (state.currentQuestion > 0) { 714 state.currentQuestion--; 715 renderQuizQuestion(); 716 } else { 717 showPage('home'); 718 } 719} 720 721// ========== SCORING ALGORITHM ========== 722const THIGH_BONELESS_BRANDS = new Set([ 723 'êµ½ë¤ì¹í¨', 'í¸ë¼ë', 'BBQ', 'ë§ì¤í°ì¹', '60ê³ì¹í¨', 'ë ¸ëíµë', 'ìë´ì¹í¨', 'í¸ìì´ëë§ë¦¬ì¹í¨', 'ëëì¤ë' 724]); 725const BREAST_BONELESS_BRANDS = new Set([ 726 'ë¤ë¤ì¹í¨', 'KFC' 727]); 728 729function inferBonelessType(item) { 730 const composition = item.composition || []; 731 const name = item.name || ''; 732 if (!composition.includes('ìì´')) return null; 733 734 if (/ì¸ì´|ë¤ë¦¬ì´|íµë¤ë¦¬/.test(name)) return 'boneless_thigh'; 735 if (/ê°ì´|ìì¬|í ë/.test(name)) return 'boneless_breast';
736 if (THIGH_BONELESS_BRANDS.has(item.brand)) return 'boneless_thigh'; 737 if (BREAST_BONELESS_BRANDS.has(item.brand)) return 'boneless_breast'; 738 return 'boneless_mix'; 739} 740 741function matchesCompositionPreference(item, preference) { 742 if (preference === 'any') return true; 743 const composition = item.composition || []; 744 745 switch (preference) { 746 case 'whole_bone': 747 return composition.includes('ì ì²´'); 748 case 'wing_combo': 749 return composition.includes('ìë´') || composition.includes('콤보'); 750 case 'boneless_thigh': 751 return composition.includes('ìì´') && inferBonelessType(item) === 'boneless_thigh'; 752 case 'boneless_breast': 753 return composition.includes('ìì´') && inferBonelessType(item) === 'boneless_breast'; 754 case 'boneless_mix': 755 return composition.includes('ìì´') && inferBonelessType(item) === 'boneless_mix'; 756 default: 757 return true; 758 } 759} 760 761function classifyTextureMethod(item) { 762 const method = item.cooking_method || ''; 763 const name = item.name || ''; 764 const crispiness = item.crispiness || 3; 765 766 if (/ì¯ë¶|조림/.test(method) || /ì¯ë¶|ìë 구ì´/.test(name) || item.brand === 'ì§ì½ë°ì¹í¨') { 767 return 'stirfry'; 768 } 769 770 if (/ì¤ë¸/.test(method) || /ì¤ë¸/.test(name)) { 771 if (crispiness >= 3 || /ë°ì¬ì|í¬ë¦¬ì¤í¼/.test(name)) return 'baked'; 772 return 'roast'; 773 } 774 775 if (/구ì´/.test(method) || /구ì´/.test(name)) return 'roast'; 776 777 if (crispiness >= 4 || /í¬ë¦¬ì¤í¼|í«í¬ë¦¬ì¤í¼/.test(name)) return 'crispy'; 778 779 return 'embossed'; 780} 781 782function classifySauceProfile(item) { 783 const name = item.name || ''; 784 const tags = item.flavor_tags || []; 785 const hasTag = (target) => tags.includes(target); 786 787 if (/ë¿ë§|ì¤ë ¸ì|ìì¦ë|ì½ìë©|íì°ë/.test(name) || hasTag('ìì¦ë') || hasTag('ì½ìë©')) { 788 return 'powder'; 789 } 790 if (/ë§ì|í¬ë¦¼|ì¹ì¦|ì´ëì¸|íì´í¸/.test(name) || hasTag('ì¹ì¦') || hasTag('ìê±°í¸') || hasTag('í¬ë¦¬ë¯¸')) { 791 return 'white'; 792 } 793 if (/ê°ì¥|ê°ë¹|ë§ì´í¹|ìì´|ë¸ë/.test(name) || hasTag('ê°ì¥') || hasTag('ë§ëê°ì¥') || hasTag('ì§ì¡°ë¦')) { 794 return 'black'; 795 } 796 if (/ìë |ë ë|í«|ì¹ ë¦¬|ê³ ì¶|ë¶ë|ë¡ì´|ë³¼ì¼ì´ë ¸/.test(name) || hasTag('매콤') || hasTag('매ì´') || hasTag('ê³ ì¶')) { 797 return 'red'; 798 } 799 return 'none'; 800} 801 802function getAdaptiveWeights(answeredIds) { 803 const answeredCount = answeredIds.length; 804 const weights = { 805 spiciness: 1, 806 crispiness_preference: 1, 807 texture_method: 1, 808 composition: 1, 809 sauce_profile: 1 810 }; 811 812 if (answeredCount <= 2) { 813 weights.spiciness = 1.45; 814 weights.crispiness_preference = 1.35; 815 weights.texture_method = 1.15; 816 weights.composition = 0.75; 817 weights.sauce_profile = 0.8; 818 } else if (answeredCount <= 4) { 819 weights.spiciness = 1.2; 820 weights.crispiness_preference = 1.2; 821 weights.texture_method = 1.25; 822 weights.composition = 1.15; 823 weights.sauce_profile = 1.2; 824 } else { 825 weights.spiciness = 1.15; 826 weights.crispiness_preference = 1.1; 827 weights.texture_method = 1.25; 828 weights.composition = 1.2; 829 weights.sauce_profile = 1.35; 830 } 831 832 return weights; 833} 834 835function scoreMenuItem(item, answers, weights) { 836 let rawScore = 0; 837 let maxScore = 0; 838 const diagnostics = {}; 839 840 const userSpice = Number(answers.spiciness); 841 if (Number.isFinite(userSpice)) { 842 const spiceDiff = Math.abs(userSpice - (item.spiciness || 3)); 843 const spiceBaseMax = 45; 844 const spiceComponent = Math.max(0, spiceBaseMax - (spiceDiff * 11)); 845 rawScore += spiceComponent * weights.spiciness; 846 maxScore += spiceBaseMax * weights.spiciness; 847 diagnostics.spiceDiff = spiceDiff; 848 } 849 850 const userCrispy = Number(answers.crispiness_preference); 851 if (Number.isFinite(userCrispy)) { 852 const crispyDiff = Math.abs(userCrispy - (item.crispiness || 3)); 853 const crispyBaseMax = 32; 854 const crispyComponent = Math.max(0, crispyBaseMax - (crispyDiff * 8)); 855 rawScore += crispyComponent * weights.crispiness_preference; 856 maxScore += crispyBaseMax * weights.crispiness_preference; 857 diagnostics.crispyDiff = crispyDiff; 858 } 859 860 const texturePref = answers.texture_method; 861 const textureType = classifyTextureMethod(item); 862 if (texturePref !== undefined) { 863 const textureBaseMax = 30; 864 const isAny = texturePref === 'any'; 865 const isMatch = textureType === texturePref; 866 const textureComponent = isAny ? 14 : (isMatch ? textureBaseMax : 0); 867 rawScore += textureComponent * weights.texture_method; 868 maxScore += textureBaseMax * weights.texture_method; 869 diagnostics.textureMatch = isAny ? true : isMatch; 870 } 871 872 const compositionPref = answers.composition; 873 if (compositionPref !== undefined) { 874 const compositionBaseMax = 28; 875 const isAny = compositionPref === 'any'; 876 const isMatch = matchesCompositionPreference(item, compositionPref); 877 const compositionComponent = isAny ? 12 : (isMatch ? compositionBaseMax : 0); 878 rawScore += compositionComponent * weights.composition; 879 maxScore += compositionBaseMax * weights.composition; 880 diagnostics.compositionMatch = isAny ? true : isMatch; 881 } 882 883 const saucePref = answers.sauce_profile; 884 const sauceType = classifySauceProfile(item); 885 if (saucePref !== undefined) { 886 const sauceBaseMax = 30; 887 const isAny = saucePref === 'any'; 888 const isMatch = sauceType === saucePref; 889 const sauceComponent = isAny ? 14 : (isMatch ? sauceBaseMax : 0); 890 rawScore += sauceComponent * weights.sauce_profile; 891 maxScore += sauceBaseMax * weights.sauce_profile; 892 diagnostics.sauceMatch = isAny ? true : isMatch; 893 } 894 895 const normalizedScore = maxScore > 0 896 ? Math.max(50, Math.min(99, Math.round((rawScore / maxScore) * 49 + 50))) 897 : 50; 898 899 return { 900 ...item, 901 score: normalizedScore, 902 rawScore, 903 diagnostics, 904 textureType, 905 sauceType 906 }; 907} 908 909function calculateRankedMatches(answers, options = {}) { 910 const { preview = false, limit = 3 } = options; 911 const answeredIds = getAnsweredQuestionIds(answers); 912 if (!answeredIds.length) return []; 913 914 const weights = getAdaptiveWeights(answeredIds); 915 const normalizedAnswers = {}; 916 answeredIds.forEach(id => { 917 normalizedAnswers[id] = answers[id]; 918 }); 919
920 let candidates = MENU_ITEMS; 921 if (!preview && normalizedAnswers.composition && normalizedAnswers.composition !== 'any') { 922 const filtered = MENU_ITEMS.filter(item => matchesCompositionPreference(item, normalizedAnswers.composition)); 923 if (filtered.length) candidates = filtered; 924 } 925 926 const scored = candidates.map(item => scoreMenuItem(item, normalizedAnswers, weights)); 927 scored.sort((a, b) => b.rawScore - a.rawScore || b.score - a.score); 928 return scored.slice(0, limit); 929} 930 931function buildLiveReason(item, lang) { 932 const parts = []; 933 const d = item.diagnostics || {}; 934 935 if (Number.isFinite(d.spiceDiff)) { 936 parts.push(lang === 'ko' ? `맵기 ì°¨ì´ Â±${d.spiceDiff}` : `Spice ±${d.spiceDiff}`); 937 } 938 if (Number.isFinite(d.crispyDiff)) { 939 parts.push(lang === 'ko' ? `ë°ìí¨ ì°¨ì´ Â±${d.crispyDiff}` : `Crispy ±${d.crispyDiff}`); 940 } 941 if (d.textureMatch !== undefined) { 942 parts.push(lang === 'ko' 943 ? (d.textureMatch ? 'ì¡°ë¦¬ë² ì¼ì¹' : 'ì¡°ë¦¬ë² ë¹ì¼ì¹') 944 : (d.textureMatch ? 'Method match' : 'Method mismatch')); 945 } 946 if (d.sauceMatch !== undefined) { 947 parts.push(lang === 'ko' 948 ? (d.sauceMatch ? 'ìì¤ ì±í¥ ì¼ì¹' : 'ìì¤ ì±í¥ ë¹ì¼ì¹') 949 : (d.sauceMatch ? 'Sauce match' : 'Sauce mismatch')); 950 } 951 if (d.compositionMatch !== undefined) { 952 parts.push(lang === 'ko' 953 ? (d.compositionMatch ? 'ë¶ì ì í¸ ì¼ì¹' : 'ë¶ì ì í¸ ë¹ì¼ì¹') 954 : (d.compositionMatch ? 'Cut match' : 'Cut mismatch')); 955 } 956 957 return parts.slice(0, 2).join(' · '); 958} 959 960function renderLiveRecommendation() { 961 const container = document.getElementById('quiz-live-reco'); 962 if (!container) return; 963 964 const strings = getStrings(); 965 const lang = state.language || 'en'; 966 const answeredIds = getAnsweredQuestionIds(state.answers); 967 const answeredCount = answeredIds.length; 968 969 if (!answeredCount) { 970 container.innerHTML = ` 971 <div class="quiz-live-header"> 972 <h3>${strings.liveRecoTitle || 'Live Guess While You Answer'}</h3> 973 <span>0 / ${QUIZ_QUESTIONS.length}</span> 974 </div> 975 <p class="quiz-live-subtitle">${strings.liveRecoHint || 'Select an option to see an early recommendation.'}</p> 976 `; 977 return; 978 } 979 980 const picks = calculateRankedMatches(state.answers, { preview: true, limit: 2 }); 981 if (!picks.length) { 982 container.innerHTML = ` 983 <div class="quiz-live-header"> 984 <h3>${strings.liveRecoTitle || 'Live Guess While You Answer'}</h3> 985 <span>${answeredCount} / ${QUIZ_QUESTIONS.length}</span> 986 </div> 987 <p class="quiz-live-subtitle">${strings.liveRecoHint || 'Select an option to see an early recommendation.'}</p> 988 `; 989 return; 990 } 991 992 container.innerHTML = ` 993 <div class="quiz-live-header"> 994 <h3>${strings.liveRecoTitle || 'Live Guess While You Answer'}</h3> 995 <span>${answeredCount} / ${QUIZ_QUESTIONS.length}</span> 996 </div> 997 <p class="quiz-live-subtitle">${strings.liveRecoSubtitle || 'Weighted recommendation updates after each step.'}</p> 998 <div class="quiz-live-cards"> 999 ${picks.map(item => { 1000 const displayName = getTranslatedName(item, lang); 1001 const reason = buildLiveReason(item, lang); 1002 return ` 1003 <article class="quiz-live-card"> 1004 <img src="${item.image}" data-fallback="${item.fallbackImage || ''}" alt="${displayName}" class="quiz-live-image" loading="lazy" decoding="async" referrerpolicy="no-referrer" onerror="if(this.dataset.fallback && this.src !== this.dataset.fallback){this.src=this.dataset.fallback;}"> 1005 <div class="quiz-live-meta"> 1006 <strong>${displayName}</strong> 1007 <span>${item.brand}</span> 1008 <div class="quiz-live-score">${strings.liveRecoConfidence || 'Confidence'} ${item.score}%</div> 1009 <p>${reason}</p> 1010 </div> 1011 </article> 1012 `; 1013 }).join('')} 1014 </div> 1015 `; 1016} 1017 1018function calculateResults() { 1019 state.matches = calculateRankedMatches(state.answers, { preview: false, limit: 3 }); 1020 1021 state.userProfile = { 1022 spiciness: Number(state.answers.spiciness) || 3, 1023 crispiness_preference: Number(state.answers.crispiness_preference) || 3, 1024 texture_method: state.answers.texture_method || 'any', 1025 composition: state.answers.composition || 'any', 1026 sauce_profile: state.answers.sauce_profile || 'any' 1027 }; 1028 1029 addToHistory(); 1030 saveState(); 1031} 1032 1033// ========== RESULTS PAGE ========== 1034function renderResults() { 1035 renderMatchCards(); 1036 renderNearbyStoresSection(); 1037 updatePremiumSection(); 1038 // Profile chips could be rendered if needed, but simplified for now 1039} 1040 1041function initNearbyStores() { 1042 const findBtn = document.getElementById('nearby-find-btn'); 1043 if (!findBtn) return; 1044 findBtn.addEventListener('click', handleFindNearbyStores); 1045 renderNearbyStoresSection(); 1046} 1047 1048function setNearbyStatus(message, type = '') { 1049 const statusEl = document.getElementById('nearby-status'); 1050 if (!statusEl) return; 1051 statusEl.className = `nearby-status ${type}`.trim(); 1052 statusEl.textContent = message || ''; 1053} 1054 1055function haversineDistanceKm(lat1, lon1, lat2, lon2) { 1056 const toRad = (value) => (value * Math.PI) / 180; 1057 const earthKm = 6371; 1058 const dLat = toRad(lat2 - lat1); 1059 const dLon = toRad(lon2 - lon1); 1060 const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + 1061 Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * 1062 Math.sin(dLon / 2) * Math.sin(dLon / 2); 1063 const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); 1064 return earthKm * c; 1065} 1066 1067function normalizeNearbyStore(rawStore, userLat, userLon) { 1068 const lat = rawStore.lat ?? rawStore.center?.lat; 1069 const lon = rawStore.lon ?? rawStore.center?.lon; 1070 if (!Number.isFinite(lat) || !Number.isFinite(lon)) return null; 1071 const tags = rawStore.tags || {}; 1072 const name = String(tags.name || '').trim(); 1073 if (!name) return null; 1074 1075 const distanceKm = haversineDistanceKm(userLat, userLon, lat, lon); 1076 return { 1077 id: `${name}-${lat.toFixed(5)}-${lon.toFixed(5)}`, 1078 name, 1079 lat, 1080 lon, 1081 distanceKm, 1082 cuisine: tags.cuisine || '', 1083 address: tags['addr:full'] || `${tags['addr:street'] || ''} ${tags['addr:housenumber'] || ''}`.trim(), 1084 mapLink: `https://www.google.com/maps/search/?api=1&query=${lat},${lon}` 1085 }; 1086} 1087 1088async function fetchNearbyChickenStores(latitude, longitude) { 1089 const overpassQuery = ` 1090[out:json][timeout:25]; 1091( 1092 node(around:${NEARBY_RADIUS_METERS},${latitude},${longitude})["cuisine"~"chicken|fried_chicken",i]; 1093 way(around:${NEARBY_RADIUS_METERS},${latitude},${longitude})["cuisine"~"chicken|fried_chicken",i]; 1094 relation(around:${NEARBY_RADIUS_METERS},${latitude},${longitude})["cuisine"~"chicken|fried_chicken",i]; 1095 node(around:${NEARBY_RADIUS_METERS},${latitude},${longitude})["name"~"${CHICKEN_NAME_REGEX}",i]["amenity"~"restaurant|fast_food",i]; 1096 way(around:${NEARBY_RADIUS_METERS},${latitude},${longitude})["name"~"${CHICKEN_NAME_REGEX}",i]["amenity"~"restaurant|fast_food",i]; 1097 relation(around:${NEARBY_RADIUS_METERS},${latitude},${longitude})["name"~"${CHICKEN_NAME_REGEX}",i]["amenity"~"restaurant|fast_food",i]; 1098); 1099out center tags; 1100`; 1101 1102 const endpoints = [ 1103 'https://overpass-api.de/api/interpreter', 1104 'https://overpass.kumi.systems/api/interpreter' 1105 ]; 1106 1107 let payload = null; 1108 for (const endpoint of endpoints) { 1109 try { 1110 const response = await fetch(endpoint, { 1111 method: 'POST', 1112 headers: { 'Content-Type': 'text/plain;charset=UTF-8' }, 1113 body: overpassQuery 1114 }); 1115 if (!response.ok) continue; 1116 payload = await response.json(); 1117 break; 1118 } catch (error) { 1119 console.warn(`Nearby lookup failed on endpoint: ${endpoint}`, error); 1120 } 1121 } 1122 if (!payload) throw new Error('Nearby fetch failed on all endpoints.'); 1123 1124 const stores = (payload.elements || []) 1125 .map(entry => normalizeNearbyStore(entry, latitude, longitude)) 1126 .filter(Boolean) 1127 .filter(store => store.distanceKm <= (NEARBY_RADIUS_METERS / 1000)); 1128 1129 const dedup = new Map();
1130 stores.forEach(store => { 1131 const key = `${store.name.toLowerCase()}-${store.lat.toFixed(4)}-${store.lon.toFixed(4)}`; 1132 if (!dedup.has(key) || dedup.get(key).distanceKm > store.distanceKm) dedup.set(key, store); 1133 }); 1134 1135 return Array.from(dedup.values()).sort((a, b) => a.distanceKm - b.distanceKm); 1136} 1137 1138function ensureNearbyMap() { 1139 const mapEl = document.getElementById('nearby-map'); 1140 if (!mapEl) return null; 1141 if (state.nearbyMap) return state.nearbyMap; 1142 if (!window.L) return null; 1143 1144 state.nearbyMap = L.map(mapEl, { zoomControl: true }); 1145 L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { 1146 maxZoom: 19, 1147 attribution: '© OpenStreetMap contributors' 1148 }).addTo(state.nearbyMap); 1149 1150 return state.nearbyMap; 1151} 1152 1153function clearNearbyMarkers() { 1154 if (!state.nearbyMap || !state.nearbyMarkers.length) return; 1155 state.nearbyMarkers.forEach(marker => marker.remove()); 1156 state.nearbyMarkers = []; 1157} 1158 1159function renderNearbyMap() { 1160 const mapEl = document.getElementById('nearby-map'); 1161 if (!mapEl) return; 1162 1163 const map = ensureNearbyMap(); 1164 if (!map || !state.nearbyLocation) return; 1165 1166 mapEl.classList.add('ready'); 1167 setTimeout(() => map.invalidateSize(), 0); 1168 clearNearbyMarkers(); 1169 1170 const userLatLng = [state.nearbyLocation.lat, state.nearbyLocation.lon]; 1171 const userMarker = L.circleMarker(userLatLng, { 1172 radius: 8, 1173 color: '#E31837',
1174 fillColor: '#FFC72C', 1175 fillOpacity: 1, 1176 weight: 2 1177 }).addTo(map).bindPopup('You are here'); 1178 state.nearbyMarkers.push(userMarker); 1179 1180 const radiusCircle = L.circle(userLatLng, { 1181 radius: NEARBY_RADIUS_METERS, 1182 color: '#E31837', 1183 fillColor: '#E31837', 1184 fillOpacity: 0.08, 1185 weight: 1 1186 }).addTo(map); 1187 state.nearbyMarkers.push(radiusCircle); 1188 1189 const bounds = [userLatLng]; 1190 state.nearbyStores.forEach(store => { 1191 const latLng = [store.lat, store.lon]; 1192 bounds.push(latLng); 1193 const marker = L.marker(latLng) 1194 .addTo(map) 1195 .bindPopup(`<strong>${store.name}</strong><br>${store.distanceKm.toFixed(2)} km`); 1196 state.nearbyMarkers.push(marker); 1197 }); 1198 1199 map.fitBounds(bounds, { padding: [20, 20] }); 1200} 1201 1202function renderNearbyStoresSection() { 1203 const listEl = document.getElementById('nearby-list'); 1204 const mapEl = document.getElementById('nearby-map'); 1205 if (!listEl || !mapEl) return; 1206 1207 const strings = getStrings(); 1208 if (!state.nearbyLocation || !state.nearbyStores.length) { 1209 mapEl.classList.remove('ready'); 1210 clearNearbyMarkers(); 1211 listEl.innerHTML = ` 1212 <div class="nearby-empty"> 1213 ${strings.nearbySubtitle || 'Use my location and list every chicken store found nearby.'} 1214 </div> 1215 `; 1216 return; 1217 } 1218 1219 listEl.innerHTML = state.nearbyStores.map(store => ` 1220 <article class="nearby-item"> 1221 <div class="nearby-item-main"> 1222 <h4>${store.name}</h4> 1223 <p>${store.address || '-'}</p> 1224 </div> 1225 <div class="nearby-item-side"> 1226 <strong>${store.distanceKm.toFixed(2)} km</strong> 1227 <a href="${store.mapLink}" target="_blank" rel="noopener">${strings.nearbyOpenMap || 'Open in map'}</a> 1228 </div> 1229 </article> 1230 `).join(''); 1231 1232 renderNearbyMap(); 1233} 1234 1235function handleFindNearbyStores() { 1236 const strings = getStrings(); 1237 if (!navigator.geolocation) { 1238 setNearbyStatus(strings.nearbyUnsupported || 'Geolocation is not supported in this browser.', 'error'); 1239 return; 1240 } 1241 1242 setNearbyStatus(strings.nearbyLoading || 'Finding nearby stores...', 'loading'); 1243 navigator.geolocation.getCurrentPosition(async (position) => { 1244 try { 1245 const lat = position.coords.latitude; 1246 const lon = position.coords.longitude; 1247 const stores = await fetchNearbyChickenStores(lat, lon); 1248 1249 state.nearbyLocation = { lat, lon }; 1250 state.nearbyStores = stores; 1251 1252 if (!stores.length) { 1253 setNearbyStatus(strings.nearbyNoStores || 'No chicken stores found within 10km.', 'warning'); 1254 } else { 1255 const lang = state.language || 'en'; 1256 const suffix = strings.nearbyFoundSuffix || 'stores found within 10km'; 1257 const countMessage = (lang === 'ko' || lang === 'zh' || lang === 'ja') 1258 ? `${stores.length}${suffix}` 1259 : `${stores.length} ${suffix}`; 1260 setNearbyStatus(countMessage, 'success'); 1261 } 1262 1263 renderNearbyStoresSection(); 1264 } catch (error) { 1265 console.error('Failed to find nearby stores:', error); 1266 const lang = state.language || 'en'; 1267 const message = lang === 'ko' 1268 ? 'ì£¼ë³ ë§¤ì¥ ì¡°íì ì¤í¨íìµëë¤. ì ì í ë¤ì ìëí´ì£¼ì¸ì.' 1269 : lang === 'zh' 1270 ? 'éè¿é¨åºæ¥è¯¢å¤±è´¥ï¼è¯·ç¨åéè¯ã' 1271 : lang === 'ja' 1272 ? 'è¿ãã®åºèæ¤ç´¢ã«å¤±æãã¾ãããæéãããã¦å試è¡ãã¦ãã ããã' 1273 : 'Nearby store lookup failed. Please try again.'; 1274 setNearbyStatus(message, 'error'); 1275 } 1276 }, () => { 1277 setNearbyStatus(strings.nearbyNeedPermission || 'Location permission is required to find nearby stores.', 'error'); 1278 }, { 1279 enableHighAccuracy: true, 1280 timeout: 15000, 1281 maximumAge: 0 1282 }); 1283} 1284 1285// ========== Translation Helpers ========== 1286function getTranslatedName(item, lang) {
1287 const koName = item.name; // always Korean from raw data 1288 if (lang === 'ko') return koName; 1289 const t = MENU_NAME_TRANSLATIONS[koName]; 1290 if (t && t[lang]) return t[lang]; 1291 return koName; // fallback to Korean 1292} 1293 1294function getTranslatedTags(item, lang) { 1295 if (lang === 'ko') return item.flavor_tags; 1296 return item.flavor_tags.map(tag => { 1297 const t = FLAVOR_TAG_TRANSLATIONS[tag]; 1298 return (t && t[lang]) ? t[lang] : tag; 1299 }); 1300} 1301 1302function getTranslatedDescription(item, lang) { 1303 // Description is always in Korean from raw data 1304 // For non-Korean, we keep Korean but show it in a helpful way 1305 if (lang === 'ko') return item.description || ''; 1306 // For other languages, we won't machine-translate inline, just show Korean with context 1307 return item.description || ''; 1308} 1309 1310function getTranslatedBadge(badge, lang) { 1311 const t = BADGE_TRANSLATIONS[badge]; 1312 if (t && t[lang]) return t[lang]; 1313 return badge; 1314} 1315 1316function renderMatchCards() { 1317 const container = document.getElementById('match-cards'); 1318 if (!container) return; 1319 1320 const lang = state.language || 'en'; 1321 const strings = getStrings(); 1322 1323 if (state.matches.length === 0) { 1324 const noMatchMsg = lang === 'ko' ? 'ì´ ì¡°ê±´ì ë§ë ê²°ê³¼ê° ììµëë¤. ì¡°ê±´ì ë³ê²½í´ë³´ì¸ì!' : 1325 lang === 'zh' ? 'æ²¡ææ¾å°å¹é çç»æï¼è¯·è°æ´æ¡ä»¶ï¼' : 1326 lang === 'ja' ? 'æ¡ä»¶ã«åãçµæãããã¾ãããæ¡ä»¶ã夿´ãã¦ã¿ã¦ãã ããï¼' : 1327 'No matches found with these exact filters. Try loosening your criteria!'; 1328 container.innerHTML = `<div style="text-align:center; padding: 2rem;">${noMatchMsg}</div>`; 1329 return; 1330 } 1331 1332 const spicyLabel = strings.spiciness || 'Spiciness'; 1333 const crispyLabel = strings.crispiness || 'Crispiness'; 1334 const featuresLabel = lang === 'ko' ? 'í¹ì§' : lang === 'zh' ? 'ç¹è²' : lang === 'ja' ? 'ç¹å¾´' : 'Features'; 1335 1336 container.innerHTML = state.matches.map(item => { 1337 const displayName = getTranslatedName(item, lang); 1338 const koName = item.name; 1339 // Show both Korean original + translated name for non-Korean users 1340 const nameDisplay = lang === 'ko' ? koName : `${displayName} <span style="font-size: 13px; color: #888; font-weight: 400;">(${koName})</span>`; 1341 const tags = getTranslatedTags(item, lang); 1342 const badge = getTranslatedBadge(item.badge, lang); 1343 const desc = item.description || ''; 1344 1345 return ` 1346 <div class="match-card"> 1347 <div class="match-card-image" onclick="window.open('${item.menuPage}', '_blank')" style="cursor: pointer; overflow: hidden;"> 1348 <img class="match-card-photo" src="${item.image}" data-fallback="${item.fallbackImage || ''}" alt="${displayName}" loading="lazy" decoding="async" referrerpolicy="no-referrer" onerror="if(this.dataset.fallback && this.src !== this.dataset.fallback) { this.src = this.dataset.fallback; }"> 1349 <div class="match-card-badge"> 1350 <span class="badge ${item.badge === 'Spicy' ? 'badge-spicy' : ''}">${badge}</span> 1351 </div> 1352 </div> 1353 <div class="match-card-content"> 1354 <div class="match-card-header"> 1355 <div class="match-card-meta" onclick="window.open('${item.website}', '_blank')"> 1356 <div class="match-card-name">${nameDisplay} <span style="font-size: 12px; vertical-align: middle;">ð</span></div> 1357 <div class="match-card-brand-row"> 1358 ${item.brandImage ? `<img class="match-card-brand-logo" src="${item.brandImage}" alt="${item.brand} logo" loading="lazy" decoding="async" referrerpolicy="no-referrer">` : ''} 1359 <div class="match-card-brand">${item.brand}</div> 1360 </div> 1361 </div> 1362 <div class="match-card-score"> 1363 <div class="match-card-score-value">${item.score}</div> 1364 <div class="match-card-score-label">${strings.matchScore || 'Match'}</div> 1365 </div> 1366 </div> 1367 1368 <div class="mini-meters"> 1369 <div class="mini-meter"> 1370 <div class="mini-meter-label">${spicyLabel}</div> 1371 <div class="mini-meter-bar"><div class="mini-meter-fill" style="width: ${item.spiciness * 20}%"></div></div> 1372 </div> 1373 <div class="mini-meter"> 1374 <div class="mini-meter-label">${crispyLabel}</div> 1375 <div class="mini-meter-bar"><div class="mini-meter-fill" style="width: ${item.crispiness * 20}%"></div></div> 1376 </div> 1377 </div> 1378 1379 <div style="margin-top: 8px; font-size: 12px; color: #666;"> 1380 <strong>${featuresLabel}:</strong> ${tags.join(', ')} 1381 </div> 1382 1383 <div class="match-card-reason">${desc}</div> 1384 </div> 1385 </div> 1386 `; 1387 }).join(''); 1388 1389 attachMatchCardImageFallbacks(container); 1390} 1391 1392function attachMatchCardImageFallbacks(container) { 1393 container.querySelectorAll('.match-card-photo').forEach(img => { 1394 img.addEventListener('error', () => { 1395 img.style.display = 'none'; 1396 }); 1397 });
1398 container.querySelectorAll('.match-card-brand-logo').forEach(img => { 1399 img.addEventListener('error', () => { 1400 img.style.display = 'none'; 1401 }); 1402 }); 1403} 1404 1405function updatePremiumSection() { 1406 const lockedSection = document.getElementById('locked-premium-section'); 1407 if (lockedSection) { 1408 lockedSection.style.display = state.isPremium ? 'none' : 'block'; 1409 } 1410} 1411 1412// ========== PREMIUM ========== 1413function initPremium() { 1414 document.querySelectorAll('[data-action="start-premium"]').forEach(btn => { 1415 btn.addEventListener('click', handleStartPremium); 1416 }); 1417 document.querySelectorAll('[data-action="restore-purchase"]').forEach(btn => { 1418 btn.addEventListener('click', handleRestorePurchase); 1419 }); 1420} 1421 1422function handleStartPremium() { 1423 state.isPremium = true; 1424 saveState(); 1425 alert('ð Welcome to Premium! Your free month has started.'); 1426 showPage('results'); 1427} 1428 1429function handleRestorePurchase() { 1430 if (state.isPremium) { 1431 alert('â Your Premium subscription has been restored!'); 1432 } else { 1433 alert('No previous subscription found.'); 1434 } 1435} 1436 1437// ========== SETTINGS ========== 1438function initSettings() { 1439 const langToggle = document.getElementById('lang-toggle'); 1440 if (langToggle) { 1441 langToggle.addEventListener('click', () => { 1442 langToggle.classList.toggle('active'); 1443 }); 1444 } 1445 1446 document.querySelectorAll('[data-action="edit-quiz"]').forEach(btn => { 1447 btn.addEventListener('click', () => { 1448 state.currentQuestion = 0; 1449 showPage('quiz'); 1450 }); 1451 }); 1452} 1453
1454function renderSettings() { 1455 // Simplified settings render 1456 const historyList = document.getElementById('history-list'); 1457 if (historyList) { 1458 if (state.history.length === 0) { 1459 historyList.innerHTML = '<p class="text-muted">No picks yet.</p>'; 1460 } else { 1461 historyList.innerHTML = state.history.slice(0, 5).map(item => ` 1462 <div class="history-item"> 1463 <div> 1464 <div class="history-name">${item.name}</div> 1465 <div class="history-date">${item.date}</div> 1466 </div> 1467 </div> 1468 `).join(''); 1469 } 1470 } 1471} 1472 1473// ========== FAQ ========== 1474function initFAQ() { 1475 document.querySelectorAll('.faq-question').forEach(btn => { 1476 btn.addEventListener('click', () => { 1477 const item = btn.closest('.faq-item'); 1478 item.classList.toggle('open'); 1479 }); 1480 }); 1481} 1482 1483// ========== HISTORY ========== 1484function addToHistory() { 1485 if (state.matches.length > 0) { 1486 const topMatch = state.matches[0]; 1487 state.history.unshift({ 1488 name: topMatch.name, 1489 score: topMatch.score, 1490 date: new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) 1491 }); 1492 state.history = state.history.slice(0, 10); 1493 } 1494} 1495 1496// ========== PERSISTENCE ========== 1497function saveState() { 1498 localStorage.setItem('kcs-state', JSON.stringify({ 1499 userProfile: state.userProfile, 1500 history: state.history, 1501 isPremium: state.isPremium, 1502 answers: state.answers 1503 })); 1504} 1505 1506function loadState() { 1507 try { 1508 const saved = localStorage.getItem('kcs-state'); 1509 if (saved) { 1510 const data = JSON.parse(saved); 1511 state.userProfile = data.userProfile || null; 1512 state.history = data.history || []; 1513 state.isPremium = data.isPremium || false; 1514 } 1515 } catch (e) { 1516 console.error('Failed to load state:', e); 1517 } 1518} 1519 1520// ========== UTILITIES ========== 1521function getStrings() { 1522 return UI_STRINGS[state.language || 'en'] || UI_STRINGS.en; 1523} 1524 1525// ========== SHARE ========== 1526function buildShareText() { 1527 const lang = state.language || 'en'; 1528 const url = 'https://k-chicken-sommelier.vercel.app'; 1529 1530 if (state.matches.length === 0) { 1531 const defaultMsg = lang === 'ko' ? 'ð K-Chicken Sommelierìì ëì ì¹í¨ ìì¸ë©ì´í¸ë¥¼ ì°¾ìì´ì!' : 1532 'ð Found my Korean chicken soulmate with K-Chicken Sommelier!'; 1533 const hashtags = lang === 'ko' 1534 ? '\n\n#KChickenSommelier #ì¹í¨ì¶ì² #íêµì¹í¨ #ì¹í¨ìì¸ë©ì´í¸' 1535 : '\n\n#KChickenSommelier #KoreanChicken #ChickenSoulmate'; 1536 return `${defaultMsg}${hashtags}\n${url}`; 1537 } 1538 1539 const top = state.matches[0]; 1540 const koName = top.name; 1541 const displayName = getTranslatedName(top, lang); 1542 const tags = getTranslatedTags(top, lang).slice(0, 3).join(', '); 1543 const badge = getTranslatedBadge(top.badge, lang); 1544 1545 let text, hashtags; 1546 if (lang === 'ko') { 1547 text = `ð ëì ì¹í¨ ìì¸ë©ì´í¸ë¥¼ ì°¾ìì´ì!\n\n` + 1548 `â ${koName} â ${top.brand}\n` + 1549 `ð·ï¸ ${badge} | ë§¤ì¹ ì ì ${top.score}ì \n` + 1550 `ð¥ ${tags}\n\n` + 1551 `30ì´ í´ì¦ë¡ ëë§ì ì¹í¨ì ì°¾ìë³´ì¸ì!`; 1552 hashtags = '\n\n#KChickenSommelier #ì¹í¨ì¶ì² #íêµì¹í¨ #ì¹í¨ìì¸ë©ì´í¸ #' + koName.replace(/\s/g, ''); 1553 } else if (lang === 'zh') { 1554 text = `ð æ¾å°äºæçç¸é¸¡çµéä¼´ä¾£ï¼\n\n` + 1555 `â ${displayName} â ${top.brand}\n` + 1556 `ð·ï¸ ${badge} | å¹é 度 ${top.score}\n` + 1557 `ð¥ ${tags}\n\n` + 1558 `30ç§æµè¯æ¾å°ä½ çä¸å±ç¸é¸¡ï¼`; 1559 hashtags = '\n\n#KChickenSommelier #é©å½ç¸é¸¡ #ç¸é¸¡æ¨è'; 1560 } else if (lang === 'ja') { 1561 text = `ð ããã³ã½ã¦ã«ã¡ã¤ããè¦ã¤ãã¾ããï¼\n\n` + 1562 `â ${displayName} â ${top.brand}\n` + 1563 `ð·ï¸ ${badge} | ããã度 ${top.score}\n` + 1564 `ð¥ ${tags}\n\n` + 1565 `30ç§ã¯ã¤ãºã§ããªãã®ããã³ãè¦ã¤ãããï¼`; 1566 hashtags = '\n\n#KChickenSommelier #éå½ããã³ #ããã³ãããã'; 1567 } else { 1568 text = `ð Found my Korean chicken soulmate!\n\n` + 1569 `â ${displayName} â ${top.brand}\n` + 1570 `ð·ï¸ ${badge} | Match Score: ${top.score}\n` + 1571 `ð¥ ${tags}\n\n` + 1572 `Take the 30-sec quiz to find yours!`; 1573 hashtags = '\n\n#KChickenSommelier #KoreanChicken #ChickenSoulmate #KFood'; 1574 } 1575 1576 return `${text}${hashtags}\n${url}`; 1577} 1578 1579function buildShareHashtags() { 1580 const lang = state.language || 'en'; 1581 if (lang === 'ko') return 'KChickenSommelier,ì¹í¨ì¶ì²,íêµì¹í¨,ì¹í¨ìì¸ë©ì´í¸'; 1582 if (lang === 'zh') return 'KChickenSommelier,é©å½ç¸é¸¡,ç¸é¸¡æ¨è'; 1583 if (lang === 'ja') return 'KChickenSommelier,éå½ããã³,ããã³ãããã'; 1584 return 'KChickenSommelier,KoreanChicken,ChickenSoulmate,KFood'; 1585} 1586 1587function showToast(message) { 1588 // Remove existing toast 1589 document.querySelector('.share-toast')?.remove(); 1590 const toast = document.createElement('div'); 1591 toast.className = 'share-toast'; 1592 toast.textContent = message; 1593 toast.style.cssText = ` 1594 position: fixed; bottom: 80px; left: 50%; transform: translateX(-50%); 1595 background: #333; color: white; padding: 12px 24px; border-radius: 25px; 1596 font-size: 14px; z-index: 10000; animation: toastFade 2.5s ease forwards; 1597 box-shadow: 0 4px 12px rgba(0,0,0,0.3); 1598 `; 1599 document.body.appendChild(toast); 1600 setTimeout(() => toast.remove(), 2600); 1601} 1602 1603function initShare() { 1604 // Results page share button â opens share modal 1605 const resShare = document.getElementById('btn-results-share'); 1606 if (resShare) { 1607 resShare.addEventListener('click', () => { 1608 openShareModal(); 1609 }); 1610 } 1611 1612 // Close handlers 1613 document.querySelectorAll('[data-share-close]').forEach(el => { 1614 el.addEventListener('click', closeShareModal); 1615 });
1616 const closeBtn = document.getElementById('share-close-btn'); 1617 if (closeBtn) { 1618 closeBtn.addEventListener('click', closeShareModal); 1619 } 1620 1621 // Platform buttons in share modal 1622 document.querySelectorAll('[data-share-platform]').forEach(btn => { 1623 btn.addEventListener('click', () => { 1624 handleSharePlatform(btn.dataset.sharePlatform); 1625 }); 1626 }); 1627} 1628 1629function openShareModal() { 1630 const modal = document.getElementById('share-modal'); 1631 if (!modal) return; 1632 1633 // Populate preview text 1634 const preview = document.getElementById('share-preview-text'); 1635 if (preview) { 1636 preview.textContent = buildShareText(); 1637 } 1638 1639 // Set title/subtitle based on language 1640 const lang = state.language || 'en'; 1641 const title = document.getElementById('share-sheet-title'); 1642 const subtitle = document.getElementById('share-sheet-subtitle'); 1643 if (title) { 1644 title.textContent = lang === 'ko' ? 'ê²°ê³¼ ê³µì í기' : 1645 lang === 'zh' ? 'åäº«ç»æ' : 1646 lang === 'ja' ? 'çµæãã·ã§ã¢' : 1647 'Share your result'; 1648 } 1649 if (subtitle) { 1650 subtitle.textContent = lang === 'ko' ? 'SNSì ìëí´ë³´ì¸ì! ë©í¸ì í´ìíê·¸ê° ìë ì ë ¥ë©ëë¤.' : 1651 lang === 'zh' ? 'å¨ç¤¾äº¤åªä½ä¸åäº«ï¼æååæ ç¾å·²èªå¨çæã' : 1652 lang === 'ja' ? 'SNSã§ã·ã§ã¢ãããï¼ããã¹ãã¨ããã·ã¥ã¿ã°ã¯èªåå ¥åããã¾ãã' : 1653 'Share on social media! Text and hashtags are auto-generated.'; 1654 } 1655 1656 modal.classList.add('open'); 1657 modal.setAttribute('aria-hidden', 'false'); 1658} 1659 1660function closeShareModal() { 1661 const modal = document.getElementById('share-modal'); 1662 if (modal) { 1663 modal.classList.remove('open'); 1664 modal.setAttribute('aria-hidden', 'true'); 1665 } 1666} 1667 1668async function copyToClipboard(text) { 1669 try { 1670 await navigator.clipboard.writeText(text); 1671 } catch (e) { 1672 const ta = document.createElement('textarea'); 1673 ta.value = text; 1674 ta.style.cssText = 'position:fixed;opacity:0'; 1675 document.body.appendChild(ta); 1676 ta.select(); 1677 document.execCommand('copy'); 1678 ta.remove(); 1679 } 1680} 1681 1682async function handleSharePlatform(platform) { 1683 const rawText = buildShareText(); 1684 const shareUrl = 'https://k-chicken-sommelier.vercel.app'; 1685 const hashtags = buildShareHashtags(); 1686 const lang = state.language || 'en'; 1687 1688 // Build a short text for Twitter (character limit) 1689 const top = state.matches.length > 0 ? state.matches[0] : null; 1690 const tweetText = top 1691 ? (lang === 'ko' 1692 ? `ð ëì ì¹í¨ ìì¸ë©ì´í¸: ${top.name} (${top.brand}) â ë§¤ì¹ ${top.score}ì !\n30ì´ í´ì¦ë¡ ëë§ì ì¹í¨ì ì°¾ìë³´ì¸ì!` 1693 : `ð My chicken soulmate: ${getTranslatedName(top, lang)} (${top.brand}) â Match ${top.score}!\nFind yours with the 30-sec quiz!`) 1694 : (lang === 'ko' ? 'ð K-Chicken Sommelierìì ì¹í¨ ìì¸ë©ì´í¸ë¥¼ ì°¾ìë³´ì¸ì!' : 'ð Find your Korean chicken soulmate!'); 1695 1696 switch (platform) { 1697 case 'x': 1698 window.open( 1699 `https://twitter.com/intent/tweet?text=${encodeURIComponent(tweetText)}&url=${encodeURIComponent(shareUrl)}&hashtags=${hashtags}`, 1700 '_blank' 1701 ); 1702 break; 1703 case 'facebook': 1704 window.open( 1705 `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(shareUrl)}"e=${encodeURIComponent(rawText)}`, 1706 '_blank' 1707 ); 1708 break; 1709 case 'instagram': 1710 // Instagram has no web share URL â copy text to clipboard 1711 await copyToClipboard(rawText); 1712 showToast(lang === 'ko' 1713 ? 'ð í ì¤í¸ê° ë³µì¬ëììµëë¤! ì¸ì¤íê·¸ë¨ì ë¶ì¬ë£ê¸° íì¸ì.' 1714 : lang === 'zh' ? 'ð æåå·²å¤å¶ï¼è¯·ç²è´´å°Instagramã' 1715 : lang === 'ja' ? 'ð ããã¹ããã³ãã¼ãã¾ããï¼Instagramã«è²¼ãä»ãã¦ãã ããã' 1716 : 'ð Text copied! Paste it on Instagram.'); 1717 break; 1718 case 'copy': 1719 await copyToClipboard(rawText); 1720 showToast(lang === 'ko' ? 'ð ë§í¬ì í ì¤í¸ê° ë³µì¬ëììµëë¤!' : 1721 lang === 'zh' ? 'ð 龿¥åæåå·²å¤å¶ï¼' : 1722 lang === 'ja' ? 'ð ãªã³ã¯ã¨ããã¹ããã³ãã¼ããã¾ããï¼' : 1723 'ð Link and text copied!'); 1724 break; 1725 } 1726 closeShareModal(); 1727} 1728 1729function applyTranslations(lang) { 1730 const strings = UI_STRINGS[lang] || UI_STRINGS['en']; 1731 1732 // safe update helper 1733 const update = (id, text) => { 1734 const el = document.getElementById(id); 1735 if (el) el.innerHTML = text; 1736 }; 1737 1738 update('hero-headline', strings.heroHeadline); 1739 update('hero-sub', strings.heroSub); 1740 update('start-quiz-btn', strings.startBtn); 1741 update('btn-hero-secondary', strings.seeTopPicks); 1742 update('brand-directory-title', strings.brandDirectoryTitle); 1743 update('brand-directory-subtitle', strings.brandDirectorySubtitle); 1744 update('header-premium-btn', lang === 'ko' ? 'í리미ì' : (lang === 'zh' ? 'é«çº§ç' : (lang === 'ja' ? 'ãã¬ãã¢ã ' : 'Premium'))); 1745 1746 // Quiz Buttons 1747 update('quiz-next', strings.nextBtn);
1747 // Default state, updateQuizButtons will override if needed 1748 update('quiz-skip', strings.skipBtn); 1749 update('back-btn-text', strings.backBtn); 1750 1751 // Results Page Buttons 1752 update('results-title', strings.resultTitle); 1753 update('share-btn-text', strings.shareBtn); 1754 update('btn-results-unlock', strings.unlockPremium); 1755 update('btn-results-retry', strings.tryAgain); 1756 update('locked-section-title', strings.premiumUnlocks); 1757 update('nearby-title', strings.nearbyTitle); 1758 update('nearby-subtitle', strings.nearbySubtitle); 1759 update('nearby-find-btn', strings.nearbyFindBtn); 1760 1761 if (state.nearbyStores.length) { 1762 const suffix = strings.nearbyFoundSuffix || 'stores found within 10km'; 1763 const countMessage = (lang === 'ko' || lang === 'zh' || lang === 'ja') 1764 ? `${state.nearbyStores.length}${suffix}` 1765 : `${state.nearbyStores.length} ${suffix}`; 1766 setNearbyStatus(countMessage, 'success'); 1767 } 1768 1769 // Recalculate stats text in new language 1770 initStats(); 1771 renderBrandDirectory(); 1772 1773 // Re-render quiz if active 1774 if (state.currentPage === 'quiz') { 1775 renderQuizQuestion(); 1776 } 1777 1778 // Re-render results/cards if active 1779 if (state.currentPage === 'results') { 1780 renderResults(); 1781 } else { 1782 renderNearbyStoresSection(); 1783 } 1784} 1785 1786// ========== LANGUAGES ========== 1787function initLanguages() { 1788 document.querySelectorAll('.lang-btn').forEach(btn => { 1789 btn.addEventListener('click', () => { 1790 const lang = btn.dataset.lang; 1791 setLanguage(lang); 1792 }); 1793 }); 1794} 1795 1796function setLanguage(lang) { 1797 state.language = lang; 1798 1799 // Update active button 1800 document.querySelectorAll('.lang-btn').forEach(btn => { 1801 btn.classList.toggle('active', btn.dataset.lang === lang); 1802 }); 1803 1804 applyTranslations(lang); 1805 saveState(); 1806}
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.