サブスク自動整理・管理サービス

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>SUBS Management - サブスク自動整理・管理サービス</title>
  <meta name="description" content="複数の定額サービスを一括管理・整理し、解約し忘れを防止して家計を賢くするプラットフォーム">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <!-- ==========================
       外部ライブラリ (CDN)
  ========================== -->
  <!-- GSAP + ScrollTrigger -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"
    integrity="sha512-xxxx" 
    crossorigin="anonymous" 
    referrerpolicy="no-referrer">
  </script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js"
    integrity="sha512-xxxx" 
    crossorigin="anonymous" 
    referrerpolicy="no-referrer">
  </script>
  <!-- Three.js -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r152/three.min.js"
    integrity="sha512-xxxx" 
    crossorigin="anonymous" 
    referrerpolicy="no-referrer">
  </script>

  <style>
    /* ==========================
       ベーススタイル
    ========================== */
    * {
      box-sizing: border-box;
    }
    html, body {
      margin: 0; 
      padding: 0;
      font-family: sans-serif;
      color: #333;
      background-color: #f9f9f9;
      line-height: 1.6;
      scroll-behavior: smooth; /* スムーズスクロール */
    }
    a {
      text-decoration: none;
      color: #0078d7;
      transition: color 0.2s ease;
    }
    a:hover {
      color: #005bb5;
    }
    h1, h2, h3, h4 {
      margin: 0;
      font-weight: normal;
    }
    p {
      margin: 0 0 1em;
    }
    ul {
      margin: 0; 
      padding: 0; 
      list-style: none;
    }
    img {
      max-width: 100%;
      display: block;
    }

    /* ==========================
       カラー変数
    ========================== */
    :root {
      --color-primary: #0078d7;
      --color-accent: #ffcc00;
      --color-bg: #f9f9f9;
      --color-white: #ffffff;
      --color-border: #eee;
      --color-text: #333;
    }

    /* ==========================
       全体のレイアウト用コンテナ
    ========================== */
    .container {
      width: 90%;
      max-width: 1200px;
      margin: 0 auto;
      padding: 60px 0;
    }

    /* ==========================
       ヘッダー
    ========================== */
    header {
      position: sticky;
      top: 0;
      z-index: 998;
      background: var(--color-white);
      box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    }
    .header-inner {
      display: flex;
      align-items: center;
      justify-content: space-between;
      height: 60px;
      width: 90%;
      max-width: 1200px;
      margin: 0 auto;
    }
    .header-logo {
      font-size: 1.5rem;
      color: var(--color-primary);
      font-weight: bold;
    }
    nav ul {
      display: flex;
      gap: 30px;
    }
    nav a {
      color: var(--color-text);
      font-size: 0.95rem;
      font-weight: bold;
    }
    nav a:hover {
      color: var(--color-primary);
    }
    .hamburger {
      display: none;
      font-size: 1.5rem;
      cursor: pointer;
    }
    .mobile-nav {
      display: none;
      position: absolute;
      top: 60px; right: 0;
      width: 100%;
      max-width: 300px;
      background: var(--color-white);
      box-shadow: 0 2px 8px rgba(0,0,0,0.2);
      z-index: 999;
    }
    .mobile-nav ul {
      display: flex; 
      flex-direction: column;
      gap: 20px; 
      padding: 20px;
    }
    .mobile-nav a {
      font-size: 1rem; 
      color: var(--color-text);
    }

    /* ==========================
       Three.js 背景キャンバス
    ========================== */
    #three-bg {
      position: fixed;
      top: 0; left: 0;
      width: 100vw; height: 100vh;
      z-index: -1;
      background: #000; /* 読み込み時の仮色 */
    }

    /* ==========================
       ヒーローセクション
    ========================== */
    .hero {
      position: relative;
      min-height: 80vh;
      display: flex;
      align-items: center;
      justify-content: center;
      text-align: center;
      color: #fff;
      padding: 100px 20px;
      overflow: hidden;
    }
    .hero h1 {
      font-size: 2.5rem;
      margin-bottom: 20px;
      line-height: 1.2;
      transform: translateY(50px);
      opacity: 0;
    }
    .hero p {
      font-size: 1.1rem;
      margin-bottom: 30px;
      transform: translateY(50px);
      opacity: 0;
    }
    .hero .cta-btn {
      background: var(--color-accent);
      color: #333;
      padding: 15px 30px;
      border-radius: 6px;
      font-size: 1rem;
      font-weight: bold;
      transition: background-color 0.2s ease;
      transform: translateY(50px);
      opacity: 0;
    }
    .hero .cta-btn:hover {
      background: #e6b800;
    }

    /* ==========================
       Statsセクション (数値カウンター)
    ========================== */
    .stats-section {
      background: var(--color-bg);
      text-align: center;
      padding: 60px 20px;
    }
    .stats-section h2 {
      color: var(--color-primary);
      margin-bottom: 40px;
    }
    .stats-grid {
      display: flex; 
      flex-wrap: wrap; 
      gap: 40px; 
      justify-content: center;
    }
    .stat-item {
      flex: 1 1 200px;
      max-width: 250px;
      background: var(--color-white);
      border-radius: 8px;
      box-shadow: 0 2px 5px rgba(0,0,0,0.1);
      padding: 20px;
      transition: transform 0.3s ease;
    }
    .stat-item:hover { transform: translateY(-5px); }
    .stat-number {
      font-size: 2rem;
      color: var(--color-primary);
      font-weight: bold;
      margin-bottom: 10px;
    }
    .stat-label {
      font-size: 0.95rem;
      color: #666;
    }

    /* ==========================
       Testimonials (カルーセル)
    ========================== */
    .testimonials-section {
      background: var(--color-white);
      padding: 60px 20px;
    }
    .testimonials-section h2 {
      text-align: center;
      color: var(--color-primary);
      margin-bottom: 40px;
    }
    .carousel-container {
      max-width: 800px; 
      margin: 0 auto; 
      position: relative;
      overflow: hidden;
    }
    .carousel-track {
      display: flex; 
      transition: transform 0.5s ease;
    }
    .carousel-slide {
      min-width: 100%;
      background: var(--color-bg);
      border-radius: 8px;
      box-shadow: 0 2px 5px rgba(0,0,0,0.1);
      margin: 0 10px;
      padding: 30px;
      text-align: center;
    }
    .carousel-text {
      font-size: 1rem;
      font-style: italic;
      color: #555;
      margin-bottom: 10px;
    }
    .carousel-author {
      font-weight: bold;
      color: #333;
      font-size: 0.9rem;
    }
    .carousel-buttons {
      text-align: center;
      margin-top: 20px;
    }
    .carousel-btn {
      background: var(--color-primary);
      color: #fff;
      border: none;
      padding: 8px 12px;
      border-radius: 4px;
      margin: 0 5px;
      cursor: pointer;
      font-size: 1rem;
    }
    .carousel-btn:hover {
      background: #005bb5;
    }

    /* ==========================
       フォームセクション (リアルタイムバリデーション)
    ========================== */
    .form-section {
      background: var(--color-accent);
      padding: 60px 20px;
    }
    .form-section h2 {
      text-align: center;
      color: var(--color-primary);
      margin-bottom: 40px;
    }
    .signup-form {
      max-width: 600px;
      margin: 0 auto;
      background: var(--color-white);
      border-radius: 8px;
      box-shadow: 0 2px 8px rgba(0,0,0,0.1);
      padding: 30px;
    }
    .form-group {
      margin-bottom: 20px;
    }
    .form-group label {
      display: block;
      margin-bottom: 5px;
      font-weight: bold;
    }
    .form-group input {
      width: 100%;
      padding: 10px;
      border-radius: 4px;
      border: 1px solid #ccc;
      font-size: 1rem;
    }
    .error-message {
      font-size: 0.85rem;
      color: red;
      display: none;
    }
    .signup-submit {
      background: var(--color-primary);
      color: #fff;
      border: none;
      padding: 12px 24px;
      border-radius: 4px;
      font-size: 1rem;
      cursor: pointer;
      transition: background 0.2s ease;
    }
    .signup-submit:hover {
      background: #005bb5;
    }

    /* ==========================
       フッター
    ========================== */
    footer {
      background: #f0f0f0; 
      padding: 20px; 
      text-align: center;
      margin-top: 60px;
    }
    footer .footer-note {
      font-size: 0.9rem;
      color: #666;
      margin-top: 10px;
    }

    /* ==========================
       レスポンシブ
    ========================== */
    @media (max-width: 768px) {
      .header-inner {
        justify-content: space-between;
      }
      nav ul { display: none; }
      .hamburger { display: block; }

      .stats-grid { display: block; }
      .stat-item { margin: 0 auto 20px; }

      .carousel-slide { margin: 0 5px; }
    }
  </style>
</head>
<body>

  <!-- Three.js 背景キャンバス -->
  <canvas id="three-bg" aria-hidden="true"></canvas>

  <!-- ヘッダー -->
  <header>
    <div class="header-inner">
      <div class="header-logo">SUBS Management</div>
      <nav>
        <ul id="navLinks">
          <li><a href="#hero">Home</a></li>
          <li><a href="#stats">Stats</a></li>
          <li><a href="#testimonials">Reviews</a></li>
          <li><a href="#signup">SignUp</a></li>
        </ul>
      </nav>
      <!-- ハンバーガーメニュー -->
      <div class="hamburger" id="hamburger">&#9776;</div>
    </div>
    <!-- モバイル用ナビ -->
    <div class="mobile-nav" id="mobileNav">
      <ul>
        <li><a href="#hero">Home</a></li>
        <li><a href="#stats">Stats</a></li>
        <li><a href="#testimonials">Reviews</a></li>
        <li><a href="#signup">SignUp</a></li>
      </ul>
    </div>
  </header>

  <!-- ヒーローセクション -->
  <section class="hero" id="hero">
    <h1>サブスクを一括管理して、賢く節約</h1>
    <p>
      使っていないサブスクを一元化・解約サポート。<br>
      無駄な出費を削減し、あなたの家計をスリムにしませんか?
    </p>
    <a href="#signup" class="cta-btn">今すぐ始める</a>
  </section>

  <!-- Statsセクション (数値カウンター) -->
  <section class="stats-section" id="stats">
    <div class="container">
      <h2>導入実績・成果</h2>
      <div class="stats-grid">
        <div class="stat-item">
          <div class="stat-number" data-target="12000">0</div>
          <div class="stat-label">累計ユーザー</div>
        </div>
        <div class="stat-item">
          <div class="stat-number" data-target="9800">0</div>
          <div class="stat-label">解約サポート実行</div>
        </div>
        <div class="stat-item">
          <div class="stat-number" data-target="95">0</div>
          <div class="stat-label">満足度(%)</div>
        </div>
        <div class="stat-item">
          <div class="stat-number" data-target="1500000">0</div>
          <div class="stat-label">総節約額(円)</div>
        </div>
      </div>
    </div>
  </section>

  <!-- Testimonials (カルーセル) -->
  <section class="testimonials-section" id="testimonials">
    <h2>利用者の声</h2>
    <div class="carousel-container">
      <div class="carousel-track" id="carouselTrack">
        <!-- Slide1 -->
        <div class="carousel-slide">
          <p class="carousel-text">
            「いくつもの動画配信サービスを契約していたのですが、まとめて管理できるから凄く楽になりました!」
          </p>
          <p class="carousel-author">- 田中さん (30代 / 会社員)</p>
        </div>
        <!-- Slide2 -->
        <div class="carousel-slide">
          <p class="carousel-text">
            「解約リマインドが来るので本当に助かります。<br>
             先月だけで2,000円以上も節約できました!」
          </p>
          <p class="carousel-author">- 佐藤さん (40代 / 主婦)</p>
        </div>
        <!-- Slide3 -->
        <div class="carousel-slide">
          <p class="carousel-text">
            「支払い情報が一元化されるのは最高。<br>
             何よりUIがシンプルで使いやすいです!」
          </p>
          <p class="carousel-author">- 鈴木さん (20代 / フリーランス)</p>
        </div>
      </div>
      <div class="carousel-buttons">
        <button class="carousel-btn" id="prevSlide"><</button>
        <button class="carousel-btn" id="nextSlide">></button>
      </div>
    </div>
  </section>

  <!-- フォームセクション (リアルタイムバリデーション) -->
  <section class="form-section" id="signup">
    <div class="container">
      <h2>新規登録 (無料)</h2>
      <div class="signup-form" id="signupForm">
        <div class="form-group">
          <label for="signupEmail">メールアドレス</label>
          <input type="email" id="signupEmail" aria-required="true" placeholder="example@example.com">
          <p class="error-message" id="emailError">正しいメールアドレスを入力してください</p>
        </div>
        <div class="form-group">
          <label for="signupPassword">パスワード (8文字以上)</label>
          <input type="password" id="signupPassword" aria-required="true" placeholder="8文字以上で入力">
          <p class="error-message" id="passwordError">パスワードは8文字以上で入力してください</p>
        </div>
        <button class="signup-submit" id="signupSubmitBtn">登録</button>
      </div>
    </div>
  </section>

  <!-- フッター -->
  <footer>
    <p class="footer-note">
      &copy; 2025 SUBS Management. All rights reserved.
    </p>
  </footer>

  <script>
    /*************************************************************
     * 1. Three.js 背景 
     *************************************************************/
    let scene, camera, renderer;
    const canvas = document.getElementById('three-bg');
    let objects = [];

    function initThreeBG() {
      scene = new THREE.Scene();
      camera = new THREE.PerspectiveCamera(
        75,
        window.innerWidth / window.innerHeight,
        0.1,
        1000
      );
      camera.position.z = 250;

      renderer = new THREE.WebGLRenderer({ canvas: canvas, alpha: true });
      renderer.setSize(window.innerWidth, window.innerHeight);

      // ライト
      const ambient = new THREE.AmbientLight(0xffffff, 0.7);
      scene.add(ambient);
      const pointLight = new THREE.PointLight(0xffffff, 0.8);
      pointLight.position.set(50, 50, 50);
      scene.add(pointLight);

      // いくつかのオブジェクトをランダム配置
      const geometryTypes = [
        new THREE.TorusGeometry(10, 3, 16, 100),
        new THREE.SphereGeometry(8, 32, 32),
        new THREE.BoxGeometry(12, 12, 12)
      ];

      for(let i=0; i<30; i++){
        const geo = geometryTypes[Math.floor(Math.random() * geometryTypes.length)];
        const material = new THREE.MeshStandardMaterial({
          color: 0xffffff
        });
        // 色相をランダム化
        material.color.setHSL(Math.random(), 0.5, 0.5);

        const mesh = new THREE.Mesh(geo, material);
        mesh.position.x = (Math.random() - 0.5) * 400;
        mesh.position.y = (Math.random() - 0.5) * 400;
        mesh.position.z = (Math.random() - 0.5) * 400;
        mesh.rotation.x = Math.random() * Math.PI;
        mesh.rotation.y = Math.random() * Math.PI;
        scene.add(mesh);
        objects.push(mesh);
      }
    }

    function animateThreeBG() {
      requestAnimationFrame(animateThreeBG);
      objects.forEach(obj => {
        obj.rotation.x += 0.001;
        obj.rotation.y += 0.001;
      });
      camera.position.z += Math.sin(Date.now() * 0.0005) * 0.05; // カメラ揺らし
      renderer.render(scene, camera);
    }

    /*************************************************************
     * 2. ハンバーガーメニュー
     *************************************************************/
    const hamburger = document.getElementById('hamburger');
    const mobileNav = document.getElementById('mobileNav');
    let navOpen = false;
    hamburger.addEventListener('click', () => {
      navOpen = !navOpen;
      mobileNav.style.display = navOpen ? 'block' : 'none';
    });

    /*************************************************************
     * 3. GSAP + ScrollTrigger
     *    - Heroセクションのタイトルやボタンをアニメ
     *    - 他のセクションもスクロールでフェードイン
     *************************************************************/
    window.addEventListener('DOMContentLoaded', () => {
      gsap.registerPlugin(ScrollTrigger);

      // Heroアニメ
      gsap.to('.hero h1', {
        duration: 1, y: 0, opacity: 1, ease: 'power2.out'
      });
      gsap.to('.hero p', {
        duration: 1, y: 0, opacity: 1, delay: 0.2, ease: 'power2.out'
      });
      gsap.to('.hero .cta-btn', {
        duration: 1, y: 0, opacity: 1, delay: 0.4, ease: 'power2.out'
      });

      // Statsセクション
      gsap.from('#stats .stats-grid', {
        scrollTrigger: {
          trigger: '#stats',
          start: 'top 80%',
        },
        y: 50, opacity: 0, duration: 1, ease: 'power2.out'
      });

      // Testimonials
      gsap.from('#testimonials .carousel-container', {
        scrollTrigger: {
          trigger: '#testimonials',
          start: 'top 80%',
        },
        y: 50, opacity: 0, duration: 1, ease: 'power2.out'
      });

      // SignUpフォーム
      gsap.from('#signupForm', {
        scrollTrigger: {
          trigger: '#signupForm',
          start: 'top 80%',
        },
        y: 50, opacity: 0, duration: 1, ease: 'power2.out'
      });
    });

    /*************************************************************
     * 4. Stats カウンター (Intersection Observer)
     *************************************************************/
    const statNumbers = document.querySelectorAll('.stat-number');
    const statsObserver = new IntersectionObserver((entries, obs) => {
      entries.forEach(entry => {
        if(entry.isIntersecting) {
          const el = entry.target;
          const targetVal = parseInt(el.getAttribute('data-target'), 10);
          let currentVal = 0;
          const increment = Math.ceil(targetVal / 100);

          const timer = setInterval(() => {
            currentVal += increment;
            if(currentVal >= targetVal) {
              currentVal = targetVal;
              clearInterval(timer);
            }
            el.textContent = currentVal.toLocaleString();
          }, 20);

          obs.unobserve(el);
        }
      });
    }, { threshold: 0.5 });

    statNumbers.forEach(num => statsObserver.observe(num));

    /*************************************************************
     * 5. Testimonials カルーセル
     *************************************************************/
    const carouselTrack = document.getElementById('carouselTrack');
    const prevSlideBtn = document.getElementById('prevSlide');
    const nextSlideBtn = document.getElementById('nextSlide');
    let currentSlideIndex = 0;
    const slides = Array.from(document.querySelectorAll('.carousel-slide'));

    function updateCarousel() {
      carouselTrack.style.transform = `translateX(${-100 * currentSlideIndex}%)`;
    }

    prevSlideBtn.addEventListener('click', () => {
      currentSlideIndex = (currentSlideIndex === 0) ? slides.length - 1 : currentSlideIndex - 1;
      updateCarousel();
    });
    nextSlideBtn.addEventListener('click', () => {
      currentSlideIndex = (currentSlideIndex === slides.length - 1) ? 0 : currentSlideIndex + 1;
      updateCarousel();
    });

    /*************************************************************
     * 6. リアルタイム・フォームバリデーション
     *************************************************************/
    const signupEmail = document.getElementById('signupEmail');
    const signupPassword = document.getElementById('signupPassword');
    const emailError = document.getElementById('emailError');
    const passwordError = document.getElementById('passwordError');
    const signupSubmitBtn = document.getElementById('signupSubmitBtn');

    function validateEmail(email) {
      return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
    }
    function validatePassword(pass) {
      return pass.length >= 8;
    }

    signupEmail.addEventListener('input', () => {
      if(!validateEmail(signupEmail.value.trim())) {
        emailError.style.display = 'block';
      } else {
        emailError.style.display = 'none';
      }
    });
    signupPassword.addEventListener('input', () => {
      if(!validatePassword(signupPassword.value.trim())) {
        passwordError.style.display = 'block';
      } else {
        passwordError.style.display = 'none';
      }
    });

    signupSubmitBtn.addEventListener('click', () => {
      const emailVal = signupEmail.value.trim();
      const passVal = signupPassword.value.trim();

      let hasError = false;
      if(!validateEmail(emailVal)) {
        emailError.style.display = 'block';
        hasError = true;
      }
      if(!validatePassword(passVal)) {
        passwordError.style.display = 'block';
        hasError = true;
      }
      if(hasError) return;

      alert('登録ありがとうございました!');
      // フォームリセット例
      signupEmail.value = '';
      signupPassword.value = '';
      emailError.style.display = 'none';
      passwordError.style.display = 'none';
    });

    /*************************************************************
     * 7. 画面リサイズ時のThree.js再設定
     *************************************************************/
    window.addEventListener('resize', () => {
      renderer.setSize(window.innerWidth, window.innerHeight);
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
    });

    /*************************************************************
     * 8. ページ読み込み完了時に Three.js 初期化 & アニメ開始
     *************************************************************/
    window.addEventListener('load', () => {
      initThreeBG();
      animateThreeBG();
    });
  </script>
</body>
</html>

Voogle.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Voogle</title>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.css">
  <style>
    /* 全体の基本設定 */
    body {
      font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
      margin: 0;
      padding: 0;
      background: #f7f7f7;
      color: #333;
      line-height: 1.6;
    }
    header {
      background: #003d66;
      color: #fff;
      padding: 20px;
      text-align: center;
    }
    header h1 {
      margin: 0;
      font-size: 2.5em;
    }
    nav {
      margin-top: 10px;
    }
    nav a {
      color: #fff;
      margin: 0 15px;
      text-decoration: none;
      font-size: 1em;
    }
    /* レイアウトコンテナ */
    .container {
      display: flex;
      max-width: 1200px;
      margin: 20px auto;
      padding: 0 20px;
    }
    /* サイドバー */
    .sidebar {
      flex: 0 0 250px;
      background: #fff;
      padding: 20px;
      margin-right: 20px;
      border-radius: 8px;
      box-shadow: 0 2px 4px rgba(0,0,0,0.1);
      height: fit-content;
    }
    .sidebar h2 {
      font-size: 1.3em;
      margin-top: 0;
      border-bottom: 1px solid #eee;
      padding-bottom: 5px;
    }
    .sidebar section {
      margin-bottom: 20px;
    }
    .sidebar label {
      display: block;
      margin-bottom: 5px;
    }
    .sidebar input[type="date"],
    .sidebar select {
      width: 100%;
      padding: 8px;
      margin-bottom: 10px;
      border: 1px solid #ccc;
      border-radius: 4px;
    }
    .sidebar .checkbox-group label {
      display: inline-block;
      margin-right: 10px;
      font-size: 0.9em;
    }
    .sidebar ul {
      list-style: none;
      padding-left: 0;
    }
    .sidebar li {
      padding: 5px 0;
      cursor: pointer;
      color: #005fa3;
    }
    .sidebar li:hover {
      text-decoration: underline;
    }
    /* メインコンテンツ */
    .main-content {
      flex: 1;
      background: #fff;
      padding: 20px;
      border-radius: 8px;
      box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    }
    /* 検索フォーム */
    .search-section {
      position: relative;
      margin-bottom: 20px;
    }
    .search-form {
      display: flex;
      flex-wrap: wrap;
      gap: 10px;
    }
    .search-form input[type="text"] {
      flex: 1;
      padding: 12px;
      font-size: 1.1em;
      border: 1px solid #ccc;
      border-radius: 4px;
      outline: none;
    }
    .search-form button {
      padding: 12px 20px;
      font-size: 1.1em;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }
    #searchButton {
      background: #005fa3;
      color: #fff;
    }
    #searchButton:hover {
      background: #00407a;
    }
    /* オートサジェスト */
    .suggestions {
      list-style: none;
      margin: 0;
      padding: 0;
      position: absolute;
      width: calc(100% - 20px);
      background: #fff;
      border: 1px solid #ccc;
      border-top: none;
      z-index: 10;
      max-height: 200px;
      overflow-y: auto;
    }
    .suggestions li {
      padding: 8px 10px;
      cursor: pointer;
    }
    .suggestions li:hover {
      background: #f0f8ff;
    }
    /* ローディングスピナー */
    .spinner {
      display: none;
      margin: 20px auto;
      width: 50px;
      height: 50px;
      border: 6px solid rgba(0,0,0,0.1);
      border-top-color: #005fa3;
      border-radius: 50%;
      animation: spin 0.8s linear infinite;
    }
    @keyframes spin {
      to { transform: rotate(360deg); }
    }
    /* 検索結果カード */
    .results {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
      gap: 20px;
    }
    .result-item {
      background: #fafafa;
      border: 1px solid #eee;
      padding: 15px;
      border-radius: 8px;
      transition: box-shadow 0.2s;
    }
    .result-item:hover {
      box-shadow: 0 2px 8px rgba(0,0,0,0.15);
    }
    .result-title {
      font-size: 1.2em;
      color: #005fa3;
      margin: 0 0 5px 0;
    }
    .result-title a {
      text-decoration: none;
      color: inherit;
    }
    .result-url {
      font-size: 0.85em;
      color: #888;
      margin-bottom: 8px;
      word-break: break-all;
    }
    .result-snippet {
      font-size: 0.95em;
      margin-bottom: 10px;
    }
    .result-meta {
      font-size: 0.8em;
      color: #666;
    }
    /* ページネーション */
    .pagination {
      text-align: center;
      margin: 30px 0;
    }
    .pagination a {
      margin: 0 5px;
      text-decoration: none;
      color: #005fa3;
      padding: 8px 12px;
      border: 1px solid #ccc;
      border-radius: 4px;
      cursor: pointer;
    }
    .pagination a.active {
      background: #005fa3;
      color: #fff;
      border-color: #005fa3;
    }
    .pagination a:hover {
      background: #f0f8ff;
    }
    /* 「さらに読み込む」ボタン */
    .load-more {
      display: block;
      width: 200px;
      margin: 20px auto;
      padding: 12px;
      text-align: center;
      background: #005fa3;
      color: #fff;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }
    .load-more:hover {
      background: #00407a;
    }
    /* フッター */
    footer {
      text-align: center;
      padding: 15px;
      font-size: 0.8em;
      color: #aaa;
      background: #f7f7f7;
      margin-top: 20px;
    }
    /* レスポンシブ対応 */
    @media (max-width: 800px) {
      .container {
        flex-direction: column;
      }
      .sidebar {
        margin-right: 0;
        margin-bottom: 20px;
      }
    }
  </style>
</head>
<body>
  <header>
    <h1>Voogle検索エンジン</h1>
    <nav>
      <a href="#">ホーム</a>
      <a href="#">使い方</a>
      <a href="#">お問い合わせ</a>
    </nav>
  </header>

  <div class="container">
    <!-- サイドバー:フィルター・ソート・検索履歴 -->
    <aside class="sidebar">
      <section>
        <h2>フィルター</h2>
        <form id="filterForm">
          <label for="startDate">開始日:</label>
          <input type="date" id="startDate">
          <label for="endDate">終了日:</label>
          <input type="date" id="endDate">
          <label>コンテンツタイプ:</label>
          <div class="checkbox-group">
            <!-- Wikipedia検索結果用として追加 -->
            <label><input type="checkbox" name="contentType" value="wikipedia" checked> Wikipedia</label>
            <!-- 他の項目は将来的な拡張用 -->
            <label><input type="checkbox" name="contentType" value="news" checked> ニュース</label>
            <label><input type="checkbox" name="contentType" value="blog" checked> ブログ</label>
            <label><input type="checkbox" name="contentType" value="forum" checked> フォーラム</label>
          </div>
          <label for="sortOrder">ソート順:</label>
          <select id="sortOrder">
            <option value="relevance">関連順</option>
            <option value="date">新着順</option>
          </select>
          <button type="submit" style="margin-top:10px; padding:8px 12px; background:#005fa3; color:#fff; border:none; border-radius:4px; cursor:pointer;">適用</button>
        </form>
      </section>
      <section>
        <h2>検索履歴</h2>
        <ul id="searchHistory">
          <!-- 最近の検索キーワードを表示 -->
        </ul>
      </section>
    </aside>

    <!-- メインコンテンツ:検索フォーム&結果 -->
    <main class="main-content">
      <div class="search-section">
        <form id="searchForm" class="search-form">
          <input type="text" id="searchInput" placeholder="検索キーワードを入力" autocomplete="off">
          <button type="submit" id="searchButton">検索</button>
        </form>
        <!-- オートサジェスト -->
        <ul id="suggestions" class="suggestions"></ul>
      </div>
      <!-- ローディングスピナー -->
      <div id="spinner" class="spinner"></div>
      <!-- 検索結果カード -->
      <div id="results" class="results">
        <!-- 結果はJavaScriptで動的レンダリング -->
      </div>
      <!-- ページネーション -->
      <div id="pagination" class="pagination"></div>
      <!-- Load More ボタン(ページネーションの代替) -->
      <button id="loadMore" class="load-more">さらに読み込む</button>
    </main>
  </div>

  <footer>
    &copy; 2025 Voogle. All rights reserved.
  </footer>

  <script>
    // --- オートサジェスト用のサンプル候補 ---
    const suggestionsData = [
      "JavaScript", "HTML", "CSS", "クローラー", "検索エンジン", "Web開発", "React", "Node.js", "Python", "データスクレイピング"
    ];

    const searchInput = document.getElementById('searchInput');
    const suggestionsList = document.getElementById('suggestions');

    searchInput.addEventListener('input', function() {
      const query = this.value.toLowerCase();
      suggestionsList.innerHTML = '';
      if(query.length === 0) return;
      const filtered = suggestionsData.filter(item => item.toLowerCase().includes(query));
      filtered.forEach(item => {
        const li = document.createElement('li');
        li.textContent = item;
        li.addEventListener('click', () => {
          searchInput.value = item;
          suggestionsList.innerHTML = '';
        });
        suggestionsList.appendChild(li);
      });
    });

    // --- 検索履歴管理 ---
    const searchHistoryEl = document.getElementById('searchHistory');
    function updateSearchHistory(query) {
      let history = JSON.parse(localStorage.getItem('searchHistory')) || [];
      if (!history.includes(query)) {
        history.unshift(query);
        if(history.length > 5) history.pop();
        localStorage.setItem('searchHistory', JSON.stringify(history));
        renderSearchHistory();
      }
    }
    function renderSearchHistory() {
      let history = JSON.parse(localStorage.getItem('searchHistory')) || [];
      searchHistoryEl.innerHTML = '';
      history.forEach(item => {
        const li = document.createElement('li');
        li.textContent = item;
        li.addEventListener('click', () => {
          searchInput.value = item;
          performSearch(item);
        });
        searchHistoryEl.appendChild(li);
      });
    }
    renderSearchHistory();

    // --- Wikipedia検索結果のレンダリング ---
    function renderResults(page, results) {
      const resultsContainer = document.getElementById('results');
      // 結果カードはすべて再レンダリング(Load Moreの場合は追記可能に変更も検討)
      resultsContainer.innerHTML = '';
      const startIndex = (page - 1) * resultsPerPage;
      const endIndex = startIndex + resultsPerPage;
      const pageResults = results.slice(startIndex, endIndex);
      if(pageResults.length === 0) {
        resultsContainer.innerHTML = '<p>該当する検索結果がありません。</p>';
        return;
      }
      pageResults.forEach(result => {
        const div = document.createElement('div');
        div.className = 'result-item';
        div.innerHTML = `
          <h2 class="result-title"><a href="${result.url}" target="_blank">${result.title}</a></h2>
          <div class="result-url">${result.url}</div>
          <p class="result-snippet">${result.snippet}</p>
          <div class="result-meta">投稿日: ${result.date} | タイプ: ${result.contentType}</div>
        `;
        resultsContainer.appendChild(div);
      });
    }

    // --- ページネーションのレンダリング ---
    function renderPagination(page, totalResults) {
      const paginationContainer = document.getElementById('pagination');
      paginationContainer.innerHTML = '';
      const totalPages = Math.ceil(totalResults / resultsPerPage);
      // 前へ
      const prev = document.createElement('a');
      prev.textContent = '«';
      prev.addEventListener('click', () => {
        if(page > 1) {
          currentPage--;
          renderResults(currentPage, filteredResults);
          renderPagination(currentPage, filteredResults.length);
        }
      });
      paginationContainer.appendChild(prev);
      // ページ番号
      for(let i = 1; i <= totalPages; i++) {
        const pageLink = document.createElement('a');
        pageLink.textContent = i;
        if(i === page) pageLink.classList.add('active');
        pageLink.addEventListener('click', () => {
          currentPage = i;
          renderResults(currentPage, filteredResults);
          renderPagination(currentPage, filteredResults.length);
        });
        paginationContainer.appendChild(pageLink);
      }
      // 次へ
      const next = document.createElement('a');
      next.textContent = '»';
      next.addEventListener('click', () => {
        if(page < totalPages) {
          currentPage++;
          renderResults(currentPage, filteredResults);
          renderPagination(currentPage, filteredResults.length);
        }
      });
      paginationContainer.appendChild(next);
    }

    // --- 定数・変数 ---
    let filteredResults = []; // Wikipedia APIから取得した結果を格納
    let currentPage = 1;
    const resultsPerPage = 10;
    const spinner = document.getElementById('spinner');

    // --- Wikipedia APIを利用した検索実行 ---
    const searchForm = document.getElementById('searchForm');
    searchForm.addEventListener('submit', function(e) {
      e.preventDefault();
      const query = searchInput.value.trim();
      performSearch(query);
    });
    function performSearch(query) {
      updateSearchHistory(query);
      spinner.style.display = 'block';
      // Wikipedia APIエンドポイント(origin=* を指定してクロスオリジン対応)
      const url = "https://ja.wikipedia.org/w/api.php?action=query&list=search&format=json&origin=*&srsearch=" + encodeURIComponent(query) + "&srlimit=20";
      fetch(url)
        .then(response => response.json())
        .then(data => {
          spinner.style.display = 'none';
          if (data.query && data.query.search && data.query.search.length > 0) {
            // APIから取得した各項目のtimestampを日付文字列に変換
            filteredResults = data.query.search.map(item => {
              return {
                title: item.title,
                snippet: item.snippet,
                url: "https://ja.wikipedia.org/?curid=" + item.pageid,
                date: new Date(item.timestamp).toISOString().split('T')[0],
                contentType: "wikipedia"
              };
            });
            currentPage = 1;
            renderResults(currentPage, filteredResults);
            renderPagination(currentPage, filteredResults.length);
          } else {
            document.getElementById('results').innerHTML = '<p>該当する検索結果がありません。</p>';
          }
        })
        .catch(error => {
          spinner.style.display = 'none';
          console.error('Error:', error);
          document.getElementById('results').innerHTML = '<p>検索結果の取得に失敗しました。</p>';
        });
    }

    // --- サイドバーのフィルター処理 ---
    const filterForm = document.getElementById('filterForm');
    filterForm.addEventListener('submit', function(e) {
      e.preventDefault();
      applySidebarFilters();
      currentPage = 1;
      renderResults(currentPage, filteredResults);
      renderPagination(currentPage, filteredResults.length);
    });
    function applySidebarFilters() {
      // フィルター設定の取得
      const startDate = document.getElementById('startDate').value;
      const endDate = document.getElementById('endDate').value;
      const sortOrder = document.getElementById('sortOrder').value;
      const checkboxes = document.querySelectorAll('input[name="contentType"]:checked');
      const types = Array.from(checkboxes).map(cb => cb.value);
      // 現在のfilteredResultsに対してさらにフィルタリング
      filteredResults = filteredResults.filter(result => {
        let valid = true;
        if(startDate && result.date < startDate) valid = false;
        if(endDate && result.date > endDate) valid = false;
        if(types.length && !types.includes(result.contentType)) valid = false;
        return valid;
      });
      // ソート処理:日付の降順(新着順)の場合
      if(sortOrder === "date") {
        filteredResults.sort((a,b) => new Date(b.date) - new Date(a.date));
      }
    }

    // --- Load More ボタン ---
    const loadMoreBtn = document.getElementById('loadMore');
    loadMoreBtn.addEventListener('click', () => {
      const totalPages = Math.ceil(filteredResults.length / resultsPerPage);
      if(currentPage < totalPages) {
        currentPage++;
        // 既存の結果に追記する方式
        const resultsContainer = document.getElementById('results');
        const startIndex = (currentPage - 1) * resultsPerPage;
        const endIndex = startIndex + resultsPerPage;
        const pageResults = filteredResults.slice(startIndex, endIndex);
        pageResults.forEach(result => {
          const div = document.createElement('div');
          div.className = 'result-item';
          div.innerHTML = `
            <h2 class="result-title"><a href="${result.url}" target="_blank">${result.title}</a></h2>
            <div class="result-url">${result.url}</div>
            <p class="result-snippet">${result.snippet}</p>
            <div class="result-meta">投稿日: ${result.date} | タイプ: ${result.contentType}</div>
          `;
          resultsContainer.appendChild(div);
        });
        renderPagination(currentPage, filteredResults.length);
      }
    });
  </script>
</body>
</html>

minisns.html

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>スフィア</title>
<style>
/* ======= 基本リセット ======= */
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: Arial, sans-serif; background: #f7f9fc; color: #333; line-height: 1.5; }
header { background: #4a90e2; color: #fff; padding: 15px; text-align: center; }
header h1 { font-size: 28px; }
main { width: 90%; max-width: 800px; margin: 20px auto; }
/* ======= ユーザーアイコン ======= */
.user-icon { width: 40px; height: 40px; border-radius: 50%; margin-right: 10px; vertical-align: middle; }
/* ======= 認証セクション ======= */
.auth-section { background: #fff; padding: 15px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.auth-section h2 { margin-bottom: 10px; }
.auth-section input { width: 100%; padding: 8px; margin-bottom: 10px; border: 1px solid #ccc; border-radius: 4px; }
.auth-section button { background: #4a90e2; color: #fff; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; }
.auth-section button:hover { background: #357ab8; }
.toggle-auth { margin-top: 10px; font-size: 14px; color: #4a90e2; cursor: pointer; text-decoration: underline; }
/* ======= 投稿フォーム ======= */
.post-form { background: #fff; padding: 15px; margin-bottom: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.post-form textarea { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 4px; resize: vertical; font-size: 14px; }
.post-form button { background: #4a90e2; color: #fff; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; margin-top: 10px; }
.post-form button:hover { background: #357ab8; }
/* ======= 検索セクション ======= */
.search-section { background: #fff; padding: 10px; margin-bottom: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.search-section input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 14px; }
/* ======= タイムライン ======= */
.timeline { display: flex; flex-direction: column; gap: 15px; }
.post-item { background: #fff; padding: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); position: relative; }
.post-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
.post-username { font-weight: bold; }
.post-meta { font-size: 12px; color: #777; }
.post-content { font-size: 16px; margin-bottom: 10px; }
.post-actions { margin-top: 10px; }
.post-actions button { background: transparent; border: none; color: #4a90e2; cursor: pointer; margin-right: 10px; font-size: 14px; }
.post-actions button:hover { text-decoration: underline; }
.replies { margin-top: 15px; border-top: 1px solid #eee; padding-top: 10px; }
.reply-item { background: #f2f2f2; padding: 10px; border-radius: 6px; margin-bottom: 10px; }
.reply-header { font-size: 13px; font-weight: bold; margin-bottom: 5px; }
.reply-meta { font-size: 11px; color: #555; text-align: right; }
.edit-area { margin-top: 10px; }
.edit-area textarea { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; }
.edit-area button { background: #4a90e2; color: #fff; border: none; padding: 6px 10px; border-radius: 4px; cursor: pointer; margin-top: 5px; }
.edit-area button:hover { background: #357ab8; }
/* ======= ユーティリティ(ログ/RSS)セクション ======= */
.utility-section { margin-top: 20px; text-align: center; }
.logs-section { background: #fff; padding: 15px; margin-top: 10px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-size: 12px; color: #555; max-height: 200px; overflow-y: auto; }
.log-entry { border-bottom: 1px solid #eee; padding: 5px 0; }
footer { text-align: center; padding: 15px; font-size: 14px; color: #777; margin-top: 20px; }
</style>
</head>
<body>
<header>
<h1>スフィア</h1>
</header>
<main>
<!-- 認証セクション(ログイン/登録 or ユーザー情報表示) -->
<section id="authSection" class="auth-section"></section>
<!-- 投稿フォーム(ログイン中のみ表示) -->
<section id="postFormSection" class="post-form" style="display: none;">
<textarea id="postContent" placeholder="いまどうしてる?"></textarea>
<button id="postButton">投稿する</button>
</section>
<!-- 検索セクション -->
<section class="search-section">
<input type="text" id="searchInput" placeholder="投稿を検索">
</section>
<!-- タイムライン -->
<section id="timeline" class="timeline"></section>
<!-- ユーティリティ:ログ表示/RSSダウンロード -->
<section class="utility-section">
<button id="toggleLogsButton">ログを表示</button>
<button id="downloadRSSButton">RSSフィードをダウンロード</button>
</section>
<!-- ログ表示セクション(初期は非表示) -->
<section id="logsSection" class="logs-section" style="display: none;"></section>
</main>
<footer>
<p>&copy; 2023 スフィア</p>
</footer>
<script>
(function(){
'use strict';

// === 定数&キー定義 ===
const POSTS_KEY = 'posts';
const USERS_KEY = 'users';
const CURRENT_USER_KEY = 'currentUser';
const LOGS_KEY = 'logs';

// === データ管理変数 ===
let posts = [];
let users = [];
let currentUser = null;
let logs = [];

// === ストレージ関連関数 ===
const saveToStorage = (key, data) => localStorage.setItem(key, JSON.stringify(data));
const loadFromStorage = (key) => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : null;
};

const loadData = () => {
posts = loadFromStorage(POSTS_KEY) || [];
users = loadFromStorage(USERS_KEY) || [];
currentUser = loadFromStorage(CURRENT_USER_KEY) || null;
logs = loadFromStorage(LOGS_KEY) || [];
};

const saveData = () => {
saveToStorage(POSTS_KEY, posts);
saveToStorage(USERS_KEY, users);
saveToStorage(CURRENT_USER_KEY, currentUser);
saveToStorage(LOGS_KEY, logs);
};

// === ログ処理 ===
const addLog = (message) => {
const entry = { timestamp: new Date(), message };
logs.push(entry);
saveToStorage(LOGS_KEY, logs);
};

const renderLogs = () => {
const logsSection = document.getElementById('logsSection');
logsSection.innerHTML = '';
logs.forEach(log => {
const div = document.createElement('div');
div.className = 'log-entry';
div.textContent = `[${formatDate(log.timestamp)}] ${log.message}`;
logsSection.appendChild(div);
});
};

// === 日付フォーマット ===
const formatDate = (date) => {
const d = new Date(date);
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
const h = String(d.getHours()).padStart(2, '0');
const min = String(d.getMinutes()).padStart(2, '0');
return `${y}-${m}-${day} ${h}:${min}`;
};

// === RSS生成 ===
const generateRSS = () => {
const now = new Date();
let rss = `<?xml version="1.0" encoding="UTF-8"?>\n`;
rss += `<rss version="2.0">\n<channel>\n`;
rss += `<title>Advanced Mini SNS RSS Feed</title>\n`;
rss += `<link>${location.origin + location.pathname}</link>\n`;
rss += `<description>RSS Feed of posts from Advanced Mini SNS</description>\n`;
rss += `<lastBuildDate>${now.toUTCString()}</lastBuildDate>\n`;
rss += `<language>ja</language>\n`;
posts.forEach(post => {
const postUrl = `${location.origin + location.pathname}?post=${post.id}`;
const title = `Post by ${post.username}: ${post.content.substring(0,20)}...`;
rss += `<item>\n`;
rss += `<title>${title}</title>\n`;
rss += `<link>${postUrl}</link>\n`;
rss += `<description><![CDATA[${post.content}]]></description>\n`;
rss += `<pubDate>${new Date(post.timestamp).toUTCString()}</pubDate>\n`;
rss += `<guid>${post.id}</guid>\n`;
rss += `</item>\n`;
});
rss += `</channel>\n</rss>`;
return rss;
};

const downloadRSS = () => {
const rssContent = generateRSS();
const blob = new Blob([rssContent], { type: 'application/rss+xml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'feed.xml';
a.click();
URL.revokeObjectURL(url);
addLog('RSSフィードをダウンロード');
};

// === 認証処理 ===
const renderAuthSection = () => {
const authSection = document.getElementById('authSection');
authSection.innerHTML = '';
if (currentUser) {
// ログイン済み:アイコンとウェルカムメッセージ、ログアウトボタン
const container = document.createElement('div');
if (currentUser.icon) {
const img = document.createElement('img');
img.src = currentUser.icon;
img.className = 'user-icon';
container.appendChild(img);
}
const welcomeDiv = document.createElement('span');
welcomeDiv.textContent = `ようこそ、${currentUser.username} さん!`;
container.appendChild(welcomeDiv);
const logoutBtn = document.createElement('button');
logoutBtn.textContent = 'ログアウト';
logoutBtn.onclick = () => {
addLog(`ユーザー ${currentUser.username} ログアウト`);
currentUser = null;
saveData();
renderAuthSection();
togglePostForm();
renderTimeline();
};
authSection.appendChild(container);
authSection.appendChild(logoutBtn);
} else {
// 未ログイン:ログインフォームを表示
renderLoginForm(authSection);
}
};

const renderLoginForm = (container) => {
container.innerHTML = '<h2>ログイン</h2>';
const usernameInput = document.createElement('input');
usernameInput.type = 'text';
usernameInput.placeholder = 'ユーザー名';
const passwordInput = document.createElement('input');
passwordInput.type = 'password';
passwordInput.placeholder = 'パスワード';
const loginBtn = document.createElement('button');
loginBtn.textContent = 'ログイン';
loginBtn.onclick = () => {
const username = usernameInput.value.trim();
const password = passwordInput.value;
if (!username || !password) { alert('ユーザー名とパスワードを入力してください。'); return; }
const user = users.find(u => u.username === username && u.password === password);
if (user) {
currentUser = user;
saveData();
addLog(`ユーザー ${username} ログイン成功`);
renderAuthSection();
togglePostForm();
renderTimeline();
} else {
alert('ユーザー名またはパスワードが正しくありません。');
}
};
container.appendChild(usernameInput);
container.appendChild(passwordInput);
container.appendChild(loginBtn);
const toggleLink = document.createElement('div');
toggleLink.className = 'toggle-auth';
toggleLink.textContent = '新規登録はこちら';
toggleLink.onclick = () => renderRegisterForm(container);
container.appendChild(toggleLink);
};

const renderRegisterForm = (container) => {
container.innerHTML = '<h2>新規登録</h2>';
const usernameInput = document.createElement('input');
usernameInput.type = 'text';
usernameInput.placeholder = 'ユーザー名';
const passwordInput = document.createElement('input');
passwordInput.type = 'password';
passwordInput.placeholder = 'パスワード';
const confirmInput = document.createElement('input');
confirmInput.type = 'password';
confirmInput.placeholder = 'パスワード確認';
// アイコンアップロード用フィールド
const iconInput = document.createElement('input');
iconInput.type = 'file';
iconInput.accept = 'image/*';
iconInput.style.marginBottom = '10px';
const registerBtn = document.createElement('button');
registerBtn.textContent = '登録';
registerBtn.onclick = () => {
const username = usernameInput.value.trim();
const password = passwordInput.value;
const confirmPassword = confirmInput.value;
if (!username || !password || !confirmPassword) { alert('全ての項目を入力してください。'); return; }
if (password !== confirmPassword) { alert('パスワードが一致しません。'); return; }
if (users.some(u => u.username === username)) { alert('そのユーザー名は既に使用されています。'); return; }
// アイコンが選択されている場合、FileReader で Data URL に変換
if (iconInput.files && iconInput.files[0]) {
const reader = new FileReader();
reader.onload = function(e) {
const iconData = e.target.result;
const newUser = { username, password, icon: iconData };
users.push(newUser);
saveData();
addLog(`ユーザー ${username} 登録完了(アイコン設定済み)`);
alert('登録が完了しました。ログインしてください。');
renderLoginForm(container);
};
reader.readAsDataURL(iconInput.files[0]);
} else {
const newUser = { username, password, icon: '' };
users.push(newUser);
saveData();
addLog(`ユーザー ${username} 登録完了(アイコン未設定)`);
alert('登録が完了しました。ログインしてください。');
renderLoginForm(container);
}
};
container.appendChild(usernameInput);
container.appendChild(passwordInput);
container.appendChild(confirmInput);
container.appendChild(iconInput);
container.appendChild(registerBtn);
const toggleLink = document.createElement('div');
toggleLink.className = 'toggle-auth';
toggleLink.textContent = 'ログインはこちら';
toggleLink.onclick = () => renderLoginForm(container);
container.appendChild(toggleLink);
};

const togglePostForm = () => {
const postFormSection = document.getElementById('postFormSection');
postFormSection.style.display = currentUser ? 'block' : 'none';
};

// === タイムライン&投稿表示 ===
const renderTimeline = () => {
const timeline = document.getElementById('timeline');
timeline.innerHTML = '';
let filteredPosts = posts.slice();
const searchValue = document.getElementById('searchInput').value.trim().toLowerCase();
if (searchValue) {
filteredPosts = filteredPosts.filter(post =>
post.content.toLowerCase().includes(searchValue) ||
post.username.toLowerCase().includes(searchValue)
);
}
// 新しい投稿順にソート
filteredPosts.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
filteredPosts.forEach(post => {
timeline.appendChild(renderPost(post));
});
};

const renderPost = (post) => {
const postDiv = document.createElement('div');
postDiv.className = 'post-item';
postDiv.setAttribute('data-id', post.id);

// ヘッダー(投稿者名&日時)
const headerDiv = document.createElement('div');
headerDiv.className = 'post-header';
const usernameSpan = document.createElement('span');
usernameSpan.className = 'post-username';
usernameSpan.textContent = post.username;
const metaSpan = document.createElement('span');
metaSpan.className = 'post-meta';
metaSpan.textContent = formatDate(post.timestamp);
headerDiv.appendChild(usernameSpan);
headerDiv.appendChild(metaSpan);
postDiv.appendChild(headerDiv);

// 投稿内容(編集モードか通常表示か)
const contentDiv = document.createElement('div');
contentDiv.className = 'post-content';
if (post.editing) {
const editArea = document.createElement('div');
editArea.className = 'edit-area';
const editTextarea = document.createElement('textarea');
editTextarea.value = post.content;
editArea.appendChild(editTextarea);
const saveBtn = document.createElement('button');
saveBtn.textContent = '保存';
saveBtn.onclick = () => {
const newContent = editTextarea.value.trim();
if (!newContent) { alert('投稿内容が空です。'); return; }
post.content = newContent;
post.editing = false;
saveData();
addLog(`投稿 ${post.id} 編集完了`);
renderTimeline();
};
const cancelBtn = document.createElement('button');
cancelBtn.textContent = 'キャンセル';
cancelBtn.style.marginLeft = '5px';
cancelBtn.onclick = () => { post.editing = false; renderTimeline(); };
editArea.appendChild(saveBtn);
editArea.appendChild(cancelBtn);
contentDiv.appendChild(editArea);
} else {
contentDiv.textContent = post.content;
}
postDiv.appendChild(contentDiv);

// アクションボタン(いいね、編集、削除、返信)
const actionsDiv = document.createElement('div');
actionsDiv.className = 'post-actions';
const likeBtn = document.createElement('button');
likeBtn.textContent = `いいね (${post.likes || 0})`;
likeBtn.onclick = () => {
post.likes = (post.likes || 0) + 1;
saveData();
addLog(`投稿 ${post.id} にいいね`);
renderTimeline();
};
actionsDiv.appendChild(likeBtn);

if (currentUser && currentUser.username === post.username) {
const editBtn = document.createElement('button');
editBtn.textContent = '編集';
editBtn.onclick = () => { post.editing = true; renderTimeline(); };
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '削除';
deleteBtn.onclick = () => {
if (confirm('本当に削除しますか?')) {
posts = posts.filter(p => p.id !== post.id);
saveData();
addLog(`投稿 ${post.id} 削除`);
renderTimeline();
}
};
actionsDiv.appendChild(editBtn);
actionsDiv.appendChild(deleteBtn);
}

if (currentUser) {
const replyBtn = document.createElement('button');
replyBtn.textContent = '返信';
replyBtn.onclick = () => toggleReplyForm(post.id);
actionsDiv.appendChild(replyBtn);
}
postDiv.appendChild(actionsDiv);

// 返信一覧表示
const repliesDiv = document.createElement('div');
repliesDiv.className = 'replies';
if (post.replies && post.replies.length > 0) {
post.replies.forEach(reply => {
repliesDiv.appendChild(renderReply(reply));
});
}
postDiv.appendChild(repliesDiv);

// 返信フォーム(初期は非表示)
const replyFormDiv = document.createElement('div');
replyFormDiv.className = 'reply-form';
replyFormDiv.style.display = 'none';
const replyTextarea = document.createElement('textarea');
replyTextarea.placeholder = '返信内容を入力';
const submitReplyBtn = document.createElement('button');
submitReplyBtn.textContent = '返信する';
submitReplyBtn.onclick = () => {
const replyContent = replyTextarea.value.trim();
if (!replyContent) { alert('返信内容が空です。'); return; }
addReply(post.id, replyContent);
replyTextarea.value = '';
toggleReplyForm(post.id, true);
};
replyFormDiv.appendChild(replyTextarea);
replyFormDiv.appendChild(submitReplyBtn);
postDiv.appendChild(replyFormDiv);

return postDiv;
};

const renderReply = (reply) => {
const replyDiv = document.createElement('div');
replyDiv.className = 'reply-item';
const replyHeader = document.createElement('div');
replyHeader.className = 'reply-header';
replyHeader.textContent = reply.username;
const replyContent = document.createElement('div');
replyContent.textContent = reply.content;
const replyMeta = document.createElement('div');
replyMeta.className = 'reply-meta';
replyMeta.textContent = formatDate(reply.timestamp);
replyDiv.appendChild(replyHeader);
replyDiv.appendChild(replyContent);
replyDiv.appendChild(replyMeta);
return replyDiv;
};

const toggleReplyForm = (postId, forceHide = false) => {
const postDiv = document.querySelector(`.post-item[data-id="${postId}"]`);
if (!postDiv) return;
const replyForm = postDiv.querySelector('.reply-form');
replyForm.style.display = (forceHide || replyForm.style.display === 'block') ? 'none' : 'block';
};

const addReply = (postId, replyContent) => {
const post = posts.find(p => p.id === postId);
if (!post) return;
if (!post.replies) { post.replies = []; }
post.replies.push({
id: Date.now(),
username: currentUser.username,
content: replyContent,
timestamp: new Date()
});
saveData();
addLog(`投稿 ${post.id} に返信追加`);
renderTimeline();
};

// === 投稿作成処理 ===
const initPostButton = () => {
document.getElementById('postButton').addEventListener('click', () => {
const postContentElem = document.getElementById('postContent');
const content = postContentElem.value.trim();
if (!content) { alert('投稿内容が空です。'); return; }
const newPost = {
id: Date.now(),
username: currentUser.username,
content: content,
timestamp: new Date(),
likes: 0,
replies: []
};
posts.push(newPost);
saveData();
addLog(`投稿 ${newPost.id} 追加`);
renderTimeline();
postContentElem.value = '';
});
};

// === ユーティリティ処理:ログ/RSS ===
const initUtilityButtons = () => {
document.getElementById('toggleLogsButton').addEventListener('click', () => {
const logsSection = document.getElementById('logsSection');
if (logsSection.style.display === 'none' || logsSection.style.display === '') {
renderLogs();
logsSection.style.display = 'block';
document.getElementById('toggleLogsButton').textContent = 'ログを非表示';
} else {
logsSection.style.display = 'none';
document.getElementById('toggleLogsButton').textContent = 'ログを表示';
}
});
document.getElementById('downloadRSSButton').addEventListener('click', downloadRSS);
};

// === 初期化処理 ===
const init = () => {
loadData();
renderAuthSection();
togglePostForm();
renderTimeline();
initPostButton();
initUtilityButtons();
document.getElementById('searchInput').addEventListener('input', renderTimeline);
};

document.addEventListener('DOMContentLoaded', init);
})();
</script>
</body>
</html>


リアルタイムRSSトレンド.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>リアルタイムRSSトレンド</title>
    <style>
        body { font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 0; }
        h1 { text-align: center; padding: 20px; background: #333; color: white; }
        #rss-feed { max-width: 800px; margin: 20px auto; padding: 20px; background: white; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); }
        .article { border-bottom: 1px solid #ccc; padding: 15px; display: flex; flex-direction: column; }
        .article:last-child { border-bottom: none; }
        .article a { text-decoration: none; color: #333; font-weight: bold; font-size: 16px; }
        .article a:hover { color: #007bff; }
        .article p { margin: 5px 0; color: #666; font-size: 14px; }
        .loader { text-align: center; font-size: 18px; color: #666; }
        .refresh-btn { display: block; width: 200px; margin: 20px auto; padding: 10px; background: #007bff; color: white; text-align: center; cursor: pointer; border-radius: 5px; }
        .refresh-btn:hover { background: #0056b3; }
        .error { text-align: center; color: red; font-weight: bold; }
    </style>
</head>
<body>
    <h1>リアルタイムRSSトレンド</h1>
    <div id="rss-feed" class="loader">読み込み中...</div>
    <div class="refresh-btn" onclick="loadRSS()">最新の情報を取得</div>
    <div id="error-message" class="error"></div>
    
    <script>
        async function fetchRSS(url) {
            try {
                const response = await fetch(`https://api.rss2json.com/v1/api.json?rss_url=${encodeURIComponent(url)}`);
                if (!response.ok) throw new Error("RSS取得に失敗しました");
                const data = await response.json();
                return data.items || [];
            } catch (error) {
                console.error("RSSの取得に失敗しました", error);
                return [];
            }
        }

        async function loadRSS() {
            const feedElement = document.getElementById("rss-feed");
            const errorElement = document.getElementById("error-message");
            feedElement.innerHTML = "読み込み中...";
            errorElement.innerHTML = "";
            
            const rssUrls = [
                "https://news.google.com/rss?hl=ja&gl=JP&ceid=JP:ja",
                "https://rss.itmedia.co.jp/rss/2.0/news_bursts.xml",
                "https://www3.nhk.or.jp/rss/news/cat0.xml"
            ];
            
            let allArticles = [];
            for (const url of rssUrls) {
                const articles = await fetchRSS(url);
                allArticles = allArticles.concat(articles);
            }
            
            if (allArticles.length === 0) {
                feedElement.innerHTML = "";
                errorElement.innerHTML = "記事が取得できませんでした。";
                return;
            }
            
            allArticles.sort((a, b) => new Date(b.pubDate) - new Date(a.pubDate));
            feedElement.innerHTML = "";
            
            allArticles.forEach(article => {
                const articleElement = document.createElement("div");
                articleElement.classList.add("article");
                articleElement.innerHTML = `
                    <a href="${article.link}" target="_blank">${article.title}</a>
                    <p>公開日時: ${new Date(article.pubDate).toLocaleString()}</p>
                `;
                feedElement.appendChild(articleElement);
            });
        }
        
        document.addEventListener("DOMContentLoaded", loadRSS);
        setInterval(loadRSS, 300000); // 5分ごとに自動更新
    </script>
</body>
</html>

VRRPG.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>VRRPG - 拡張版 AR/VR RPG</title>
  <!-- A-Frame ライブラリ -->
  <script src="https://aframe.io/releases/1.4.0/aframe.min.js"></script>
  <!-- Particle system コンポーネント(パーティクル演出用) -->
  <script src="https://cdn.jsdelivr.net/gh/IdeaSpaceVR/aframe-particle-system-component@master/dist/aframe-particle-system-component.min.js"></script>
  <style>
    body { margin: 0; overflow: hidden; }
    /* 各種オーバーレイ */
    #mainMenuOverlay, #upgradeOverlay, #pauseOverlay, #gameOverOverlay {
      position: absolute;
      top: 0; left: 0;
      width: 100%; height: 100%;
      background: rgba(0,0,0,0.8);
      color: #FFF;
      display: flex;
      justify-content: center;
      align-items: center;
      font-size: 48px;
      z-index: 999;
      display: none;
      text-align: center;
      flex-direction: column;
    }
    #mainMenuOverlay button, #upgradeOverlay button, #gameOverOverlay button {
      font-size: 36px;
      padding: 20px 40px;
      margin-top: 20px;
    }
  </style>
</head>
<body>
  <!-- メインメニュー -->
  <div id="mainMenuOverlay" style="display: flex;">
    <div>
      <div>VRRPG - 拡張版 AR/VR RPG</div>
      <button id="startButton">Start Game</button>
    </div>
  </div>
  <!-- アップグレードストア -->
  <div id="upgradeOverlay">
    <div>
      <div>Upgrade Store</div>
      <div>Press 1: Increase Sword Damage (+10) (Cost: 50 Score)</div>
      <div>Press 2: Increase Max Health (+20) (Cost: 50 Score)</div>
      <button id="closeUpgrade">Close</button>
    </div>
  </div>
  <!-- ポーズオーバーレイ -->
  <div id="pauseOverlay">Paused</div>
  <!-- ゲームオーバーオーバーレイ -->
  <div id="gameOverOverlay">
    <div>
      <div>Game Over!</div>
      <div id="finalScore">Final Score: 0</div>
      <button id="restartButton">Restart</button>
    </div>
  </div>

  <!-- AR/VRシーン:XRモードを AR に設定(Oculusパススルー利用) -->
  <a-scene xr="mode: ar; referenceSpaceType: local-floor">
    <!-- 背景音楽 -->
    <a-entity id="bg-music" sound="src: url(bg-music.mp3); autoplay: true; loop: true; volume: 0.3"></a-entity>
    <!-- 環境 -->
    <a-sky color="#88ccee"></a-sky>
    <a-plane position="0 0 0" rotation="-90 0 0" width="30" height="30" color="#77aa55"></a-plane>
    <a-light type="directional" intensity="0.8" position="0 10 5"></a-light>

    <!-- プレイヤー(カメラ、HUD、ポーズ対応) -->
    <a-entity id="player" weapon-switcher position="0 1.6 5">
      <a-camera wasd-controls look-controls>
        <!-- 右手:カメラ内右下に固定表示(装備品) -->
        <a-entity id="rightHand" position="0.5 -0.3 -1"></a-entity>
      </a-camera>
      <!-- HUD -->
      <a-entity id="hud" position="0 -0.5 -1.5">
        <a-text id="scoreText" value="Score: 0" position="-1 0.7 0" color="#FFF" width="4"></a-text>
        <a-text id="healthText" value="Health: 100" position="-1 0.4 0" color="#FFF" width="4"></a-text>
        <a-text id="waveText" value="Wave: 1" position="-1 0.1 0" color="#FFF" width="4"></a-text>
        <a-text id="levelText" value="Lv: 1 Exp: 0" position="-1 -0.2 0" color="#FFF" width="4"></a-text>
        <a-text id="weaponText" value="Weapon: None" position="-1 -0.5 0" color="#FFF" width="4"></a-text>
      </a-entity>
    </a-entity>

    <!-- 落ちている剣(Sword) -->
    <a-entity id="sword" position="0.3 1 -2" sword-swing pickup>
      <!-- ブレード -->
      <a-entity geometry="primitive: box; height: 1; width: 0.1; depth: 0.05" 
                material="color: silver; metalness: 0.8; roughness: 0.2" 
                position="0 0.5 0"></a-entity>
      <!-- ガード -->
      <a-entity geometry="primitive: box; height: 0.2; width: 0.3; depth: 0.05" 
                material="color: gold" 
                position="0 0.05 0"></a-entity>
      <!-- ハンドル -->
      <a-entity geometry="primitive: cylinder; radius: 0.05; height: 0.4" 
                material="color: brown" 
                position="0 -0.3 0" rotation="90 0 0"></a-entity>
      <!-- 回転アニメーション(拾われるまで) -->
      <a-animation attribute="rotation" dur="3000" fill="forwards" to="0 360 0" repeat="indefinite"></a-animation>
    </a-entity>

    <!-- 魔法の杖(Magic Wand) ※ 未使用 -->
    <a-entity id="magicWand" geometry="primitive: cylinder; height: 0.8; radius: 0.05" 
              material="color: purple; emissive: #aa00ff" 
              position="0.3 1 -0.5" rotation="0 0 0" wand-fire visible="false"></a-entity>

    <!-- 敵スポーン用エリア -->
    <a-entity id="enemy-spawn"></a-entity>

    <!-- サウンド設定 -->
    <a-entity id="sword-sound" sound="src: url(sword-swing.mp3); on: none"></a-entity>
    <a-entity id="pickup-sound" sound="src: url(pickup.mp3); on: none"></a-entity>
    <a-entity id="wand-sound" sound="src: url(wand-fire.mp3); on: none"></a-entity>

    <!-- インストラクション表示(初回のみ) -->
    <a-entity id="instructions" position="0 2 -3">
      <a-text value="Controls: Oculus Touch / Gamepad / WASD+Mouse&#10;クリックで剣を拾い、剣をクリックで振る&#10;Pキーでポーズ / Uキーでアップグレード" 
              align="center" color="#FFF" width="6"></a-text>
    </a-entity>

    <!-- ウェーブ管理 -->
    <a-entity wave-manager></a-entity>
  </a-scene>

  <script>
    /************ ゲームデータ管理 ************/
    var gameData = {
      score: 0,
      playerHealth: 100,
      wave: 1,
      playerLevel: 1,
      experience: 0,
      currentWeapon: "None",
      swordDamage: 50,
      maxHealth: 100,
      hasSword: false,
      paused: false,
      gameState: "menu" // "menu", "playing", "paused", "gameover"
    };

    /************ HUD 更新関数 ************/
    function updateHUD() {
      document.querySelector('#scoreText').setAttribute('value', 'Score: ' + gameData.score);
      document.querySelector('#healthText').setAttribute('value', 'Health: ' + gameData.playerHealth);
      document.querySelector('#waveText').setAttribute('value', 'Wave: ' + gameData.wave);
      document.querySelector('#levelText').setAttribute('value', 'Lv: ' + gameData.playerLevel + ' Exp: ' + gameData.experience);
      document.querySelector('#weaponText').setAttribute('value', 'Weapon: ' + gameData.currentWeapon);
    }

    /************ ゲームオーバーチェック ************/
    function checkGameOver() {
      if (gameData.playerHealth <= 0) {
        gameData.gameState = "gameover";
        document.getElementById('gameOverOverlay').style.display = "flex";
        document.getElementById('finalScore').innerText = "Final Score: " + gameData.score;
      }
    }

    /************ 経験値加算&レベルアップ ************/
    function addExperience(exp) {
      gameData.experience += exp;
      if (gameData.experience >= 100) {
        gameData.experience -= 100;
        gameData.playerLevel++;
        gameData.playerHealth = Math.min(gameData.maxHealth, gameData.playerHealth + 20);
        openUpgradeStore();
      }
      updateHUD();
    }

    /************ 敵撃破時の演出 ************/
    function killEnemy(enemy) {
      if (!enemy) return;
      let healthBar = enemy.querySelector('.health-bar');
      if (healthBar) { healthBar.parentNode.removeChild(healthBar); }
      let explosion = document.createElement('a-entity');
      explosion.setAttribute('particle-system', 'preset: dust; particleCount: 100; color: #FFAA00, #FF0000;');
      explosion.setAttribute('position', enemy.getAttribute('position'));
      enemy.parentNode.appendChild(explosion);
      setTimeout(function(){ if(explosion.parentNode) explosion.parentNode.removeChild(explosion); }, 1000);
      enemy.setAttribute('animation', 'property: scale; to: 0 0 0; dur: 500; easing: easeInOutQuad');
      setTimeout(function(){ if(enemy.parentNode) enemy.parentNode.removeChild(enemy); }, 500);
    }

    /************ カメラシェイク ************/
    function cameraShake() {
      let camera = document.querySelector('a-camera');
      if (!camera) return;
      let origPos = camera.getAttribute('position');
      let shakePos = {
        x: origPos.x + (Math.random()-0.5)*0.1,
        y: origPos.y + (Math.random()-0.5)*0.1,
        z: origPos.z
      };
      camera.setAttribute('position', shakePos);
      setTimeout(function(){ camera.setAttribute('position', origPos); }, 100);
    }

    /************ アップグレードストア ************/
    function openUpgradeStore() {
      document.getElementById('upgradeOverlay').style.display = "flex";
      gameData.paused = true;
    }
    function closeUpgradeStore() {
      document.getElementById('upgradeOverlay').style.display = "none";
      gameData.paused = false;
    }
    document.getElementById('closeUpgrade').addEventListener('click', closeUpgradeStore);

    /************ メインメニュー&リスタート ************/
    document.getElementById('startButton').addEventListener('click', function(){
      document.getElementById('mainMenuOverlay').style.display = "none";
      gameData.gameState = "playing";
    });
    document.getElementById('restartButton').addEventListener('click', function(){
      window.location.reload();
    });

    /************ キー操作 ************/
    document.addEventListener('keydown', function(e) {
      if(e.key.toLowerCase() === 'p') {
        gameData.paused = !gameData.paused;
        document.getElementById('pauseOverlay').style.display = gameData.paused ? "flex" : "none";
      }
      if(e.key === 'u' && gameData.gameState === "playing" && !gameData.paused) {
        openUpgradeStore();
      }
      if(document.getElementById('upgradeOverlay').style.display === "flex") {
        if(e.key === '1') {
          if(gameData.score >= 50) {
            gameData.swordDamage += 10;
            gameData.score -= 50;
            updateHUD();
          }
        }
        if(e.key === '2') {
          if(gameData.score >= 50) {
            gameData.maxHealth += 20;
            gameData.score -= 50;
            updateHUD();
          }
        }
      }
    });

    /************ pickup コンポーネント ************/
    AFRAME.registerComponent('pickup', {
      init: function() {
        let el = this.el;
        el.addEventListener('click', function () {
          if(gameData.paused || gameData.gameState !== "playing") return;
          let player = document.querySelector('#player');
          let playerPos = new THREE.Vector3();
          player.object3D.getWorldPosition(playerPos);
          let itemPos = new THREE.Vector3();
          el.object3D.getWorldPosition(itemPos);
          if(playerPos.distanceTo(itemPos) < 2) {
            if(!gameData.hasSword) {
              let pickupSound = document.querySelector('#pickup-sound');
              if(pickupSound && pickupSound.components.sound) {
                pickupSound.components.sound.playSound();
              }
              let rightHand = document.querySelector('#rightHand');
              if(rightHand) {
                rightHand.appendChild(el);
                el.setAttribute('position', '0 0 0');
              } else {
                player.appendChild(el);
                el.setAttribute('position', '0.3 0 -0.5');
              }
              gameData.currentWeapon = "Sword";
              gameData.hasSword = true;
              updateHUD();
              console.log("Sword picked up!");
              el.removeAttribute('animation');
            } else {
              console.log("Already holding a sword.");
            }
          }
        });
      }
    });

    /************ enemy-ai コンポーネント ************/
    AFRAME.registerComponent('enemy-ai', {
      schema: {
        speed: {type: 'number', default: 0.02},
        damage: {type: 'number', default: 5}
      },
      init: function() { this.attackCooldown = 0; },
      tick: function(time, timeDelta) {
        if(gameData.paused) return;
        let player = document.querySelector('#player');
        if(!player) return;
        let enemy = this.el;
        let enemyPos = enemy.object3D.position;
        let playerPos = player.object3D.position;
        let direction = new THREE.Vector3().subVectors(playerPos, enemyPos);
        let distance = direction.length();
        if(distance > 0.1) {
          direction.normalize();
          enemy.object3D.position.add(direction.multiplyScalar(this.data.speed * (timeDelta/16)));
        }
        if(distance < 1 && this.attackCooldown <= 0) {
          gameData.playerHealth -= this.data.damage;
          updateHUD();
          cameraShake();
          checkGameOver();
          this.attackCooldown = 1000;
        } else {
          this.attackCooldown -= timeDelta;
        }
      }
    });

    /************ enemy-health コンポーネント ************/
    AFRAME.registerComponent('enemy-health', {
      schema: {
        hp: {type: 'number', default: 100},
        maxHp: {type: 'number', default: 100}
      },
      init: function(){
        let bar = document.createElement('a-plane');
        bar.setAttribute('class', 'health-bar');
        bar.setAttribute('width', '1');
        bar.setAttribute('height', '0.1');
        bar.setAttribute('color', 'green');
        bar.setAttribute('position', '0 0.8 0');
        this.el.appendChild(bar);
      },
      updateHealthBar: function(){
        let healthBar = this.el.querySelector('.health-bar');
        if(healthBar) {
          let hp = this.data.hp, max = this.data.maxHp;
          let scaleX = Math.max(0, hp/max);
          healthBar.setAttribute('scale', `${scaleX} 1 1`);
          let color = (scaleX > 0.5) ? "green" : (scaleX > 0.2 ? "yellow" : "red");
          healthBar.setAttribute('color', color);
        }
      }
    });

    /************ sword-swing コンポーネント ************/
    AFRAME.registerComponent('sword-swing', {
      init: function(){
        let sword = this.el;
        let self = this;
        sword.addEventListener('triggerdown', function(){ self.swing(); });
        sword.addEventListener('click', function(){ self.swing(); });
      },
      swing: function(){
        if(gameData.paused) return;
        this.el.emit('swing');
        let soundEl = document.querySelector('#sword-sound');
        if(soundEl && soundEl.components.sound){
          soundEl.components.sound.playSound();
        }
        let swordPos = new THREE.Vector3();
        this.el.object3D.getWorldPosition(swordPos);
        let enemies = document.querySelectorAll('.enemy');
        enemies.forEach(function(enemy){
          let enemyPos = new THREE.Vector3();
          enemy.object3D.getWorldPosition(enemyPos);
          if(swordPos.distanceTo(enemyPos) < 1){
            let eh = enemy.getAttribute('enemy-health');
            eh.hp -= gameData.swordDamage;
            enemy.setAttribute('enemy-health', 'hp', eh.hp);
            enemy.components['enemy-health'].updateHealthBar();
            if(eh.hp <= 0){
              killEnemy(enemy);
              gameData.score += 10;
              addExperience(20);
            } else {
              enemy.setAttribute('material', 'color', '#ff4444');
              setTimeout(function(){ enemy.setAttribute('material', 'color', '#66ff66'); }, 200);
            }
            updateHUD();
          }
        });
      }
    });

    /************ wand-fire コンポーネント ************/
    AFRAME.registerComponent('wand-fire', {
      init: function(){
        let wand = this.el;
        let self = this;
        wand.addEventListener('triggerdown', function(){ self.fire(); });
        wand.addEventListener('click', function(){ self.fire(); });
      },
      fire: function(){
        if(gameData.paused) return;
        let wandSound = document.querySelector('#wand-sound');
        if(wandSound && wandSound.components.sound){
          wandSound.components.sound.playSound();
        }
        let projectile = document.createElement('a-sphere');
        projectile.setAttribute('radius', '0.1');
        projectile.setAttribute('color', 'orange');
        let startPos = new THREE.Vector3();
        this.el.object3D.getWorldPosition(startPos);
        projectile.setAttribute('position', startPos);
        projectile.setAttribute('projectile', '');
        this.el.sceneEl.appendChild(projectile);
      }
    });

    /************ projectile コンポーネント ************/
    AFRAME.registerComponent('projectile', {
      schema: { speed: {type: 'number', default: 0.1}, damage: {type: 'number', default: 30} },
      init: function(){
        this.direction = new THREE.Vector3();
        this.el.object3D.getWorldDirection(this.direction);
      },
      tick: function(time, timeDelta){
        if(gameData.paused) return;
        let distance = this.data.speed * (timeDelta/16);
        this.el.object3D.position.add(this.direction.clone().multiplyScalar(distance));
        let projectilePos = new THREE.Vector3();
        this.el.object3D.getWorldPosition(projectilePos);
        let enemies = document.querySelectorAll('.enemy');
        for(let i=0; i<enemies.length; i++){
          let enemy = enemies[i];
          let enemyPos = new THREE.Vector3();
          enemy.object3D.getWorldPosition(enemyPos);
          if(projectilePos.distanceTo(enemyPos) < 0.5){
            let eh = enemy.getAttribute('enemy-health');
            eh.hp -= this.data.damage;
            enemy.setAttribute('enemy-health', 'hp', eh.hp);
            enemy.components['enemy-health'].updateHealthBar();
            if(eh.hp <= 0){
              killEnemy(enemy);
              gameData.score += 10;
              addExperience(20);
            } else {
              enemy.setAttribute('material', 'color', '#ff4444');
              setTimeout(function(){ enemy.setAttribute('material', 'color', '#66ff66'); }, 200);
            }
            updateHUD();
            this.el.parentNode.removeChild(this.el);
            return;
          }
        }
        if(projectilePos.length() > 50){
          this.el.parentNode.removeChild(this.el);
        }
      }
    });

    /************ weapon-switcher コンポーネント ************/
    AFRAME.registerComponent('weapon-switcher', {
      init: function(){
        window.addEventListener('keydown', function(event){
          if(event.key === '1'){
            if(gameData.hasSword){
              gameData.currentWeapon = "Sword";
              document.querySelector('#rightHand').setAttribute('visible', 'true');
              document.querySelector('#magicWand').setAttribute('visible', 'false');
            }
            updateHUD();
          } else if(event.key === '2'){
            gameData.currentWeapon = "Magic";
            document.querySelector('#sword').setAttribute('visible', 'false');
            document.querySelector('#magicWand').setAttribute('visible', 'true');
            updateHUD();
          }
        });
      }
    });

    /************ wave-manager コンポーネント ************/
    // 敵が全滅したら次のウェーブを生成。ウェーブ番号が5の倍数の場合はボス出現。
    AFRAME.registerComponent('wave-manager', {
      tick: function(){
        if(gameData.paused) return;
        let spawnZone = document.querySelector('#enemy-spawn');
        if(spawnZone.children.length === 0){
          gameData.wave += 1;
          updateHUD();
          this.spawnWave();
        }
      },
      spawnWave: function(){
        let spawnZone = document.querySelector('#enemy-spawn');
        if(gameData.wave % 5 === 0){
          // ボスウェーブ
          let boss = document.createElement('a-entity');
          boss.classList.add('enemy');
          boss.setAttribute('position', '0 1 -6');
          boss.setAttribute('geometry', 'primitive: sphere; radius: 1');
          boss.setAttribute('material', 'color: #aa0000; opacity: 0.9; transparent: true');
          boss.setAttribute('animation__rotate', 'property: rotation; to: 0 360 0; dur: 6000; loop: true');
          boss.setAttribute('enemy-ai', 'speed: 0.015; damage: 10');
          boss.setAttribute('enemy-health', 'hp: 300; maxHp: 300');
          spawnZone.appendChild(boss);
        } else {
          let enemyCount = 3 + gameData.wave - 1;
          for(let i=0; i<enemyCount; i++){
            let enemy = document.createElement('a-entity');
            enemy.classList.add('enemy');
            let angle = Math.random() * Math.PI * 2;
            let radius = 5 + Math.random() * 5;
            let x = Math.cos(angle) * radius;
            let z = Math.sin(angle) * radius;
            enemy.setAttribute('position', `${x} 1 ${z}`);
            enemy.setAttribute('geometry', 'primitive: sphere; radius: 0.5');
            enemy.setAttribute('material', 'color: #66ff66; opacity: 0.8; transparent: true');
            enemy.setAttribute('animation__wobble', 'property: scale; to: 1.1 0.9 1.1; dur: 1000; dir: alternate; loop: true');
            enemy.setAttribute('enemy-ai', 'speed: 0.02; damage: 5');
            enemy.setAttribute('enemy-health', 'hp: 100; maxHp: 100');
            spawnZone.appendChild(enemy);
          }
        }
      }
    });

    /************ 初回 HUD 更新 & インストラクション削除 ************/
    document.addEventListener('DOMContentLoaded', function(){
      updateHUD();
      setTimeout(function(){
        let instructions = document.getElementById('instructions');
        if(instructions){ instructions.parentNode.removeChild(instructions); }
      }, 5000);
    });
  </script>
</body>
</html>

Automation Studio

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Automation Studio</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
    <style>
        :root {
            --primary-color: #00c7b7;
            --secondary-color: #2d3436;
            --background-color: #f8f9fa;
            --card-shadow: 0 4px 6px rgba(0,0,0,0.1);
        }

        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            font-family: 'Segoe UI', system-ui;
        }

        body {
            background: var(--background-color);
            color: var(--secondary-color);
        }

        /* ヘッダー */
        .dashboard-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 1.5rem;
            background: white;
            box-shadow: var(--card-shadow);
        }

        .brand {
            display: flex;
            align-items: center;
            gap: 1rem;
        }

        .brand-logo {
            width: 40px;
            height: 40px;
        }

        /* メインコンテンツ */
        .main-container {
            max-width: 1200px;
            margin: 2rem auto;
            padding: 0 1rem;
        }

        /* アプレットカード */
        .applet-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
            gap: 1.5rem;
            margin-top: 2rem;
        }

        .applet-card {
            background: white;
            border-radius: 12px;
            padding: 1.5rem;
            box-shadow: var(--card-shadow);
            transition: transform 0.2s;
            position: relative;
        }

        .applet-card:hover {
            transform: translateY(-5px);
        }

        .applet-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 1rem;
        }

        .toggle-switch {
            position: relative;
            display: inline-block;
            width: 48px;
            height: 24px;
        }

        .toggle-slider {
            position: absolute;
            cursor: pointer;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background-color: #ccc;
            transition: .4s;
            border-radius: 34px;
        }

        .toggle-slider:before {
            position: absolute;
            content: "";
            height: 16px;
            width: 16px;
            left: 4px;
            bottom: 4px;
            background-color: white;
            transition: .4s;
            border-radius: 50%;
        }

        input:checked + .toggle-slider {
            background-color: var(--primary-color);
        }

        input:checked + .toggle-slider:before {
            transform: translateX(24px);
        }

        .service-flow {
            display: flex;
            align-items: center;
            gap: 1rem;
            margin: 1.5rem 0;
        }

        .service-icon {
            width: 48px;
            height: 48px;
            border-radius: 12px;
            padding: 8px;
            box-shadow: var(--card-shadow);
        }

        .divider-arrow {
            flex-grow: 1;
            border-top: 2px dashed #ddd;
            position: relative;
        }

        .divider-arrow::after {
            content: "➔";
            position: absolute;
            right: -10px;
            top: -12px;
            background: white;
            padding: 0 4px;
            color: var(--primary-color);
        }

        /* 作成モーダル */
        .modal-overlay {
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: rgba(0,0,0,0.5);
            display: none;
            align-items: center;
            justify-content: center;
            z-index: 1000;
        }

        .modal-content {
            background: white;
            border-radius: 16px;
            width: 90%;
            max-width: 600px;
            max-height: 90vh;
            overflow-y: auto;
            padding: 2rem;
            animation: modalSlideIn 0.3s ease-out;
        }

        @keyframes modalSlideIn {
            from { transform: translateY(20px); opacity: 0; }
            to { transform: translateY(0); opacity: 1; }
        }

        .service-picker {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            gap: 1rem;
            margin: 2rem 0;
        }

        .service-option {
            text-align: center;
            padding: 1rem;
            border: 2px solid #eee;
            border-radius: 8px;
            cursor: pointer;
            transition: all 0.2s;
        }

        .service-option:hover {
            border-color: var(--primary-color);
            background: #f7f7f7;
        }

        .service-option img {
            width: 64px;
            height: 64px;
            margin-bottom: 0.5rem;
        }

        /* レスポンシブデザイン */
        @media (max-width: 768px) {
            .applet-grid {
                grid-template-columns: 1fr;
            }

            .service-picker {
                grid-template-columns: repeat(2, 1fr);
            }
        }
    </style>
</head>
<body>
    <header class="dashboard-header">
        <div class="brand">
            <img src="https://img.icons8.com/color/96/automation.png" class="brand-logo" alt="Logo">
            <h1>Automation Studio</h1>
        </div>
        <button class="create-button" onclick="openModal()">
            <i class="fas fa-plus"></i> New Automation
        </button>
    </header>

    <div class="main-container">
        <div class="applet-grid">
            <!-- アプレットが動的に追加されます -->
        </div>
    </div>

    <!-- 作成モーダル -->
    <div class="modal-overlay" id="modal">
        <div class="modal-content">
            <h2>Create New Automation</h2>
            <div class="service-picker" id="triggerServices">
                <!-- トリガーサービスが動的に追加されます -->
            </div>
            
            <div class="service-picker" id="actionServices">
                <!-- アクションサービスが動的に追加されます -->
            </div>

            <div class="modal-actions">
                <button class="btn-secondary" onclick="closeModal()">Cancel</button>
                <button class="btn-primary" onclick="createApplet()">Create</button>
            </div>
        </div>
    </div>

    <script>
        // ダミーデータ
        const services = {
            triggers: [
                { id: 'email', name: 'Email', icon: 'https://img.icons8.com/color/96/new-post.png' },
                { id: 'calendar', name: 'Calendar', icon: 'https://img.icons8.com/color/96/google-calendar.png' },
                { id: 'weather', name: 'Weather', icon: 'https://img.icons8.com/color/96/partly-cloudy-day.png' }
            ],
            actions: [
                { id: 'slack', name: 'Slack', icon: 'https://img.icons8.com/color/96/slack.png' },
                { id: 'drive', name: 'Google Drive', icon: 'https://img.icons8.com/color/96/google-drive.png' },
                { id: 'notification', name: 'Notification', icon: 'https://img.icons8.com/color/96/appointment-reminders.png' }
            ]
        };

        // モーダル制御
        function openModal() {
            document.getElementById('modal').style.display = 'flex';
            populateServices();
        }

        function closeModal() {
            document.getElementById('modal').style.display = 'none';
        }

        // サービスの表示
        function populateServices() {
            const triggerContainer = document.getElementById('triggerServices');
            const actionContainer = document.getElementById('actionServices');

            triggerContainer.innerHTML = services.triggers.map(service => `
                <div class="service-option" onclick="selectTrigger('${service.id}')">
                    <img src="${service.icon}" alt="${service.name}">
                    <div>${service.name}</div>
                </div>
            `).join('');

            actionContainer.innerHTML = services.actions.map(service => `
                <div class="service-option" onclick="selectAction('${service.id}')">
                    <img src="${service.icon}" alt="${service.name}">
                    <div>${service.name}</div>
                </div>
            `).join('');
        }

        // アプレット作成
        let selectedTrigger = null;
        let selectedAction = null;

        function selectTrigger(serviceId) {
            selectedTrigger = services.triggers.find(s => s.id === serviceId);
            updateSelections();
        }

        function selectAction(serviceId) {
            selectedAction = services.actions.find(s => s.id === serviceId);
            updateSelections();
        }

        function updateSelections() {
            document.querySelectorAll('.service-option').forEach(el => {
                el.style.borderColor = '#eee';
            });
            
            if (selectedTrigger) {
                const triggerEl = document.querySelector(`[onclick="selectTrigger('${selectedTrigger.id}')"]`);
                triggerEl.style.borderColor = 'var(--primary-color)';
            }

            if (selectedAction) {
                const actionEl = document.querySelector(`[onclick="selectAction('${selectedAction.id}')"]`);
                actionEl.style.borderColor = 'var(--primary-color)';
            }
        }

        function createApplet() {
            if (!selectedTrigger || !selectedAction) return;

            const appletGrid = document.querySelector('.applet-grid');
            const newApplet = document.createElement('div');
            newApplet.className = 'applet-card';
            newApplet.innerHTML = `
                <div class="applet-header">
                    <h3>${selectedTrigger.name} → ${selectedAction.name}</h3>
                    <label class="toggle-switch">
                        <input type="checkbox">
                        <span class="toggle-slider"></span>
                    </label>
                </div>
                <div class="service-flow">
                    <img src="${selectedTrigger.icon}" class="service-icon">
                    <div class="divider-arrow"></div>
                    <img src="${selectedAction.icon}" class="service-icon">
                </div>
                <p>Last triggered: Never</p>
            `;

            appletGrid.appendChild(newApplet);
            closeModal();
            selectedTrigger = null;
            selectedAction = null;
        }

        // モーダル外をクリックで閉じる
        window.onclick = function(event) {
            if (event.target.classList.contains('modal-overlay')) {
                closeModal();
            }
        }
    </script>
</body>
</html>

ライブストリーミングサイト

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <title>疑似配信サイト - PC画面&システム音声</title>
  <style>
    /* ベースリセット */
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }

    body {
      font-family: sans-serif;
      background: #f5f5f5;
      color: #333;
    }

    header, footer {
      background: #333;
      color: #fff;
      padding: 15px;
      text-align: center;
    }

    header h1 {
      margin: 0;
      font-size: 1.5rem;
      letter-spacing: 0.05em;
    }

    footer p {
      margin: 0;
      font-size: 0.9rem;
    }

    main {
      max-width: 1200px;
      margin: 20px auto;
      padding: 20px;
      background: #fff;
      box-shadow: 0 0 10px rgba(0,0,0,0.1);
    }

    .page-title {
      text-align: center;
      margin-bottom: 20px;
      font-size: 1.4rem;
    }

    /* 2カラムレイアウト */
    .container {
      display: flex;
      flex-wrap: wrap;
      gap: 20px;
    }
    .video-section {
      flex: 1 1 600px;
      min-width: 300px;
    }
    .aside-section {
      flex: 1 1 350px;
      min-width: 300px;
      background: #fafafa;
      border-left: 1px solid #ccc;
      padding: 10px;
    }

    /* 動画表示領域 */
    .video-wrapper {
      background: #000;
      overflow: hidden;
    }
    .video-wrapper video {
      display: block;
      width: 100%;
      height: auto;
      background: #000;
    }
    .video-title {
      margin: 10px 0;
      font-weight: bold;
    }
    .video-controls {
      text-align: center;
      margin-top: 10px;
    }
    .video-controls button {
      margin: 0 5px;
      padding: 8px 16px;
      font-size: 1rem;
      cursor: pointer;
    }

    /* チャットUI */
    .chat-section {
      display: flex;
      flex-direction: column;
      height: 400px;
    }
    .chat-log {
      flex: 1;
      border: 1px solid #ccc;
      padding: 10px;
      overflow-y: auto;
      margin-bottom: 10px;
      background: #fff;
    }
    .chat-log p {
      margin: 0 0 5px 0;
    }
    .chat-log p span.username {
      font-weight: bold;
      color: #3366cc;
      margin-right: 5px;
    }
    .chat-input-area {
      display: flex;
      gap: 5px;
    }
    .chat-input-area input[type="text"] {
      flex: 1;
      padding: 8px;
      font-size: 1rem;
    }
    .chat-input-area button {
      padding: 8px 16px;
      font-size: 1rem;
      cursor: pointer;
    }

    /* スケジュール */
    .schedule {
      margin-top: 20px;
    }
    .schedule h2 {
      font-size: 1.2rem;
      margin-bottom: 10px;
    }
    .schedule table {
      width: 100%;
      border-collapse: collapse;
    }
    .schedule th, .schedule td {
      border: 1px solid #ccc;
      padding: 10px;
      text-align: center;
    }
    .schedule th {
      background: #f0f0f0;
    }
    .schedule tr:nth-child(even) {
      background: #fafafa;
    }
  </style>
</head>
<body>

<!-- ヘッダー -->
<header>
  <h1>My Advanced Live Streaming (Local Demo)</h1>
</header>

<!-- メインコンテンツ -->
<main>
  <div class="page-title">PC画面&システム音声をローカル再生するデモ</div>

  <div class="container">

    <!-- ▼ 動画エリア ▼ -->
    <section class="video-section">
      <div class="video-title">疑似ライブ映像</div>
      <div class="video-wrapper">
        <video id="liveVideo" controls autoplay></video>
      </div>

      <!-- ボタン類 -->
      <div style="text-align: center; margin-top: 10px;">
        <button id="screenShareBtn">画面共有(システム音声)</button>
        <button id="cameraShareBtn">カメラ&マイク</button>
        <button id="stopBtn">停止</button>
      </div>

      <!-- 再生/一時停止/ミュート/全画面 -->
      <div class="video-controls">
        <button id="playBtn">再生</button>
        <button id="pauseBtn">一時停止</button>
        <button id="muteBtn">ミュート/解除</button>
        <button id="fullscreenBtn">全画面</button>
      </div>

      <div class="schedule">
        <h2>配信スケジュール(仮)</h2>
        <table>
          <thead>
            <tr>
              <th>日付</th>
              <th>配信タイトル</th>
              <th>開始時間</th>
              <th>予定</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td>2/15</td>
              <td>PC画面共有テスト</td>
              <td>18:00</td>
              <td>30分</td>
            </tr>
            <tr>
              <td>2/18</td>
              <td>音声ミキシング練習</td>
              <td>20:00</td>
              <td>1時間</td>
            </tr>
            <tr>
              <td>2/20</td>
              <td>週末雑談</td>
              <td>21:30</td>
              <td>2時間</td>
            </tr>
          </tbody>
        </table>
      </div>
    </section>

    <!-- ▼ チャットエリア ▼ -->
    <aside class="aside-section">
      <h2>チャット</h2>
      <div class="chat-section">
        <!-- チャットログ -->
        <div id="chatLog" class="chat-log"></div>
        <!-- 入力フォーム -->
        <div class="chat-input-area">
          <input type="text" id="usernameInput" placeholder="ユーザー名" />
          <input type="text" id="messageInput" placeholder="メッセージを入力…" />
          <button id="sendBtn">送信</button>
        </div>
      </div>
    </aside>

  </div><!-- /container -->

</main>

<!-- フッター -->
<footer>
  <p>© 2025 My Advanced Live Streaming (Local Demo)</p>
</footer>

<script>
/*****************************************************************
 * 1. PC画面&システム音声 or カメラ&マイクを取得
 *****************************************************************/
const video = document.getElementById('liveVideo');
const screenShareBtn = document.getElementById('screenShareBtn');
const cameraShareBtn = document.getElementById('cameraShareBtn');
const stopBtn = document.getElementById('stopBtn');

let localStream = null;

// 画面共有 (PC画面 + システム音声)
async function startScreenShare() {
  stopMedia(); // 既存ストリームを止めてから開始
  try {
    // getDisplayMediaで画面を共有
    // audio:true -> Chromeの場合「システム音声を共有」オプションが表示される
    localStream = await navigator.mediaDevices.getDisplayMedia({
      video: true,
      audio: true // ただしブラウザやOSによっては無視される可能性あり
    });
    video.srcObject = localStream;
    // システム音声をモニタリングする場合、ミュートOFFにする
    video.muted = false; 
    console.log('画面共有開始 (システム音声含む可能性)');
  } catch (err) {
    alert('画面共有を開始できませんでした: ' + err.message);
  }
}

// カメラ&マイク
async function startCameraShare() {
  stopMedia();
  try {
    localStream = await navigator.mediaDevices.getUserMedia({video: true, audio: true});
    video.srcObject = localStream;
    // 自分のマイク音声がループしないようにしたい場合は video.muted = true;
    video.muted = true; 
    console.log('カメラ&マイク開始');
  } catch (err) {
    alert('カメラ/マイクを取得できませんでした: ' + err.message);
  }
}

// 停止
function stopMedia() {
  if (!localStream) return;
  localStream.getTracks().forEach(track => track.stop());
  localStream = null;
  video.srcObject = null;
  console.log('メディア停止');
}

screenShareBtn.addEventListener('click', startScreenShare);
cameraShareBtn.addEventListener('click', startCameraShare);
stopBtn.addEventListener('click', stopMedia);


/*****************************************************************
 * 2. 再生/一時停止/ミュート/全画面など動画コントロール
 *****************************************************************/
const playBtn = document.getElementById('playBtn');
const pauseBtn = document.getElementById('pauseBtn');
const muteBtn = document.getElementById('muteBtn');
const fullscreenBtn = document.getElementById('fullscreenBtn');

playBtn.addEventListener('click', () => {
  video.play();
});
pauseBtn.addEventListener('click', () => {
  video.pause();
});
muteBtn.addEventListener('click', () => {
  video.muted = !video.muted;
});
fullscreenBtn.addEventListener('click', () => {
  if (!document.fullscreenElement) {
    video.requestFullscreen().catch(err => {
      console.warn('フルスクリーンモードにできません:', err);
    });
  } else {
    document.exitFullscreen();
  }
});


/*****************************************************************
 * 3. 簡易チャット機能(ローカルのみ)
 *****************************************************************/
const chatLog = document.getElementById('chatLog');
const usernameInput = document.getElementById('usernameInput');
const messageInput = document.getElementById('messageInput');
const sendBtn = document.getElementById('sendBtn');

let messages = [];

function renderChatLog() {
  chatLog.innerHTML = '';
  messages.forEach(msg => {
    const p = document.createElement('p');
    const userSpan = document.createElement('span');
    userSpan.className = 'username';
    userSpan.textContent = msg.username + ': ';
    const textNode = document.createTextNode(msg.text);
    p.appendChild(userSpan);
    p.appendChild(textNode);
    chatLog.appendChild(p);
  });
  chatLog.scrollTop = chatLog.scrollHeight;
}

function sendMessage() {
  const username = usernameInput.value.trim();
  const text = messageInput.value.trim();
  if (!username || !text) return;

  messages.push({ username, text });
  renderChatLog();
  messageInput.value = '';
}

sendBtn.addEventListener('click', sendMessage);
messageInput.addEventListener('keydown', e => {
  if (e.key === 'Enter') sendMessage();
});

// デモ用メッセージ
(function initDemoChat() {
  messages.push(
    { username: 'Alice', text: 'こんにちは!' },
    { username: 'Bob', text: 'システム音声聞こえる?' },
    { username: 'Carol', text: 'タブの音は共有できてます〜' }
  );
  renderChatLog();
})();
</script>

</body>
</html>

ARTBOOK

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ARTBOOK - アートSNS</title>
    <style>
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            margin: 0;
            padding: 0;
            background-color: #f9fafb;
            color: #333;
        }
        header {
            background-color: #4a5568;
            color: white;
            padding: 1.5rem;
            text-align: center;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
        }
        header h1 {
            margin: 0;
            font-size: 2.5rem;
        }
        header p {
            margin: 0.5rem 0;
            font-size: 1.2rem;
        }
        header nav {
            margin-top: 1rem;
        }
        header nav a {
            color: white;
            text-decoration: none;
            margin: 0 1rem;
            font-weight: bold;
            font-size: 1.1rem;
            padding: 0.5rem 1rem;
            border-radius: 5px;
            transition: background-color 0.3s;
        }
        header nav a:hover {
            background-color: #2d3748;
        }
        .container {
            max-width: 1200px;
            margin: 2rem auto;
            padding: 1rem;
        }
        .form-group {
            margin-bottom: 1rem;
        }
        .form-group label {
            display: block;
            margin-bottom: 0.5rem;
            font-weight: bold;
        }
        .form-group input, .form-group textarea {
            width: 100%;
            padding: 0.8rem;
            border: 1px solid #d2d6dc;
            border-radius: 6px;
            background-color: #f7fafc;
            font-size: 1rem;
        }
        .form-group button {
            background-color: #3182ce;
            color: white;
            border: none;
            padding: 0.8rem 1.5rem;
            font-size: 1rem;
            border-radius: 6px;
            cursor: pointer;
            transition: background-color 0.3s;
        }
        .form-group button:hover {
            background-color: #2b6cb0;
        }
        .profile {
            text-align: center;
        }
        .profile img {
            width: 150px;
            height: 150px;
            border-radius: 50%;
            object-fit: cover;
            margin-bottom: 1rem;
        }
        footer {
            text-align: center;
            padding: 1.5rem;
            background-color: #4a5568;
            color: white;
            margin-top: 2rem;
        }
        footer p {
            margin: 0;
            font-size: 1rem;
        }
    </style>
</head>
<body>
    <header>
        <h1>ARTBOOK</h1>
        <p>あなたのアートをシェアしよう!</p>
        <nav>
            <a href="#home" onclick="navigateTo('home')">ホーム</a>
            <a href="#gallery" onclick="navigateTo('gallery')">作品一覧</a>
            <a href="#post" onclick="navigateTo('post')">投稿する</a>
            <a href="#profile" onclick="navigateTo('profile')">プロフィール</a>
        </nav>
    </header>

    <div id="content" class="container">
        <!-- コンテンツがここに動的に挿入されます -->
    </div>

    <footer>
        <p>&copy; 2025 ARTBOOK. All Rights Reserved.</p>
    </footer>

    <script>
        let posts = JSON.parse(localStorage.getItem('posts')) || [];
        let profile = JSON.parse(localStorage.getItem('profile')) || {
            name: "未設定",
            bio: "ここに自己紹介が表示されます",
            image: "https://via.placeholder.com/150"
        };

        function navigateTo(section) {
            const content = document.getElementById('content');
            content.innerHTML = '';

            if (section === 'home') {
                content.innerHTML = `<h2>ホーム</h2><p>ようこそ、ARTBOOKへ!最新のアート作品をご覧ください。</p>`;
            } else if (section === 'gallery') {
                content.innerHTML = `<h2>作品一覧</h2><div id='gallery'>${renderPosts()}</div>`;
            } else if (section === 'post') {
                content.innerHTML = `
                    <h2>投稿する</h2>
                    <form id="postForm">
                        <div class="form-group">
                            <label for="title">タイトル</label>
                            <input type="text" id="title" name="title" required>
                        </div>
                        <div class="form-group">
                            <label for="description">説明</label>
                            <textarea id="description" name="description" rows="4" required></textarea>
                        </div>
                        <div class="form-group">
                            <label for="upload">画像アップロード</label>
                            <input type="file" id="upload" name="upload" accept="image/*" required>
                        </div>
                        <div class="form-group">
                            <button type="button" onclick="submitPost()">投稿する</button>
                        </div>
                    </form>`;
            } else if (section === 'profile') {
                content.innerHTML = `
                    <h2>プロフィール</h2>
                    <div class="profile">
                        <img src="${profile.image}" alt="プロフィール画像">
                        <h3>${profile.name}</h3>
                        <p>${profile.bio}</p>
                        <form id="profileForm">
                            <div class="form-group">
                                <label for="name">名前</label>
                                <input type="text" id="name" value="${profile.name}" required>
                            </div>
                            <div class="form-group">
                                <label for="bio">自己紹介</label>
                                <textarea id="bio" rows="4" required>${profile.bio}</textarea>
                            </div>
                            <div class="form-group">
                                <label for="profileImage">プロフィール画像</label>
                                <input type="file" id="profileImage" accept="image/*">
                            </div>
                            <div class="form-group">
                                <button type="button" onclick="updateProfile()">更新する</button>
                            </div>
                        </form>
                    </div>`;
            }
        }

        function submitPost() {
            const title = document.getElementById('title').value;
            const description = document.getElementById('description').value;
            const upload = document.getElementById('upload').files[0];

            if (title && description && upload) {
                const reader = new FileReader();

                reader.onload = function(event) {
                    posts.push({ title, description, image: event.target.result });
                    localStorage.setItem('posts', JSON.stringify(posts));
                    alert('投稿が完了しました!');
                    navigateTo('gallery');
                };

                reader.readAsDataURL(upload);
            } else {
                alert('すべてのフィールドを入力してください。');
            }
        }

        function updateProfile() {
            const name = document.getElementById('name').value;
            const bio = document.getElementById('bio').value;
            const profileImage = document.getElementById('profileImage').files[0];

            if (name && bio) {
                if (profileImage) {
                    const reader = new FileReader();
                    reader.onload = function(event) {
                        profile.image = event.target.result;
                        saveProfile(name, bio);
                    };
                    reader.readAsDataURL(profileImage);
                } else {
                    saveProfile(name, bio);
                }
            } else {
                alert('すべてのフィールドを入力してください。');
            }
        }

        function saveProfile(name, bio) {
            profile.name = name;
            profile.bio = bio;
            localStorage.setItem('profile', JSON.stringify(profile));
            alert('プロフィールが更新されました!');
            navigateTo('profile');
        }

        function renderPosts() {
            if (posts.length === 0) {
                return '<p>まだ投稿がありません。</p>';
            }

            return posts.map(post => `
                <div class="art-card">
                    <img src="${post.image}" alt="${post.title}">
                    <h2>${post.title}</h2>
                    <p>${post.description}</p>
                </div>`).join('');
        }

        // 初期表示
        navigateTo('home');
    </script>
</body>
</html>

百科事典サイト

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Encyclopedia</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 0;
            background-color: #f4f4f4;
        }
        header {
            background-color: #2c3e50;
            color: white;
            padding: 20px;
            text-align: center;
        }
        nav {
            display: flex;
            justify-content: center;
            background-color: #34495e;
            padding: 10px;
        }
        nav a {
            color: white;
            text-decoration: none;
            margin: 0 15px;
            font-weight: bold;
        }
        nav a:hover {
            text-decoration: underline;
        }
        .search-bar {
            display: flex;
            justify-content: center;
            margin: 20px 0;
        }
        .search-bar input {
            width: 60%;
            padding: 10px;
            border: 1px solid #ccc;
            border-radius: 5px;
            font-size: 16px;
        }
        .search-bar button {
            padding: 10px 20px;
            background-color: #2c3e50;
            color: white;
            border: none;
            border-radius: 5px;
            margin-left: 5px;
            cursor: pointer;
            font-size: 16px;
        }
        .search-bar button:hover {
            background-color: #34495e;
        }
        .categories, .articles, .about {
            margin: 20px;
            padding: 20px;
            background-color: white;
            border-radius: 5px;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
        }
        .categories h2, .articles h2, .about h2 {
            margin-bottom: 15px;
            font-size: 24px;
        }
        .categories ul, .articles ul {
            list-style: none;
            padding: 0;
        }
        .categories li, .articles li {
            margin-bottom: 10px;
        }
        .categories a, .articles a {
            color: #2c3e50;
            text-decoration: none;
            font-size: 18px;
        }
        .categories a:hover, .articles a:hover {
            text-decoration: underline;
        }
        .about p {
            line-height: 1.6;
            font-size: 18px;
            color: #333;
        }
        .featured {
            margin: 20px;
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
            gap: 20px;
        }
        .featured-item {
            background-color: white;
            border-radius: 5px;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
            overflow: hidden;
        }
        .featured-item img {
            width: 100%;
            height: auto;
        }
        .featured-item h3 {
            margin: 15px;
            font-size: 20px;
            color: #2c3e50;
        }
        .featured-item p {
            margin: 15px;
            line-height: 1.5;
            color: #666;
        }
        .featured-item a {
            display: block;
            text-align: center;
            padding: 10px;
            margin: 15px;
            background-color: #2c3e50;
            color: white;
            text-decoration: none;
            border-radius: 5px;
        }
        .featured-item a:hover {
            background-color: #34495e;
        }
        footer {
            background-color: #2c3e50;
            color: white;
            text-align: center;
            padding: 10px 0;
            margin-top: 20px;
        }
    </style>
</head>
<body>

<header>
    <h1>Welcome to the Encyclopedia</h1>
    <p>Your go-to source for comprehensive knowledge</p>
</header>

<nav>
    <a href="#">Home</a>
    <a href="#categories">Categories</a>
    <a href="#articles">Popular Articles</a>
    <a href="#about">About</a>
</nav>

<div class="search-bar">
    <input type="text" placeholder="Search articles...">
    <button>Search</button>
</div>

<section id="categories" class="categories">
    <h2>Categories</h2>
    <ul>
        <li><a href="#">Science</a></li>
        <li><a href="#">Technology</a></li>
        <li><a href="#">History</a></li>
        <li><a href="#">Geography</a></li>
        <li><a href="#">Culture</a></li>
    </ul>
</section>

<section id="articles" class="articles">
    <h2>Featured Articles</h2>
    <ul>
        <li><a href="#">The Wonders of Space Exploration</a></li>
        <li><a href="#">The Rise and Fall of Ancient Civilizations</a></li>
        <li><a href="#">Understanding Quantum Mechanics</a></li>
        <li><a href="#">The Impact of AI on Modern Society</a></li>
        <li><a href="#">World's Most Breathtaking Landscapes</a></li>
    </ul>
</section>

<section class="featured">
    <div class="featured-item">
        <img src="https://via.placeholder.com/300" alt="Space Exploration">
        <h3>The Wonders of Space Exploration</h3>
        <p>Discover the mysteries of the universe and the advances in space technology that bring us closer to the stars.</p>
        <a href="#">Read More</a>
    </div>
    <div class="featured-item">
        <img src="https://via.placeholder.com/300" alt="Ancient Civilizations">
        <h3>The Rise and Fall of Ancient Civilizations</h3>
        <p>Explore the history of ancient empires and their lasting impact on the modern world.</p>
        <a href="#">Read More</a>
    </div>
    <div class="featured-item">
        <img src="https://via.placeholder.com/300" alt="Quantum Mechanics">
        <h3>Understanding Quantum Mechanics</h3>
        <p>Dive into the complex and fascinating world of quantum physics and its real-world applications.</p>
        <a href="#">Read More</a>
    </div>
</section>

<section id="about" class="about">
    <h2>About This Encyclopedia</h2>
    <p>This online encyclopedia is dedicated to providing reliable and well-researched information on a wide range of topics. From science and technology to history and culture, we aim to empower curious minds with knowledge.</p>
    <p>Our content is curated by experts and enthusiasts from around the world, ensuring a comprehensive and engaging learning experience for all users.</p>
</section>

<footer>
    <p>&copy; 2025 Encyclopedia. All rights reserved.</p>
</footer>

</body>
</html>

MyCarousel

index.html

<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My Carousel</title>
    <link rel="stylesheet" href="css/style.css">
</head>

<body>
    <section class="carousel">
        <div class="container">
            <ul>
                <li><img src="img/pic1.png"></li>
                <li><img src="img/pic2.png"></li>
                <li><img src="img/pic3.png"></li>
                <li><img src="img/pic4.png"></li>
            </ul>

            <button id="prev">&laquo;</button>
            <button id="next">&raquo;</button>
        </div>

        <nav>
        </nav>
    </section>

    <script src="js/main.js"></script>
</body>

</html>

js/main.js

'use strict';

{
    const next = document.getElementById('next');
    const prev = document.getElementById('prev');
    const ul = document.querySelector('ul');
    const slides = ul.children;
    const dots = [];
    let currentIndex = 0;

    function updateButtons() {
        prev.classList.remove('hidden');
        next.classList.remove('hidden');

        if (currentIndex === 0) {
            prev.classList.add('hidden');
        }
        if (currentIndex === slides.length - 1) {
            next.classList.add('hidden');
        }
    }

    function moveSlides() {
        const slideWidth = slides[0].getBoundingClientRect().width;
        ul.style.transform = `translateX(${-1 * slideWidth * currentIndex}px)`;
    }

    function setupDots() {
        for (let i = 0; i < slides.length; i++) {
            const button = document.createElement('button');
            button.addEventListener('click', () => {
                currentIndex = i;
                updateDots();
                updateButtons();
                moveSlides();
            });
            dots.push(button);
            document.querySelector('nav').appendChild(button);
        }

        dots[0].classList.add('current');
    }

    function updateDots() {
        dots.forEach(dot => {
            dot.classList.remove('current');
        });
        dots[currentIndex].classList.add('current');
    }

    updateButtons();
    setupDots();

    next.addEventListener('click', () => {
        currentIndex++;
        updateButtons();
        updateDots();
        moveSlides();
    });

    prev.addEventListener('click', () => {
        currentIndex--;
        updateButtons();
        updateDots();
        moveSlides();
    });

    window.addEventListener('resize', () => {
        moveSlides();
    });
}

css/style.css

.carousel {
    width: 80%;
    margin: 16px auto;
}

.container {
    width: 100%;
    height: 220px;
    overflow: hidden;
    position: relative;
}

ul {
    list-style: none;
    margin: 0;
    padding: 0;
    height: 100%;
    display: flex;
    transition: transform .3s;
}

li {
    height: 100%;
    min-width: 100%;
}

li img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

#prev,
#next {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    border: none;
    background: rgba(0, 0, 0, .8);
    color: #fff;
    font-size: 24px;
    padding: 0 8px 4px;
    cursor: pointer;
}

#prev:hover,
#next:hover {
    opacity: .8;
}

#prev {
    left: 0;
}

#next {
    right: 0;
}

.hidden {
    display: none;
}

nav {
    margin-top: 16px;
    text-align: center;
}

nav button+button {
    margin-left: 8px;
}

nav button {
    border: none;
    width: 16px;
    height: 16px;
    background: #ddd;
    border-radius: 50%;
    cursor: pointer;
}

nav .current {
    background: #999;
}