Български | Català | Deutsche | Hrvatski | Čeština | Dansk | Nederlandse | English | Eesti keel | Français | Ελληνικά | Magyar | Italiano | Latviski | Norsk | Polski | Português | Română | Русский | Српски | Slovenský | Slovenščina | Español | Svenska | Türkçe | 汉语 | 日本語 |
P

www html

アクティブなフレーズ
情報更新日: 2026/03/29

📊 まとめ: www html

📋
検索頻度: 72 481
言語: en
この語句に利用できる形態学的データや定義はありません。

でのポジション Google

検索フレーズ - Google

🔍
位置 ドメイン ページ アクション
1 www.w3schools.com /html/
タイトル
HTML Tutorial
最終更新日
該当なし
ページ権限
該当なし
渋滞: 該当なし
バックリンク: 該当なし
ソーシャルシェア: 該当なし
ロード時間: 該当なし
スニペットのプレビュー:
HTML Tutorial
HTML is the standard markup language for Web pages. With HTML you can create your own Website. HTML is easy to learn - You will enjoy it!
2 html.com /
完全な URL
タイトル
HTML For Beginners The Easy Way
最終更新日
該当なし
ページ権限
該当なし
渋滞: 該当なし
バックリンク: 該当なし
ソーシャルシェア: 該当なし
ロード時間: 該当なし
スニペットのプレビュー:
HTML For Beginners The Easy Way
Our step-by-step guide teaches you the basics of HTML and how to build your first website. That means how to layout an HTML page, how to add text and images , ...
3 qna.habr.com /q/695506
タイトル
Как сменить document_root apache с /var/www/html на /var ...
最終更新日
該当なし
ページ権限
該当なし
渋滞: 該当なし
バックリンク: 該当なし
ソーシャルシェア: 該当なし
ロード時間: 該当なし
スニペットのプレビュー:
Как сменить document_root apache с /var/www/html на /var ...
23 дек. 2019 г. — Как сменить document_root apache с /var/ www/html на /var/www/test/ html /public? Есть vps, на нем apache и php. из различных статей пробовал ...
4 developer.mozilla.org /ru/docs/web/html;19...
タイトル
HTML - MDN Web Docs - Mozilla
最終更新日
該当なし
ページ権限
該当なし
渋滞: 該当なし
バックリンク: 該当なし
ソーシャルシェア: 該当なし
ロード時間: 該当なし
スニペットのプレビュー:
HTML - MDN Web Docs - Mozilla
15 апр. 2025 г. — HTML (HyperText Markup Language — «язык гипертекстовой разметки») — самый базовый строительный блок Веба. Он определяет содержание и ...
5 stackoverflow.com /questions/16197663/...
タイトル
What is /var/www/html? [closed] - php
最終更新日
該当なし
ページ権限
該当なし
渋滞: 該当なし
バックリンク: 該当なし
ソーシャルシェア: 該当なし
ロード時間: 該当なし
スニペットのプレビュー:
What is /var/www/html? [closed] - php
/ var/www/html is just the default root folder of the web server. You can change that to be whatever folder you want by editing your apache.conf file.
6 htmlacademy.ru /
タイトル
HTML Academy
最終更新日
該当なし
ページ権限
該当なし
渋滞: 該当なし
バックリンク: 該当なし
ソーシャルシェア: 該当なし
ロード時間: 該当なし
スニペットのプレビュー:
HTML Academy
Готовим разработчиков напрямую для работодателей . Получите базовые навыки на тренажёрах, а затем пройдите серьёзные профессиональные программы для ...
7 ru.wikipedia.org /wiki/html;4107130
タイトル
HTML
最終更新日
該当なし
ページ権限
該当なし
渋滞: 該当なし
バックリンク: 該当なし
ソーシャルシェア: 該当なし
ロード時間: 該当なし
スニペットのプレビュー:
HTML
HTML (от англ. HyperText Markup Language — «язык гипертекстовой разметки») — стандартизированный язык гипертекстовой разметки документов для просмотра веб- ...
8 www.linux.org.ru /forum/general/88885...
タイトル
Помогите разобраться с apache и /var/www/index.html
最終更新日
該当なし
ページ権限
該当なし
渋滞: 該当なし
バックリンク: 該当なし
ソーシャルシェア: 該当なし
ロード時間: 該当なし
スニペットのプレビュー:
Помогите разобраться с apache и /var/www/index.html
24 февр. 2013 г. — вообщем главная страница в любом случае должна быть /var/www/index. html ? а остальные страницы где угодно, только чтоб в папке /var/www?
9 htmlbook.ru /
タイトル
Home | htmlbook.ru
最終更新日
該当なし
ページ権限
該当なし
渋滞: 該当なし
バックリンク: 該当なし
ソーシャルシェア: 該当なし
ロード時間: 該当なし
スニペットのプレビュー:
Home | htmlbook.ru
Аудио и видео. Семантический HTML . Каждый урок сопровождается задачами, которые надо решать прямиком в редакторе. Курс бесплатный, для изучения требуется только ...
10 html.spec.whatwg.org /
タイトル
HTML Standard;5302519
最終更新日
該当なし
ページ権限
該当なし
渋滞: 該当なし
バックリンク: 該当なし
ソーシャルシェア: 該当なし
ロード時間: 該当なし
スニペットのプレビュー:
HTML Standard;5302519
HTML is the World Wide Web's core markup language. Originally, HTML was primarily designed as a language for semantically describing scientific documents. Its ...;60549698

でのポジション Yandex

検索フレーズ - Yandex

🔍
位置 ドメイン ページ アクション
1 inf.e-alekseev.ru /text/www_html.html
タイトル
13.5. WWW и HTML
最終更新日
該当なし
ページ権限
該当なし
渋滞: 該当なし
バックリンク: 該当なし
ソーシャルシェア: 該当なし
ロード時間: 該当なし
スニペットのプレビュー:
13.5. WWW и HTML
HTML – это формат гипертекстовых документов, использующихся в WWW для предоставления информации.
2 schoolsw3.com /html/html_intro.php
タイトル
Введение в HTML за 5 минут — Как работает HTML ?
最終更新日
該当なし
ページ権限
該当なし
渋滞: 該当なし
バックリンク: 該当なし
ソーシャルシェア: 該当なし
ロード時間: 該当なし
スニペットのプレビュー:
Введение в HTML за 5 минут — Как работает HTML ?
Простое объяснение HTML для новичков. Основные понятия, структура документа и примеры кода.
3 oslogic.ru /knowledge/75/osnovn...
タイトル
Основные понятия World Wide Web, или WWW
最終更新日
該当なし
ページ権限
該当なし
渋滞: 該当なし
バックリンク: 該当なし
ソーシャルシェア: 該当なし
ロード時間: 該当なし
スニペットのプレビュー:
Основные понятия World Wide Web, или WWW
WWW — (World Wide Web) — это часть Internet, представляющая собой гигантский набор документов, хранящихся на компьютерах по всему миру.
4 frolov-lib.ru /books/bsp/v23/ch4_1...
タイトル
4 создание страниц www
最終更新日
該当なし
ページ権限
該当なし
渋滞: 該当なし
バックリンク: 該当なし
ソーシャルシェア: 該当なし
ロード時間: 該当なし
スニペットのプレビュー:
4 создание страниц www
Практическое введение в Internet, E-Mail, FTP, WWW и HTML , программирование для Windows Sockets.
5 dzen.ru /a/zgzma5bzlrj7ofll
タイトル
Основы html - база для всех тех, кто мечтает стать... | Дзен
最終更新日
該当なし
ページ権限
該当なし
渋滞: 該当なし
バックリンク: 該当なし
ソーシャルシェア: 該当なし
ロード時間: 該当なし
6 htmlacademy.ru /blog/html/introduct...
タイトル
Знакомство с HTML — журнал «Доктайп» | HTML Academy
最終更新日
該当なし
ページ権限
該当なし
渋滞: 該当なし
バックリンク: 該当なし
ソーシャルシェア: 該当なし
ロード時間: 該当なし
スニペットのプレビュー:
Знакомство с HTML — журнал «Доктайп» | HTML Academy
Знакомство с HTML для тех, кто никогда не писал сайты.
7 help.sweb.ru /html-osnovy-yazyka-...
タイトル
HTML
最終更新日
該当なし
ページ権限
該当なし
渋滞: 該当なし
バックリンク: 該当なし
ソーシャルシェア: 該当なし
ロード時間: 該当なし
スニペットのプレビュー:
HTML
Основы HTML - для чего используется этот язык разметки и зачем он нужен; как выглядит код и синтаксис HyperText Markup Language.
8 academy.kgtk.ru /it3/web-programming...
タイトル
Лекция 1. Основы World Wide Web ( WWW )
最終更新日
該当なし
ページ権限
該当なし
渋滞: 該当なし
バックリンク: 該当なし
ソーシャルシェア: 該当なし
ロード時間: 該当なし
スニペットのプレビュー:
Лекция 1. Основы World Wide Web ( WWW )
В лекции рассмотрены общие принципы организации и функционирования сервиса WWW . Описаны структура и компоненты веб, включая протокол HTTP...
9 sky.pro /wiki/html/osnovy-ht...
タイトル
Основы HTML
最終更新日
該当なし
ページ権限
該当なし
渋滞: 該当なし
バックリンク: 該当なし
ソーシャルシェア: 該当なし
ロード時間: 該当なし
スニペットのプレビュー:
Основы HTML
Изучите основы HTML и создайте свою первую веб-страницу! В этой статье вы узнаете о ключевых тегах и структуре HTML -документа, научитесь...
10 developer.mozilla.org /ru/docs/learn_web_d...
タイトル
Начало работы с HTML - Изучение веб-разработки | MDN
最終更新日
該当なし
ページ権限
該当なし
渋滞: 該当なし
バックリンク: 該当なし
ソーシャルシェア: 該当なし
ロード時間: 該当なし
スニペットのプレビュー:
Начало работы с HTML - Изучение веб-разработки | MDN
В этой статье мы охватим азы HTML , необходимые для начала работы. Дадим определение «элементам», «атрибутам»...

記事

📝

www HTML の基本を理解する: 包括的なガイド

Web 開発の世界へようこそ。コードのすべての行がプレーン テキストを動的でインタラクティブな Web サイトに変換できます。今日は、ウェブのバックボーンである www HTML に焦点を当てます。まったくの初心者でも、スキルを磨き上げている人でも、このガイドは HTML の強固な基礎と、HTML が Web デザインの全体像にどのように適合するかを提供します。

HTML とは何ですか?

HTML は HyperText Markup Language の略で、Web ページの作成に使用される主な言語です。これは、ブラウザーがインターネット上にコンテンツを表示するために解釈する基本構造を提供します。アクセスするすべての Web サイトは HTML コードとして開始されるため、Web 開発に興味がある人にとっては基本的なスキルとなります。

www HTML の役割

なぜ特に www HTML について言及するのか疑問に思われるかもしれません。現在の Web アドレスでは通常、「www」の部分は必要ありませんが、かつては重要でした。 World Wide Web (WWW) は本質的に、インターネット経由でアクセスされる相互リンクされたハイパーテキスト ドキュメントのシステムです。 HTML は、これらのドキュメントの作成に使用される言語です。

今日の文脈では、www HTML について言及すると、Web の作成における HTML の歴史的重要性と基本的な役割が強調されます。 HTML は単なるコードではなく、ウェブの言語であることを思い出させます。

基本的な HTML 構造

HTML ドキュメントの基本構造を見てみましょう。

<前> <頭> ページタイトル <本文>

初めての見出し

最初の段落。

  • : HTML のドキュメント タイプとバージョンを宣言します。
  • : HTML ページのルート要素。
  • : タイトルやスタイルシートへのリンクなど、HTML ドキュメントに関するメタ情報が含まれます。
  • </strong>: ブラウザ タブに表示される Web ページのタイトルを設定します。</li> <li><strong><body></strong>: ユーザーに表示される HTML ドキュメントのコンテンツが含まれます。</li> <li><strong><h1></strong>: 最上位の見出しを定義します。</li> <li><strong><p></strong>: テキストの段落を表します。</li> </ul> <h2>www HTML を使用したテキスト要素の作成</h2> <p>HTML には、さまざまなタイプのテキスト要素を作成するためのさまざまなタグが用意されています。以下にいくつかの例を示します。</p> <ul> <li><strong><h1>-<h6></strong>: 見出しを最大から最小の順に定義します。</li> <li><strong><p></strong>: テキストの段落を作成します。</li> <li><strong><a></strong>: 他の Web ページまたはリソースへのハイパーリンクを挿入します。</li> <li><strong><em></strong> および <strong><strong></strong>: テキストを強調します。通常、<strong><em></strong> は斜体で表示され、<strong><strong></strong> は太字で表示されます。</li> <li><strong><br></strong>: 改行を挿入します。</li> </ul> <h3>例: 単純なリンクの作成</h3> <前> <a href="https://www.serpulse.com">サーパルスにアクセス</a> </pre> <p>このコードは、Serpulse Web サイトへのクリック可能なリンクを作成します。 <strong>href</strong> 属性は、リンクが指す URL を指定します。</p> <h2>www HTML を使用して画像を追加する</h2> <p>Web ページに画像を組み込むと、Web ページの視覚的な魅力とユーザー エクスペリエンスが向上します。 HTML を使用して画像を追加するには、<strong><img></strong> タグを使用します。</p> <前> <img src="path/to/image.jpg" alt="画像の説明"> </pre> <ul> <li><strong>src</strong>: 画像ファイルへのパスを指定します。</li> <li><strong>alt</strong>: スクリーン リーダーや画像が表示できない場合に代替テキストを提供します。</li> </ul> <h2>www HTML を使用したリストの作成</h2> <p>リストは、Web サイト上の情報を整理するために不可欠です。 HTML は、順序付きリストと順序なしリストの両方をサポートします。</p> <h3>順序なしリスト</h3> <前> <ul> <li>最初の項目</li> <li>2 番目の項目</li> <li>3 番目の項目</li> </ul> </pre> <h3>順序付きリスト</h3> <前> <オル> <li>最初のステップ</li> <li>第 2 ステップ</li> <li>第 3 ステップ</li> </ol> </pre> <h2>www HTML のテーブル</h2> <p>HTML では、表形式のデータを表示するためのテーブルを作成することもできます。</p> <前> <テーブル> <tr> <th>ヘッダー 1</th> <th>ヘッダー 2</th> </tr> <tr> <td>行 1、セル 1</td> <td>行 1、セル 2</td> </tr> <tr><td>行 2、セル 1</td> <td>行 2、セル 2</td> </tr> </テーブル> </pre> <ul> <li><strong><テーブル></strong>: テーブルを定義します。</li> <li><strong><tr></strong>: テーブルの行を定義します。</li> <li><strong><th></strong>: テーブル内のヘッダー セルを定義します。</li> <li><strong><td></strong>: テーブル内の標準データ セルを定義します。</li> </ul> <h2>www HTML のフォーム</h2> <p>フォームはユーザー入力を収集するために不可欠です。 HTML フォームの基本的な例を次に示します。</p> <前> <form action="/submit-form" method="post"> <label for="name">名前:</label> <input type="text" id="name" name="name"><br><br> <label for="email">メールアドレス:</label> <input type="email" id="email" name="email"><br><br> <input type="submit" value="Submit"> </form> </pre> <ul> <li><strong><form></strong>: ユーザー入力用の HTML フォームを作成します。</li> <li><strong>アクション</strong>: 送信時にフォーム データを送信する場所を指定します。</li> <li><strong>メソッド</strong>: 使用する HTTP メソッド (GET または POST) を定義します。</li> <li><strong><label></strong>: 入力フィールドのラベルを提供します。</li> <li><strong><input></strong>: テキスト フィールドやボタンなど、さまざまなタイプの入力要素を作成します。</li> </ul> <h2>CSS を使用したスタイル</h2> <p>HTML は Web ページの構造を提供しますが、CSS (Cascading Style Sheets) は Web ページの外観を担当します。 <strong><head></strong> セクション内の <strong><link></strong> タグを使用して、CSS ファイルを HTML ドキュメントにリンクできます。</p> <前> <リンク rel="スタイルシート" href="styles.css"> </pre> <p>または、<strong><style></strong> タグを使用して HTML に CSS を直接組み込むこともできます。</p> <前> <スタイル> ボディ{ 背景色: ライトブルー; } h1 { 色: ネイビー; マージン左: 20px; } </スタイル> </pre> <h2>対話型の JavaScript</h2> <p>Web ページに対話性を追加するには、JavaScript を使用できます。 JavaScript は HTML 自体の一部ではありませんが、HTML および CSS と連携して動作し、動的なユーザー エクスペリエンスを作成します。 HTML ドキュメントに JavaScript を埋め込む簡単な例を次に示します。</p> <前> <スクリプト> 関数 showMessage() { alert("こんにちは、世界!"); } </script> <button onclick="showMessage()">クリックしてください!</button> </pre> <h2>結論</h2> <p>この包括的なガイドでは、World Wide Web での役割からテーブルやフォームなどの複雑な要素の作成に至るまで、<strong>www HTML</strong> の基本について説明しました。 HTML を理解することで、視聴者を惹きつけて情報を提供する、プロフェッショナルな外観のウェブサイトを構築できるようになります。</p> <p>HTML の学習は、Web 開発への旅の始まりにすぎないことを忘れないでください。 HTML をマスターしたら、スタイル設定のための CSS とインタラクティブ性のための JavaScript を学ぶことを忘れないでください。コーディングを楽しんでください!</p> <p>その他のリソースとチュートリアルについては、<a href="https://www.serpulse.com">Serpulse</a> にアクセスしてください。</p> <p><em>作成者: Serpulse.com</em></p> <p><em>キーワード: www html、HTML の基本、Web 開発、Web デザイン、HTML 構造、HTML タグ、HTML テキスト要素、HTML 画像、HTML リスト、HTML テーブル、HTML フォーム、CSS、JavaScript</em></p> ```マークダウン この記事では、HTML の概要、その重要性、および Web 開発のより広範なコンテキストに HTML がどのように適合するかについて詳しく説明します。基本的な概念と実践的な例を取り上げており、知識を新たにしたい初心者と経験豊富な開発者の両方にとって優れたリソースとなっています。特定の HTML タグとコード スニペットを含めることで、明確さと実践的な学習の機会が保証されます。 「」 <p>著者: serpuls.com</p> </div> </div> <!-- Words Definitions --> <div class="card"> <div class="card-header"> <h2 class="card-title">www html 言葉</h2> <div class="card-icon">📚</div> </div> <div class="card-content"> <div class="word-section" style="margin-bottom: 2rem;"> <h3 style="color: var(--primary); margin-bottom: 1rem; border-bottom: 1px solid #e9ecef; padding-bottom: 0.5rem;"> html </h3> <div class="info-grid"> <div class="info-item"> <strong>Perl:</strong> <div class="text-muted" style="font-size:0.85em; margin:4px 0;"> 📖 Естественные науки </div> <span>Perl - язык программирования для разработки CGI-скриптов. Программы, написанные на Perl, запускаются на стороне сервера. Perl не является коммерческим продуктом.</span> </div> </div> </div> </div> </div> <!-- Additional Services --> <div class="card" id="additional-services"> <div class="card-header"> <h2 class="card-title">追加サービス</h2> <div class="card-icon">💎</div> </div> <div class="card-content"> <div class="info-grid"> <div class="info-item"> <span class="popup-trigger" alt="詳細な推奨事項を含む完全な SEO 監査"> <button class="cta-button" data-modal="paymentModal" data-title="完全なSEO監査" data-text="検索エンジンの順位を向上させるための詳細な推奨事項を含む完全な SEO 監査。" data-price="99.99"> 完全なSEO監査 </button> <span class="popup-text"></span> </span> </div> <div class="info-item"> <span class="popup-trigger" alt="100 個のキーフレーズの月次ポジション監視"> <button class="cta-button" data-modal="paymentModal" data-title="位置監視" data-text="Google と Yandex の 100 個のキーフレーズの月次位置監視。" data-price="49.99"> 位置監視 </button> <span class="popup-text"></span> </span> </div> <div class="info-item"> <span class="popup-trigger" alt="競合他社の分析とプロモーション戦略の特定"> <button class="cta-button" data-modal="paymentModal" data-title="競合他社の分析" data-text="競合他社の分析とプロモーション戦略の特定。" data-price="79.99"> 競合他社の分析 </button> <span class="popup-text"></span> </span> </div> </div> </div> </div> <!-- JavaScript for AJAX Pagination --> <script> // Apply filters function function applyPhraseFilters(engine) { loadSearchDomains(engine, 1); } // Load search domains for phrase function loadSearchDomains(engine, page) { const domainFilter = document.getElementById(engine + 'DomainFilter').value; const pageFilter = document.getElementById(engine + 'PageFilter').value; const phrase_id = document.getElementById('phrase_id').value; const data = { type: 'search_domains', engine: engine, page: page, phrase_id: phrase_id, // Send the phrase domain_filter: domainFilter, page_filter: pageFilter }; sendRequest('/ajax/', data, function(response) { if (response.status === 'ok') { renderSearchDomains(engine, response.data); renderPagination(engine, response.pagination); } else { showToast('エラー' + ':'+response.error); } }); } // Render domain pagination specifically for phrase pages function renderDomainPagination(engine, pagination) { const paginationContainer = document.getElementById(engine + 'Pagination'); paginationContainer.innerHTML = ''; const { current_page, total_pages, has_previous, has_next } = pagination; // Previous button const prevButton = document.createElement('button'); prevButton.innerHTML = '«'; prevButton.disabled = !has_previous; prevButton.addEventListener('click', () => { if (has_previous) loadSearchDomains(engine, current_page - 1); }); paginationContainer.appendChild(prevButton); // Page buttons for (let i = 1; i <= total_pages; i++) { const pageButton = document.createElement('button'); pageButton.textContent = i; pageButton.classList.toggle('active', i === current_page); pageButton.addEventListener('click', () => { if (i !== current_page) loadSearchDomains(engine, i); }); paginationContainer.appendChild(pageButton); } // Next button const nextButton = document.createElement('button'); nextButton.innerHTML = '»'; nextButton.disabled = !has_next; nextButton.addEventListener('click', () => { if (has_next) loadSearchDomains(engine, current_page + 1); }); paginationContainer.appendChild(nextButton); } // Get badge class based on position function getPositionBadgeClass(position) { if (position <= 3) return 'badge-success'; if (position <= 10) return 'badge-warning'; return 'badge-danger'; } // Toggle snippet visibility function toggleSnippet(button) { const snippet = button.nextElementSibling; const isVisible = snippet.style.display === 'block'; snippet.style.display = isVisible ? 'none' : 'block'; button.textContent = isVisible ? 'スニペットを表示' : 'スニペットを隠す'; } </script> <script> // Toggle page details visibility function togglePageDetails(button) { const row = button.closest('tr'); const detailsRow = row.nextElementSibling; const pageDetails = detailsRow.querySelector('.page-details'); // Close all other open page details document.querySelectorAll('.page-details').forEach(detail => { if (detail !== pageDetails) { detail.classList.remove('active'); } }); // Update all button texts document.querySelectorAll('.show-page-btn').forEach(btn => { if (btn !== button) { btn.textContent = 'ページの詳細を表示'; } }); // Toggle current page details if (pageDetails.classList.contains('active')) { pageDetails.classList.remove('active'); button.textContent = 'ページの詳細を表示'; } else { pageDetails.classList.add('active'); button.textContent = 'ページの詳細を非表示にする'; } } // Close all page details when clicking outside document.addEventListener('click', function(e) { if (!e.target.closest('.show-page-btn') && !e.target.closest('.page-details')) { document.querySelectorAll('.page-details').forEach(detail => { detail.classList.remove('active'); }); document.querySelectorAll('.show-page-btn').forEach(btn => { btn.textContent = 'ページの詳細を表示'; }); } }); // Update the renderSearchDomains function to include show page buttons function renderSearchDomains(engine, data) { const tbody = document.getElementById(engine + 'PhrasesBody'); tbody.innerHTML = ''; data.forEach((item, index) => { const row = document.createElement('tr'); // Format position with change let positionHtml = `<span class="badge ${getPositionBadgeClass(item.position)}">${item.position}</span>`; if (item.change !== null && item.change !== 0) { const changeClass = item.change > 0 ? 'positive' : 'negative'; const changeSymbol = item.change > 0 ? '+' : ''; positionHtml += `<span class="position-change ${changeClass}">(${changeSymbol}${item.change})</span>`; } row.innerHTML = ` <td>${positionHtml}</td> <td><noindex><a href="/ja/domain/${item.domain}/" rel="nofollow">${item.domain}</a></noindex></td> <td>${item.page}</td> <td> <button class="show-page-btn" onclick="togglePageDetails(this)">ページの詳細を表示</button> </td> `; tbody.appendChild(row); // Add page details row const detailsRow = document.createElement('tr'); detailsRow.className = 'page-details-row'; detailsRow.innerHTML = ` <td colspan="4"> <div class="page-details" id="page-details-${engine}-${index}"> <div class="page-details-grid"> <div class="page-detail-item"> <div class="page-detail-label">Full URL</div> <div class="page-detail-value"> <a href="${item.full_url || '#'}" class="page-url" target="_blank" rel="nofollow"> ${item.full_url || 'N/A'} </a> </div> </div> <div class="page-detail-item"> <div class="page-detail-label">Title</div> <div class="page-detail-value">${item.page_title || 'N/A'}</div> </div> <div class="page-detail-item"> <div class="page-detail-label">Last Updated</div> <div class="page-detail-value">${item.last_updated || 'N/A'}</div> </div> <div class="page-detail-item"> <div class="page-detail-label">Page Authority</div> <div class="page-detail-value">${item.page_authority || 'N/A'}</div> </div> </div> <div class="page-metrics"> <div class="page-metric"> <span class="page-detail-label">Traffic:</span> <span class="page-metric-value">${item.traffic || 'N/A'}</span> </div> <div class="page-metric"> <span class="page-detail-label">Backlinks:</span> <span class="page-metric-value">${item.backlinks || 'N/A'}</span> </div> <div class="page-metric"> <span class="page-detail-label">Social Shares:</span> <span class="page-metric-value">${item.social_shares || 'N/A'}</span> </div> <div class="page-metric"> <span class="page-detail-label">Load Time:</span> <span class="page-metric-value">${item.load_time || 'N/A'}</span> </div> </div> <div class="snippet" style="margin-top: 1rem;"> <strong>Snippet Preview:</strong><br> ${item.snippet || 'No snippet available'} </div> </div> </td> `; tbody.appendChild(detailsRow); }); } </script> <script> (function() { 'use strict'; // --- Configuration --- const STORAGE_KEY = 'serpuls_phrase_sections_state'; const SECTIONS_TO_COLLAPSE = [ { id: 'morphological-lemmas', title: '形態論的な補題', btnText: '補題' }, { id: 'google-search-section', title: '検索フレーズ - Google', btnText: 'Google' }, { id: 'yandex-search-section', title: '検索フレーズ - Yandex', btnText: 'Yandex' }, { id: 'book-samples-section', title: '本 を使った例文・フレーズ', btnText: '本' }, { id: 'additional-services', title: '追加サービス', btnText: 'サービス' } ]; // --- Helper Functions --- function getSectionIdFromTitle(title) { if (title.includes('形態論的な補題')) return 'morphological-lemmas'; if (title.includes('検索フレーズ - Google')) return 'google-search-section'; if (title.includes('検索フレーズ - Yandex')) return 'yandex-search-section'; if (title.includes('追加サービス')) return 'additional-services'; return null; } function saveSectionState(sectionId, isCollapsed) { const savedState = localStorage.getItem(STORAGE_KEY); const state = savedState ? JSON.parse(savedState) : {}; state[sectionId] = isCollapsed; localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } function loadSectionState(sectionId) { const savedState = localStorage.getItem(STORAGE_KEY); if (!savedState) return false; const state = JSON.parse(savedState); return state[sectionId] || false; } // --- Create Floating TOC --- function createFloatingTOC() { // Find all major sections (cards with a card-header) const sections = Array.from(document.querySelectorAll('.card')).map(card => { const headerEl = card.querySelector('.card-header'); if (!headerEl) return null; const titleEl = headerEl.querySelector('.card-title'); if (!titleEl) return null; const title = titleEl.innerText.trim(); // Skip sections that are too small or not needed const skipTitles = [ ]; let shouldSkip = false; for (const skipTitle of skipTitles) { if (title.includes(skipTitle) || title === skipTitle) { shouldSkip = true; break; } } if (shouldSkip) return null; // Create a unique ID for the section if it doesn't have one let id = card.id; if (!id) { id = 'section-' + Math.random().toString(36).substr(2, 8); card.id = id; } return { id, title, element: card }; }).filter(s => s !== null); if (sections.length < 2) return; // Create TOC container const tocContainer = document.createElement('div'); tocContainer.id = 'floating-toc'; tocContainer.className = 'floating-toc'; tocContainer.innerHTML = ` <div class="floating-toc-header"> <span>📑 目次</span> <button id="toc-toggle-btn" class="toc-toggle-btn" aria-label="目次を折りたたむ">−</button> </div> <div id="toc-links-container" class="toc-links-container"> ${sections.map(section => `<a href="#${section.id}" class="toc-link" data-id="${section.id}">${escapeHtml(section.title)}</a>`).join('')} </div> `; document.body.appendChild(tocContainer); // Toggle TOC visibility const toggleBtn = document.getElementById('toc-toggle-btn'); const linksContainer = document.getElementById('toc-links-container'); let isTocExpanded = true; toggleBtn.addEventListener('click', () => { if (isTocExpanded) { linksContainer.style.display = 'none'; toggleBtn.textContent = '+'; tocContainer.classList.add('collapsed'); } else { linksContainer.style.display = 'block'; toggleBtn.textContent = '−'; tocContainer.classList.remove('collapsed'); } isTocExpanded = !isTocExpanded; }); // Highlight active section on scroll const tocLinks = document.querySelectorAll('.toc-link'); const sectionElements = sections.map(s => s.element); function updateActiveLink() { let current = ''; const scrollPosition = window.scrollY + 100; for (const section of sectionElements) { const sectionTop = section.offsetTop; const sectionBottom = sectionTop + section.offsetHeight; if (scrollPosition >= sectionTop && scrollPosition < sectionBottom) { current = section.id; break; } } tocLinks.forEach(link => { link.classList.remove('active'); if (link.getAttribute('data-id') === current) { link.classList.add('active'); } }); } window.addEventListener('scroll', updateActiveLink); updateActiveLink(); // Smooth scroll tocLinks.forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); const targetId = link.getAttribute('data-id'); const targetElement = document.getElementById(targetId); if (targetElement) { targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }); }); } // --- Add Collapsible Buttons to Sections --- function addCollapsibleButtons() { SECTIONS_TO_COLLAPSE.forEach(section => { // Find the card by its title let targetCard = null; const cards = document.querySelectorAll('.card'); for (const card of cards) { const titleEl = card.querySelector('.card-title'); if (titleEl && titleEl.innerText.trim() === section.title) { targetCard = card; break; } } if (!targetCard) return; // Set ID if not exists if (!targetCard.id) targetCard.id = section.id; // Create button const button = document.createElement('button'); button.className = 'collapse-section-btn'; button.setAttribute('data-section-id', section.id); button.setAttribute('aria-label', 'セクションを折りたたむ/展開する'); // Get initial state const isCollapsed = loadSectionState(section.id); if (isCollapsed) { button.innerHTML = '➕'; targetCard.classList.add('section-collapsed'); const content = targetCard.querySelector('.card-content'); if (content) content.style.display = 'none'; } else { button.innerHTML = '➖'; targetCard.classList.remove('section-collapsed'); const content = targetCard.querySelector('.card-content'); if (content) content.style.display = ''; } // Insert button into card-header const cardHeader = targetCard.querySelector('.card-header'); if (cardHeader) { const iconDiv = cardHeader.querySelector('.card-icon'); if (iconDiv) { cardHeader.insertBefore(button, iconDiv); } else { cardHeader.appendChild(button); } // Add click handler button.addEventListener('click', (e) => { e.stopPropagation(); const sectionId = button.getAttribute('data-section-id'); const card = document.getElementById(sectionId); if (!card) return; const content = card.querySelector('.card-content'); if (!content) return; if (card.classList.contains('section-collapsed')) { // Expand content.style.display = ''; card.classList.remove('section-collapsed'); button.innerHTML = '➖'; saveSectionState(sectionId, false); } else { // Collapse content.style.display = 'none'; card.classList.add('section-collapsed'); button.innerHTML = '➕'; saveSectionState(sectionId, true); } }); } }); } // --- Create Back to Top Button --- function createBackToTopButton() { const btn = document.createElement('button'); btn.id = 'back-to-top'; btn.innerHTML = '↑'; btn.setAttribute('aria-label', 'トップに戻る'); btn.className = 'back-to-top-btn'; document.body.appendChild(btn); window.addEventListener('scroll', () => { if (window.scrollY > 500) { btn.classList.add('visible'); } else { btn.classList.remove('visible'); } }); btn.addEventListener('click', () => { window.scrollTo({ top: 0, behavior: 'smooth' }); }); } // --- Improve Table Responsiveness (add scroll hints) --- function improveTables() { const tables = document.querySelectorAll('.data-table, .tasks-table'); tables.forEach(table => { const container = table.closest('.table-responsive') || table.parentElement; if (container && !container.querySelector('.scroll-hint')) { const hint = document.createElement('div'); hint.className = 'scroll-hint'; hint.innerHTML = '← → スワイプしてさらに表示'; hint.style.cssText = ` display: none; text-align: center; font-size: 0.7rem; color: var(--gray); margin-top: 0.5rem; padding: 0.25rem; background: rgba(67, 97, 238, 0.05); border-radius: 12px; `; container.style.position = 'relative'; container.appendChild(hint); // Show hint if table is wider than container function checkScroll() { if (container.scrollWidth > container.clientWidth) { hint.style.display = 'block'; } else { hint.style.display = 'none'; } } checkScroll(); window.addEventListener('resize', checkScroll); container.addEventListener('scroll', () => { hint.style.opacity = '0.5'; setTimeout(() => { hint.style.opacity = '1'; }, 300); }); } }); } // --- Escape HTML helper --- function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // --- Add custom styles for new elements --- function addCustomStyles() { const style = document.createElement('style'); style.textContent = ` /* Floating TOC */ .floating-toc { position: fixed; top: 80px; right: 20px; z-index: 1000; background: white; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.15); border: 1px solid #e9ecef; width: 260px; max-width: calc(100% - 40px); transition: all 0.3s ease; backdrop-filter: blur(8px); background: rgba(255,255,255,0.95); } .floating-toc-header { display: flex; justify-content: space-between; align-items: center; padding: 10px 15px; background: linear-gradient(135deg, var(--primary), var(--secondary)); color: white; border-radius: 12px 12px 0 0; font-weight: 600; font-size: 0.9rem; cursor: pointer; } .toc-toggle-btn { background: none; border: none; color: white; font-size: 1.2rem; cursor: pointer; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; border-radius: 50%; transition: background 0.2s; } .toc-toggle-btn:hover { background: rgba(255,255,255,0.2); } .floating-toc.collapsed { width: auto; } .floating-toc.collapsed .floating-toc-header { border-radius: 12px; } .toc-links-container { max-height: 400px; overflow-y: auto; padding: 8px 0; } .toc-link { display: block; padding: 8px 15px; color: var(--dark); text-decoration: none; font-size: 0.85rem; transition: all 0.2s; border-left: 3px solid transparent; } .toc-link:hover { background: rgba(67, 97, 238, 0.05); color: var(--primary); padding-left: 20px; } .toc-link.active { background: rgba(67, 97, 238, 0.1); border-left-color: var(--primary); color: var(--primary); font-weight: 500; } /* Collapse button in cards */ .collapse-section-btn { background: none; border: none; font-size: 1.1rem; cursor: pointer; width: 28px; height: 28px; border-radius: 6px; display: inline-flex; align-items: center; justify-content: center; transition: all 0.2s; color: var(--gray); margin-right: 8px; } .collapse-section-btn:hover { background: rgba(67, 97, 238, 0.1); color: var(--primary); } .card.section-collapsed .card-header { border-bottom: none; } /* Back to Top Button */ .back-to-top-btn { position: fixed; bottom: 30px; right: 30px; width: 48px; height: 48px; border-radius: 50%; background: linear-gradient(135deg, var(--primary), var(--secondary)); color: white; border: none; cursor: pointer; font-size: 1.5rem; display: flex; align-items: center; justify-content: center; opacity: 0; visibility: hidden; transition: all 0.3s ease; z-index: 999; box-shadow: 0 4px 12px rgba(67, 97, 238, 0.4); } .back-to-top-btn.visible { opacity: 1; visibility: visible; } .back-to-top-btn:hover { transform: translateY(-3px); box-shadow: 0 6px 16px rgba(67, 97, 238, 0.5); } /* Mobile adjustments */ @media (max-width: 768px) { .floating-toc { top: auto; bottom: 20px; right: 20px; left: auto; width: 48px; overflow: hidden; } .floating-toc .floating-toc-header span { display: none; } .floating-toc .floating-toc-header { justify-content: center; padding: 12px; } .floating-toc .toc-toggle-btn { margin: 0; } .floating-toc:not(.collapsed) { width: 260px; left: auto; right: 20px; bottom: 20px; top: auto; } .floating-toc:not(.collapsed) .floating-toc-header span { display: inline; } .floating-toc:not(.collapsed) .floating-toc-header { justify-content: space-between; } .back-to-top-btn { bottom: 80px; right: 20px; width: 40px; height: 40px; font-size: 1.2rem; } .scroll-hint { display: block !important; font-size: 0.65rem; } } /* Smooth scroll behavior */ html { scroll-behavior: smooth; } /* Better card transitions */ .card .card-content { transition: display 0.2s ease; } /* RTL support for languages like Arabic/Hebrew */ [dir="rtl"] .floating-toc { right: auto; left: 20px; } [dir="rtl"] .back-to-top-btn { right: auto; left: 30px; } [dir="rtl"] .collapse-section-btn { margin-right: 0; margin-left: 8px; } `; document.head.appendChild(style); } // --- Initialize everything when DOM is ready --- function init() { addCustomStyles(); createFloatingTOC(); addCollapsibleButtons(); createBackToTopButton(); improveTables(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })(); </script></div> <!-- Footer --> <footer> <div class="footer-container"> <div class="footer-content"> <div class="footer-section"> <h3>About serpuls.com</h3> <div class="footer-info"> <p>検索エンジン分析、順位監視、SEO 最適化のためのプロフェッショナル ツール。</p> <p style="margin-top: 1rem;"><strong>Skynet360 LP.</strong></p> <p><strong>会社番号:</strong> SL027564</p> <p><strong>住所:</strong> Suite 1 4 Queen Street, Edinburgh, Scotland, EH2 1JE</p> </div> </div> <div class="footer-section"> <h3>クイックリンク</h3> <ul class="footer-links"> <li><a href="/ja/">家</a></li> <li><a href="/ja/profile/">プロフィール</a></li> <li><a href="/ja/tasks/">監視タスク</a></li> <li><a href="/ja/api_docs/">APIドキュメント</a></li> <li><a href="/ja/team/">私たちのチーム</a></li> </ul> </div> <div class="footer-section"> <h3>リソース</h3> <ul class="footer-links"> <li><a href="/ja/blogs/">ブログ</a></li> <li><a href="/ja/faq/">よくある質問</a></li> <li><a href="/ja/support/">サポート</a></li> </ul> </div> <div class="footer-section"> <h3>連絡先</h3> <div class="footer-info"> <p>電子メール: <a href="mailto:info@serpuls.com">info@serpuls.com</a></p> <p>電報: <a href="https://t.me/serpulse_support">@serpulse_support</a></p> <div class="social-links"> <a href="#" class="social-link" title="電報">📱</a> <a href="https://github.com/SkyNetJose/serpuls" class="social-link" title="GitHub">💻</a> <a href="#" class="social-link" title="ツイッター">🐦</a> </div> </div> </div> </div> <div class="footer-bottom"> <p class="copyright">© 2026 serpuls.com. 無断転載を禁じます. Skynet360 LP.</p> <div class="footer-bottom-links"> <a href="/ja/privacy/">プライバシーポリシー</a> <a href="/ja/terms/">利用規約</a> <a href="/ja/agreement/">ユーザー同意書</a> <a href="/ja/cookie/">クッキーポリシー</a> </div> </div> </div> </footer> <!-- Updated Payment Modal HTML --> <div class="modal" id="paymentModal"> <div class="modal-content"> <span class="close-btn">×</span> <div class="modal-success" id="paymentSuccess"> <div class="success-icon">✓</div> <h3>支払いが成功しました!</h3> <p>ご注文ありがとうございます。すぐにご連絡させていただきます。</p> <button class="cta-button" onclick="closeModal('paymentModal')">近い</button> </div> <form id="paymentForm" onsubmit="return processPayment(event)"> <h2 class="modal-title" id="modalTitle">サービスの支払い</h2> <p class="modal-text" id="modalText">サービスの説明</p> <!-- Hidden fields for payment type and ID --> <input type="hidden" id="paymentType" name="payment_type" value=""> <input type="hidden" id="paymentItemId" name="payment_item_id" value=""> <!-- Payment Options --> <div class="payment-options" id="paymentOptions"> <!-- Options will be dynamically generated here --> </div> <!-- Manual Price Input (Optional) - Now dynamically shown based on config --> <div class="form-group" id="manualPriceGroup" style="display: none;"> <label for="manualPrice">金額 (USD) を入力してください:</label> <input type="number" id="manualPrice" class="form-control" placeholder="金額を入力してください" min="1" max="1000" step="0.01" oninput="updateManualPrice(this.value)"> <div class="error-message" id="manualPriceError"></div> </div> <!-- Custom Price Option Toggle - Now dynamically shown --> <div class="form-group" id="customPriceToggle" style="display: none; margin-bottom: 1rem;"> <label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;"> <input type="radio" id="customPriceRadio" name="paymentOption" value="custom" onchange="toggleCustomPriceSelection(this.checked)"> カスタム金額を入力してください </label> </div> <div class="form-group"> <label for="modalName">名前:</label> <input type="text" id="modalName" class="form-control" placeholder="あなたの名前を入力してください" required> <div class="error-message" id="modalNameError"></div> </div> <div class="form-group"> <label for="modalEmail">電子メール:</label> <input type="email" id="modalEmail" class="form-control" placeholder="メールアドレスを入力してください" required> <div class="error-message" id="modalEmailError"></div> </div> <p class="modal-price" id="modalPrice">価格: $0.00</p> <button type="submit" class="cta-button" id="payButton">支払う $0.00</button> </form> </div> </div> <!-- Redirect Modal (for iframe) --> <div class="modal" id="redirectModal"> <div class="modal-content modal-large"> <span class="close-btn">×</span> <h2 class="modal-title">支払い</h2> <iframe id="redirectIframe" class="modal-iframe" src=""></iframe> </div> </div> <!-- Auth Modal --> <div class="modal" id="authModal"> <div class="modal-content"> <span class="close-btn">×</span> <div class="auth-tabs"> <div class="auth-tab active" data-tab="login">ログイン</div> <div class="auth-tab" data-tab="register">登録する</div> </div> <div class="auth-form active" id="loginForm"> <h2 class="modal-title">アカウントにサインインする</h2> <div class="form-group"> <label for="loginEmail">電子メール:</label> <input type="email" id="loginEmail" class="form-control" autocomplete="email" placeholder="メールアドレスを入力してください"> <div class="error-message" id="loginEmailError"></div> </div> <div class="form-group"> <label for="loginPassword">パスワード:</label> <input type="password" id="loginPassword" class="form-control" autocomplete="current-password" placeholder="パスワードを入力してください"> <div class="error-message" id="loginPasswordError"></div> </div> <button class="cta-button" style="width:100%" onclick="processLogin()">サインイン</button> <div style="text-align:center;margin:1rem 0;color:var(--gray);border-top:1px solid #e9ecef;padding-top:1rem;">または続行してください</div> <button class="cta-button secondary" style="width:100%;margin-bottom:.6rem" onclick="startOAuth('google')">グーグル</button> <button class="cta-button secondary" style="width:100%;margin-bottom:.6rem" onclick="startOAuth('github')">GitHub</button> <button class="cta-button" style="background:#0088cc;width:100%" onclick="loginWithTelegram()">📱 Telegram でサインインする</button> <p style="margin-top:.5rem;color:var(--gray);font-size:.9rem">Telegram にサインインするには、まず上記の Telegram に接続されている Serpulse アカウントの電子メールを入力します。</p> </div> <div class="auth-form" id="registerForm"> <h2 class="modal-title">アカウントを作成する</h2> <div class="form-group"><label for="registerName">名前:</label><input type="text" id="registerName" class="form-control" autocomplete="name" required><div class="error-message" id="registerNameError"></div></div> <div class="form-group"><label for="registerEmail">電子メール:</label><input type="email" id="registerEmail" class="form-control" autocomplete="email" required><div class="error-message" id="registerEmailError"></div></div> <div class="form-group"><label for="registerPassword">パスワード:</label><input type="password" id="registerPassword" class="form-control" autocomplete="new-password" minlength="8" required><div class="error-message" id="registerPasswordError"></div></div> <div class="form-group"><label for="registerConfirmPassword">パスワードを認証する:</label><input type="password" id="registerConfirmPassword" class="form-control" autocomplete="new-password" minlength="8" required><div class="error-message" id="registerConfirmPasswordError"></div></div> <button class="cta-button" style="width:100%" onclick="processRegistration()">アカウントを作成する</button> <div style="text-align:center;margin:1rem 0;color:var(--gray);border-top:1px solid #e9ecef;padding-top:1rem;">またはアカウントを作成してください</div> <button class="cta-button secondary" style="width:100%;margin-bottom:.6rem" onclick="startOAuth('google')">グーグル</button> <button class="cta-button secondary" style="width:100%;margin-bottom:.6rem" onclick="startOAuth('github')">GitHub</button> <button class="cta-button" style="background:#0088cc;width:100%" onclick="startTelegramRegistration()">📱 テレグラムに登録する</button> <p style="margin-top:1rem;color:var(--gray);font-size:.9rem">パスワードとテレグラム登録にはメール認証が必要です。 Google および GitHub の登録では、プロバイダーから返された確認済みメールが使用されます。</p> </div> </div> </div> <!-- Telegram Code Input Modal --> <div class="modal" id="telegramCodeModal"> <div class="modal-content"> <span class="close-btn" onclick="closeModal('telegramCodeModal')">×</span> <div class="centered-content"> <div class="success-icon">🔐</div> <h2 class="modal-title">検証コード</h2> <p class="modal-text">コードは Telegram に送信されました。以下に入力してください。</p> <div class="form-group"> <input type="text" id="telegramLoginCode" class="form-control verify-code-input" placeholder="0000000" maxlength="7" style="font-size: 1.5rem; letter-spacing: 0.5rem;"> <div class="error-message" id="telegramLoginCodeError"></div> </div> <button class="cta-button" style="width: 100%; margin-top: 1rem;" onclick="verifyTelegramLoginCode()"> ログインの確認 </button> <div class="timer-container" id="loginCodeTimer"> 有効なコード: <span id="loginTimer">05:00</span> </div> </div> </div> </div> <!-- Updated Telegram Verification Modal --> <div class="modal" id="telegramVerifyModal"> <div class="modal-content"> <span class="close-btn" onclick="closeTelegramVerifyModal()">×</span> <div class="centered-content"> <div class="success-icon">📱</div> <h2 class="modal-title">Telegramへの登録を完了する</h2> <p class="modal-text" id="telegramModalText"> Telegram が新しいタブで開きました。<br> ボットが自動的に開かなかった場合は、下のボタンを使用するか、QR コードをスキャンしてください。 </p> <!-- QR Code Container --> <div id="qrCodeContainer" style="margin: 1.5rem 0; text-align: center;"> <div id="qrcode" style="display: inline-block;"></div> <p style="font-size: 0.9rem; color: var(--gray); margin-top: 0.5rem;"> QR コードをスキャンしてボットを開きます </p> </div> <!-- Timer and Status --> <div id="telegramTimer" class="timer-container"> 確認を待っています... <span id="timer">05:00</span> </div> <!-- Action Buttons --> <div style="display: flex; gap: 1rem; margin-top: 1.5rem; flex-wrap: wrap; justify-content: center;"> <button class="cta-button" style="background: #0088cc;" onclick="openTelegramBot()" id="openTelegramBtn"> 📱 テレグラムをもう一度開く </button> <button class="cta-button secondary" onclick="refreshTelegramQR()" id="refreshQRBtn"> 🔄 QRコードを更新 </button> </div> <!-- Status Info --> <div class="info-box" style="margin-top: 1.5rem;"> <p style="margin: 0; font-size: 0.9rem;"> <strong>仕組み:</strong><br> 1. [Open Telegram] をクリックするか、QR コードをスキャンします<br> 2. ボットで [開始] をクリックし、電話番号を共有します。<br> 3. 確認ステータスは自動的に更新されます </p> </div> </div> </div> </div> <!-- Email Verification Modal --> <div class="modal" id="emailVerifyModal"> <div class="modal-content"> <span class="close-btn" onclick="closeEmailVerifyModal()">×</span> <div class="centered-content"> <div class="success-icon">📧</div> <h2 class="modal-title">メールアドレスを確認してください</h2> <p class="modal-text" id="emailVerifyMessage">受信箱を確認し、送信された確認リンクをクリックしてください。電子メールが認証されるまで、パスワードを使用してサインインすることはできません。</p> <p id="emailVerifyAddress" style="font-weight:600;word-break:break-all"></p> <button id="resendEmailBtn" class="cta-button secondary" style="width:100%;margin-top:1rem" onclick="resendEmailVerification()">確認メールを再送信する</button> <button class="cta-button" style="width:100%;margin-top:.75rem" onclick="closeEmailVerifyModal()">近い</button> </div> </div> </div> <!-- Image reusable Modal --> <div class="modal" id="imgModal"> <div class="modal-content"> <span class="close-btn">×</span> <img id="modalImg" src="" style="max-width:90%; max-height:90%; margin-top:40px; border-radius:8px;"> </div> </div> <!--Loading overlay --> <div class="loading-overlay" id="loadingOverlay"> <div class="loading-spinner"></div> </div> <div class="toast" id="toast"></div> <script> // Get language from URL path like /en/about or /ru/products function getLangFromUrl() { const path = window.location.pathname; const match = path.match(/^\/([a-z]{2})(\/|$)/i); return match ? match[1] : 'en'; // default to 'en' if no language code found } // Store in global variable window.currentLang = getLangFromUrl(); // Show loading overlay function showLoading() { document.getElementById('loadingOverlay').classList.add('active'); } // Hide loading overlay function hideLoading() { document.getElementById('loadingOverlay').classList.remove('active'); } // Show toast notification function showToast(message, type = 'info') { const toast = document.getElementById('toast'); toast.textContent = message; toast.className = 'toast ' + type; toast.classList.add('show'); setTimeout(() => { toast.classList.remove('show'); }, 3000); } // Check user login status on page load document.addEventListener('DOMContentLoaded', function() { checkUserLoginStatus(); }); // Function to check if user is logged in function checkUserLoginStatus() { const userInfoElement = document.getElementById('userInfo'); const guestButtonsElement = document.getElementById('guestButtons'); const userNameElement = document.getElementById('userName'); // Show loading state or keep defaults while checking if (guestButtonsElement) { guestButtonsElement.style.display = 'flex'; } if (userInfoElement) { userInfoElement.style.display = 'none'; } const data = { type: 'check_user_logined' }; sendRequest('/ajax/', data, function(response) { if (response.status === 'ok') { if (response.user_logined === 1) { // User is logged in if (userInfoElement) { userInfoElement.style.display = 'block'; } if (guestButtonsElement) { guestButtonsElement.style.display = 'none'; } if (userNameElement) { // Use logined_user_name if available, otherwise show default userNameElement.textContent = response.logined_user_name || 'プロフィール'; } // Optionally store minimal info in localStorage for quick access // but don't rely on it for auth state if (response.logined_user_id) { localStorage.setItem('user_id', response.logined_user_id); } if (response.logined_user_name) { localStorage.setItem('user_name', response.logined_user_name); } } else { // User is not logged in if (userInfoElement) { userInfoElement.style.display = 'none'; } if (guestButtonsElement) { guestButtonsElement.style.display = 'flex'; } // Clear any previously stored user data localStorage.removeItem('user_id'); localStorage.removeItem('user_name'); localStorage.removeItem('user_info'); } } else { console.error('ログインステータスチェック中にエラーが発生しました:', response.error); // On error, default to guest view if (userInfoElement) { userInfoElement.style.display = 'none'; } if (guestButtonsElement) { guestButtonsElement.style.display = 'flex'; } } }, false); // Don't show loading overlay for this quick check } // Logout function function logout() { // Send logout request to server const data = { type: 'logout' }; sendRequest('/ajax/', data, function(response) { // Clear all user data from localStorage regardless of response localStorage.removeItem('user_info'); localStorage.removeItem('user_id'); localStorage.removeItem('user_name'); localStorage.removeItem('current_user_token'); // Update UI checkUserLoginStatus(); // Redirect to main page window.location.href = '/'; }); } </script> <script> // Mobile menu toggle - Combined approach document.getElementById('hamburger').addEventListener('click', function() { const mobileMenu = document.getElementById('mobileMenu'); mobileMenu.classList.toggle('active'); }); // Close mobile menu when clicking on links or buttons document.querySelectorAll('.nav-links a, .auth-buttons button').forEach(element => { element.addEventListener('click', function() { document.getElementById('mobileMenu').classList.remove('active'); }); }); // Close mobile menu when clicking outside document.addEventListener('click', function(event) { const mobileMenu = document.getElementById('mobileMenu'); const hamburger = document.getElementById('hamburger'); const navContainer = document.querySelector('.nav-container'); if (!navContainer.contains(event.target) && mobileMenu.classList.contains('active')) { mobileMenu.classList.remove('active'); } }); // Simple animation on scroll document.addEventListener('DOMContentLoaded', function() { const cards = document.querySelectorAll('.card, .domain-header, .chart-container, .lemma-card'); const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.style.opacity = '1'; entry.target.style.transform = 'translateY(0)'; } }); }, { threshold: 0.0001, // Optional: keep small percentage rootMargin: '350px 0px 350px 0px' // Trigger 100px before element enters }); cards.forEach(card => { card.style.opacity = '0'; card.style.transform = 'translateY(20px)'; card.style.transition = 'opacity 0.5s ease, transform 0.5s ease'; observer.observe(card); }); }); // Toggle links visibility function toggleLinks(className, btn, max_count = 5) { const list = document.getElementById(className); const items = list.querySelectorAll('li'); const hiddenItems = list.querySelectorAll('li.hidden'); if (hiddenItems.length > 0) { // Show all list.querySelectorAll('li.hidden').forEach(el => el.classList.remove('hidden')); btn.textContent = '隠れる'; } else { // Hide all beyond the first 5 items.forEach((el, index) => { if (index >= max_count) el.classList.add('hidden'); }); btn.textContent = 'もっと見る'; } } // Toggle raw data visibility function toggleRawData(elementId, btn) { const element = document.getElementById(elementId); if (element.style.maxHeight === '200px') { element.style.maxHeight = 'none'; btn.textContent = '完全なデータを隠す'; } else { element.style.maxHeight = '200px'; btn.textContent = '完全なデータを表示'; } } </script> <script> // Payment Modal // Global variable to store payment config let paymentConfig = { allowCustomAmount: false, minAmount: 1, maxAmount: 1000 }; // Payment options functionality document.addEventListener('DOMContentLoaded', function() { // Replace the entire payment options event listener with this: document.addEventListener('change', function(e) { if (e.target && e.target.name === 'paymentOption') { // Update selected state for regular options document.querySelectorAll('.payment-option').forEach(opt => { opt.classList.remove('selected'); }); // Handle custom price radio separately if (e.target.id === 'customPriceRadio') { // For custom price, we don't have a .payment-option wrapper // Just update the custom price toggle UI document.querySelectorAll('.payment-option').forEach(opt => { opt.classList.remove('selected'); }); // You might want to add visual feedback for custom price selection const customPriceToggle = document.getElementById('customPriceToggle'); if (customPriceToggle) { customPriceToggle.style.backgroundColor = 'rgba(67, 97, 238, 0.1)'; customPriceToggle.style.borderLeft = '3px solid var(--primary)'; customPriceToggle.style.padding = '0.5rem'; customPriceToggle.style.borderRadius = 'var(--border-radius)'; } } else { // For regular options, highlight the .payment-option container const paymentOption = e.target.closest('.payment-option'); if (paymentOption) { paymentOption.classList.add('selected'); // Reset custom price toggle styling if it was highlighted const customPriceToggle = document.getElementById('customPriceToggle'); if (customPriceToggle) { customPriceToggle.style.backgroundColor = ''; customPriceToggle.style.borderLeft = ''; customPriceToggle.style.padding = ''; } } } // Update price (this should work for all options) updatePaymentPrice(e.target.value); } }); }); // Update the updatePaymentPrice function to handle custom price: function updatePaymentPrice(price) { const modalPrice = document.getElementById('modalPrice'); const payButton = document.getElementById('payButton'); const customPriceRadio = document.getElementById('customPriceRadio'); // Uncheck custom price radio if regular option is selected if (customPriceRadio && customPriceRadio.checked === false) { document.getElementById('manualPriceGroup').style.display = 'none'; // Reset custom price toggle styling const customPriceToggle = document.getElementById('customPriceToggle'); if (customPriceToggle) { customPriceToggle.style.backgroundColor = ''; customPriceToggle.style.borderLeft = ''; customPriceToggle.style.padding = ''; } } modalPrice.textContent = '価格: $' + parseFloat(price).toFixed(2); payButton.textContent = '支払う $' + parseFloat(price).toFixed(2); payButton.disabled = false; payButton.style.opacity = '1'; } // Generate payment options based on config function generatePaymentOptions(optionsData, paymentType = 'balance', paymentItemId = '', config = {}) { const paymentOptionsContainer = document.getElementById('paymentOptions'); const paymentTypeInput = document.getElementById('paymentType'); const paymentItemIdInput = document.getElementById('paymentItemId'); const customPriceToggle = document.getElementById('customPriceToggle'); const manualPriceGroup = document.getElementById('manualPriceGroup'); // Set hidden fields paymentTypeInput.value = paymentType; paymentItemIdInput.value = paymentItemId; // Store config globally paymentConfig = { allowCustomAmount: config.allowCustomAmount || false, minAmount: config.minAmount || 1, maxAmount: config.maxAmount || 1000 }; // Clear container paymentOptionsContainer.innerHTML = ''; // Generate regular options if (optionsData && optionsData.length > 0) { optionsData.forEach((option, index) => { const optionElement = document.createElement('div'); optionElement.className = 'payment-option'; optionElement.innerHTML = ` <input type="radio" id="option${index}" name="paymentOption" value="${option.price}" data-label="${option.label}" ${option.default ? 'checked' : ''} onchange="updatePaymentPrice('${option.price}')"> <label for="option${index}">${option.label} - $${option.price}</label> `; paymentOptionsContainer.appendChild(optionElement); }); paymentOptionsContainer.style.display = 'block'; } else { paymentOptionsContainer.style.display = 'none'; } // Show/hide custom amount option based on config if (paymentConfig.allowCustomAmount) { customPriceToggle.style.display = 'block'; // Update manual price input constraints document.getElementById('manualPrice').min = paymentConfig.minAmount; document.getElementById('manualPrice').max = paymentConfig.maxAmount; document.getElementById('manualPrice').placeholder = 'から $${paymentConfig.minAmount} に $${paymentConfig.maxAmount}'; } else { customPriceToggle.style.display = 'none'; manualPriceGroup.style.display = 'none'; } // Select first option by default if none is marked as default if (optionsData && optionsData.length > 0) { const defaultOption = paymentOptionsContainer.querySelector('input[type="radio"][checked]') || paymentOptionsContainer.querySelector('input[type="radio"]'); if (defaultOption) { defaultOption.checked = true; defaultOption.closest('.payment-option').classList.add('selected'); updatePaymentPrice(defaultOption.value); } } else if (paymentConfig.allowCustomAmount) { // If no options but custom amount is allowed, show custom by default document.getElementById('customPriceRadio').checked = true; toggleCustomPriceSelection(true); } } // Also update the toggleCustomPriceSelection function: function toggleCustomPriceSelection(isSelected) { const manualPriceGroup = document.getElementById('manualPriceGroup'); const paymentOptions = document.getElementById('paymentOptions'); const customPriceRadio = document.getElementById('customPriceRadio'); if (isSelected) { manualPriceGroup.style.display = 'block'; // Deselect all regular options document.querySelectorAll('input[name="paymentOption"]').forEach(radio => { if (radio.id !== 'customPriceRadio') { radio.checked = false; const paymentOption = radio.closest('.payment-option'); if (paymentOption) { paymentOption.classList.remove('selected'); } } }); // Visual feedback for custom price selection const customPriceToggle = document.getElementById('customPriceToggle'); if (customPriceToggle) { customPriceToggle.style.backgroundColor = 'rgba(67, 97, 238, 0.1)'; customPriceToggle.style.borderLeft = '3px solid var(--primary)'; customPriceToggle.style.padding = '0.5rem'; customPriceToggle.style.borderRadius = 'var(--border-radius)'; customPriceToggle.style.transition = 'var(--transition)'; } // Focus on manual price input setTimeout(() => { const manualInput = document.getElementById('manualPrice'); manualInput.focus(); // Update price if there's already a value if (manualInput.value) { updateManualPrice(manualInput.value); } else { // Set default to min amount updateManualPrice(paymentConfig.minAmount); manualInput.value = paymentConfig.minAmount; } }, 100); } else { manualPriceGroup.style.display = 'none'; // Reset custom price toggle styling const customPriceToggle = document.getElementById('customPriceToggle'); if (customPriceToggle) { customPriceToggle.style.backgroundColor = ''; customPriceToggle.style.borderLeft = ''; customPriceToggle.style.padding = ''; } } } // Update price from manual input in real-time function updateManualPrice(price) { if (!price || price < paymentConfig.minAmount || price > paymentConfig.maxAmount) { const modalPrice = document.getElementById('modalPrice'); const payButton = document.getElementById('payButton'); if (price && (price < paymentConfig.minAmount || price > paymentConfig.maxAmount)) { modalPrice.textContent = '価格: から $${paymentConfig.minAmount} に $${paymentConfig.maxAmount}'; payButton.textContent = '無効な金額です'; payButton.disabled = true; payButton.style.opacity = '0.6'; } else { modalPrice.textContent = '価格: $0.00'; payButton.textContent = '支払う $0.00'; payButton.disabled = true; payButton.style.opacity = '0.6'; } return; } const modalPrice = document.getElementById('modalPrice'); const payButton = document.getElementById('payButton'); modalPrice.textContent = '価格: $' + parseFloat(price).toFixed(2); payButton.textContent = '支払う $' + parseFloat(price).toFixed(2); payButton.disabled = false; payButton.style.opacity = '1'; } const paymentModal = document.getElementById('paymentModal'); const paymentCloseBtn = paymentModal.querySelector('.close-btn'); // Elements inside modal const modalTitle = document.getElementById('modalTitle'); const modalText = document.getElementById('modalText'); const modalPrice = document.getElementById('modalPrice'); const payButton = document.getElementById('payButton'); const paymentForm = document.getElementById('paymentForm'); const paymentSuccess = document.getElementById('paymentSuccess'); // Updated modal opening handler document.querySelectorAll('[data-modal="paymentModal"]').forEach(trigger => { trigger.addEventListener('click', function(e) { e.preventDefault(); // Get data attributes const title = this.getAttribute('data-title'); const text = this.getAttribute('data-text'); const price = this.getAttribute('data-price'); const paymentType = this.getAttribute('data-payment-type') || 'balance'; const paymentItemId = this.getAttribute('data-payment-item-id') || ''; // Get payment config from data attributes const allowCustomAmount = this.getAttribute('data-allow-custom') === 'true'; const minAmount = parseFloat(this.getAttribute('data-min-amount')) || 1; const maxAmount = parseFloat(this.getAttribute('data-max-amount')) || 1000; // Get payment options from data attribute const optionsData = this.getAttribute('data-options'); let paymentOptions = []; if (optionsData) { try { paymentOptions = JSON.parse(optionsData); } catch (e) { console.error('支払いオプションの解析エラー:', e); } } // Fill modal content document.getElementById('modalTitle').textContent = title; document.getElementById('modalText').textContent = text; // Generate payment options with config const config = { allowCustomAmount: allowCustomAmount, minAmount: minAmount, maxAmount: maxAmount }; generatePaymentOptions(paymentOptions, paymentType, paymentItemId, config); // If no options and custom amount not allowed, use default price if (paymentOptions.length === 0 && !allowCustomAmount) { updatePaymentPrice(price); } // Reset form resetForm('paymentModal'); document.getElementById('paymentForm').style.display = 'block'; document.getElementById('paymentSuccess').style.display = 'none'; // Show modal document.getElementById('paymentModal').style.display = 'flex'; }); }); // Helper function to reset form function resetPaymentForm() { document.getElementById('modalName').value = ''; document.getElementById('modalEmail').value = ''; document.getElementById('manualPrice').value = ''; if (document.getElementById('customPriceRadio')) { document.getElementById('customPriceRadio').checked = false; } clearErrors(['nameError', 'emailError', 'manualPriceError']); } // Close payment modal paymentCloseBtn.addEventListener('click', function() { paymentModal.style.display = 'none'; }); // Close on outside click window.addEventListener('click', function(e) { if (e.target === paymentModal) { paymentModal.style.display = 'none'; } }); // Fill tooltip with alt text automatically document.querySelectorAll('.popup-trigger').forEach(trigger => { const altText = trigger.getAttribute('alt'); const popupText = trigger.querySelector('.popup-text'); if (popupText && altText) { popupText.textContent = altText; } }); </script> <script> // Fill tooltip with alt text automatically document.querySelectorAll('.popup-trigger').forEach(trigger => { const altText = trigger.getAttribute('alt'); const popupText = trigger.querySelector('.popup-text'); if (popupText && altText) { popupText.textContent = altText; } }); // Auth Modal const authModal = document.getElementById('authModal'); const authCloseBtn = authModal.querySelector('.close-btn'); const loginBtn = document.getElementById('loginBtn'); const registerBtn = document.getElementById('registerBtn'); const authTabs = document.querySelectorAll('.auth-tab'); const authForms = document.querySelectorAll('.auth-form'); // Open auth modal loginBtn.addEventListener('click', function() { authModal.style.display = 'flex'; switchAuthTab('login'); resetForm('authModal'); }); registerBtn.addEventListener('click', function() { authModal.style.display = 'flex'; switchAuthTab('register'); resetForm('authModal'); }); // Close auth modal authCloseBtn.addEventListener('click', function() { authModal.style.display = 'none'; }); // Close on outside click window.addEventListener('click', function(e) { if (e.target === authModal) { authModal.style.display = 'none'; } }); // Switch auth tabs function switchAuthTab(tabName) { authTabs.forEach(tab => { if (tab.getAttribute('data-tab') === tabName) { tab.classList.add('active'); } else { tab.classList.remove('active'); } }); authForms.forEach(form => { if (form.id === tabName + 'Form') { form.classList.add('active'); } else { form.classList.remove('active'); } }); } // Add click events to auth tabs authTabs.forEach(tab => { tab.addEventListener('click', function() { const tabName = this.getAttribute('data-tab'); switchAuthTab(tabName); resetForm('authModal'); }); }); // Reset form and errors function resetForm(modalType) { if (modalType === 'paymentModal') { document.getElementById('modalName').value = ''; document.getElementById('modalEmail').value = ''; clearErrors(['nameError', 'emailError']); } else if (modalType === 'authModal') { document.getElementById('loginEmail').value = ''; document.getElementById('loginPassword').value = ''; document.getElementById('registerName').value = ''; document.getElementById('registerEmail').value = ''; if(document.getElementById('registerPassword'))document.getElementById('registerPassword').value = ''; if(document.getElementById('registerConfirmPassword'))document.getElementById('registerConfirmPassword').value = ''; clearErrors(['loginEmailError', 'loginPasswordError', 'registerNameError', 'registerEmailError', 'registerPasswordError', 'registerConfirmPasswordError']); } } // Clear error messages function clearErrors(errorIds) { errorIds.forEach(id => { const errorElement = document.getElementById(id); if (errorElement) { errorElement.textContent = ''; errorElement.classList.remove('show'); const inputElement = document.getElementById(id.replace('Error', '')); if (inputElement) { inputElement.classList.remove('error'); } } }); } // Show error message function showError(fieldId, message) { const errorElement = document.getElementById(fieldId + 'Error'); const inputElement = document.getElementById(fieldId); if (errorElement) { errorElement.textContent = message; errorElement.classList.add('show'); } if (inputElement) { inputElement.classList.add('error'); } } // Enhanced close modal function for general use function closeModal(modalId) { // Clear timers based on which modal is being closed switch(modalId) { case 'telegramVerifyModal': clearTelegramTimers(); break; case 'authModal': // Clear any potential timers in auth modal break; } document.getElementById(modalId).style.display = 'none'; } // Enhanced processPayment function function processPayment(event) { if (event) event.preventDefault(); const name = document.getElementById('modalName').value; const email = document.getElementById('modalEmail').value; const paymentType = document.getElementById('paymentType').value; const paymentItemId = document.getElementById('paymentItemId').value; // Get price - check if using custom amount or selected option let price; let optionLabel = ''; const customPriceRadio = document.getElementById('customPriceRadio'); if (customPriceRadio && customPriceRadio.checked) { // Custom amount price = document.getElementById('manualPrice').value; optionLabel = 'custom_amount'; // Validate custom amount if (!price || price < paymentConfig.minAmount || price > paymentConfig.maxAmount) { showError('manualPrice', '金額は次の範囲内である必要があります $${paymentConfig.minAmount} そして $${paymentConfig.maxAmount}'); return false; } } else { // Regular option const selectedOption = document.querySelector('input[name="paymentOption"]:checked'); if (selectedOption) { price = selectedOption.value; optionLabel = selectedOption.getAttribute('data-label') || 'selected_option'; } else { // Fallback to default price price = document.getElementById('modalPrice').textContent.match(/\d+\.\d+/)?.[0] || '0'; optionLabel = 'default'; } } const service = document.getElementById('modalTitle').textContent; // Clear previous errors clearErrors(['nameError', 'emailError', 'manualPriceError']); // Validate form let hasErrors = false; if (!name) { showError('modalName', 'あなたの名前を入力してください'); hasErrors = true; } if (!email) { showError('modalEmail', 'メールアドレスを入力してください'); hasErrors = true; } else if (!/\S+@\S+\.\S+/.test(email)) { showError('modalEmail', '有効なメールアドレスを入力してください'); hasErrors = true; } if (hasErrors) return false; // Prepare data for API request const data = { type: 'payment', name: name, email: email, price: parseFloat(price).toFixed(2), service: service, payment_type: paymentType, payment_item_id: paymentItemId, option: optionLabel, is_custom_amount: (customPriceRadio && customPriceRadio.checked) ? 1 : 0 }; // Send request to API sendRequest('/ajax/', data, function(response) { if (response.status === 'ok') { if (response.redirect_url) { handlePaymentRedirect(response.redirect_url, response.redirect_type); } else { // Show success message document.getElementById('paymentForm').style.display = 'none'; document.getElementById('paymentSuccess').style.display = 'block'; } } else { // Show error message showToast('エラー: ' + response.error, 'error'); } }); return false; } // Handle payment redirect function handlePaymentRedirect(redirectUrl, redirectType) { switch (redirectType) { case 'new_window': window.open(redirectUrl, '_blank'); closeModal('paymentModal'); break; case 'iframe': document.getElementById('redirectIframe').src = redirectUrl; closeModal('paymentModal'); document.getElementById('redirectModal').style.display = 'flex'; break; case 'default': default: window.location.href = redirectUrl; break; } } const serpulseCsrfToken = "8985e60d52670f2a1997de6732ea0b78e85c20060aeec43f08c30c9ea7cb6fa0"; // Send AJAX request function sendRequest(url, data, callback,need_loading_overlay=true) { // Show loading overlay when request starts if(need_loading_overlay)showLoading(); //console.log("XMLHttpRequest started"); const xhr = new XMLHttpRequest(); xhr.open('POST', url, true); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.setRequestHeader('X-CSRF-Token', serpulseCsrfToken); xhr.onreadystatechange = function() { //console.log("XMLHttpRequest onready fired, readyState:", xhr.readyState); if (xhr.readyState === 4) { // Hide loading overlay when request completes if(need_loading_overlay)hideLoading(); //console.log("Request complete, status:", xhr.status); //console.log("responseText:", xhr.responseText); if (xhr.status === 200) { let is_good = true; let response; // Declare response variable outside try block try { response = JSON.parse(xhr.responseText); } catch (e) { is_good=false; console.error("JSON parse error:", e); callback({status: 'error', error: 'サーバーからの無効な応答'}); } if(is_good) callback(response); } else { callback({status: 'error', error: 'サーバーエラー: ' + xhr.status}); } } }; xhr.send(JSON.stringify(data)); } // Close redirect modal document.getElementById('redirectModal').querySelector('.close-btn').addEventListener('click', function() { document.getElementById('redirectModal').style.display = 'none'; }); // Mock chart data (in a real implementation, you would use Google Charts) function initCharts() { // This is a placeholder for actual chart implementation //console.log('Charts would be initialized here with Google Charts API'); } // Image modal functionality document.addEventListener('DOMContentLoaded', function() { // Image modal const imgModal = document.getElementById('imgModal'); const modalImg = document.getElementById('modalImg'); const imgCloseBtn = imgModal.querySelector('.close-btn'); // Open image modal document.querySelectorAll('[data-modal="imgModal"]').forEach(img => { img.addEventListener('click', function() { modalImg.src = this.src; imgModal.style.display = 'flex'; }); }); // Close image modal imgCloseBtn.addEventListener('click', function() { imgModal.style.display = 'none'; }); // Close on outside click window.addEventListener('click', function(e) { if (e.target === imgModal) { imgModal.style.display = 'none'; } }); }); </script> <script> //console.log("herei"); // Analysis functionality document.addEventListener('DOMContentLoaded', function() { //console.log("DOMContentLoaded"); const analyzeBtn = document.getElementById('analyzeNowBtn'); const analysisStatus = document.getElementById('analysisStatus'); if (analyzeBtn) { analyzeBtn.addEventListener('click', function() { //console.log("click"); // Show captcha modal showCaptchaModal(); }); } }); function showCaptchaModal() { //console.log("showCaptchaModal"); // Create modal for captcha const modal = document.createElement('div'); modal.className = 'modal'; modal.id = 'captchaModal'; modal.style.display = 'flex'; modal.innerHTML = ` <div class="modal-content"> <span class="close-btn" onclick="closeCaptchaModal()">×</span> <h2 class="modal-title">あなたがロボットではないことを確認してください</h2> <p class="modal-text">画像のテキストを入力します。</p> <div class="captcha-container"> <img src="/captcha.php?t=` + new Date().getTime() + `" alt="CAPTCHA" class="captcha-image" id="captchaImage"> <button type="button" class="refresh-captcha" onclick="refreshCaptcha()">リフレッシュコード</button> <input type="text" class="captcha-input" id="captchaInput" placeholder="コードを入力してください" maxlength="6"> </div> <button class="cta-button" onclick="submitAnalysis()" style="margin-top: 1rem;">分析のために送信する</button> </div> `; document.body.appendChild(modal); } function closeCaptchaModal() { const modal = document.getElementById('captchaModal'); if (modal) { modal.remove(); } } function refreshCaptcha() { const captchaImage = document.getElementById('captchaImage'); if (captchaImage) { // Add timestamp to prevent caching captchaImage.src = '/captcha.php?t=' + new Date().getTime(); } } function submitAnalysis() { const captchaInput = document.getElementById('captchaInput'); const captchaCode = captchaInput ? captchaInput.value.trim() : ''; const analysisStatus = document.getElementById('analysisStatus'); if (!captchaCode) { showToast('画像のコードを入力してください', 'error'); return; } // Show loading state analysisStatus.textContent = 'リクエストを送信中...'; analysisStatus.className = 'analysis-status'; analysisStatus.style.display = 'block'; // Prepare data for API request type = "none"; if(document.getElementById('domain_name')){ type = "domain_analysis"; sendData = document.getElementById('domain_name').value; } if(document.getElementById('phrase')){ type = "phrase_analysis"; sendData = document.getElementById('phrase').value; } if(type=="none"){ // Show error message closeCaptchaModal(); analysisStatus.textContent = 'エラー: タイプはなしです'; analysisStatus.className = 'analysis-status error'; analysisStatus.style.display = 'block'; return false; } const data = { type: type, data: sendData, captcha_code: captchaCode }; // Send request to API sendRequest('/ajax/', data, function(response) { // Close captcha modal closeCaptchaModal(); if (response.status === 'ok') { // Show success message analysisStatus.textContent = 'リクエストが送信されました。ドメインの検証をお待ちください。'; analysisStatus.className = 'analysis-status success'; } else { // Show error message analysisStatus.textContent = 'エラー: ' + response.error; analysisStatus.className = 'analysis-status error'; // Refresh captcha on error if (response.refresh_captcha) { setTimeout(refreshCaptcha, 500); } } analysisStatus.style.display = 'block'; // Clear captcha input if (captchaInput) { captchaInput.value = ''; } }); } </script> <script> // Full screenshot functionality function openFullScreenshot(src) { const modal = document.getElementById('fullScreenshotModal'); const img = document.getElementById('fullScreenshotImg'); img.src = src; modal.style.display = 'flex'; // Prevent body scroll when modal is open document.body.style.overflow = 'hidden'; } function closeFullScreenshot() { const modal = document.getElementById('fullScreenshotModal'); modal.style.display = 'none'; // Restore body scroll document.body.style.overflow = 'auto'; } function downloadScreenshot() { const img = document.getElementById('fullScreenshotImg'); const link = document.createElement('a'); link.download = `screenshot-${document.getElementById('domain_name').value}-${new Date().getTime()}.jpg`; link.href = img.src; link.click(); } // Close modal on outside click document.addEventListener('click', function(e) { const modal = document.getElementById('fullScreenshotModal'); if (e.target === modal) { closeFullScreenshot(); } }); // Close modal on Escape key document.addEventListener('keydown', function(e) { if (e.key === 'Escape') { closeFullScreenshot(); } }); </script> <script> // Unified authentication controller. All user-visible source strings are English and translated server-side. let telegramStatusInterval = null; let telegramTimerInterval = null; let qrcodeInstance = null; window.telegramBotUrl = null; function authText(key) { const messages = { registrationVerification: 'アカウントが作成されました。サインインする前にメールを確認し、確認リンクをクリックしてください。', loginSuccess: '正常にサインインしました。', emailVerified: 'メールアドレスが確認されました。アカウントはアクティブであり、サインインしています。', emailTelegramPending: 'メールアドレスが確認されました。 Telegram の認証を完了してアカウントを有効にします。', emailVerificationFailed: 'この電子メール検証リンクは無効であるか、期限切れです。', googleSuccess: 'Googleでサインインしました。', githubSuccess: 'GitHub でサインインしました。', oauthCancelled: '外部サインインがキャンセルされました。', oauthFailed: '外部サインインに失敗しました。もう一度試してください。', telegramVerifiedEmailPending: '電報の認証が完了しました。メールを確認し、確認リンクをクリックしてアカウントを有効にします。', telegramRegistrationComplete: '登録が完了しました。これでサインインしました。', telegramLoginSuccess: 'Telegram でサインインしました。', invalidResponse: 'サーバーからの無効な応答' }; return messages[key] || key; } function startOAuth(provider) { if (!['google', 'github'].includes(provider)) { showToast('サポートされていないサインイン プロバイダーです。', 'error'); return; } window.location.href = '/auth/' + provider + '.php?lang=' + encodeURIComponent(window.currentLang || 'en'); } function showEmailVerificationModal(email, message) { const normalizedEmail = (email || '').trim().toLowerCase(); if (normalizedEmail) localStorage.setItem('pending_verification_email', normalizedEmail); const address = document.getElementById('emailVerifyAddress'); const messageEl = document.getElementById('emailVerifyMessage'); if (address) address.textContent = normalizedEmail; if (messageEl) messageEl.textContent = message || '受信箱を確認し、送信された確認リンクをクリックしてください。電子メールが認証されるまで、パスワードを使用してサインインすることはできません。'; const modal = document.getElementById('emailVerifyModal'); if (modal) modal.style.display = 'flex'; } function closeEmailVerifyModal() { const modal = document.getElementById('emailVerifyModal'); if (modal) modal.style.display = 'none'; } function resendEmailVerification() { const email = (localStorage.getItem('pending_verification_email') || '').trim().toLowerCase(); if (!email) { showToast('確認メッセージを再送信するには、電子メールを再度入力してください。', 'error'); closeEmailVerifyModal(); return; } const button = document.getElementById('resendEmailBtn'); if (button) button.disabled = true; sendRequest('/ajax/', {type: 'resend_email_verification', email: email}, function(response) { if (button) button.disabled = false; if (response.status === 'ok') showToast(response.message, 'success'); else showToast(response.error || '現在確認メールを送信できませんでした。後でもう一度試してください。', 'error'); }); } function processLogin() { const email = document.getElementById('loginEmail').value.trim().toLowerCase(); const password = document.getElementById('loginPassword').value; clearErrors(['loginEmail', 'loginPassword']); let hasErrors = false; if (!email) { showError('loginEmail', 'メールアドレスを入力してください'); hasErrors = true; } else if (!/\S+@\S+\.\S+/.test(email)) { showError('loginEmail', '有効なメールアドレスを入力してください'); hasErrors = true; } if (!password) { showError('loginPassword', 'パスワードを入力してください'); hasErrors = true; } if (hasErrors) return; sendRequest('/ajax/', {type: 'login', email: email, password: password}, function(response) { if (response.status === 'ok') { closeModal('authModal'); showToast(response.message || authText('loginSuccess'), 'success'); window.setTimeout(function(){ window.location.href = '/' + window.currentLang + '/profile/'; }, 250); return; } if (response.status === 'verification_required') { closeModal('authModal'); showEmailVerificationModal(response.email || email, response.message); showToast(response.message, 'info'); return; } showError('loginPassword', response.error || 'メールアドレスまたはパスワードが無効です。'); }); } function processRegistration() { const name = document.getElementById('registerName').value.trim(); const email = document.getElementById('registerEmail').value.trim().toLowerCase(); const password = document.getElementById('registerPassword').value; const confirmPassword = document.getElementById('registerConfirmPassword').value; clearErrors(['registerName', 'registerEmail', 'registerPassword', 'registerConfirmPassword']); let hasErrors = false; if (!name) { showError('registerName', 'あなたの名前を入力してください'); hasErrors = true; } if (!email) { showError('registerEmail', 'メールアドレスを入力してください'); hasErrors = true; } else if (!/\S+@\S+\.\S+/.test(email)) { showError('registerEmail', '有効なメールアドレスを入力してください'); hasErrors = true; } if (!password) { showError('registerPassword', 'パスワードを作成してください'); hasErrors = true; } else if (password.length < 8) { showError('registerPassword', 'パスワードは8文字以上である必要があります'); hasErrors = true; } if (!confirmPassword) { showError('registerConfirmPassword', 'パスワードを確認してください'); hasErrors = true; } else if (password !== confirmPassword) { showError('registerConfirmPassword', 'パスワードが一致しません'); hasErrors = true; } if (hasErrors) return; sendRequest('/ajax/', {type: 'registration', name: name, email: email, password: password}, function(response) { if (response.status === 'verification_required') { localStorage.setItem('pending_verification_email', response.email || email); closeModal('authModal'); showEmailVerificationModal(response.email || email, response.message || authText('registrationVerification')); showToast(response.message || authText('registrationVerification'), response.email_sent === false ? 'info' : 'success'); return; } if (response.status === 'ok') { showToast(response.message || authText('registrationVerification'), 'success'); return; } showToast(response.error || '登録を完了できませんでした。もう一度試してください。', 'error'); }); } function clearRegistrationForm() { ['registerName','registerEmail','registerPassword','registerConfirmPassword'].forEach(function(id){ const el=document.getElementById(id); if (el) el.value=''; }); clearErrors(['registerName','registerEmail','registerPassword','registerConfirmPassword']); } function generateQRCode(url) { const container = document.getElementById('qrcode'); if (!container) return; container.innerHTML = ''; if (typeof QRCode === 'undefined') return; qrcodeInstance = new QRCode(container, {text: url, width: 200, height: 200, correctLevel: QRCode.CorrectLevel.M}); } function openTelegramBot() { if (window.telegramBotUrl) window.open(window.telegramBotUrl, '_blank', 'noopener'); } function refreshTelegramQR() { if (window.telegramBotUrl) generateQRCode(window.telegramBotUrl); } function clearTelegramTimers() { if (telegramTimerInterval) { clearInterval(telegramTimerInterval); telegramTimerInterval = null; } if (telegramStatusInterval) { clearInterval(telegramStatusInterval); telegramStatusInterval = null; } } function startTelegramTimer(duration) { clearInterval(telegramTimerInterval); let remaining = Math.max(0, parseInt(duration || 300, 10)); const timer = document.getElementById('timer'); function render() { if (!timer) return; const m = Math.floor(remaining / 60), s = remaining % 60; timer.textContent = String(m).padStart(2,'0') + ':' + String(s).padStart(2,'0'); } render(); telegramTimerInterval = setInterval(function(){ remaining -= 1; render(); if (remaining < 0) { clearTelegramTimers(); showToast('テレグラム検証リンクの有効期限が切れました。もう一度始めてください。', 'error'); } }, 1000); } function showTelegramVerifyModal(botUsername, telegramToken, deepLink, expiresRemaining) { window.telegramBotUrl = deepLink || ('https://t.me/' + botUsername + '?start=' + encodeURIComponent(telegramToken)); const title = document.querySelector('#telegramVerifyModal .modal-title'); const text = document.getElementById('telegramModalText'); if (title) title.textContent = 'Telegramへの登録を完了する'; if (text) text.innerHTML = 'Telegram が新しいタブで開きました。<br>自分の電話番号をボットと共有してから、ここに戻ってください。メール認証も必要です。'; generateQRCode(window.telegramBotUrl); startTelegramTimer(expiresRemaining || 600); const modal = document.getElementById('telegramVerifyModal'); if (modal) modal.style.display='flex'; window.open(window.telegramBotUrl, '_blank', 'noopener'); } function closeTelegramVerifyModal() { clearTelegramTimers(); const modal = document.getElementById('telegramVerifyModal'); if (modal) modal.style.display='none'; } function startTelegramRegistration() { const name = document.getElementById('registerName').value.trim(); const email = document.getElementById('registerEmail').value.trim().toLowerCase(); clearErrors(['registerName','registerEmail']); let hasErrors=false; if (!name) { showError('registerName','あなたの名前を入力してください'); hasErrors=true; } if (!email) { showError('registerEmail','メールアドレスを入力してください'); hasErrors=true; } else if (!/\S+@\S+\.\S+/.test(email)) { showError('registerEmail','有効なメールアドレスを入力してください'); hasErrors=true; } if (hasErrors) return; localStorage.setItem('pending_verification_email', email); sendRequest('/ajax/', {type:'telegram_registration_start',name:name,email:email}, function(response){ if (response.status === 'continue_email_verification') { closeModal('authModal'); showEmailVerificationModal(email, 'Telegram はこのアカウントに対してすでに認証されています。メールアドレスを確認してアカウントを有効にします。'); showToast('メール認証は引き続き必要です。', 'info'); return; } if (response.status !== 'ok') { showToast(response.error || '電報登録を開始できませんでした。','error'); return; } localStorage.setItem('current_user_token', response.user_token); closeModal('authModal'); if (response.email_sent === false) { showToast(response.message || '確認メールを送信できませんでした。確認メールの再送信をご利用ください。','info'); } showTelegramVerifyModal(response.bot_username,response.telegram_token,response.deep_link,response.telegram_token_expires_remaining); startTelegramRegistrationStatusCheck(response.user_token, email); }); } function startTelegramRegistrationStatusCheck(userToken, email) { clearInterval(telegramStatusInterval); telegramStatusInterval=setInterval(function(){ checkTelegramVerificationStatus(userToken,email); }, 4000); checkTelegramVerificationStatus(userToken,email); } function checkTelegramVerificationStatus(userToken, email) { sendRequest('/ajax/', {type:'check_telegram_verification',user_token:userToken}, function(response){ if (response.status !== 'ok') return; if (response.account_verified) { clearTelegramTimers(); completeRegistration(); return; } if (response.telegram_verified && !response.email_verified) { clearTelegramTimers(); closeTelegramVerifyModal(); showEmailVerificationModal(email, authText('telegramVerifiedEmailPending')); showToast(authText('telegramVerifiedEmailPending'),'info'); } }, false); } function completeRegistration() { const token=localStorage.getItem('current_user_token') || ''; if (!token) return; sendRequest('/ajax/', {type:'complete_registration',user_token:token}, function(response){ if (response.status !== 'ok') { showToast(response.error || '登録を完了できませんでした。','error'); return; } localStorage.removeItem('current_user_token'); localStorage.removeItem('pending_verification_email'); closeTelegramVerifyModal(); closeEmailVerifyModal(); showToast(response.message || authText('telegramRegistrationComplete'),'success'); window.setTimeout(function(){window.location.href='/' + window.currentLang + '/profile/';},250); }); } function loginWithTelegram() { const email=document.getElementById('loginEmail').value.trim().toLowerCase(); clearErrors(['loginEmail']); if (!email) { showError('loginEmail','メールアドレスを入力してください'); return; } if (!/\S+@\S+\.\S+/.test(email)) { showError('loginEmail','有効なメールアドレスを入力してください'); return; } generateTelegramLoginLink(email); } function generateTelegramLoginLink(email) { sendRequest('/ajax/', {type:'generate_telegram_login_link',email:email}, function(response){ if (response.status !== 'ok') { showToast(response.error || 'Telegram ログインを開始できません','error'); return; } localStorage.setItem('current_user_token',response.user_token); closeModal('authModal'); showTelegramLoginModal(response.bot_username,response.deep_link,response.telegram_token_expires_remaining); }); } function showTelegramLoginModal(botUsername,deepLink,expiresRemaining) { window.telegramBotUrl=deepLink; const title=document.querySelector('#telegramVerifyModal .modal-title'); const text=document.getElementById('telegramModalText'); if (title) title.textContent='テレグラム経由でログイン'; if (text) text.innerHTML='Telegram ボットを開いてサインイン コードを受け取り、この Web サイトにコードを入力します。'; generateQRCode(window.telegramBotUrl); startTelegramTimer(expiresRemaining || 300); const modal=document.getElementById('telegramVerifyModal'); if (modal) modal.style.display='flex'; window.open(window.telegramBotUrl,'_blank','noopener'); startTelegramLoginStatusCheck(); } function startTelegramLoginStatusCheck() { const token=localStorage.getItem('current_user_token') || ''; clearInterval(telegramStatusInterval); telegramStatusInterval=setInterval(function(){checkTelegramLoginStatus(token);},3000); checkTelegramLoginStatus(token); } function checkTelegramLoginStatus(userToken) { if (!userToken) return; sendRequest('/ajax/', {type:'check_telegram_login_status',user_token:userToken}, function(response){ if (response.status==='ok' && response.login_code_sended) { clearTelegramTimers(); closeTelegramVerifyModal(); const codeModal=document.getElementById('telegramCodeModal'); if (codeModal) codeModal.style.display='flex'; startLoginCodeTimer(300); } },false); } function startLoginCodeTimer(duration) { let remaining=parseInt(duration||300,10); const timer=document.getElementById('loginTimer'); if (window.loginCodeTimer) clearInterval(window.loginCodeTimer); window.loginCodeTimer=setInterval(function(){ const m=Math.max(0,Math.floor(remaining/60)), s=Math.max(0,remaining%60); if (timer) timer.textContent=String(m).padStart(2,'0')+':'+String(s).padStart(2,'0'); remaining--; if (remaining<0) { clearInterval(window.loginCodeTimer); showToast('コードの有効期限が切れました。最初からやり直してください。','error'); } },1000); } function verifyTelegramLoginCode() { const token=localStorage.getItem('current_user_token') || ''; const input=document.getElementById('telegramLoginCode'); const code=input ? input.value.trim() : ''; clearErrors(['telegramLoginCode']); if (!/^\\d{7}$/.test(code)) { showError('telegramLoginCode','コードには 7 桁の数字が含まれている必要があります'); return; } sendRequest('/ajax/', {type:'verify_telegram_login_code',user_token:token,verify_code:code}, function(response){ if (response.status!=='ok') { showError('telegramLoginCode',response.error || 'コードが間違っているか、期限切れです'); return; } if (window.loginCodeTimer) clearInterval(window.loginCodeTimer); localStorage.removeItem('current_user_token'); closeModal('telegramCodeModal'); showToast(response.message || authText('telegramLoginSuccess'),'success'); window.setTimeout(function(){window.location.href='/' + window.currentLang + '/profile/';},250); }); } // Old registration-status probing is intentionally disabled: it leaked account state and duplicated the new verification flow. function checkRegistrationStatus() { return; } function closeVisibleAuthModal() { for (const id of ['telegramCodeModal','telegramVerifyModal','emailVerifyModal','authModal']) { const modal=document.getElementById(id); if (!modal || modal.style.display !== 'flex') continue; if (id === 'telegramVerifyModal') closeTelegramVerifyModal(); else if (id === 'emailVerifyModal') closeEmailVerifyModal(); else closeModal(id); return; } } document.addEventListener('click', function(event){ const target=event.target; if (!(target instanceof HTMLElement) || !target.classList.contains('modal')) return; const id=target.id; if (!['telegramCodeModal','telegramVerifyModal','emailVerifyModal'].includes(id)) return; if (id === 'telegramVerifyModal') closeTelegramVerifyModal(); else if (id === 'emailVerifyModal') closeEmailVerifyModal(); else closeModal(id); }); document.addEventListener('keydown', function(event){ if (event.key === 'Escape') closeVisibleAuthModal(); }); function handleAuthNotice() { const url=new URL(window.location.href); const notice=url.searchParams.get('auth'); if (!notice) return; const map={ email_verified:[authText('emailVerified'),'success'], email_verified_telegram_pending:[authText('emailTelegramPending'),'info'], email_verification_failed:[authText('emailVerificationFailed'),'error'], google_success:[authText('googleSuccess'),'success'], github_success:[authText('githubSuccess'),'success'], oauth_cancelled:[authText('oauthCancelled'),'info'], oauth_failed:[authText('oauthFailed'),'error'] }; if (map[notice]) showToast(map[notice][0],map[notice][1]); url.searchParams.delete('auth'); window.history.replaceState({},document.title,url.pathname+url.search+url.hash); } document.addEventListener('DOMContentLoaded',handleAuthNotice); </script> <!-- Add this JavaScript before closing body tag --> <script> // Animation on scroll document.addEventListener('DOMContentLoaded', function() { const fadeElements = document.querySelectorAll('.fade-in'); const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add('visible'); } }); }, { threshold: 0.1 }); fadeElements.forEach(element => { observer.observe(element); }); // Add click handlers for project CTAs document.querySelectorAll('.project-cta').forEach(button => { button.addEventListener('click', function() { const projectTitle = this.closest('.project-card').querySelector('.project-title').textContent; // In production, this would navigate to the appropriate page }); }); }); </script> <script> // Cookie consent management (function() { // Configuration const COOKIE_CONSENT_KEY = 'serpuls_cookie_consent'; const COOKIE_EXPIRY_DAYS = 365; // Create cookie consent banner if not already accepted function initCookieConsent() { // Check if user has already made a choice const consent = getCookieConsent(); if (consent !== null) { // User already made a choice, apply preferences applyCookiePreferences(consent); return; } // Create banner createCookieBanner(); } // Create the cookie banner HTML function createCookieBanner() { const banner = document.createElement('div'); banner.id = 'cookie-consent-banner'; banner.style.cssText = ` position: fixed; bottom: 0; left: 0; right: 0; background: white; box-shadow: 0 -4px 20px rgba(0,0,0,0.15); padding: 1.5rem; z-index: 9999; border-top: 3px solid var(--primary, #4361ee); animation: slideUp 0.5s ease; `; // Add animation style if not exists if (!document.getElementById('cookie-animation-style')) { const style = document.createElement('style'); style.id = 'cookie-animation-style'; style.textContent = ` @keyframes slideUp { from { transform: translateY(100%); } to { transform: translateY(0); } } `; document.head.appendChild(style); } banner.innerHTML = ` <div style="max-width: 1200px; margin: 0 auto; display: flex; flex-wrap: wrap; gap: 2rem; align-items: center; justify-content: space-between;"> <div style="flex: 2; min-width: 280px;"> <h3 style="margin: 0 0 0.5rem 0; color: var(--dark, #212529); font-size: 1.25rem;">🍪 クッキーの同意</h3> <p style="margin: 0; color: var(--gray, #6c757d); line-height: 1.5;"> 当社は、ブラウジングエクスペリエンスを向上させ、パーソナライズされたコンテンツを提供し、トラフィックを分析するために Cookie を使用します。 [すべて同意する] をクリックすると、Cookie の使用に同意したことになります。 私たちの記事を読んでください <a href="/en/cookie/" style="color: var(--primary, #4361ee); text-decoration: underline;">クッキーポリシー</a> そして <a href="/en/privacy/" style="color: var(--primary, #4361ee); text-decoration: underline;">プライバシーポリシー</a>. </p> </div> <div style="display: flex; gap: 1rem; flex-wrap: wrap; align-items: center;"> <button id="cookie-accept-all" style="padding: 0.75rem 1.5rem; background: linear-gradient(135deg, var(--primary, #4361ee), var(--secondary, #7209b7)); color: white; border: none; border-radius: var(--border-radius, 8px); font-weight: 600; cursor: pointer; transition: all 0.3s ease; box-shadow: 0 4px 6px rgba(67,97,238,0.3);"> すべてを受け入れる </button> <button id="cookie-accept-essential" style="padding: 0.75rem 1.5rem; background: white; color: var(--dark, #212529); border: 1px solid var(--gray, #6c757d); border-radius: var(--border-radius, 8px); font-weight: 600; cursor: pointer; transition: all 0.3s ease;"> 必須のみ </button> <button id="cookie-settings" style="padding: 0.75rem 1.5rem; background: transparent; color: var(--primary, #4361ee); border: none; font-weight: 600; cursor: pointer; text-decoration: underline;"> カスタマイズ </button> </div> </div> `; document.body.appendChild(banner); // Add event listeners document.getElementById('cookie-accept-all').addEventListener('click', function() { acceptAllCookies(); }); document.getElementById('cookie-accept-essential').addEventListener('click', function() { acceptEssentialCookies(); }); document.getElementById('cookie-settings').addEventListener('click', function() { showCookieSettings(); }); } // Show cookie settings modal function showCookieSettings() { // Remove existing modal if any const existingModal = document.getElementById('cookie-settings-modal'); if (existingModal) existingModal.remove(); const modal = document.createElement('div'); modal.id = 'cookie-settings-modal'; modal.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 10000; animation: fadeIn 0.3s ease; `; modal.innerHTML = ` <div style="background: white; border-radius: var(--border-radius, 8px); max-width: 500px; width: 90%; max-height: 80vh; overflow-y: auto; box-shadow: 0 10px 25px rgba(0,0,0,0.2);"> <div style="padding: 1.5rem; border-bottom: 1px solid #e9ecef;"> <h3 style="margin: 0; color: var(--dark, #212529);">クッキーの設定</h3> </div> <div style="padding: 1.5rem;"> <p style="color: var(--gray, #6c757d); margin-bottom: 1.5rem;">許可する Cookie を選択します。これらの設定はいつでも変更できます。</p> <div style="margin-bottom: 1rem; padding: 1rem; background: #f8f9fa; border-radius: var(--border-radius, 8px);"> <label style="display: flex; align-items: center; gap: 0.75rem; cursor: not-allowed;"> <input type="checkbox" checked disabled style="width: 18px; height: 18px;"> <div> <strong style="color: var(--dark, #212529);">必須のクッキー</strong> <p style="margin: 0.25rem 0 0; font-size: 0.9rem; color: var(--gray, #6c757d);">Web サイトが機能するために必要です。無効にすることはできません。</p> </div> </label> </div> <div style="margin-bottom: 1rem; padding: 1rem; background: #f8f9fa; border-radius: var(--border-radius, 8px);"> <label style="display: flex; align-items: center; gap: 0.75rem; cursor: pointer;"> <input type="checkbox" id="cookie-pref-analytics" checked style="width: 18px; height: 18px;"> <div> <strong style="color: var(--dark, #212529);">分析クッキー</strong> <p style="margin: 0.25rem 0 0; font-size: 0.9rem; color: var(--gray, #6c757d);">訪問者が当社のサイトとどのようにやり取りするかを理解するのに役立ちます。</p> </div> </label> </div> <div style="margin-bottom: 1rem; padding: 1rem; background: #f8f9fa; border-radius: var(--border-radius, 8px);"> <label style="display: flex; align-items: center; gap: 0.75rem; cursor: pointer;"> <input type="checkbox" id="cookie-pref-functional" checked style="width: 18px; height: 18px;"> <div> <strong style="color: var(--dark, #212529);">ファンクショナルクッキー</strong> <p style="margin: 0.25rem 0 0; font-size: 0.9rem; color: var(--gray, #6c757d);">設定と設定を覚えておいてください。</p> </div> </label> </div> <div style="margin-bottom: 1rem; padding: 1rem; background: #f8f9fa; border-radius: var(--border-radius, 8px);"> <label style="display: flex; align-items: center; gap: 0.75rem; cursor: pointer;"> <input type="checkbox" id="cookie-pref-targeting" checked style="width: 18px; height: 18px;"> <div> <strong style="color: var(--dark, #212529);">ターゲティング Cookie</strong> <p style="margin: 0.25rem 0 0; font-size: 0.9rem; color: var(--gray, #6c757d);">関連するコンテンツや広告を配信するために使用されます。</p> </div> </label> </div> </div> <div style="padding: 1.5rem; border-top: 1px solid #e9ecef; display: flex; gap: 1rem; justify-content: flex-end;"> <button id="cookie-save-preferences" style="padding: 0.75rem 1.5rem; background: linear-gradient(135deg, var(--primary, #4361ee), var(--secondary, #7209b7)); color: white; border: none; border-radius: var(--border-radius, 8px); font-weight: 600; cursor: pointer;"> 設定の保存 </button> <button id="cookie-close-modal" style="padding: 0.75rem 1.5rem; background: white; color: var(--dark, #212529); border: 1px solid var(--gray, #6c757d); border-radius: var(--border-radius, 8px); font-weight: 600; cursor: pointer;"> キャンセル </button> </div> </div> `; document.body.appendChild(modal); // Close modal handlers document.getElementById('cookie-close-modal').addEventListener('click', function() { modal.remove(); }); document.getElementById('cookie-save-preferences').addEventListener('click', function() { const preferences = { essential: true, // Always true analytics: document.getElementById('cookie-pref-analytics').checked, functional: document.getElementById('cookie-pref-functional').checked, targeting: document.getElementById('cookie-pref-targeting').checked }; saveCookiePreferences(preferences); modal.remove(); removeCookieBanner(); }); // Click outside to close modal.addEventListener('click', function(e) { if (e.target === modal) { modal.remove(); } }); } // Accept all cookies function acceptAllCookies() { const preferences = { essential: true, analytics: true, functional: true, targeting: true }; saveCookiePreferences(preferences); removeCookieBanner(); } // Accept only essential cookies function acceptEssentialCookies() { const preferences = { essential: true, analytics: false, functional: false, targeting: false }; saveCookiePreferences(preferences); removeCookieBanner(); } // Save cookie preferences function saveCookiePreferences(preferences) { // Save to cookie const expiryDate = new Date(); expiryDate.setDate(expiryDate.getDate() + COOKIE_EXPIRY_DAYS); const cookieValue = JSON.stringify(preferences); document.cookie = `${COOKIE_CONSENT_KEY}=${encodeURIComponent(cookieValue)}; expires=${expiryDate.toUTCString()}; path=/; SameSite=Lax`; // Apply preferences applyCookiePreferences(preferences); // Store in localStorage for quick access localStorage.setItem(COOKIE_CONSENT_KEY, cookieValue); // Optional: send event to analytics if (typeof window.gtag !== 'undefined' && preferences.analytics) { window.gtag('consent', 'update', { 'analytics_storage': 'granted' }); } } // Get cookie consent from cookie or localStorage function getCookieConsent() { // Try to get from cookie first const match = document.cookie.match(new RegExp('(^| )' + COOKIE_CONSENT_KEY + '=([^;]+)')); if (match) { try { return JSON.parse(decodeURIComponent(match[2])); } catch (e) { console.error('Cookie 同意の解析エラー:', e); } } // Try localStorage const stored = localStorage.getItem(COOKIE_CONSENT_KEY); if (stored) { try { return JSON.parse(stored); } catch (e) { console.error('localStorage 同意の解析エラー:', e); } } return null; } // Apply cookie preferences (enable/disable features) function applyCookiePreferences(preferences) { if (!preferences) return; // Example: Disable analytics if not consented if (!preferences.analytics) { // Disable Google Analytics if present if (typeof window.gtag !== 'undefined') { window.gtag('consent', 'default', { 'analytics_storage': 'denied' }); } // Optionally remove existing analytics cookies // This is handled by the analytics script itself usually } // Disable targeting cookies if (!preferences.targeting) { // Disable ad-related tracking // Remove Facebook pixel, etc. } // Store preferences in data attribute for other scripts to check document.documentElement.setAttribute('data-cookie-consent', JSON.stringify(preferences)); // Dispatch event for other scripts to react window.dispatchEvent(new CustomEvent('cookieConsentChanged', { detail: preferences })); } // Remove cookie banner function removeCookieBanner() { const banner = document.getElementById('cookie-consent-banner'); if (banner) { banner.style.animation = 'slideDown 0.5s ease forwards'; setTimeout(() => { if (banner.parentNode) banner.remove(); }, 500); // Add slideDown animation if not exists if (!document.getElementById('cookie-animation-slidedown')) { const style = document.createElement('style'); style.id = 'cookie-animation-slidedown'; style.textContent = ` @keyframes slideDown { from { transform: translateY(0); } to { transform: translateY(100%); } } `; document.head.appendChild(style); } } } // Add a floating button to reopen cookie settings (optional) function addCookieSettingsButton() { const btn = document.createElement('button'); btn.id = 'cookie-settings-button'; btn.innerHTML = '🍪'; btn.setAttribute('aria-label', 'クッキーの設定'); btn.style.cssText = ` position: fixed; bottom: 20px; left: 20px; width: 48px; height: 48px; border-radius: 50%; background: white; border: 1px solid var(--gray, #6c757d); box-shadow: var(--box-shadow, 0 4px 6px rgba(0,0,0,0.1)); cursor: pointer; font-size: 1.2rem; display: flex; align-items: center; justify-content: center; z-index: 9998; transition: var(--transition, all 0.3s ease); opacity: 0.8; `; btn.addEventListener('mouseenter', function() { this.style.opacity = '1'; this.style.transform = 'scale(1.1)'; }); btn.addEventListener('mouseleave', function() { this.style.opacity = '0.8'; this.style.transform = 'scale(1)'; }); btn.addEventListener('click', function() { showCookieSettings(); }); document.body.appendChild(btn); } // Initialize on DOM ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initCookieConsent); } else { initCookieConsent(); } // Optional: Add settings button after consent is given window.addEventListener('cookieConsentChanged', function() { // Only show settings button if not already present if (!document.getElementById('cookie-settings-button')) { //addCookieSettingsButton(); } }); // If consent already exists, add settings button const existingConsent = getCookieConsent(); if (existingConsent && !document.getElementById('cookie-settings-button')) { document.addEventListener('DOMContentLoaded', function() { //addCookieSettingsButton(); }); } })(); </script> </body> </html>