Tsumugi.html(SNS)

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  <title>Tsumugi</title>

  <!-- Favicon -->
  <link rel="shortcut icon"
        href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path fill='%23667eea' d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z'/></svg>"/>

  <!-- Tailwind CSS v2 -->
  <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet"/>

  <!-- Font Awesome -->
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.4.0/css/all.min.css"/>

  <style>
    :root {
      --grad-a: #667eea;
      --grad-b: #764ba2;
    }
    body {
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      background: linear-gradient(135deg, var(--grad-a) 0%, var(--grad-b) 100%);
      min-height: 100vh;
    }
    .glass-effect {
      background: rgba(255,255,255,0.25);
      backdrop-filter: blur(10px);
      border-radius: 16px;
      border: 1px solid rgba(255,255,255,0.18);
    }
    .card-hover { transition: all 0.25s ease; }
    .card-hover:hover { transform: translateY(-3px); box-shadow: 0 20px 30px -12px rgba(0,0,0,0.25); }

    .gradient-text {
      background: linear-gradient(45deg, var(--grad-a), var(--grad-b));
      -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
    }

    .timeline-post {
      background: white;
      border-radius: 16px;
      box-shadow: 0 10px 20px -12px rgba(0,0,0,0.2);
      transition: all 0.25s ease;
      border-left: 4px solid var(--grad-a);
    }
    .timeline-post:hover { transform: translateX(4px); }

    .profile-avatar {
      width: 100px; height: 100px; border-radius: 50%;
      object-fit: cover; border: 4px solid white;
      box-shadow: 0 10px 24px rgba(0,0,0,0.15);
    }
    .btn-primary {
      background: linear-gradient(45deg, var(--grad-a), var(--grad-b));
      border: none; transition: all 0.2s ease;
    }
    .btn-primary:hover { transform: translateY(-1px); box-shadow: 0 10px 18px rgba(0,0,0,0.18); }

    .section-divider {
      height: 3px; background: linear-gradient(90deg, var(--grad-a), var(--grad-b));
      border-radius: 2px; margin: 2rem 0;
    }
    .username-badge {
      background: linear-gradient(45deg, var(--grad-a), var(--grad-b));
      color: white; padding: 0.2rem 0.5rem; border-radius: 1rem;
      font-size: 0.75rem; font-weight: 600; display: inline-block; margin-left: 0.5rem;
    }
    .share-menu { position: absolute; z-index: 50; min-width: 180px; right: 0; top: 110%; background: white; border-radius: 10px; box-shadow: 0 8px 24px rgba(0,0,0,0.13); }
    .share-menu button { width: 100%; text-align: left; padding: 10px 20px; border: none; background: none; cursor: pointer; font-size: 0.95rem; }
    .share-menu button:hover { background: #f0f4ff; }

    .status-indicator { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; }
    .status-active { background-color: #10b981; animation: pulse 2s infinite; }
    .status-inactive { background-color: #6b7280; }

    .log-container {
      max-height: 180px; overflow-y: auto; background: rgba(255,255,255,0.12);
      border-radius: 10px; padding: 10px; margin-top: 10px; font-size: 0.8rem; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
    }
    .error-message { color: #ef4444; background: rgba(239, 68, 68, 0.08); padding: 8px; border-radius: 8px; margin: 5px 0; }
    .success-message { color: #10b981; background: rgba(16, 185, 129, 0.08); padding: 8px; border-radius: 8px; margin: 5px 0; }

    @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } }

    .dark .glass-effect { background: rgba(0,0,0,0.30); border: 1px solid rgba(255,255,255,0.12); }
    .dark .timeline-post { background: #1f2937; color: #f9fafb; border-left-color: #4f46e5; }
    .dark .share-menu { background: #111827; color: #e5e7eb; }
    .dark .success-message { background: rgba(16,185,129,0.12); }
    .dark .error-message { background: rgba(239,68,68,0.12); }

    @media print {
      body { background: white !important; -webkit-print-color-adjust: exact; }
      .glass-effect { background: white !important; backdrop-filter: none !important; border: 1px solid #e5e7eb !important; }
    }
  </style>
</head>

<body>
  <!-- ログイン/登録モーダル -->
  <div id="auth-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50" style="display:none">
    <div class="bg-white rounded-2xl shadow-2xl w-full max-w-sm p-6">
      <h2 class="text-2xl font-bold mb-4 text-center" id="auth-title">ログイン</h2>
      <div id="auth-error" class="error-message mb-2" style="display:none"></div>
      <form id="auth-form" autocomplete="off">
        <div class="mb-3">
          <label class="block mb-1 text-sm font-semibold">メールアドレス</label>
          <input type="email" id="auth-email" class="w-full border rounded px-3 py-2" required>
        </div>
        <div class="mb-3">
          <label class="block mb-1 text-sm font-semibold">パスワード</label>
          <input type="password" id="auth-password" class="w-full border rounded px-3 py-2" required>
        </div>
        <button type="submit" class="btn-primary w-full py-2 rounded-lg text-white font-semibold">ログイン</button>
      </form>
      <div class="mt-4 text-center">
        <button id="toggle-auth-mode" class="text-blue-600 underline text-sm">新規登録はこちら</button>
      </div>
    </div>
  </div>

  <!-- ヘッダー -->
  <header class="glass-effect mx-4 mt-4 p-6">
    <div class="text-center">
      <h1 class="text-4xl font-bold text-white mb-2">
        <i class="fas fa-comments mr-3"></i>Tsumugi
        <span class="text-sm opacity-80 ml-2"></span>
      </h1>
      <p class="text-white text-lg opacity-90">次世代ソーシャルネットワーク</p>
      <div class="mt-4 flex justify-center items-center space-x-4">
        <div class="flex items-center">
          <img id="header-profile-icon" class="profile-avatar" src="https://via.placeholder.com/100" alt="プロフィール">
          <div class="ml-3 text-white text-left">
            <div class="font-semibold text-lg" id="header-username">未設定</div>
            <div class="text-sm opacity-75" id="header-user-email"></div>
          </div>
        </div>
        <button onclick="toggleDarkMode()" class="btn-primary px-4 py-2 rounded-full text-white">
          <i class="fas fa-moon mr-2"></i>ダークモード
        </button>
        <button onclick="showSystemStatus()" class="btn-primary px-4 py-2 rounded-full text-white">
          <i class="fas fa-info-circle mr-2"></i>ステータス
        </button>
        <button id="logout-btn" onclick="logout()" class="btn-primary px-4 py-2 rounded-full text-white" style="display:none">
          <i class="fas fa-sign-out-alt mr-2"></i>ログアウト
        </button>
      </div>
    </div>
  </header>

  <!-- メイン -->
  <div class="max-w-6xl mx-auto px-4 py-6" id="main-content" style="display:none">
    <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
      <!-- 左カラム:プロフィール/BOT/RSS -->
      <div class="lg:col-span-1 space-y-6">
        <!-- プロフィール -->
        <div class="glass-effect p-6 card-hover">
          <h3 class="text-2xl font-bold gradient-text mb-4"><i class="fas fa-user-circle mr-2"></i>プロフィール</h3>
          <div class="text-center mb-6">
            <img id="profile-icon" class="profile-avatar mx-auto mb-4" src="https://via.placeholder.com/100" alt="プロフィール">
            <input type="file" id="profile-upload" accept="image/*" onchange="uploadProfileIcon(event)" class="hidden">
            <button onclick="document.getElementById('profile-upload').click()" class="btn-primary px-4 py-2 rounded-full text-white">
              <i class="fas fa-camera mr-2"></i>画像変更
            </button>
          </div>
          <div class="space-y-4">
            <div>
              <label class="block text-white font-semibold mb-2">ユーザー名</label>
              <input type="text" id="username" class="w-full p-3 border rounded-lg bg-white bg-opacity-90" placeholder="ユーザー名を入力してください" maxlength="20">
            </div>
            <div>
              <label class="block text-white font-semibold mb-2">自己紹介</label>
              <textarea id="self-intro" class="w-full p-3 border rounded-lg bg-white bg-opacity-90" rows="4" placeholder="自己紹介を入力してください"></textarea>
            </div>
            <button onclick="saveProfile()" class="btn-primary w-full py-2 rounded-lg text-white">
              <i class="fas fa-save mr-2"></i>プロフィール保存
            </button>
            <div class="p-3 bg-white bg-opacity-80 rounded-lg">
              <h5 class="font-semibold text-gray-800 mb-2">プレビュー:</h5>
              <div class="text-gray-700">
                <div class="font-semibold mb-1" id="username-preview">未設定</div>
                <div id="self-intro-preview" class="text-sm whitespace-pre-line min-h-8">まだ自己紹介がありません</div>
              </div>
            </div>
          </div>
        </div>

        <!-- RSS自動投稿機能(全体共有 + 個別ON/OFF) -->
        <div class="glass-effect p-6 card-hover">
          <h3 class="text-xl font-bold text-white mb-4">
            <i class="fas fa-rss mr-2"></i>RSS自動投稿(全体共有)
            <span class="status-indicator" id="rss-status"></span>
            <span id="rss-status-text" class="text-xs opacity-75">停止中</span>
          </h3>
          <div>
            <input id="rss-url" type="text" class="w-full p-2 border rounded-lg bg-white bg-opacity-90 mb-2" placeholder="RSSフィードURLを入力">
            <button onclick="addRssFeed()" class="btn-primary w-full py-2 rounded-lg text-white mb-2">
              <i class="fas fa-plus mr-2"></i>追加
            </button>

            <div id="rss-list" class="mb-3"></div>

            <div class="flex items-center space-x-2 mb-2">
              <input type="number" id="rss-interval" class="w-1/2 p-2 border rounded-lg bg-white bg-opacity-90" min="10" max="3600" value="300" placeholder="間隔(秒)">
              <button onclick="setRssInterval()" class="btn-primary flex-1 py-2 rounded-lg text-white">
                <i class="fas fa-clock mr-2"></i>間隔設定
              </button>
            </div>

            <div class="flex items-center space-x-2 mb-2">
              <button onclick="fetchRssNow()" class="btn-primary flex-1 py-2 rounded-lg text-white">
                <i class="fas fa-sync mr-2"></i>今すぐ取得
              </button>
              <button onclick="stopRssAuto()" class="bg-red-500 hover:bg-red-600 flex-1 py-2 rounded-lg text-white">
                <i class="fas fa-stop mr-2"></i>自動停止
              </button>
            </div>

            <div class="flex items-center space-x-2">
              <button onclick="setAllRssEnabled(true)" class="btn-primary flex-1 py-2 rounded-lg text-white text-sm">
                <i class="fas fa-toggle-on mr-1"></i>すべてON
              </button>
              <button onclick="setAllRssEnabled(false)" class="bg-gray-500 hover:bg-gray-600 flex-1 py-2 rounded-lg text-white text-sm">
                <i class="fas fa-toggle-off mr-1"></i>すべてOFF
              </button>
            </div>

            <div id="rss-log" class="log-container text-white text-xs mt-3"></div>
          </div>
        </div>

        <!-- BOT機能 -->
        <div class="glass-effect p-6 card-hover">
          <h3 class="text-xl font-bold text-white mb-4">
            <i class="fas fa-robot mr-2"></i>BOT機能
            <span class="status-indicator" id="bot-status"></span>
            <span id="bot-status-text" class="text-xs opacity-75">停止中</span>
          </h3>
          <div class="space-y-4">
            <div>
              <textarea id="botContent" class="w-full p-3 border rounded-lg bg-white bg-opacity-90" rows="3" placeholder="BOT投稿内容"></textarea>
              <button onclick="postBotMessage()" class="btn-primary w-full mt-2 py-2 rounded-lg text-white">
                <i class="fas fa-robot mr-2"></i>BOT投稿
              </button>
            </div>
            <div>
              <input type="number" id="botIntervalSec" class="w-full p-2 border rounded-lg bg-white bg-opacity-90" placeholder="マルコフ自動投稿間隔(秒)" min="10" max="3600" value="60">
              <div class="flex space-x-2 mt-2">
                <button onclick="postMarkovBot()" class="btn-primary flex-1 py-2 rounded-lg text-white">
                  <i class="fas fa-dice mr-2"></i>マルコフ生成
                </button>
                <button onclick="startBotAutoPost()" class="btn-primary flex-1 py-2 rounded-lg text-white">
                  <i class="fas fa-play mr-2"></i>自動開始
                </button>
                <button onclick="stopBotAutoPost()" class="bg-red-500 hover:bg-red-600 flex-1 py-2 rounded-lg text-white">
                  <i class="fas fa-stop mr-2"></i>停止
                </button>
              </div>
            </div>
            <div class="text-white text-xs opacity-75">
              <i class="fas fa-info-circle mr-1"></i>
              マルコフ連鎖では過去の投稿からランダムな文章を生成します(FEED本文は省略し、タイトルのみを学習対象にしません)。
            </div>
            <div id="bot-log" class="log-container text-white text-xs"></div>
          </div>
        </div>
      </div>

      <!-- 右カラム:投稿&タイムライン -->
      <div class="lg:col-span-2 space-y-6">
        <!-- 新規投稿 -->
        <div class="glass-effect p-6 card-hover">
          <h3 class="text-2xl font-bold gradient-text mb-4"><i class="fas fa-edit mr-2"></i>新規投稿</h3>
          <div>
            <textarea id="postContent" class="w-full p-4 border rounded-lg bg-white bg-opacity-90" rows="4" placeholder="今何を考えていますか?" maxlength="500"></textarea>
            <div class="mt-4 flex justify-between items-center">
              <div class="text-white text-sm opacity-90">
                <i class="fas fa-info-circle mr-1"></i>
                あなたの思いを共有しましょう
                <span id="char-count" class="ml-2">(0/500)</span>
              </div>
              <button onclick="createUserPost()" class="btn-primary px-6 py-3 rounded-lg text-white font-semibold">
                <i class="fas fa-paper-plane mr-2"></i>投稿する
              </button>
            </div>
          </div>
        </div>

        <div class="section-divider"></div>

        <!-- タイムライン -->
        <div class="glass-effect p-6">
          <div class="flex justify-between items-center mb-6">
            <h3 class="text-2xl font-bold text-white">
              <i class="fas fa-stream mr-2"></i>タイムライン
              <span id="post-count" class="text-sm font-normal opacity-75 ml-2">(0件の投稿)</span>
            </h3>
            <div class="flex space-x-2">
              <button onclick="clearAllPosts()" class="bg-red-500 hover:bg-red-600 px-3 py-1 rounded text-white text-sm">
                <i class="fas fa-trash mr-1"></i>全削除
              </button>
              <button onclick="exportData()" class="bg-green-500 hover:bg-green-600 px-3 py-1 rounded text-white text-sm">
                <i class="fas fa-download mr-1"></i>エクスポート
              </button>
            </div>
          </div>
          <div id="timeline" class="space-y-4"></div>
          <div id="empty-timeline" class="text-center py-12 text-white opacity-80">
            <i class="fas fa-comments text-4xl mb-4"></i>
            <p class="text-lg">まだ投稿がありません</p>
            <p class="text-sm">最初の投稿をして、タイムラインを始めましょう!</p>
          </div>
        </div>
      </div>
    </div>
  </div>

  <footer class="glass-effect mx-4 mb-4 p-4 text-center">
    <p class="text-white opacity-80">
      <i class="fas fa-copyright mr-2"></i>2025 Verse – 次世代ソーシャルネットワーク v2.3
      <span class="ml-4"><i class="fas fa-rss mr-1"></i>共有RSS / 個別ON/OFF対応(安全なフィード例)</span>
    </p>
  </footer>

  <script>
    // ==== 初期RSS(安全な例のみ。成人向け/不適切なサイトは除外) ====
    const PRESET_RSS = [
               "http://2ch-2.net/rss/all.xml",
    "http://2ch-ranking.net/rss/livemarket1.rdf",
    "http://2ch-ranking.net/rss/livemarket2.rdf",
    "http://kabumatome.doorblog.jp/index.rdf",
    "http://momoniji.com/feed",
    "http://oekakigakusyuu.blog97.fc2.com/?xml",
    "http://otanews.livedoor.biz/atom.xml",
    "http://otanews.livedoor.biz/index.rdf",
    "http://news4vip.livedoor.biz/index.rdf",
    "http://news.kakaku.com/prdnews/rss.asp",
    "http://www.jma-net.go.jp/rss/jma.rss",
    "http://rss.asahi.com/rss/asahi/newsheadlines.rdf",
    "https://uploadvr.com/feed/",
    "http://www.atmarkit.co.jp/rss/rss2dc.xml",
    "http://liginc.co.jp/feed",
    "http://liginc.co.jp/feed/",
    "http://blog.livedoor.jp/shachiani/index.rdf",
    "http://manga.lemon-s.com/atom.xml",
    "http://b.hatena.ne.jp/search/text?safe=on&q=%E3%82%BB%E3%82%AD%E3%83%A5%E3%83%AA%E3%83%86%E3%82%A3&users=500&mode=rss",
    "http://creator-life.net/feed/",
    "http://feedblog.ameba.jp/rss/ameblo/ca-1pixel/rss20.xml",
    "http://rssblog.ameba.jp/ca-1pixel/rss20.xml",
    "http://weekly.ascii.jp/cate/1/rss.xml",
    "http://blog.livedoor.jp/coleblog/atom.xml",
    "http://2chantena.antenam.biz/rss1s.rss",
    "http://www.4gamer.net/rss/index.xml",
    "http://www.4gamer.net/rss/news_topics.xml",
    "http://nyan.eggtree.net/feed.xml",
    "http://nunnnunn.hatenablog.com/rss",
    "http://www.nikkansports.com/general/atom.xml",
    "http://feeds.afpbb.com/rss/afpbb/access_ranking",
    "http://akiba-pc.watch.impress.co.jp/cda/rss/akiba-pc.rdf",
    "https://area.autodesk.jp/rss.xml",
    "http://av.watch.impress.co.jp/sublink/av.rdf",
    "http://rss.allabout.co.jp/aa/latest/ch/netpc/",
    "http://www.ar-ch.org/atom.xml",
    "http://feeds.arstechnica.com/arstechnica/BAaf",
    "https://feeds.feedburner.com/awwwards-sites-of-the-day",
    "http://news.bbc.co.uk/rss/newsonline_uk_edition/front_page/rss091.xml",
    "http://www.criteo.com/blog/rss/",
    "https://blueskyweb.xyz/rss.xml",
    "http://boingboing.net/rss.xml",
    "http://www.cc2.co.jp/blog/?feed=rss2",
    "http://cgarena.com/cgarena.xml",
    "http://cgtracking.net/feed",
    "http://japan.cnet.com/rss/index.rdf",
    "http://newclassic.jp/feed",
    "https://www.cssmania.com/feed/",
    "http://ceron.jp/top/?type=rss",
    "http://blog.btrax.com/jp/comments/feed/",
    "http://2ch-ranking.net/rss/wildplus.rdf",
    "http://design-spice.com/feed/",
    "http://dev.classmethod.jp/feed/",
    "http://ishida-a-coicoi.blog.so-net.ne.jp/atom.xml",
    "http://feeds.feedburner.com/ura-akiba?format=xml",
    "http://game.watch.impress.co.jp/sublink/game.rdf",
    "http://gigazine.co.jp/feed/",
    "http://labs.gree.jp/blog/comments/feed/",
    "http://www.gamespark.jp/rss/index.rdf",
    "http://www.gamebusiness.jp/rss/index.rdf",
    "http://www.gamebusiness.jp/rss/rss.php",
    "http://hackread.com/feed/",
    "https://io3000.com/feed/",
    "http://www.inside-games.jp/rss/index.rdf",
    "http://blog.livedoor.jp/itsoku/index.rdf",
    "http://rss.itmedia.co.jp/rss/1.0/news_bursts.xml",
    "http://octoba.net/feed",
    "http://www.ota-suke.jp/index.xml",
    "http://blog.livedoor.jp/kaigai_no/index.rdf",
    "https://land-book.com/feed/",
    "http://2ch.logpo.jp/1hour.xml",
    "http://menthas.com/javascript/rss",
    "http://www.nhk.or.jp/rss/news/cat0.xml",
    "http://ozpa-h4.com/feed/",
    "https://www.youtube.com/feeds/videos.xml?channel_id=UC1DCedRgGHBdm81E1llLhOQ",
    "http://rass.blog43.fc2.com/?xml",
    "http://stackoverflow.com/feeds",
    "http://www.slideshare.net/rss/latest",
    "http://www.jp.square-enix.com/whatsnew2/whatsnew.rdf",
    "http://www.ituore.com/feed",
    "http://synodos.jp/comments/feed",
    "http://www.shinkigensha.co.jp/feed/",
    "http://e-shuushuu.net/index.rss",
    "http://slashdot.org/index.rss",
    "http://feeds.feedburner.com/TheHackersNews?format=xml",
    "http://googleblog.blogspot.com/atom.xml",
    "http://www.theregister.co.uk/tonys/slashdot.rdf",
    "http://thinkit.co.jp/rss.xml",
    "http://blog.livedoor.jp/news23vip/atom.xml",
    "http://blog.livedoor.jp/news23vip/index.rdf",
    "http://www.webcreatorbox.com/feed/",
    "http://web-d.navigater.info/atom.xml",
    "http://2ch-c.net/?xml_all",
    "http://smhn.info/feed",
    "http://feeds.japan.zdnet.com/rss/zdnet/all.rdf",
    "http://20kaido.com/index.rdf",
    "http://2chnode.com/rss/feed/all",
    "http://akiba-souken.com/feed/all/",
    "http://amaebi.net/index.rdf",
    "http://amakakeru.blog59.fc2.com/?xml",
    "http://artskype.com/rss/feed.xml",
    "http://asitagamienai.blog118.fc2.com/?xml",
    "http://beta.egmnow.com/feed/",
    "http://blog.livedoor.jp/ogenre/index.rdf",
    "http://blog.nicovideo.jp/atom.xml",
    "http://blog.tsubuani.com/feed",
    "http://blogs.adobe.com/flex/atom.xml",
    "http://blogs.adobe.com/index.xml",
    "http://bm.s5-style.com/feed",
    "http://business.nikkeibp.co.jp/rss/all_nbo.rdf",
    "http://createlier.sitemix.jp/feed/",
    "http://crocro.com/news/nc.cgi?action=search&skin=rdf_srch_xml",
    "http://d.hatena.ne.jp/thk/rss",
    "http://damage0.blomaga.jp/index.rdf",
    "http://danbooru.donmai.us/posts.atom",
    "http://danbooru.donmai.us/posts.atom?tags=rss",
    "http://dengekionline.com/cate/11/rss.xml",
    "http://dictionary.reference.com/wordoftheday/wotd.rss",
    "http://doujin-games88.net/feed",
    "http://doujin.sekurosu.com/rss",
    "http://dousyoko.blog.fc2.com/?xml",
    "http://eroaniblog.blog.fc2.com/?xml",
    "http://eroanimedougakan.blog.fc2.com/?xml",
    "http://erogetrailers.com/api?md=latest",
    "http://eronizimage.blog.fc2.com/?xml",
    "http://erosanime.blog121.fc2.com/?xml",
    "http://erotaganime.blog.fc2.com/?xml",
    "http://feed.nikkeibp.co.jp/rss/nikkeibp/index.rdf",
    "http://feed.rssad.jp/rss/gigazine/rss_2.0",
    "http://feed.rssad.jp/rss/jcast/index.xml",
    "http://feed.rssad.jp/rss/klug/fxnews/rss5.xml",
    "http://feedblog.ameba.jp/rss/ameblo/yusayusa0211/rss20.xml",
    "http://feeds.adobe.com/xml/rss.cfm?query=byMostRecent&languages=1",
    "http://feeds.builder.japan.zdnet.com/rss/builder/all.rdf",
    "http://feeds.fc2.com/fc2/xml?host=anrism.blog&format=xml",
    "http://feeds.fc2.com/fc2/xml?host=kahouha2jigen.blog&format=xml",
    "http://feeds.feedburner.com/gekiura",
    "http://feeds.journal.mycom.co.jp/rss/mycom/index",
    "http://feeds.reuters.com/reuters/JPTopNews?format=xml",
    "http://galten705.blog.fc2.com/?xml",
    "http://gamanjiru.net/feed",
    "http://gamanjiru.net/feed/atom",
    "http://gamebiz.jp/?feed=rss",
    "http://gamenode.jp/rss/feed/all",
    "http://ggsoku.com/feed/atom/",
    "http://girlcelly.blog.fc2.com/?xml&trackback",
    "http://hairana.blog.fc2.com/?xml",
    "http://haruka-yumenoato.net/static/rss/index.rss",
    "http://headline.harikonotora.net/rss2.xml",
    "http://hentaidoujinanime.com/?xml",
    "http://homepage1.nifty.com/maname/index.rdf",
    "http://horiemon.com/feed/",
    "http://ideahacker.net/feed/",
    "http://itpro.nikkeibp.co.jp/rss/develop.rdf",
    "http://itpro.nikkeibp.co.jp/rss/news.rss",
    "http://itpro.nikkeibp.co.jp/rss/oss.rdf",
    "http://itpro.nikkeibp.co.jp/rss/win.rdf",
    "http://japan.internet.com/rss/rdf/index.rdf",
    "http://jp.leopard-raws.org/rss.php",
    "http://jp.techcrunch.com/feed/",
    "http://kakaku.com/trendnews/rss.xml",
    "http://kamisoku.blog47.fc2.com/?xml",
    "http://kanesoku.com/index.rdf",
    "http://kibougamotenai.blog.fc2.com/?xml",
    "http://kiisu.jpn.org/rss/now.xml",
    "http://konachan.com/post/piclens?page=1&tags=loli",
    "http://labo.tv/2chnews/index.xml",
    "http://lineblog.me/yamamotoichiro/atom.xml",
    "http://majimougen.blog.fc2.com/?xml",
    "http://mantan-web.jp/rss/mantan.xml",
    "http://matome.naver.jp/feed/hot",
    "http://matome.naver.jp/feed/tech",
    "http://matome.sekurosu.com/rss",
    "http://mizuhonokuni2ch.com/?xml",
    "http://momoiroanime.blog.fc2.com/?xml",
    "http://moroahedoujin.com/?xml",
    "http://nesingazou.blog.fc2.com/?xml",
    "http://newnews-moe.com/index.rdf",
    "http://news.ameba.jp/index.xml",
    "http://news.com.com/2547-1_3-0-5.xml",
    "http://news.nicovideo.jp/?rss=2.0",
    "http://news.nicovideo.jp/ranking/hot?rss=2.0",
    "http://newsbiz.yahoo.co.jp/topnews.rss",
    "http://nijitora.blog.fc2.com/?xml",
    "http://nodvd21ver2.blog.fc2.com/?xml",
    "http://orebibou.com/feed/",
    "http://osu.ppy.sh/feed/ranked/",
    "http://otakomu.jp/feed",
    "http://pcgameconquest.blog.fc2.com/?xml",
    "http://picks.dir.yahoo.co.jp/dailypicks/rss/",
    "http://piknik2ch.blog76.fc2.com/?xml",
    "http://plus.appgiga.jp/feed/user",
    "http://purisoku.com/index.rdf",
    "http://rdsig.yahoo.co.jp/RV=1/RU=aHR0cDovL3NlYXJjaHJhbmtpbmcueWFob28uY28uanAvcnNzL2J1cnN0X3JhbmtpbmctcnNzLnhtbA--;_ylt=A2RhjFhfAi9XEi0A6Glhdu57",
    "http://read2ch.net/rss/",
    "http://rss.dailynews.yahoo.co.jp/fc/computer/rss.xml",
    "http://rss.rssad.jp/rss/akibapc/akiba-pc.rdf",
    "http://rss.rssad.jp/rss/ascii/biz/rss.xml",
    "http://rss.rssad.jp/rss/ascii/hobby/rss.xml",
    "http://rss.rssad.jp/rss/ascii/it/rss.xml",
    "http://rss.rssad.jp/rss/ascii/mac/rss.xml",
    "http://rss.rssad.jp/rss/ascii/pc/rss.xml",
    "http://rss.rssad.jp/rss/ascii/rss.xml",
    "http://rss.rssad.jp/rss/codezine/new/20/index.xml",
    "http://rss.rssad.jp/rss/forest/rss.xml",
    "http://rss.rssad.jp/rss/gihyo/feed/atom",
    "http://rss.rssad.jp/rss/headline/headline.rdf",
    "http://rss.rssad.jp/rss/impresswatch/pcwatch.rdf",
    "http://rss.rssad.jp/rss/itm/1.0/makoto.xml",
    "http://rss.rssad.jp/rss/itm/1.0/netlab.xml",
    "http://rss.rssad.jp/rss/itm/2.0/kw_akiba.xml",
    "http://rss.rssad.jp/rss/itm/2.0/kw_android_appli.xml",
    "http://rss.rssad.jp/rss/itm/2.0/kw_apple.xml",
    "http://rss.rssad.jp/rss/itm/2.0/kw_facebook.xml",
    "http://rss.rssad.jp/rss/itm/2.0/kw_google.xml",
    "http://rss.rssad.jp/rss/itm/2.0/kw_ipad.xml",
    "http://rss.rssad.jp/rss/itm/2.0/kw_iphone.xml",
    "http://rss.rssad.jp/rss/itm/2.0/kw_iphone_appli.xml",
    "http://rss.rssad.jp/rss/itm/2.0/kw_mixi.xml",
    "http://rss.rssad.jp/rss/itm/2.0/kw_smartphone.xml",
    "http://rss.rssad.jp/rss/itm/2.0/kw_twitter.xml",
    "http://rss.rssad.jp/rss/itm/2.0/kw_ustream.xml",
    "http://rss.rssad.jp/rss/itmbizid/1.0/bizid.xml",
    "http://rss.rssad.jp/rss/itmnews/2.0/news_bursts.xml",
    "http://rss.rssad.jp/rss/japaninternetcom/index.rdf",
    "http://rss.rssad.jp/rss/oshietekun/atom.xml",
    "http://rss.rssad.jp/rss/slashdot/slashdot.rss",
    "http://rss.rssad.jp/rss/zaikeishimbun/main.xml",
    "http://rssc.dokoda.jp/r/8a1dd8f128047929ba4390dab3c8065e/http/searchranking.yahoo.co.jp/realtime_buzz/",
    "http://sakurabaryo.com/feed/",
    "http://sankei.jp.msn.com/rss/news/points.xml",
    "http://sankei.jp.msn.com/rss/news/west_points.xml",
    "http://search.goo.ne.jp/rss/newkw.rdf",
    "http://sekurosu.com/rss",
    "http://streaming.yahoo.co.jp/rss/newly/anime/",
    "http://sub0000528116.hmk-temp.com/wordpress/?feed=rss2",
    "http://sukebei.nyaa.se/?page=rss&sort=2",
    "http://tenshoku.mynavi.jp/knowhow/rss.xml",
    "http://tensinyakimeshi.blog98.fc2.com/?xml",
    "http://thefreedom12.blog41.fc2.com/?xml",
    "http://togetter.com/rss/hot/culture/62",
    "http://togetter.com/rss/hot/culture/63",
    "http://torimatome.main.jp/blogs/comments/feed",
    "http://torimatome.main.jp/blogs/feed",
    "http://toshinokyouko.com/rss.php",
    "http://tvanimedouga.blog93.fc2.com/?xml",
    "http://uranourainformation.blog21.fc2.com/?xml",
    "http://video.fc2.com/a/feed_popular.php?m=week",
    "http://weather.livedoor.com/forecast/rss/area/400010.xml",
    "http://wotopi.jp/feed",
    "http://www.100shiki.com/feed",
    "http://www.alistapart.com/rss.xml",
    "http://www.anime-sharing.com/forum/external.php?type=RSS2&forumids=36",
    "http://www.anime-sharing.com/forum/external.php?type=RSS2&forumids=38",
    "http://www.anime-sharing.com/forum/external.php?type=RSS2&forumids=47",
    "http://www.blosxom.com/?feed=rss2",
    "http://www.britannica.com/eb/dailycontent/rss",
    "http://www.csmonitor.com/rss/top.rss",
    "http://www.ehackingnews.com/feeds/posts/default",
    "http://www.falcom.co.jp/new.xml",
    "http://www.famitsu.com/rss/category/fcom_game.rdf",
    "http://www.famitsu.com/rss/fcom_all.rdf",
    "http://www.ganganonline.com/rss/index.xml",
    "http://www.ideaxidea.com/feed",
    "http://www.itnews711.com/index.rdf",
    "http://www.jp.playstation.com/whatsnew/whatsnew.rdf",
    "http://www.keyman.or.jp/rss/v1/?rss_type=all",
    "http://www.koubo.co.jp/rss.xml",
    "http://www.nyaa.se/?page=rss&sort=2",
    "http://www.nyaa.se/?page=rss&user=118009",
    "http://www.nytimes.com/services/xml/rss/userland/HomePage.xml",
    "http://www.phianime.tv/feed/",
    "http://www.rebootdevelop.hr/feed/",
    "http://www.rictus.com/muchado/feed/",
    "http://www.sbcr.jp/atom.xml",
    "http://www.slashgear.com/comments/feed/",
    "http://www.torrent-anime.com/feed",
    "http://www.torrent-anime.com/feed/",
    "http://www.webimemo.com/feed/",
    "http://www.wired.com/news_drop/netcenter/netcenter.rdf",
    "http://www.xvideos.com/rss/rss.xml",
    "http://www.youtube.com/rss/user/KADOKAWAanime/videos.rss",
    "http://www.youtube.com/rss/user/demosouko/videos.rss",
    "http://www.yukawanet.com/index.rdf",
    "http://www.zou3.net/php/rss/nikkei2rss.php?head=main",
    "http://xml.ehgt.org/ehtracker.xml",
    "http://xml.metafilter.com/rss.xml",
    "http://xvideos.2jiero.info/feed",
    "http://yaraon.blog109.fc2.com/?xml",
    "http://yusaani.com/home/feed/",
    "http://zipdeyaruo.blog42.fc2.com/?xml",
    "http://www.portalgraphics.net/rss/latest_image_list.xml",
    "http://api.syosetu.com/writernovel/430380.Atom",
    "http://creive.me/feed/",
    "http://gihyo.jp/dev/feed/atom",
    "http://gihyo.jp/feed/rss1",
    "http://hakase255.blog135.fc2.com/?xml",
    "http://2ch-ranking.net/rss/zenban.rdf",
    "http://www.isus.jp/feed/",
    "http://www.jiji.com/rss/ranking.rdf",
    "http://jp.gamesindustry.biz/rss/index.xml",
    "https://www.youtube.com/feeds/videos.xml?channel_id=UCx1nAvtVDIsaGmCMSe8ofsQ",
    "http://zakuzaku911.com/index.rdf",
    "http://ke-tai.org/blog/feed/",
    "http://data.newantenna.net/ero/rss/all.xml",
    "http://developer.mixi.co.jp/feed/atom",
    "http://neoneetch.blog.fc2.com/?xml",
    "http://rss.itmedia.co.jp/rss/1.0/netlab.xml",
    "http://netgeek.biz/feed",
    "http://blog.esuteru.com/index.rdf",
    "http://b.hatena.ne.jp/hotentry/game.rss",
    "http://b.hatena.ne.jp/hotentry.rss",
    "http://mobile.seisyun.net/rss/hot.rdf",
    "http://yomi.mobi/rss/hot.rdf",
    "http://saymygame.com/feed/",
    "http://blog.webcreativepark.net/atom.xml",
    "http://buhidoh.net/?xml",
    "http://www.webcyou.com/?feed=rss2",
    "http://withnews.jp/rss/consumer/new.rdf",
    "https://yande.re/post/atom?tags=loli",
    "http://blog.livedoor.jp/nizigami/atom.xml",
    "http://nvmzaq.blog.fc2.com/?xml",
    "http://keieimanga.net/index.rdf",
    "http://megumi.ldblog.jp/atom.xml",
    "http://kirik.tea-nifty.com/diary/index.rdf",
    "http://sinri.net/comments/feed",
    "http://himasoku.com/atom.xml",
    "http://himasoku.com/index.rdf",
    "http://20kaido.com/index.rdf",
    "http://h723.blog.fc2.com/?xml",
    "http://onecall2ch.com/index.rdf",
    "http://www.forest.impress.co.jp/rss.xml",
    "http://www.zaikei.co.jp/rss/sections/it.xml",
    "http://akiba.keizai.biz/rss.xml",
    "http://agag.tw/feed/2d-popular.rss",
    "http://adult-vr.jp/feed/",
    "http://www.anige-sokuhouvip.com/?xml",
    "http://animeanime.jp/rss/index.rdf",
    "http://alfalfalfa.com/index.rdf",
    "http://feeds.feedburner.com/fc2/GhfA",
    "http://erogetaiken072.blog.fc2.com/?xml",
    "http://otanew.jp/atom.xml",
    "http://jin115.com/index.rdf",
    "http://www.onlinegamer.jp/rss/news.rdf",
    "http://karapaia.livedoor.biz/index.rdf",
    "http://getnews.jp/feed/ext/orig",
    "http://www.gungho.co.jp/news/xml/rss.xml",
    "http://blog.livedoor.jp/kinisoku/index.rdf",
    "http://feeds.gizmodo.jp/rss/gizmodo/index.xml",
    "http://himado.in/?sort=movie_id&rss=1",
    "http://k-tai.impress.co.jp/cda/rss/ktai.rdf",
    "http://gehasoku.com/atom.xml",
    "http://feedblog.ameba.jp/rss/ameblo/principia-ca/rss20.xml",
    "http://zai.diamond.jp/list/feed/rssfxnews",
    "http://capacitor.blog.fc2.com/?xml",
    "http://blog.livedoor.jp/vipsister23/index.rdf",
    "http://vipsister23.com/atom.xml",
    "http://b.hatena.ne.jp/search/tag?safe=on&q=2ch&users=500&mode=rss",
    "http://b.hatena.ne.jp/search/tag?safe=on&q=%E3%83%8D%E3%83%83%E3%83%88%E3%83%AF%E3%83%BC%E3%82%AF&users=500&mode=rss",
    "http://b.hatena.ne.jp/search/tag?safe=off&q=%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0&users=500&mode=rss",
    "http://shikaku2ch.doorblog.jp/atom.xml",
    "http://dotinstall.com/lessons.rss",
    "http://2ch-ranking.net/rss/news4vip.rdf",
    "http://blog.livedoor.jp/insidears/index.rdf",
    "http://2ch-ranking.net/rss/newsplus.rdf",
    "http://2ch-ranking.net/rss/news.rdf",
    "http://nullpoantenna.com/game.rdf",
    "http://workingnews.blog117.fc2.com/?xml",
    "http://bm.s5-style.com/feed",
    "http://2ch-ranking.net/rss/ghard.rdf",
    "http://www.724685.com/blog/rss.xml",
    "http://www.yukawanet.com/index.rdf",
    "http://2ch-ranking.net/rss/bizplus.rdf",
    "http://www.nicovideo.jp/ranking/fav/daily/all?rss=2.0&lang=ja-jp",
    "http://www.tarikin.net/rss0.rdf",
    "http://blog.livedoor.jp/dqnplus/index.rdf",
    "http://www.seojapan.com/blog/feed",
    "http://2ch-ranking.net/rss/morningcoffee.rdf",
    "http://2ch-ranking.net/mt50k.rdf",
    "http://rssblog.ameba.jp/yandereotto/rss20.xml",
    "https://business.nikkei.com/rss/sns/nb.rdf",
    "http://daredemopc.blog51.fc2.com/?xml",
    "http://erogetaikenban.blog65.fc2.com/?xml",
    "http://news.goo.ne.jp/rss/topstories/gootop/index.rdf",
    "http://lanovelien.blog121.fc2.com/?xml",
    "http://news.livedoor.com/topics/rss/eco.xml",
    "http://ragnarokonline.gungho.jp/index.rdf",
    "http://rocketnews24.com/feed/",
    "https://news.denfaminicogamer.jp/feed",
    "http://www.igda.jp/?feed=rss2",
    "http://feeds.cnn.co.jp/cnn/rss"
    ];

    // ==== 既存データのロード ====
    if (!localStorage.getItem('verse_shared_rssFeeds')) {
      localStorage.setItem('verse_shared_rssFeeds', JSON.stringify(PRESET_RSS));
    }

    // アプリ状態
    let users = JSON.parse(localStorage.getItem('verse_users') || '[]');
    let currentUser = JSON.parse(localStorage.getItem('verse_currentUser') || 'null');
    let posts = JSON.parse(localStorage.getItem('verse_posts') || '[]');
    let isDarkMode = localStorage.getItem('verse_darkMode') === 'true';
    let isInitialized = false;

    // RSS/BOT状態
    let botInterval = null;
    let rssInterval = null;

    // 共有RSS設定
    let sharedRssFeeds = JSON.parse(localStorage.getItem('verse_shared_rssFeeds') || '[]');
    let sharedRssInterval = Number(localStorage.getItem('verse_shared_rssInterval')) || 300;
    let sharedRssLastIds = JSON.parse(localStorage.getItem('verse_shared_rssLastIds') || '{}');
    // 各フィードのON/OFF(true=ON)。未設定はデフォルトON。
    let sharedRssEnabled = JSON.parse(localStorage.getItem('verse_shared_rssEnabled') || '{}');

    // ===== 認証UI =====
    function showAuthModal(mode = 'login', errorMsg = '') {
      document.getElementById('auth-title').textContent = (mode === 'register') ? '新規登録' : 'ログイン';
      document.getElementById('auth-form').authMode = mode;
      document.getElementById('auth-email').value = '';
      document.getElementById('auth-password').value = '';
      document.getElementById('auth-modal').style.display = '';
      document.getElementById('main-content').style.display = 'none';
      document.getElementById('auth-error').textContent = errorMsg || '';
      document.getElementById('auth-error').style.display = errorMsg ? '' : 'none';
      document.getElementById('toggle-auth-mode').textContent = (mode === 'register') ? 'ログインはこちら' : '新規登録はこちら';
    }
    function hideAuthModal() {
      document.getElementById('auth-modal').style.display = 'none';
      document.getElementById('main-content').style.display = '';
    }

    document.addEventListener('DOMContentLoaded', () => {
      document.getElementById('auth-form').onsubmit = function(e) {
        e.preventDefault();
        const email = document.getElementById('auth-email').value.trim().toLowerCase();
        const password = document.getElementById('auth-password').value;
        if (!email || !password) {
          showAuthModal(this.authMode, 'メールアドレスとパスワードを入力してください');
          return;
        }
        if (this.authMode === 'register') {
          if (users.find(u => u.email === email)) {
            showAuthModal('register', 'このメールアドレスは既に登録されています');
            return;
          }
          const newUser = {
            email,
            password,
            profile: { icon: 'https://via.placeholder.com/100', username: email.split('@')[0], selfIntro: '' }
          };
          users.push(newUser);
          localStorage.setItem('verse_users', JSON.stringify(users));
          currentUser = { email };
          localStorage.setItem('verse_currentUser', JSON.stringify(currentUser));
          showAuthModal('login', '登録完了!ログインしてください');
        } else {
          const user = users.find(u => u.email === email && u.password === password);
          if (!user) { showAuthModal('login', 'メールアドレスまたはパスワードが違います'); return; }
          currentUser = { email };
          localStorage.setItem('verse_currentUser', JSON.stringify(currentUser));
          hideAuthModal();
          initializeApp();
        }
      };

      document.getElementById('toggle-auth-mode').onclick = function() {
        const mode = (document.getElementById('auth-title').textContent === '新規登録') ? 'login' : 'register';
        showAuthModal(mode);
      };

      if (!currentUser) showAuthModal('login'); else { hideAuthModal(); initializeApp(); }
    });

    function logout() {
      localStorage.removeItem('verse_currentUser');
      currentUser = null;
      stopRssAuto();
      showAuthModal('login');
    }

    // ===== 初期化 =====
    function initializeApp() {
      if (isInitialized) return;
      if (!currentUser) { showAuthModal('login'); return; }

      users = JSON.parse(localStorage.getItem('verse_users') || '[]');
      posts = JSON.parse(localStorage.getItem('verse_posts') || '[]');
      isDarkMode = localStorage.getItem('verse_darkMode') === 'true';

      const user = users.find(u => u.email === currentUser.email);
      window.profile = user ? user.profile : { icon: 'https://via.placeholder.com/100', username: 'ゲストユーザー', selfIntro: '' };

      updateAllUI();
      updateStatusIndicators();
      updateRssUI();

      if (isDarkMode) {
        document.body.classList.add('dark');
        document.body.style.background = 'linear-gradient(135deg, #1a202c 0%, #2d3748 100%)';
      } else {
        document.body.classList.remove('dark');
        document.body.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';
      }

      document.getElementById('main-content').style.display = '';
      document.getElementById('logout-btn').style.display = '';
      isInitialized = true;

      startRssAuto();
      addLog('bot-log', 'BOT機能初期化完了', 'success');
      addLog('rss-log', 'RSS自動投稿(全体共有)初期化完了', 'success');
    }

    // ===== 投稿(ユーザー/BOT) =====
    function createUserPost() {
      const ta = document.getElementById('postContent');
      const txt = ta.value.trim();
      if (!txt) return alert('投稿内容を入力してください。');
      if (!currentUser) return alert('ログインが必要です。');
      createPost(txt, 'user', profile.username, profile.icon);
      ta.value = '';
      updateCharCount();
    }

    function createPost(content, type = 'user', username = null, icon = null, extra = {}) {
      if (!content || !content.trim()) return false;
      const post = {
        id: Date.now() + Math.random(),
        content: content.trim(),
        likes: 0,
        timestamp: new Date().toLocaleString('ja-JP'),
        type,
        username: username || profile.username,
        icon: icon || profile.icon,
        userEmail: currentUser ? currentUser.email : '',
        ...extra
      };
      posts.unshift(post);
      saveData();
      renderTimeline();
      return true;
    }

    function likePost(id) {
      const idx = posts.findIndex(p => p.id === id);
      if (idx >= 0) {
        posts[idx].likes++;
        saveData();
        renderTimeline();
      }
    }

    function deletePost(id) {
      if (!confirm('この投稿を削除しますか?')) return;
      posts = posts.filter(p => p.id !== id);
      saveData();
      renderTimeline();
    }

    function clearAllPosts() {
      if (!confirm('全ての投稿を削除しますか?')) return;
      posts = [];
      saveData();
      renderTimeline();
    }

    function exportData() {
      const blob = new Blob([JSON.stringify(posts, null, 2)], { type: 'application/json' });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = 'verse_posts.json';
      a.click();
      URL.revokeObjectURL(url);
    }

    // ===== タイムライン描画(JSX混入の修正・無害化) =====
    function escapeHtml(s) {
      return (s || '').replace(/[&<>"']/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;','\'':'&#39;'}[m]));
    }

    function renderTimeline() {
      const tl = document.getElementById('timeline');
      const emp = document.getElementById('empty-timeline');
      const cnt = document.getElementById('post-count');
      if (!tl || !emp || !cnt) return;

      const allPosts = posts;
      if (allPosts.length === 0) {
        tl.innerHTML = '';
        emp.style.display = 'block';
        cnt.textContent = '(0件の投稿)';
        return;
      }
      emp.style.display = 'none';
      cnt.textContent = `(${allPosts.length}件の投稿)`;

      tl.innerHTML = allPosts.map(p => {
        const info = { bot: '🤖 BOT', markov: '🎲 MarkovBOT', user: '👤 ユーザー', feed: '📰 FEEDBOT' }[p.type] || '👤';
        const main = p.link
          ? `<a href="${p.link}" target="_blank" class="text-blue-600 underline">${escapeHtml(p.content)}</a>`
          : `${escapeHtml(p.content)}`;
        return `
          <div class="timeline-post p-6">
            <div class="flex justify-between items-start mb-4">
              <div class="flex items-center space-x-3">
                <img src="${p.icon}" class="w-10 h-10 rounded-full object-cover" onerror="this.src='https://via.placeholder.com/40'">
                <div>
                  <div class="flex items-center">
                    <span class="font-semibold text-gray-800 dark:text-white">${escapeHtml(p.username)}</span>
                    <span class="username-badge">${info}</span>
                  </div>
                  <div class="text-sm text-gray-500 dark:text-gray-400">${p.timestamp}</div>
                </div>
              </div>
            </div>
            <div class="text-gray-800 dark:text-gray-200 mb-4 leading-relaxed">${main}</div>
            <div class="flex items-center space-x-4 pt-4 border-t border-gray-100 dark:border-gray-600">
              <button onclick="likePost(${p.id})" class="flex items-center space-x-2 text-gray-600 dark:text-gray-400 hover:text-red-500">
                <i class="fas fa-heart"></i><span>${p.likes}</span>
              </button>
              <div class="relative">
                <button onclick="toggleShareMenu(${p.id})" class="flex items-center space-x-2 text-gray-600 dark:text-gray-400 hover:text-blue-500">
                  <i class="fas fa-share"></i><span>シェア</span>
                </button>
                <div id="share-menu-${p.id}" class="share-menu hidden">
                  <button onclick="shareToX(${p.id})"><i class="fab fa-x-twitter text-blue-400 mr-2"></i>Xでシェア</button>
                  <button onclick="shareToLine(${p.id})"><i class="fab fa-line text-green-400 mr-2"></i>LINEでシェア</button>
                  <button onclick="copyPost(${p.id})"><i class="fas fa-copy mr-2"></i>コピー</button>
                </div>
              </div>
              <button onclick="deletePost(${p.id})" class="flex items-center space-x-2 text-gray-600 dark:text-gray-400 hover:text-red-500 ml-auto">
                <i class="fas fa-trash"></i><span>削除</span>
              </button>
            </div>
          </div>
        `;
      }).join('');
    }

    function getPostContentText(id) {
      const p = posts.find(x => x.id === id);
      if (!p) return '';
      const tmp = document.createElement('div');
      tmp.innerHTML = p.content;
      return tmp.textContent || tmp.innerText || '';
    }
    function toggleShareMenu(id) {
      document.querySelectorAll('[id^="share-menu-"]')
        .forEach(el => el.classList.add('hidden'));
      const m = document.getElementById('share-menu-' + id);
      if (m) m.classList.toggle('hidden');
    }
    function shareToX(id) {
      const t = encodeURIComponent(getPostContentText(id));
      const u = encodeURIComponent(location.href);
      window.open(`https://twitter.com/intent/tweet?text=${t}&url=${u}`, '_blank');
    }
    function shareToLine(id) {
      const u = encodeURIComponent(location.href);
      window.open(`https://social-plugins.line.me/lineit/share?url=${u}`, '_blank');
    }
    function copyPost(id) {
      const t = getPostContentText(id);
      if (navigator.clipboard) {
        navigator.clipboard.writeText(t).then(() => alert('コピーしました')).catch(() => fallbackCopy(t));
      } else fallbackCopy(t);
    }
    function fallbackCopy(t) {
      const ta = document.createElement('textarea');
      ta.value = t; document.body.appendChild(ta);
      ta.select(); document.execCommand('copy');
      document.body.removeChild(ta);
      alert('コピーしました');
    }

    // ===== RSS UI(個別ON/OFF + 一括ON/OFF) =====
    function updateRssUI() {
      const listDiv = document.getElementById('rss-list');
      if (!listDiv) return;

      if (!sharedRssFeeds || sharedRssFeeds.length === 0) {
        listDiv.innerHTML = '<div class="text-white text-xs opacity-80">RSSフィード未登録</div>';
      } else {
        listDiv.innerHTML = sharedRssFeeds.map((url, i) => {
          const enabled = sharedRssEnabled[url] !== false; // 既定ON
          const enc = encodeURIComponent(url);
          return `
            <div class="flex items-center space-x-2 bg-white bg-opacity-90 rounded px-2 py-2 mb-1">
              <input type="checkbox" ${enabled ? 'checked' : ''} onchange="toggleRssEnabled('${enc}', this.checked)" title="ON/OFF">
              <div class="truncate flex-1 text-xs" title="${escapeHtml(url)}">${escapeHtml(url)}</div>
              <button onclick="delRssFeed(${i})" class="text-red-500 hover:text-red-700" title="削除"><i class="fas fa-trash"></i></button>
            </div>
          `;
        }).join('');
      }
      const iv = document.getElementById('rss-interval');
      if (iv) iv.value = sharedRssInterval || 300;
    }

    function toggleRssEnabled(encUrl, on) {
      const url = decodeURIComponent(encUrl);
      sharedRssEnabled[url] = !!on;
      saveSharedRss();
      addLog('rss-log', `FEED ${on ? 'ON' : 'OFF'}: ${url}`, 'info');
    }
    function setAllRssEnabled(on) {
      (sharedRssFeeds || []).forEach(u => sharedRssEnabled[u] = !!on);
      saveSharedRss();
      updateRssUI();
      addLog('rss-log', `全フィードを${on ? 'ON' : 'OFF'}にしました`, 'success');
    }

    function saveSharedRss() {
      localStorage.setItem('verse_shared_rssFeeds', JSON.stringify(sharedRssFeeds));
      localStorage.setItem('verse_shared_rssInterval', String(sharedRssInterval));
      localStorage.setItem('verse_shared_rssLastIds', JSON.stringify(sharedRssLastIds));
      localStorage.setItem('verse_shared_rssEnabled', JSON.stringify(sharedRssEnabled));
    }

    function addRssFeed() {
      const url = document.getElementById('rss-url').value.trim();
      if (!/^https?:\/\/.+/.test(url)) { addLog('rss-log', '正しいRSSフィードURLを入力してください', 'error'); return; }
      if (!sharedRssFeeds) sharedRssFeeds = [];
      if (sharedRssFeeds.includes(url)) { addLog('rss-log', 'すでに登録済みです', 'error'); return; }
      sharedRssFeeds.push(url);
      sharedRssEnabled[url] = true; // 既定ON
      saveSharedRss();
      updateRssUI();
      addLog('rss-log', `RSS追加: ${url}`, 'success');
      document.getElementById('rss-url').value = '';
    }

    function delRssFeed(i) {
      if (!sharedRssFeeds[i]) return;
      if (!confirm('このフィードを削除しますか?')) return;
      const url = sharedRssFeeds[i];
      sharedRssFeeds.splice(i, 1);
      delete sharedRssEnabled[url];
      delete sharedRssLastIds[url];
      saveSharedRss();
      updateRssUI();
      addLog('rss-log', 'RSS削除', 'info');
    }

    function setRssInterval() {
      const iv = document.getElementById('rss-interval').valueAsNumber || 300;
      if (iv < 10) return alert('間隔は10秒以上で設定してください。');
      sharedRssInterval = iv;
      saveSharedRss();
      updateRssUI();
      startRssAuto();
      addLog('rss-log', `自動投稿間隔を${iv}秒に設定`, 'success');
    }

    function fetchRssNow() { fetchRssFeeds(); }

    // ===== RSS取得(OFFのフィードはスキップ) =====
    function fetchRssFeeds() {
      if (!sharedRssFeeds || sharedRssFeeds.length === 0) return;
      sharedRssFeeds.forEach(feedUrl => {
        if (sharedRssEnabled[feedUrl] === false) {
          addLog('rss-log', `OFFのため取得スキップ: ${feedUrl}`, 'info');
          return;
        }
        fetch('https://api.rss2json.com/v1/api.json?rss_url=' + encodeURIComponent(feedUrl))
          .then(resp => resp.json())
          .then(data => {
            if (!data.items || !data.items.length) return;
            let lastId = sharedRssLastIds[feedUrl] || '';
            let newItems = [];
            for (const item of data.items) {
              const guid = item.guid || item.link || item.pubDate || item.title;
              if (!lastId || String(guid) > String(lastId)) newItems.push(item);
            }
            if (newItems.length === 0) return;

            newItems.reverse().forEach(item => {
              const guid = item.guid || item.link || item.pubDate || item.title;
              // FEED本文は省略(タイトル+リンクのみポスト)
              if (!posts.some(p => p.type === 'feed' && p.link === item.link)) {
                createPost(item.title, 'feed', 'FEEDBOT', 'https://cdn-icons-png.flaticon.com/512/3416/3416046.png', { link: item.link });
                addLog('rss-log', `新しい記事: ${item.title}`, 'success');
              }
              sharedRssLastIds[feedUrl] = guid;
            });
            saveSharedRss();
          })
          .catch(() => addLog('rss-log', 'RSS取得エラー: ' + feedUrl, 'error'));
      });
    }

    function startRssAuto() {
      stopRssAuto();
      fetchRssFeeds();
      rssInterval = setInterval(fetchRssFeeds, (sharedRssInterval || 300) * 1000);
      updateStatusIndicators();
      addLog('rss-log', `RSS自動投稿を開始 (${sharedRssInterval}秒間隔)`, 'success');
    }

    function stopRssAuto() {
      if (rssInterval) clearInterval(rssInterval);
      rssInterval = null;
      updateStatusIndicators();
      addLog('rss-log', 'RSS自動投稿を停止しました', 'info');
    }

    // ===== BOT =====
    function postBotMessage() {
      const ta = document.getElementById('botContent');
      const txt = ta.value.trim();
      if (!txt) return alert('BOT投稿内容を入力してください。');
      if (!currentUser) return alert('ログインが必要です。');
      if (createPost(txt, 'bot', 'BOT', profile.icon)) {
        ta.value = '';
        addLog('bot-log', `BOT投稿: "${txt.substring(0, 30)}..."`, 'success');
      }
    }

    function generateMarkovText() {
      // FEED(外部記事)は学習対象から除外し、ユーザーとBOT投稿のみ学習
      let text = posts
        .filter(p => ['user','bot'].includes(p.type))
        .map(p => {
          const d = document.createElement('div');
          d.innerHTML = p.content;
          return (d.textContent || d.innerText || '')
            .replace(/\s+/g, ' ').replace(/https?:\/\/\S+/g, '').trim();
        }).join(' ');

      if (text.length < 20) {
        const fallbacks = ["今日はいい天気ですね!","最近面白いニュースありましたか?","新しいアイデアが浮かんできました。","皆さんはどう思いますか?"];
        return fallbacks[Math.floor(Math.random() * fallbacks.length)];
      }

      const tokens = text.match(/[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\w]+|[。、!?\r\n]/g) || [];
      if (tokens.length < 2) return tokens.join('');

      const markov = {};
      for (let i = 0; i < tokens.length - 2; i++) {
        const key = tokens[i] + '|' + tokens[i+1];
        if (!markov[key]) markov[key] = [];
        markov[key].push(tokens[i+2]);
      }

      let idx = Math.floor(Math.random() * (tokens.length - 2));
      let key = tokens[idx] + '|' + tokens[idx+1];
      let result = [tokens[idx], tokens[idx+1]];
      let maxLen = 60 + Math.floor(Math.random() * 40);

      for (let i = 0; i < maxLen; i++) {
        const nexts = markov[key];
        if (!nexts || nexts.length === 0) break;
        const next = nexts[Math.floor(Math.random() * nexts.length)];
        result.push(next);
        if (/[。!?\n]/.test(next)) break;
        key = result[result.length - 2] + '|' + result[result.length - 1];
      }
      return result.join('').replace(/\n/g, '');
    }

    function postMarkovBot() {
      const txt = generateMarkovText();
      if (createPost(txt, 'markov', 'MarkovBOT', profile.icon)) {
        addLog('bot-log', `マルコフ投稿: "${txt.substring(0, 40)}..."`, 'success');
      }
    }

    function startBotAutoPost() {
      const iv = document.getElementById('botIntervalSec').valueAsNumber || 60;
      if (iv < 10) { alert('間隔は10秒以上で設定してください。'); return; }
      stopBotAutoPost();
      setTimeout(postMarkovBot, 3000);
      botInterval = setInterval(postMarkovBot, iv * 1000);
      updateStatusIndicators();
      addLog('bot-log', `マルコフBOT自動投稿開始 (${iv}秒間隔)`, 'success');
    }

    function stopBotAutoPost() {
      if (botInterval) {
        clearInterval(botInterval);
        botInterval = null;
        updateStatusIndicators();
        addLog('bot-log', 'マルコフBOT自動投稿を停止しました', 'info');
      }
    }

    // ===== 共通UI/保存 =====
    function addLog(id, msg, type = 'info') {
      const el = document.getElementById(id);
      const ts = new Date().toLocaleTimeString('ja-JP');
      const div = document.createElement('div');
      const cls = { error: 'error-message', success: 'success-message', info: 'text-white opacity-90' }[type] || 'text-white opacity-90';
      div.className = cls;
      div.innerHTML = `<span class="opacity-75">[${ts}]</span> ${escapeHtml(msg)}`;
      if (el) {
        el.appendChild(div);
        el.scrollTop = el.scrollHeight;
        while (el.children.length > 100) el.removeChild(el.firstChild);
      }
      try { console.log(`[${ts}] ${msg}`); } catch(_) {}
    }

    function updateStatusIndicators() {
      const botI = document.getElementById('bot-status');
      const botT = document.getElementById('bot-status-text');
      if (botI && botT) {
        const active = botInterval !== null;
        botI.className = `status-indicator ${active ? 'status-active' : 'status-inactive'}`;
        botT.textContent = active ? '動作中' : '停止中';
      }
      const rssI = document.getElementById('rss-status');
      const rssT = document.getElementById('rss-status-text');
      if (rssI && rssT) {
        const active = rssInterval !== null;
        rssI.className = `status-indicator ${active ? 'status-active' : 'status-inactive'}`;
        rssT.textContent = active ? '動作中' : '停止中';
      }
    }

    function showSystemStatus() {
      alert(
`=== Verse システムステータス ===\n全体投稿数: ${posts.length}\nRSS登録数: ${sharedRssFeeds.length}\nBOT投稿数: ${posts.filter(p => ['bot', 'markov'].includes(p.type)).length}\n\nBOT自動投稿: ${botInterval ? '動作中' : '停止中'}\nRSS自動投稿: ${rssInterval ? '動作中' : '停止中'}`
      );
    }

    function uploadProfileIcon(e) {
      const f = e.target.files[0];
      if (!f) return;
      if (f.size > 5 * 1024 * 1024) { alert('5MB以下にしてください。'); return; }
      const r = new FileReader();
      r.onload = () => {
        profile.icon = r.result;
        saveProfileNoAlert();
        updateAllUI();
        alert('プロフィール画像更新!');
      };
      r.readAsDataURL(f);
    }

    function saveProfile() {
      const un = document.getElementById('username').value.trim();
      const si = document.getElementById('self-intro').value.trim();
      if (un.length > 20) { alert('ユーザー名は20文字以内で。'); return; }
      profile.username = un || 'ゲストユーザー';
      profile.selfIntro = si;
      saveProfileNoAlert();
      updateAllUI();
      alert('プロフィール保存!');
    }

    function saveProfileNoAlert() {
      users = JSON.parse(localStorage.getItem('verse_users') || '[]');
      const idx = users.findIndex(u => u.email === (currentUser && currentUser.email));
      if (idx >= 0) {
        users[idx].profile = profile;
        localStorage.setItem('verse_users', JSON.stringify(users));
      }
    }

    function updateAllUI() {
      const pi = document.getElementById('profile-icon');
      const hi = document.getElementById('header-profile-icon');
      if (pi) pi.src = profile.icon;
      if (hi) hi.src = profile.icon;
      ['username-preview','header-username'].forEach(id => {
        const el = document.getElementById(id);
        if (el) el.textContent = profile.username;
      });
      const sip = document.getElementById('self-intro-preview');
      if (sip) sip.textContent = profile.selfIntro || 'まだ自己紹介がありません';
      const emailEl = document.getElementById('header-user-email');
      if (emailEl && currentUser) emailEl.textContent = currentUser.email;
      renderTimeline();
    }

    function updateCharCount() {
      const pc = document.getElementById('postContent');
      const cc = document.getElementById('char-count');
      if (pc && cc) {
        const l = pc.value.length;
        cc.textContent = `(${l}/500)`;
        cc.style.color = l > 450 ? '#ef4444' : '';
      }
    }

    function toggleDarkMode() {
      isDarkMode = !isDarkMode;
      if (isDarkMode) {
        document.body.classList.add('dark');
        document.body.style.background = 'linear-gradient(135deg, #1a202c 0%, #2d3748 100%)';
      } else {
        document.body.classList.remove('dark');
        document.body.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';
      }
      localStorage.setItem('verse_darkMode', isDarkMode.toString());
    }

    function saveData() { localStorage.setItem('verse_posts', JSON.stringify(posts)); }

    // 入力UIフック
    document.addEventListener('DOMContentLoaded', () => {
      const pc = document.getElementById('postContent');
      if (pc) {
        pc.addEventListener('input', updateCharCount);
        pc.addEventListener('keydown', e => { if (e.key === 'Enter' && e.ctrlKey) { e.preventDefault(); createUserPost(); } });
      }
      const ui = document.getElementById('username');
      if (ui) ui.addEventListener('input', () => {
        const v = ui.value.trim() || 'ゲストユーザー';
        ['username-preview','header-username'].forEach(id => {
          const el = document.getElementById(id);
          if (el) el.textContent = v;
        });
      });
      const si = document.getElementById('self-intro');
      if (si) si.addEventListener('input', () => {
        const v = si.value.trim() || 'まだ自己紹介がありません';
        const el = document.getElementById('self-intro-preview');
        if (el) el.textContent = v;
      });
    });

    // 終了処理・エラー
    window.addEventListener('beforeunload', () => { stopBotAutoPost(); stopRssAuto(); saveData(); });
    window.addEventListener('error', e => { addLog('bot-log', `システムエラー: ${e.message}`, 'error'); });
  </script>
</body>
</html>

15パズル javascript

index.html

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

<head>
  <meta charset="utf-8">
  <title>15Puzzle</title>
  <style>
    canvas {
      background: pink;
      display: block;
      margin: 0 auto;
      cursor: pointer;
    }
  </style>
</head>

<body>
  <canvas width="280" height="280">
    Canvas not supported.
  </canvas>

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

</html>

main.js

'use strict';

(() => {
    class PuzzleRenderer {
        constructor(puzzle, canvas) {
            this.puzzle = puzzle;
            this.canvas = canvas;
            this.ctx = this.canvas.getContext('2d');
            this.TILE_SIZE = 70;
            this.img = document.createElement('img');
            this.img.src = 'img/animal1.png';
            this.img.addEventListener('load', () => {
                this.render();
            });
            this.canvas.addEventListener('click', e => {
                if (this.puzzle.getCompletedStatus()) {
                    return;
                }

                const rect = this.canvas.getBoundingClientRect();
                const col = Math.floor((e.clientX - rect.left) / this.TILE_SIZE);
                const row = Math.floor((e.clientY - rect.top) / this.TILE_SIZE);
                this.puzzle.swapTiles(col, row);
                this.render();

                if (this.puzzle.isComplete()) {
                    this.puzzle.setCompletedStatus(true);
                    this.renderGameClear();
                }
            });
        }

        renderGameClear() {
            this.ctx.fillStyle = 'rgba(0, 0, 0, 0.4)';
            this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
            this.ctx.font = '28px Arial';
            this.ctx.fillStyle = '#fff';
            this.ctx.fillText('GAME CLEAR!!', 40, 150);
        }

        render() {
            for (let row = 0; row < this.puzzle.getBoardSize(); row++) {
                for (let col = 0; col < this.puzzle.getBoardSize(); col++) {
                    this.renderTile(this.puzzle.getTile(row, col), col, row);
                }
            }
        }

        renderTile(n, col, row) {
            if (n === this.puzzle.getBlankIndex()) {
                this.ctx.fillStyle = '#eeeeee';
                this.ctx.fillRect(
                    col * this.TILE_SIZE,
                    row * this.TILE_SIZE,
                    this.TILE_SIZE,
                    this.TILE_SIZE
                );
            } else {
                this.ctx.drawImage(
                    this.img,
                    (n % this.puzzle.getBoardSize()) * this.TILE_SIZE,
                    Math.floor(n / this.puzzle.getBoardSize()) * this.TILE_SIZE,
                    this.TILE_SIZE,
                    this.TILE_SIZE,
                    col * this.TILE_SIZE,
                    row * this.TILE_SIZE,
                    this.TILE_SIZE,
                    this.TILE_SIZE
                );
            }
        }
    }

    class Puzzle {
        constructor(level) {
            this.level = level;
            this.tiles = [
                [0, 1, 2, 3],
                [4, 5, 6, 7],
                [8, 9, 10, 11],
                [12, 13, 14, 15],
            ];
            this.UDLR = [
                [0, -1], // up
                [0, 1], // down
                [-1, 0], // left
                [1, 0], // right
            ];
            this.isCompleted = false;
            this.BOARD_SIZE = this.tiles.length;
            this.BLANK_INDEX = this.BOARD_SIZE ** 2 - 1;
            do {
                this.shuffle(this.level);
            } while (this.isComplete());
        }

        getBoardSize() {
            return this.BOARD_SIZE;
        }

        getBlankIndex() {
            return this.BLANK_INDEX;
        }

        getCompletedStatus() {
            return this.isCompleted;
        }

        setCompletedStatus(value) {
            this.isCompleted = value;
        }

        getTile(row, col) {
            return this.tiles[row][col];
        }

        shuffle(n) {
            let blankCol = this.BOARD_SIZE - 1;
            let blankRow = this.BOARD_SIZE - 1;

            for (let i = 0; i < n; i++) {
                let destCol;
                let destRow;
                do {
                    const dir = Math.floor(Math.random() * this.UDLR.length);
                    destCol = blankCol + this.UDLR[dir][0];
                    destRow = blankRow + this.UDLR[dir][1];
                } while (this.isOutside(destCol, destRow));

                [
                    this.tiles[blankRow][blankCol],
                    this.tiles[destRow][destCol],
                ] = [
                        this.tiles[destRow][destCol],
                        this.tiles[blankRow][blankCol],
                    ];

                [blankCol, blankRow] = [destCol, destRow];
            }
        }

        swapTiles(col, row) {
            if (this.tiles[row][col] === this.BLANK_INDEX) {
                return;
            }

            for (let i = 0; i < this.UDLR.length; i++) {
                const destCol = col + this.UDLR[i][0];
                const destRow = row + this.UDLR[i][1];

                if (this.isOutside(destCol, destRow)) {
                    continue;
                }

                if (this.tiles[destRow][destCol] === this.BLANK_INDEX) {
                    [
                        this.tiles[row][col],
                        this.tiles[destRow][destCol],
                    ] = [
                            this.tiles[destRow][destCol],
                            this.tiles[row][col],
                        ];
                    break;
                }
            }
        }

        isOutside(destCol, destRow) {
            return (
                destCol < 0 || destCol > this.BOARD_SIZE - 1 ||
                destRow < 0 || destRow > this.BOARD_SIZE - 1
            );
        }

        isComplete() {
            let i = 0;
            for (let row = 0; row < this.BOARD_SIZE; row++) {
                for (let col = 0; col < this.BOARD_SIZE; col++) {
                    if (this.tiles[row][col] !== i++) {
                        return false;
                    }
                }
            }
            return true;
        }
    }

    const canvas = document.querySelector('canvas');
    if (typeof canvas.getContext === 'undefined') {
        return;
    }

    new PuzzleRenderer(new Puzzle(2), canvas);
})();

Javascript 迷路

index.html

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

<head>
    <meta charset="utf-8">
    <title>My Maze</title>
</head>

<body>
    <canvas>
        Canvas not supported ...
    </canvas>

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

</html>

main.js

'use strict';

(() => {
    class MazeRenderer {
        constructor(canvas) {
            this.ctx = canvas.getContext('2d');
            this.WALL_SIZE = 10;
        }

        render(data) {
            canvas.height = data.length * this.WALL_SIZE;
            canvas.width = data[0].length * this.WALL_SIZE;

            for (let row = 0; row < data.length; row++) {
                for (let col = 0; col < data[0].length; col++) {
                    if (data[row][col] === 1) {
                        this.ctx.fillRect(
                            col * this.WALL_SIZE,
                            row * this.WALL_SIZE,
                            this.WALL_SIZE,
                            this.WALL_SIZE
                        );
                    }
                }
            }
        }
    }

    class Maze {
        constructor(row, col, renderer) {
            if (row < 5 || col < 5 || row % 2 === 0 || col % 2 === 0) {
                alert('Size not valid!');
                return;
            }

            this.renderer = renderer;
            this.row = row;
            this.col = col;
            this.data = this.getData();
        }

        getData() {
            const data = [];

            for (let row = 0; row < this.row; row++) {
                data[row] = [];
                for (let col = 0; col < this.col; col++) {
                    data[row][col] = 1;
                }
            }

            for (let row = 1; row < this.row - 1; row++) {
                for (let col = 1; col < this.col - 1; col++) {
                    data[row][col] = 0;
                }
            }

            for (let row = 2; row < this.row - 2; row += 2) {
                for (let col = 2; col < this.col - 2; col += 2) {
                    data[row][col] = 1;
                }
            }

            for (let row = 2; row < this.row - 2; row += 2) {
                for (let col = 2; col < this.col - 2; col += 2) {
                    let destRow;
                    let destCol;

                    do {
                        const dir = row === 2 ?
                            Math.floor(Math.random() * 4) :
                            Math.floor(Math.random() * 3) + 1;
                        switch (dir) {
                            case 0: // up
                                destRow = row - 1;
                                destCol = col;
                                break;
                            case 1: // down
                                destRow = row + 1;
                                destCol = col;
                                break;
                            case 2: // left
                                destRow = row;
                                destCol = col - 1;
                                break;
                            case 3: // right
                                destRow = row;
                                destCol = col + 1;
                                break;
                        }
                    } while (data[destRow][destCol] === 1);

                    data[destRow][destCol] = 1;
                }
            }

            return data;
        }

        render() {
            this.renderer.render(this.data);
        }
    }

    const canvas = document.querySelector('canvas');
    if (typeof canvas.getContext === 'undefined') {
        return;
    }

    const maze = new Maze(21, 15, new MazeRenderer(canvas));
    maze.render();
})();

JavaScriptモーダルウィンドウ

index.html

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

<head>
    <meta charset="utf-8">
    <title>Modal Window</title>
    <link rel="stylesheet" href="css/styles.css">
</head>

<body>
    <div id="open">
        詳細を見る
    </div>

    <div id="mask" class="hidden"></div>

    <section id="modal" class="hidden">
        <p>こんにちは。こんにちは。こんにちは。こんにちは。こんにちは。こんにちは。こんにちは。こんにちは。こんにちは。こんにちは。</p>
        <div id="close">
            閉じる
        </div>
    </section>

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

</html>

css/style.css

body {
    font-size: 14px;
}

#open,
#close {
    cursor: pointer;
    width: 200px;
    border: 1px solid #ccc;
    border-radius: 4px;
    text-align: center;
    padding: 12px 0;
    margin: 16px auto 0;
}

#mask {
    background: rgba(0, 0, 0, 0.4);
    position: fixed;
    top: 0;
    bottom: 0;
    right: 0;
    left: 0;
    z-index: 1;
}

#modal {
    background: #fff;
    width: 300px;
    padding: 20px;
    border-radius: 4px;
    position: absolute;
    top: 40px;
    left: 0;
    right: 0;
    margin: 0 auto;
    transition: transform 0.4s;
    z-index: 2;
}

#modal>p {
    margin: 0 0 20px;
}

#mask.hidden {
    display: none;
}

#modal.hidden {
    transform: translate(0, -500px);
}

/js/main.js

'use strict';

{
    const open = document.getElementById('open');
    const close = document.getElementById('close');
    const modal = document.getElementById('modal');
    const mask = document.getElementById('mask');

    open.addEventListener('click', () => {
        modal.classList.remove('hidden');
        mask.classList.remove('hidden');
    });

    close.addEventListener('click', () => {
        modal.classList.add('hidden');
        mask.classList.add('hidden');
    });

    mask.addEventListener('click', () => {
        // modal.classList.add('hidden');
        // mask.classList.add('hidden');
        close.click();
    });
}

Javascript RPG


<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Epic RPG</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      margin: 20px;
    }
    #game-log {
      background: #f4f4f4;
      padding: 10px;
      margin-bottom: 20px;
      height: 200px;
      overflow-y: auto;
      border: 1px solid #ddd;
    }
    button {
      margin: 5px;
      padding: 10px;
    }
    #player-stats {
      margin-bottom: 20px;
    }
  </style>
</head>
<body>
  <h1>Epic RPG</h1>
  <div id="player-stats">
    <p><strong>Name:</strong> <span id="player-name">Hero</span></p>
    <p><strong>HP:</strong> <span id="player-hp">100</span>/<span id="player-max-hp">100</span></p>
    <p><strong>MP:</strong> <span id="player-mp">50</span>/<span id="player-max-mp">50</span></p>
    <p><strong>Level:</strong> <span id="player-level">1</span></p>
    <p><strong>EXP:</strong> <span id="player-exp">0</span>/100</p>
    <p><strong>Gold:</strong> <span id="player-gold">0</span></p>
    <p><strong>Inventory:</strong> <span id="player-inventory">Potion x1</span></p>
    <p><strong>Skills:</strong> <span id="player-skills">Fireball</span></p>
    <p><strong>Equipped Weapon:</strong> <span id="player-weapon">None</span></p>
    <p><strong>Equipped Armor:</strong> <span id="player-armor">None</span></p>
    <p><strong>Current Quest:</strong> <span id="player-quest">None</span></p>
    <p><strong>Stage:</strong> <span id="current-stage">1</span></p>
  </div>
  <div id="game-log"></div>
  <button onclick="attack()">Attack</button>
  <button onclick="useSkill()">Use Skill</button>
  <button onclick="heal()">Heal</button>
  <button onclick="openShop()">Shop</button>
  <button onclick="acceptQuest()">Quest</button>
  <button onclick="craftItem()">Craft Item</button>
  <button onclick="nextStage()">Next Stage</button>
  <button onclick="restart()">Restart</button>
  <script>
    // プレイヤーと敵のデータ
    let player = {
      name: "Hero",
      hp: 100,
      maxHp: 100,
      mp: 50,
      maxMp: 50,
      attackPower: 10,
      defense: 5,
      exp: 0,
      level: 1,
      gold: 50,
      inventory: ["Potion", "Iron Ore"],
      skills: ["Fireball"],
      weapon: null,
      armor: null,
      quest: null,
      stage: 1,
    };

    let enemy = {
      name: "Goblin",
      hp: 50,
      maxHp: 50,
      attackPower: 8,
      defense: 3,
    };

    const quests = [
      { name: "Defeat 3 Goblins", progress: 0, goal: 3, reward: 100 },
      { name: "Collect 2 Potions", progress: 0, goal: 2, reward: 50 },
    ];

    const stages = [
      { stage: 1, description: "The Forest of Beginnings", enemies: ["Goblin", "Orc"] },
      { stage: 2, description: "The Cursed Mines", enemies: ["Dark Bat", "Skeleton"] },
      { stage: 3, description: "The Dragon's Lair", enemies: ["Fire Dragon"] },
    ];

    // ゲームログ表示関数
    function log(message) {
      const logDiv = document.getElementById("game-log");
      logDiv.innerHTML += `<p>${message}</p>`;
      logDiv.scrollTop = logDiv.scrollHeight;
    }

    // プレイヤーの攻撃
    function attack() {
      const damage = Math.max(Math.floor(Math.random() * player.attackPower) - enemy.defense, 1);
      enemy.hp -= damage;
      log(`You attack the ${enemy.name} for ${damage} damage!`);
      if (enemy.hp <= 0) {
        log(`You defeated the ${enemy.name}!`);
        gainExp(20);
        gainGold(Math.floor(Math.random() * 20) + 10);
        updateQuestProgress("Defeat 3 Goblins");
        spawnNewEnemy();
        return;
      }
      enemyAttack();
    }

    // 敵の攻撃
    function enemyAttack() {
      const damage = Math.max(Math.floor(Math.random() * enemy.attackPower) - player.defense, 0);
      player.hp -= damage;
      log(`The ${enemy.name} attacks you for ${damage} damage! Current HP: ${player.hp}`);
      if (player.hp <= 0) {
        log("You have been defeated...");
        log("Press 'Restart' to try again.");
      }
      updateStats();
    }

    // ステージ移動
    function nextStage() {
      player.stage++;
      const currentStage = stages.find(stage => stage.stage === player.stage);
      if (!currentStage) {
        log("Congratulations! You have completed the game!");
        return;
      }
      log(`You enter the ${currentStage.description}.`);
      spawnNewEnemy();
      updateStats();
    }

    // 新しい敵を生成
    function spawnNewEnemy() {
      const currentStage = stages.find(stage => stage.stage === player.stage);
      const randomEnemyName = currentStage.enemies[Math.floor(Math.random() * currentStage.enemies.length)];
      enemy = {
        name: randomEnemyName,
        hp: Math.floor(Math.random() * 30 + 50),
        maxHp: Math.floor(Math.random() * 30 + 50),
        attackPower: Math.floor(Math.random() * 5 + 10),
        defense: Math.floor(Math.random() * 5),
      };
      log(`A wild ${enemy.name} appears with ${enemy.hp} HP!`);
    }

    // アイテムクラフト
    function craftItem() {
      if (player.inventory.includes("Iron Ore")) {
        player.inventory.splice(player.inventory.indexOf("Iron Ore"), 1);
        player.weapon = "Iron Sword";
        player.attackPower += 5;
        log("You crafted an Iron Sword! Attack power increased by 5.");
      } else {
        log("You don't have the required materials to craft an item.");
      }
      updateStats();
    }

    // ステータス更新
    function updateStats() {
      document.getElementById("player-name").innerText = player.name;
      document.getElementById("player-hp").innerText = player.hp;
      document.getElementById("player-max-hp").innerText = player.maxHp;
      document.getElementById("player-mp").innerText = player.mp;
      document.getElementById("player-max-mp").innerText = player.maxMp;
      document.getElementById("player-level").innerText = player.level;
      document.getElementById("player-exp").innerText = player.exp;
      document.getElementById("player-gold").innerText = player.gold;
      document.getElementById("player-inventory").innerText = player.inventory.join(", ") || "Empty";
      document.getElementById("player-skills").innerText = player.skills.join(", ") || "None";
      document.getElementById("player-weapon").innerText = player.weapon || "None";
      document.getElementById("player-armor").innerText = player.armor || "None";
      document.getElementById("player-quest").innerText = player.quest ? player.quest.name : "None";
      document.getElementById("current-stage").innerText = player.stage;
    }

    // 初期化
    log("Welcome to the RPG! A Goblin appears!");
    updateStats();
    spawnNewEnemy();
  </script>
</body>
</html>

Javascript 四角を描画

'use strict';

{
  function draw() {
    const canvas = document.querySelector('canvas');
    if (typeof canvas.getContext === 'undefined') {
      return;
    }
    const ctx = canvas.getContext('2d');

    // ctx.fillRect(50, 50, 50, 50);
    ctx.strokeRect(50, 50, 50, 50);
  }

  draw();
}
body {
  background: #222;
}

canvas {
  background: #fff;
}
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>My Canvas</title>
  <link rel="stylesheet" href="css/styles.css">
</head>
<body>
  <canvas width="600" height="240">
    Canvas not supported.
  </canvas>

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

Javascript 日時を更新してみよう

main.js

'use strict';

{
    // 2000 4 11
    // const d = new Date(2000, 3, 11);
    // 2000 2 ??
    const d = new Date(2000, 3, 0);
    d.setDate(d.getDate() + 100);


    console.log(d.toLocaleString());
}

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 JavaScript</title>
</head>

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

</html>

ニコニコ動画風サイト

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ニコニコ動画風サイト</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 0;
            background-color: #f4f4f4;
        }
        header {
            background-color: #333;
            color: #fff;
            padding: 10px 20px;
            text-align: center;
        }
        nav {
            background-color: #555;
            color: #fff;
            display: flex;
            justify-content: space-around;
            padding: 10px 0;
        }
        nav a {
            color: #fff;
            text-decoration: none;
            padding: 10px 20px;
        }
        main {
            display: flex;
            margin: 20px;
        }
        aside {
            width: 25%;
            background-color: #fff;
            padding: 20px;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
        }
        section {
            width: 75%;
            padding: 20px;
        }
        .video-player {
            background-color: #000;
            height: 400px;
            margin-bottom: 20px;
            position: relative;
        }
        .video-player video {
            width: 100%;
            height: 100%;
        }
        footer {
            background-color: #333;
            color: #fff;
            text-align: center;
            padding: 10px 0;
        }
        .comments {
            list-style: none;
            padding: 0;
        }
        .comments li {
            background-color: #fff;
            margin-bottom: 10px;
            padding: 10px;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
        }
        .comment-form {
            display: flex;
            margin-top: 20px;
        }
        .comment-form input {
            flex: 1;
            padding: 10px;
            margin-right: 10px;
            border: 1px solid #ddd;
            border-radius: 5px;
        }
        .comment-form button {
            padding: 10px 20px;
            border: none;
            background-color: #333;
            color: #fff;
            border-radius: 5px;
            cursor: pointer;
        }
        .thumbnail {
            display: flex;
            align-items: center;
            margin-bottom: 20px;
        }
        .thumbnail img {
            width: 120px;
            height: 90px;
            margin-right: 10px;
        }
        .login-form, .register-form, .upload-form, .profile {
            background-color: #fff;
            padding: 20px;
            margin-bottom: 20px;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
        }
        .login-form h2, .register-form h2, .upload-form h2, .profile h2 {
            margin-top: 0;
        }
        .login-form input, .register-form input, .upload-form input {
            width: 100%;
            padding: 10px;
            margin-bottom: 10px;
            border: 1px solid #ddd;
            border-radius: 5px;
        }
        .login-form button, .register-form button, .upload-form button {
            width: 100%;
            padding: 10px;
            border: none;
            background-color: #333;
            color: #fff;
            border-radius: 5px;
            cursor: pointer;
        }
        .search-form {
            display: flex;
            justify-content: center;
            margin: 20px 0;
        }
        .search-form input {
            width: 70%;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 5px;
        }
        .search-form button {
            padding: 10px 20px;
            border: none;
            background-color: #333;
            color: #fff;
            border-radius: 5px;
            cursor: pointer;
        }
        .rating {
            display: flex;
            align-items: center;
            margin-top: 10px;
        }
        .rating button {
            border: none;
            background: none;
            cursor: pointer;
            font-size: 1.2em;
            margin-right: 10px;
        }
        .rating span {
            margin-right: 20px;
        }
    </style>
</head>
<body>
    <header>
        <h1>ニコニコ動画風サイト</h1>
    </header>
    <nav>
        <a href="index.html">ホーム</a>
        <a href="ranking.html">ランキング</a>
        <a href="categories.html">カテゴリー</a>
        <a href="mypage.html">マイページ</a>
    </nav>
    <div class="search-form">
        <input type="text" id="searchInput" placeholder="検索...">
        <button onclick="search()">検索</button>
    </div>
    <main>
        <aside>
            <h2>おすすめ動画</h2>
            <div class="thumbnail">
                <a href="video.html"><img src="thumbnail1.jpg" alt="動画1"></a>
                <a href="video.html">動画1のタイトル</a>
            </div>
            <div class="thumbnail">
                <a href="video.html"><img src="thumbnail2.jpg" alt="動画2"></a>
                <a href="video.html">動画2のタイトル</a>
            </div>
            <div class="thumbnail">
                <a href="video.html"><img src="thumbnail3.jpg" alt="動画3"></a>
                <a href="video.html">動画3のタイトル</a>
            </div>
            <div class="thumbnail">
                <a href="video.html"><img src="thumbnail4.jpg" alt="動画4"></a>
                <a href="video.html">動画4のタイトル</a>
            </div>
        </aside>
        <section>
            <div class="video-player">
                <video controls>
                    <source src="sample.mp4" type="video/mp4">
                    あなたのブラウザは動画タグに対応していません。
                </video>
            </div>
            <h2>動画タイトル</h2>
            <p>動画の説明文がここに入ります。</p>
            <div class="rating">
                <button onclick="like()">👍</button><span id="likeCount">0</span>
                <button onclick="dislike()">👎</button><span id="dislikeCount">0</span>
            </div>
            <h3>コメント</h3>
            <ul class="comments" id="comments">
                <li><span class="timestamp">12:34</span> コメント1</li>
                <li><span class="timestamp">12:35</span> コメント2</li>
                <li><span class="timestamp">12:36</span> コメント3</li>
                <li><span class="timestamp">12:37</span> コメント4</li>
            </ul>
            <div class="comment-form">
                <input type="text" id="commentInput" placeholder="コメントを入力してください">
                <button onclick="addComment()">コメントを投稿</button>
            </div>
        </section>
    </main>
    <footer>
    </footer>

    <!-- Register Form -->
    <div class="register-form">
        <h2>ユーザー登録</h2>
        <input type="text" id="registerUsername" placeholder="ユーザー名">
        <input type="password" id="registerPassword" placeholder="パスワード">
        <button onclick="register()">登録</button>
    </div>

    <!-- Login Form -->
    <div class="login-form">
        <h2>ログイン</h2>
        <input type="text" id="loginUsername" placeholder="ユーザー名">
        <input type="password" id="loginPassword" placeholder="パスワード">
        <button onclick="login()">ログイン</button>
    </div>

    <!-- Upload Form -->
    <div class="upload-form">
        <h2>動画アップロード</h2>
        <input type="file" id="uploadVideo">
        <input type="text" id="uploadTitle" placeholder="タイトル">
        <button onclick="upload()">アップロード</button>
    </div>

    <!-- Profile Page -->
    <div class="profile">
        <h2>プロフィール</h2>
        <p>ユーザー名: <span id="profileUsername"></span></p>
        <p>登録日: <span id="profileDate"></span></p>
    </div>

    <script>
        let likeCount = 0;
        let dislikeCount = 0;

        function addComment() {
            var commentInput = document.getElementById('commentInput');
            var commentText = commentInput.value.trim();
            if (commentText !== "") {
                var commentsList = document.getElementById('comments');
                var newComment = document.createElement('li');
                var timestamp = new Date().toLocaleTimeString();
                newComment.innerHTML = '<span class="timestamp">' + timestamp + '</span> ' + commentText;
                commentsList.appendChild(newComment);
                commentInput.value = "";
            }
        }

        function like() {
            likeCount++;
            document.getElementById('likeCount').innerText = likeCount;
        }

        function dislike() {
            dislikeCount++;
            document.getElementById('dislikeCount').innerText = dislikeCount;
        }

        function search() {
            var searchInput = document.getElementById('searchInput').value.trim();
            if (searchInput !== "") {
                alert('検索結果: ' + searchInput);
            }
        }

        function register() {
            var username = document.getElementById('registerUsername').value.trim();
            var password = document.getElementById('registerPassword').value.trim();
            if (username !== "" && password !== "") {
                alert('ユーザー登録が完了しました: ' + username);
                localStorage.setItem('username', username);
                localStorage.setItem('password', password);
                document.getElementById('registerUsername').value = "";
                document.getElementById('registerPassword').value = "";
            }
        }

        function login() {
            var username = document.getElementById('loginUsername').value.trim();
            var password = document.getElementById('loginPassword').value.trim();
            var storedUsername = localStorage.getItem('username');
            var storedPassword = localStorage.getItem('password');
            if (username === storedUsername && password === storedPassword) {
                alert('ログイン成功');
                document.getElementById('profileUsername').innerText = username;
                document.getElementById('profileDate').innerText = new Date().toLocaleDateString();
                document.getElementById('loginUsername').value = "";
                document.getElementById('loginPassword').value = "";
            } else {
                alert('ユーザー名またはパスワードが間違っています');
            }
        }

        function upload() {
            var video = document.getElementById('uploadVideo').files[0];
            var title = document.getElementById('uploadTitle').value.trim();
            if (video && title !== "") {
                alert('動画アップロードが完了しました: ' + title);
                document.getElementById('uploadVideo').value = "";
                document.getElementById('uploadTitle').value = "";
            }
        }
    </script>
</body>
</html>

Social Networking Service

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Social Networking Service</title>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
<style>
  body {
    padding: 20px;
  }
  .card {
    margin-bottom: 20px;
  }
</style>
</head>
<body>

<div class="container">
  <h1 class="text-center mb-4">Social Networking Service</h1>

  <!-- ログインフォーム -->
  <div id="loginForm" class="card w-50 mx-auto">
    <div class="card-body">
      <h2 class="card-title text-center mb-4">Login</h2>
      <div class="form-group">
        <input type="text" id="loginUsername" class="form-control" placeholder="Username">
      </div>
      <div class="form-group">
        <input type="password" id="loginPassword" class="form-control" placeholder="Password">
      </div>
      <button onclick="login()" class="btn btn-primary btn-block">Login</button>
      <button onclick="registerForm()" class="btn btn-secondary btn-block">Register</button>
    </div>
  </div>

  <!-- 登録フォーム -->
  <div id="registerForm" class="card w-50 mx-auto" style="display: none;">
    <div class="card-body">
      <h2 class="card-title text-center mb-4">Register</h2>
      <div class="form-group">
        <input type="text" id="registerName" class="form-control" placeholder="Full Name">
      </div>
      <div class="form-group">
        <input type="text" id="registerUsername" class="form-control" placeholder="Username">
      </div>
      <div class="form-group">
        <input type="password" id="registerPassword" class="form-control" placeholder="Password">
      </div>
      <button onclick="register()" class="btn btn-primary btn-block">Register</button>
      <button onclick="loginForm()" class="btn btn-secondary btn-block">Back to Login</button>
    </div>
  </div>

  <!-- プロフィール -->
  <div id="profile" class="card w-50 mx-auto" style="display: none;">
    <div class="card-body">
      <h2 class="card-title text-center mb-4">Profile</h2>
      <p><strong>Name:</strong> <span id="profileName"></span></p>
      <p><strong>Username:</strong> <span id="profileUsername"></span></p>
      <button onclick="logout()" class="btn btn-danger btn-block">Logout</button>
    </div>
  </div>

  <!-- 投稿フォーム -->
  <div id="postForm" class="card w-75 mx-auto" style="display: none;">
    <div class="card-body">
      <h2 class="card-title text-center mb-4">Create Post</h2>
      <div class="form-group">
        <textarea id="postContent" class="form-control" rows="3" placeholder="What's on your mind?"></textarea>
      </div>
      <button onclick="createPost()" class="btn btn-primary btn-block">Post</button>
    </div>
  </div>

  <!-- 投稿一覧 -->
  <div id="postList" class="w-75 mx-auto mt-4"></div>
</div>

<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.5.4/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
<script>
  let currentUser = null; // 現在のログインユーザー
  let users = []; // ユーザーの配列
  let posts = []; // 投稿の配列

  function login() {
    const username = document.getElementById('loginUsername').value;
    const password = document.getElementById('loginPassword').value;
    const user = users.find(u => u.username === username && u.password === password);
    if (user) {
      currentUser = user;
      showProfile();
    } else {
      alert('Invalid username or password.');
    }
  }

  function logout() {
    currentUser = null;
    hideAll();
    document.getElementById('loginForm').style.display = 'block';
  }

  function registerForm() {
    hideAll();
    document.getElementById('registerForm').style.display = 'block';
  }

  function register() {
    const name = document.getElementById('registerName').value;
    const username = document.getElementById('registerUsername').value;
    const password = document.getElementById('registerPassword').value;
    users.push({ name, username, password });
    alert('Registration successful! Please login.');
    loginForm();
  }

  function loginForm() {
    hideAll();
    document.getElementById('loginForm').style.display = 'block';
  }

  function showProfile() {
    hideAll();
    document.getElementById('profile').style.display = 'block';
    document.getElementById('profileName').textContent = currentUser.name;
    document.getElementById('profileUsername').textContent = currentUser.username;
    document.getElementById('postForm').style.display = 'block';
    displayPosts();
  }

  function createPost() {
    const postContent = document.getElementById('postContent').value;
    if (postContent.trim() !== '') {
      const post = {
        id: Date.now(),
        content: postContent,
        author: currentUser.name,
        authorUsername: currentUser.username,
        likes: 0,
        comments: []
      };
      posts.unshift(post); // 最新の投稿を先頭に追加
      displayPosts();
      document.getElementById('postContent').value = ''; // 投稿後、入力欄を空にする
    }
  }

  function displayPosts() {
    const postList = document.getElementById('postList');
    postList.innerHTML = '';
    posts.forEach(post => {
      const postElement = document.createElement('div');
      postElement.innerHTML = `
        <div class="card mb-3">
          <div class="card-body">
            <p class="card-text">${post.content}</p>
            <small class="text-muted">Posted by ${post.author} (@${post.authorUsername}) at ${new Date(post.id).toLocaleString()}</small><br>
            <button onclick="likePost(${post.id})" class="btn btn-primary btn-sm mt-2">Like (${post.likes})</button>
            <button onclick="showComments(${post.id})" class="btn btn-secondary btn-sm mt-2">Comments</button>
          </div>
        </div>
      `;
      postList.appendChild(postElement);
    });
  }

  function likePost(postId) {
    const post = posts.find(p => p.id === postId);
    post.likes++;
    displayPosts();
  }

  function showComments(postId) {
    const post = posts.find(p => p.id === postId);
    const comments = prompt('Enter your comment:');
    if (comments !== null && comments.trim() !== '') {
      post.comments.push({ author: currentUser.name, content: comments });
      displayPosts();
    }
  }

  function hideAll() {
    document.getElementById('loginForm').style.display = 'none';
    document.getElementById('registerForm').style.display = 'none';
    document.getElementById('profile').style.display = 'none';
    document.getElementById('postForm').style.display = 'none';
  }

  users.push({ name: 'User One', username: 'user1', password: 'password1' });
  users.push({ name: 'User Two', username: 'user2', password: 'password2' });

  hideAll();
  document.getElementById('loginForm').style.display = 'block';
</script>

</body>
</html>

Javascript 文字列の整形

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 JavaScript</title>
</head>
<body>
  <script src="main.js"></script>
</body>
</html>

main.js

'use strict';

{
  const string = prompt('Name?');
  //if(string.toLowerCase() === 'taro'){
  if(string.toUpperCase().trim() === 'TARO'){
    console.log('Corrent!');
  }else{
    console.log('Wrong!');
  }
}