1/* Inquiry form: question selection â first response â optional office contact. */ 2import { CATEGORIES, getQuestion, CLUB_GUIDANCE, questionText } from './inquiry-options.js?v=20260917'; 3 4const initialType = new URLSearchParams(location.search).get('type') === 'owner' ? 'owner' : 'member'; 5const fab = document.createElement('button'); 6fab.className = 'inquiry-fab'; 7fab.type = 'button'; 8fab.setAttribute('aria-haspopup', 'dialog'); 9fab.setAttribute('aria-controls', 'fanclove-inquiry'); 10fab.innerHTML = '<img src="assets/mascot_3.png" alt="">ãåãåããã¯ãã¡ã'; 11const dialog = document.createElement('dialog'); 12dialog.id = 'fanclove-inquiry'; 13dialog.className = 'inquiry-panel'; 14dialog.setAttribute('aria-labelledby', 'inquiry-title'); 15dialog.innerHTML = ` 16 <header class="inquiry-head"><div><span class="inquiry-brand">Fanclove ãµãã¼ã</span><h2 id="inquiry-title">ãåãåãã</h2></div><button type="button" class="inquiry-close" aria-label="ãåãåãããéãã">Ã</button></header> 17 <div class="inquiry-body"> 18 <ol class="inquiry-steps" aria-label="ãåãåããã®æµã"><li data-step="question">1 å 容ã®å ¥å</li><li data-step="answer">2 åçã®ç¢ºèª</li><li data-step="contact">å¿ è¦ãªå ´åã«ç¸è«</li></ol> 19 <section data-view="question"> 20 <h3 tabindex="-1">ãå°ãã®å 容ãæãã¦ãã ãã</h3> 21 <p class="inquiry-intro">è§£æ±ºæ¹æ³ããæ¡å ãã¾ãã</p> 22 <form id="inquiry-question"> 23 <fieldset class="inquiry-fields"> 24 <div class="inquiry-field"><label for="inquiry-type">ããªãã«ã¤ãã¦</label><select id="inquiry-type"><option value="member">ã¡ã³ãã¼ï¼ãã¡ã³ï¼ã®æ¹</option><option value="owner">ã¯ã©ãéå¶è ã»éè¨ãæ¤è¨ä¸ã®æ¹</option></select></div> 25 <div class="inquiry-field"><label for="inquiry-category">質åã®ç¨®é¡ <span class="inquiry-required">å¿ é </span></label><select id="inquiry-category" required><option value="">鏿ãã¦ãã ãã</option></select></div> 26 <div class="inquiry-field" id="inquiry-topic-field" hidden><label for="inquiry-topic">ãå°ãã®ã㨠<span class="inquiry-required">å¿ é </span></label><select id="inquiry-topic" required disabled><option value="">鏿ãã¦ãã ãã</option></select></div> 27 <aside class="inquiry-club-guide" id="inquiry-club-hint" hidden><strong>ã¯ã©ãéå¶è æ§ã¸ç´æ¥ãåãåãããã ãã</strong><p>ã¯ã©ãå ã®äºç´ã»ç¬èªãµã¤ãã®ä½¿ãæ¹ã»ç¹å ¸ã®è©³ç´°ã¯ãåã¯ã©ãã®éå¶è æ§ããæ¡å ãã¦ãã¾ããã¯ã©ãã®ç´¹ä»ãã¼ã¸ãæç¨¿ãªã©ã«è¨è¼ã®é£çµ¡å ãã確èªãã ããã</p><a href="https://fanclove.jp/" target="_blank" rel="noopener">Fancloveã§ã¯ã©ãã確èªãã â</a></aside> 28 <div class="inquiry-field" id="inquiry-details-field"><label for="inquiry-details">åèã»è©³ããç¶æ³ <span id="inquiry-details-required" class="inquiry-optional">ä»»æ</span></label><div id="inquiry-details-note" class="inquiry-details-notice"><strong>ããã«ã¯å人æ å ±ãå ¥åããªãã§ãã ãã</strong><span>ã¡ã¼ã«ã¢ãã¬ã¹ã»ãã¹ã¯ã¼ãã»ã«ã¼ãçªå·ãªã©ã¯æ¸ããããå°ãã®ç¶æ³ã ãããè¨å ¥ãã ããã</span></div><textarea id="inquiry-details" rows="4" maxlength="2000" placeholder="ä¾
28ï¼ãã°ã¤ã³ãããã¨ããã¨ãã¨ã©ã¼ã表示ããã¾ãã" aria-describedby="inquiry-details-note"></textarea></div> 29 <p id="inquiry-question-error" class="inquiry-error" role="alert"></p> 30 <button class="inquiry-button primary" type="submit" id="inquiry-answer-button">åçã確èªãã</button> 31 </fieldset> 32 </form> 33 </section> 34 <section data-view="answer" hidden> 35 <h3 tabindex="-1" id="inquiry-answer-title">ã»ã¨ãã©ã®ã±ã¼ã¹ã§è§£æ±ºã§ããæ¹æ³ããã¡ãã§ãï¼</h3> 36 <div class="inquiry-answer-card"><div id="inquiry-answer-text" class="inquiry-answer-text"></div></div> 37 <div id="inquiry-answer-actions"> 38 <div id="inquiry-resolution"><p class="inquiry-action-heading">ãã¡ãã®æ¡å ã§è§£æ±ºã§ãã¾ãããï¼</p><button type="button" class="inquiry-button primary" id="inquiry-resolved">解決ãã</button></div> 39 <button type="button" class="inquiry-text-button" id="inquiry-reread">åçãããä¸åº¦ç¢ºèªãã</button> 40 <div class="inquiry-contact-option"><p id="inquiry-contact-context">æ¡å ã¨ç¶æ³ãç°ãªãå ´åããæé ã試ãã¦ã解決ããªãå ´åã¯ãéå¶äºåå±ã確èªãã¾ãã</p><button type="button" class="inquiry-contact-link" id="inquiry-unresolved">éå¶äºåå±ã«åãåããã â</button></div> 41 </div> 42 <a id="inquiry-club-link" class="inquiry-button primary" href="https://fanclove.jp/" target="_blank" rel="noopener" hidden>Fancloveã§ã¯ã©ãã確èªãã â</a> 43 <button type="button" class="inquiry-text-button" id="inquiry-edit">質åã®ç¨®é¡ã»å 容ã夿´ãã</button> 44 </section> 45 <section data-view="contact" hidden> 46 <h3 tabindex="-1">Fancloveéå¶äºåå±ã«åãåããã</h3> 47 <p class="inquiry-intro">å ¥åæ¸ã¿ã®ãåãåããå 容ã¨ãããã¾ã§ã®ãæ¡å ãå¼ãç¶ãã¾ããåå ¥åã¯ä¸è¦ã§ãã</p> 48 <form id="inquiry-contact"> 49 <fieldset class="inquiry-fields"> 50 <div class="inquiry-field inquiry-handoff"> 51 <div class="inquiry-handoff-heading"><label for="inquiry-message">ãåãåããå 容 <span class="inquiry-entered">å ¥åæ¸ã¿</span></label><button type="button" id="inquiry-edit-message" class="inquiry-edit-message" aria-controls="inquiry-message" aria-pressed="false">ç·¨éãã</button></div> 52 <textarea id="inquiry-message" rows="5" required readonly maxlength="3500" aria-describedby="inquiry-message-note"></textarea> 53 <small id="inquiry-message-note">ãã®å 容ããã®ã¾ã¾éä¿¡ãã¾ããä¿®æ£ã»è¿½è¨ãããå ´åã¯ãç·¨éããããæ¼ãã¦ãã ããã</small> 54 <p class="inquiry-error" id="inquiry-message-error" role="alert"></p> 55 <details class="inquiry-handoff-answer"><summary>ããã¾ã§ã®ãæ¡å ãä¸ç·ã«éä¿¡ãã¾ãï¼å 容ã確èªï¼</summary><div id="inquiry-handoff-answer-text" class="inquiry-answer-text"></div></details> 56 </div> 57 <p class="inquiry-reply-note">è¿ä¿¡å ããå ¥åãã ãããæ å½è ãå 容ã確èªããã¡ã¼ã«ã§è¿ä¿¡ãã¾ãã</p> 58 <div class="inquiry-field"><label for="inquiry-email">Fancloveã«ç»é²ãã¦ããã¡ã¼ã«ã¢ãã¬ã¹ <span class="inquiry-required">å¿ é </span></label><input id="inquiry-email" type="email" required maxlength="200" autocomplete="email" inputmode="email" autocapitalize="none" spellcheck="false"></div> 59 <div class="inquiry-field"><label for="inquiry-nickname">Fancloveã«ç»é²ãã¦ããããã¯ãã¼ã <span class="inquiry-required">å¿ é </span></label><input id="inquiry-nickname" type="text" required maxlength="100" autocomplete="nickname"></div> 60 <small class="inquiry-registration-note">ç»é²åã®ãç¸è«ã¯ãé£çµ¡ãåãåããã¡ã¼ã«ã¢ãã¬ã¹ã¨ã叿ã®ããã¯ãã¼ã ããå ¥åãã ããã</small> 61 <div class="inquiry-field"><label for="inquiry-club">ã¯ã©ãå <span class="inquiry-optional">ä»»æ</span></label><input id="inquiry-club" type="text" maxlength="100"></div> 62 <p class="inquiry-platform-note">ã¯ã©ãç¬èªã®äºç´ã»ãµã¤ãã»ç¹å ¸ã«ã¤ãã¦ã¯ãã¯ã©ãéå¶è æ§ã¸ç´æ¥ãåãåãããã ããã</p> 63 <label class="inquiry-check"><input id="inquiry-not-sales" type="checkbox" required><span>å¶æ¥ã»åç¨®ãæ¡å ã»å£²ãè¾¼ã¿ãç®çã¨ããåãåããã§ã¯ããã¾ããã<span class="inquiry-required">å¿ é </span></span></label> 64 <p class="inquiry-privacy"><a href="https://fanclove.jp/static/privacy" target="_blank" rel="noopener">ãã©ã¤ãã·ã¼ããªã·ã¼</a>ãã確èªã®ãããéä¿¡ãã¦ãã ããã</p>
65 <p id="inquiry-contact-error" class="inquiry-error" role="alert"></p> 66 <button class="inquiry-button primary" type="submit" id="inquiry-send">éå¶äºåå±ã«éä¿¡ãã</button> 67 </fieldset> 68 </form> 69 <button type="button" class="inquiry-text-button" id="inquiry-back-answer">åçã«æ»ã</button> 70 </section> 71 <section data-view="done" class="inquiry-done" hidden><span class="inquiry-done-mark" aria-hidden="true">â</span><h3 tabindex="-1" id="inquiry-done-title"></h3><p id="inquiry-done-text"></p><button type="button" class="inquiry-button primary" id="inquiry-finish">éãã</button><button type="button" class="inquiry-text-button" id="inquiry-new">å¥ã®å 容ãåãåããã</button></section> 72 <p id="inquiry-status" class="inquiry-status" role="status" aria-live="polite"></p> 73 </div>`; 74document.body.append(fab, dialog); 75const $ = id => dialog.querySelector('#inquiry-' + id); 76const state = { stage: 'question', busy: false, lastQuestion: '', answer: '', topic: '', type: initialType, clubSpecific: false, feedback: null, transcript: [] }; 77let returnFocus = fab; 78$('type').value = initialType; 79 80function categories() { 81 $('category').replaceChildren(new Option('鏿ãã¦ãã ãã', '')); 82 CATEGORIES.filter(c => !c.ownerOnly || $('type').value === 'owner').forEach(c => $('category').add(new Option(c.label, c.id))); 83 questions(); 84} 85function questions() { 86 const category = CATEGORIES.find(c => c.id === $('category').value); 87 $('topic').replaceChildren(new Option('鏿ãã¦ãã ãã', '')); 88 for (const [id, label] of category?.questions || []) $('topic').add(new Option(label, id)); 89 $('topic').disabled = !category; 90 $('topic-field').hidden = !category; 91 if (category?.questions.length === 1) $('topic').value = category.questions[0][0]; 92 const club = !!category?.clubSpecific; 93 $('club-hint').hidden = !club; 94 $('details-field').hidden = club; 95 $('details').required = category?.id === 'other'; 96 $('details-required').textContent = category?.id === 'other' ? 'å¿ é ' : 'ä»»æ'; 97 $('details-required').className = category?.id === 'other' ? 'inquiry-required' : 'inquiry-optional'; 98 $('answer-button').textContent = club ? 'åãåããå ã確èªãã' : 'åçã確èªãã'; 99 $('question-error').textContent = ''; 100} 101categories(); 102$('type').addEventListener('change', categories); 103$('category').addEventListener('change', questions); 104 105function show(stage) { 106 state.stage = stage; 107 dialog.querySelectorAll('[data-view]').forEach(view => { view.hidden = view.dataset.view !== stage; }); 108 dialog.querySelectorAll('[data-step]').forEach(step => { 109 if (step.dataset.step === stage) step.setAttribute('aria-current', 'step'); 110 else step.removeAttribute('aria-current'); 111 }); 112 dialog.querySelector('.inquiry-body').scrollTop = 0; 113 if (dialog.open) dialog.querySelector(`[data-view="${stage}"] h3`).focus(); 114} 115function open(options = {}) { 116 if (options.question && !state.busy && state.stage === 'question') { 117 $('type').value = options.type === 'owner' ? 'owner' : 'member'; 118 categories(); $('category').value = 'other'; questions(); 119 $('details').value = options.question.slice(0, 2000); 120 } 121 if (!dialog.open) { 122 returnFocus = document.activeElement; 123 dialog.showModal(); 124 document.body.classList.add('inquiry-is-open'); 125 } 126 show(state.stage); 127} 128function close() { dialog.close(); } 129dialog.addEventListener('close', () => { 130 document.body.classList.remove('inquiry-is-open'); 131 (returnFocus?.isConnected ? returnFocus : fab).focus(); 132}); 133fab.addEventListener('click', () => open()); 134dialog.querySelector('.inquiry-close').addEventListener('click', close); 135window.FancloveInquiry = { open }; 136 137function renderText(element, text) { 138 // Build text nodes and validated links so neither user nor AI content becomes HTML. 139 element.replaceChildren(); 140 for (const part of String(text).split(/(https?:\/\/[^\s<>"'ãï¼)]+)/g)) { 141 if (/^https?:\/\//.test(part)) { 142 const a = document.createElement('a'); 143 a.href = part; a.textContent = part; a.target = '_blank'; a.rel = 'noopener noreferrer'; element.append(a); 144 } else element.append(document.createTextNode(part)); 145 } 146} 147function busy(value, status = '') { 148 state.busy = value; 149 dialog.querySelectorAll('.inquiry-fields').forEach(f => { f.disabled = value; }); 150 $('back-answer').disabled = value; 151 $('status').textContent = status; 152} 153async function post(url, body) { 154 const controller = new AbortController(); 155 const timeout = setTimeout(() => controller.abort(), 45000); 156 try { 157 const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), signal: controller.signal }); 158 const result = await response.json(); 159 if (!response.ok || result.ok === false) throw new Error(result.error || 'å¦çã«å¤±æãã¾ãããæéãããã¦ã試ããã ããã'); 160 return result; 161 } finally { clearTimeout(timeout); } 162} 163function setMessageEditing(editing) { 164 $('message').readOnly = !editing; 165 $('edit-message').textContent = editing ? 'ç·¨éãå®äº' : 'ç·¨éãã'; 166 $('edit-message').setAttribute('aria-pressed', String(editing)); 167 $('message-note').textContent = editing 168 ? '解決ããªãã£ãç¹ãªã©ã追è¨ã§ãã¾ãããã¹ã¯ã¼ãã»ã«ã¼ãçªå·ã¯å ¥åããªãã§ãã ããã' 169 : 'ãã®å 容ããã®ã¾ã¾éä¿¡ãã¾ããä¿®æ£ã»è¿½è¨ãããå ´åã¯ãç·¨éããããæ¼ãã¦ãã ããã'; 170 $('message-error').textContent = ''; 171} 172$('edit-message').addEventListener('click', () => { 173 if (state.busy) return; 174 const editing = $('message').readOnly; 175 if (!editing && !$('message').value.trim()) { 176 $('message-error').textContent = 'ãåãåããå 容ããå ¥åãã ããã'; 177 $('message').focus(); return; 178 } 179 setMessageEditing(editing); 180 if (editing) $('message').focus(); 181}); 182 183function renderAnswer(result) { 184 state.answer = result.reply; 185 state.clubSpecific = result.route === 'club'; 186 state.transcript = [{ role: 'user', text: state.lastQuestion }, { role: result.ai ? 'ai' : 'guide', text: result.reply }]; 187 $('answer-title').textContent = state.clubSpecific ? 'ã¯ã©ãéå¶è æ§ã¸ãåãåãããã ãã' : result.unavailable ? 'åçã確èªã§ãã¾ããã§ãã' : 'ã»ã¨ãã©ã®ã±ã¼ã¹ã§è§£æ±ºã§ããæ¹æ³ããã¡ãã§ãï¼'; 188 renderText($('answer-text'), result.reply); 189 $('answer-actions').hidden = state.clubSpecific; 190 $('resolution').hidden = !!result.unavailable; 191 $('reread').hidden = !!result.unavailable; 192 $('contact-context').textContent = result.unavailable 193 ? 'åçãåå¾ã§ããªãå ´åã¯ãéå¶äºåå±ã¸ãåãåããããã ãã¾ãã' 194 : result.needsSupport 195 ? 'è«æ±å 容ãã¢ã«ã¦ã³ãã®åå¥ç¢ºèªãå¿ è¦ãªå ´åã¯ãéå¶äºåå±ã¸ãåãåãããã ããã' 196 : 'æ¡å ã¨ç¶æ³ãç°ãªãå ´åããæé ã試ãã¦ã解決ããªãå ´åã¯ãéå¶äºåå±ã確èªãã¾ãã'; 197 $('unresolved').className = result.unavailable ? 'inquiry-button secondary' : 'inquiry-contact-link'; 198 $('club-link').hidden = !state.clubSpecific; 199 $('message').value = state.lastQuestion; 200 setMessageEditing(false); 201 renderText($('handoff-answer-text'), result.reply); 202 dialog.querySelector('.inquiry-handoff-answer').open = false;
203 show('answer'); 204} 205$('question').addEventListener('submit', async event => { 206 event.preventDefault(); 207 if (state.busy || !$('question').reportValidity()) return; 208 const topic = getQuestion($('topic').value); 209 if (!topic) return; 210 const details = $('details').value.trim(); 211 if (topic.id === 'other' && !details) { $('question-error').textContent = 'åèæ¬ã«ããå°ãã®ãã¨ããè¨å ¥ãã ããã'; $('details').focus(); return; } 212 state.type = $('type').value; state.topic = topic.id; state.feedback = null; 213 state.lastQuestion = questionText(topic.id, topic.clubSpecific ? '' : details); 214 if (topic.clubSpecific) { renderAnswer({ reply: CLUB_GUIDANCE, route: 'club', ai: false }); return; } 215 busy(true, 'å 容ã«åãåçã確èªãã¦ãã¾ãâ¦'); 216 $('answer-button').textContent = 'åçã確èªãã¦ãã¾ãâ¦'; 217 try { 218 const result = await post('/api/chat', { type: state.type, topic: state.topic, details, message: state.lastQuestion }); 219 if (!result.reply) throw new Error('åçãåå¾ã§ãã¾ããã§ããã'); 220 renderAnswer(result); 221 } catch (error) { 222 renderAnswer({ reply: 'åçãåå¾ã§ãã¾ããã§ãããæéãããã¦ããä¸åº¦ã試ãããã ãããä¸ã®ãã¿ã³ããFancloveéå¶äºåå±ã¸ãåãåãããã ããã', ai: false, unavailable: true }); 223 } finally { busy(false); $('answer-button').textContent = 'åçã確èªãã'; } 224}); 225function feedback(resolved) { 226 if (state.feedback === resolved) return; 227 state.feedback = resolved; 228 post('/api/feedback', { type: state.type, q: state.lastQuestion, resolved }).catch(() => {}); 229} 230$('edit').addEventListener('click', () => show('question')); 231$('reread').addEventListener('click', () => { show('answer'); $('answer-title').focus(); }); 232$('back-answer').addEventListener('click', () => { if (!state.busy) show('answer'); }); 233$('unresolved').addEventListener('click', () => { if (!state.clubSpecific) { feedback(false); show('contact'); } }); 234$('resolved').addEventListener('click', () => { 235 feedback(true); 236 $('done-title').textContent = 'ã確èªãããã¨ããããã¾ãã'; 237 $('done-text').textContent = 'ã»ãã«ãå°ãã®ãã¨ãããã°ããã¤ã§ããå©ç¨ãã ããã'; 238 show('done'); 239}); 240$('contact').addEventListener('submit', async event => { 241 event.preventDefault(); 242 if (state.busy || state.clubSpecific || !$('contact').reportValidity()) return; 243 const name = $('nickname').value.trim(), email = $('email').value.trim(), message = $('message').value.trim(); 244 if (!name || !email || !message) { $('contact-error').textContent = 'ã¡ã¼ã«ã¢ãã¬ã¹ã»ããã¯ãã¼ã ã»ãåãåããå 容ããå ¥åãã ããã'; return; } 245 $('contact-error').textContent = ''; 246 busy(true, 'ãåãåãããéä¿¡ãã¦ãã¾ãâ¦'); $('send').textContent = 'éä¿¡ãã¦ãã¾ãâ¦'; 247 try { 248 const result = await post('/api/inquiries', { type: state.type, name, email, club: $('club').value.trim(), message, notSales: $('not-sales').checked, topic: state.topic, reason: 'ãæ¡å ã確èªå¾ã®ãåãåããï¼ãã©ã¼ã ï¼', conversation: state.transcript }); 249 if (result.filtered) { 250 $('contact-error').textContent = 'æãå ¥ãã¾ããã侮辱çã»æ»æçãªè¡¨ç¾ãå«ããåãåããã«ã¯å¯¾å¿ãããããã¾ããå 容ãè¦ç´ãã¦ãããä¸åº¦ãéããã ããã'; 251 return; 252 } 253 $('done-title').textContent = 'ãåãåãããåãä»ãã¾ãã'; 254 $('done-text').textContent = 'æ å½è ãå 容ã確èªãããå ¥åã®ã¡ã¼ã«ã¢ãã¬ã¹ã¸è¿ä¿¡ãã¾ãã'; 255 // Retain only confirmation after success; do not keep account details in the form. 256 $('contact').reset(); $('details').value = ''; state.transcript = []; state.lastQuestion = ''; state.answer = ''; 257 show('done'); 258 } catch (error) { 259 $('contact-error').textContent = error.name === 'AbortError' ? 'éä¿¡çµæã確èªã§ãã¾ããã§ãããéä¿¡ç¶æ³ãã確èªãã ãããç¹°ãè¿ãéä¿¡ããåã«ãè¿ä¿¡ãå±ãã¦ããªããã確èªãã ããã' : error.message || 'éä¿¡ã§ãã¾ããã§ãããéä¿¡ç¶æ³ãã確èªãã ããã'; 260 } finally { busy(false); $('send').textContent = 'éå¶äºåå±ã«éä¿¡ãã'; } 261}); 262function reset() { 263 $('question').reset(); $('contact').reset(); $('type').value = initialType; 264 categories(); state.lastQuestion = ''; state.answer = ''; state.transcript = []; state.topic = ''; state.clubSpecific = false;
265 $('contact-error').textContent = ''; setMessageEditing(false); 266 $('handoff-answer-text').replaceChildren(); 267 dialog.querySelector('.inquiry-handoff-answer').open = false; 268 show('question'); 269} 270$('finish').addEventListener('click', () => { close(); reset(); }); 271$('new').addEventListener('click', reset); 272show('question');
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.