PageSourceSearch

https://fanclove-support.pages.dev/chat.js?v=20260918-4

js fanclove-support.pages.dev collected 2026-10-03 05:09:51 UTC 20,496 bytes, 272 lines download raw bytes

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.