ELDER.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>ELDER - ソーシャルネットワーキングサービス</title>
  <!-- CSSファイルの読み込み -->
  <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet"> 
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" rel="stylesheet">
  <!-- RiTa.jsライブラリの読み込み -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/rita/1.3.63/rita-full.min.js"></script>
  <style>
    /* 既存のスタイルシート */
    body {
      padding-top: 20px;
      background: linear-gradient(to right, #f8f9fa, #e9ecef);
      font-family: 'Arial', sans-serif;
      color: #343a40;
    }

    .navbar {
      background-color: #007bff;
      box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
    }

    .navbar-brand,
    .nav-link {
      color: #fff !important;
      font-weight: bold;
    }

    .navbar-toggler {
      border: none;
    }

    .card {
      border-radius: 12px;
      box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
      transition: transform 0.3s, box-shadow 0.3s;
    }

    .card:hover {
      transform: translateY(-5px);
      box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
    }

    .card-body {
      padding: 30px;
    }

    .form-control {
      border-radius: 20px;
      border: 1px solid #ced4da;
      padding: 15px;
      transition: border-color 0.3s;
    }

    .form-control:focus {
      border-color: #007bff;
      box-shadow: none;
    }

    .btn-primary {
      background-color: #007bff;
      border: none;
      border-radius: 20px;
      padding: 10px 20px;
      font-weight: bold;
      transition: background-color 0.3s, transform 0.3s;
    }

    .btn-primary:hover {
      background-color: #0056b3;
      transform: translateY(-3px);
    }

    .btn-secondary {
      border-radius: 20px;
      padding: 10px 20px;
      font-weight: bold;
      transition: background-color 0.3s, transform 0.3s;
    }

    .btn-secondary:hover {
      background-color: #6c757d;
      transform: translateY(-3px);
    }

    .profile-img {
      width: 80px;
      height: 80px;
      object-fit: cover;
      border-radius: 50%;
      box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
    }

    .text-center h1 {
      font-weight: 700;
      font-size: 2.5em;
      margin-bottom: 20px;
      color: #007bff;
    }

    .comment-section {
      margin-top: 20px;
      padding: 20px;
      background-color: #f1f3f5;
      border-radius: 8px;
    }

    .comment-section .card {
      border: none;
      background: #ffffff;
      border-radius: 8px;
      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
    }

    .modal-content {
      border-radius: 15px;
      overflow: hidden;
    }

    .modal-header {
      background-color: #007bff;
      color: white;
      border-bottom: none;
    }

    .modal-footer {
      border-top: none;
    }

    .form-error {
      color: red;
      font-size: 0.9em;
    }

    /* タイムラインのデザイン */
    #timeline {
      margin-top: 20px;
    }

    #timeline .card {
      border-radius: 15px;
      box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
      margin-bottom: 20px;
      overflow: hidden;
      background-color: #fff;
    }

    #timeline .card-body {
      padding: 20px;
    }

    #timeline .btn {
      border-radius: 20px;
    }

    /* 投稿のシェアボタン */
    .btn-share {
      background-color: #ff9800;
      border: none;
      border-radius: 20px;
      padding: 5px 10px;
      color: #fff;
      font-weight: bold;
      transition: background-color 0.3s;
    }

    .btn-share:hover {
      background-color: #e68900;
    }

    /* X(旧Twitter)シェアボタン */
    .btn-share-twitter {
      background-color: #1da1f2;
      border: none;
      border-radius: 20px;
      padding: 5px 10px;
      color: #fff;
      font-weight: bold;
      transition: background-color 0.3s;
    }

    .btn-share-twitter:hover {
      background-color: #1480bf;
    }

    /* 通知エリア */
    #notificationArea {
      position: fixed;
      top: 80px;
      right: 20px;
      z-index: 1000;
      max-width: 300px;
    }

    /* FEEDアイコンをRSSの文字に変更 */
    .nav-item .rss-icon {
      font-weight: bold;
      color: #fff;
    }
  </style>
</head>
<body>

<!-- ナビゲーションバー -->
<nav class="navbar navbar-expand-lg navbar-light bg-light">
  <a class="navbar-brand" href="#">ELDER</a>
  <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="ナビゲーションの切り替え">
    <span class="navbar-toggler-icon"></span>
  </button>
  <div class="collapse navbar-collapse" id="navbarNav">
    <ul class="navbar-nav ml-auto">
      <li class="nav-item">
        <a class="nav-link" href="#" onclick="showLoginForm()">ログイン</a>
      </li>
      <li class="nav-item">
        <a class="nav-link" href="#" onclick="showRegisterForm()">登録</a>
      </li>
      <li class="nav-item">
        <a class="nav-link" href="#" onclick="showProfile()" style="display: none;" id="navProfile">プロフィール</a>
      </li>
      <li class="nav-item">
        <a class="nav-link" href="#" onclick="logout()" style="display: none;" id="navLogout">ログアウト</a>
      </li>
      <!-- FEEDアイコンをRSSの文字に変更 -->
      <li class="nav-item" id="navFeed" style="display: none;">
        <a class="nav-link rss-icon" href="#" onclick="showFeedForm()">RSS</a>
      </li>
    </ul>
  </div>
</nav>

<div class="container">
  <!-- ログインフォーム -->
  <div id="loginForm" class="card w-50 mx-auto">
    <div class="card-body">
      <h2 class="card-title text-center mb-4">ログイン</h2>
      <div class="form-group">
        <input type="text" id="loginUsername" class="form-control" placeholder="ユーザー名">
        <span id="loginUsernameError" class="form-error" style="display: none;">ユーザー名を入力してください</span>
      </div>
      <div class="form-group">
        <input type="password" id="loginPassword" class="form-control" placeholder="パスワード">
        <span id="loginPasswordError" class="form-error" style="display: none;">パスワードを入力してください</span>
      </div>
      <button onclick="login()" class="btn btn-primary btn-block">ログイン</button>
      <button onclick="showRegisterForm()" class="btn btn-secondary btn-block">登録</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">登録</h2>
      <div class="form-group">
        <input type="text" id="registerName" class="form-control" placeholder="氏名">
        <span id="registerNameError" class="form-error" style="display: none;">氏名を入力してください</span>
      </div>
      <div class="form-group">
        <input type="text" id="registerUsername" class="form-control" placeholder="ユーザー名">
        <span id="registerUsernameError" class="form-error" style="display: none;">ユーザー名を入力してください</span>
      </div>
      <div class="form-group">
        <input type="password" id="registerPassword" class="form-control" placeholder="パスワード">
        <span id="registerPasswordError" class="form-error" style="display: none;">パスワードを入力してください</span>
      </div>
      <div class="form-group">
        <input type="password" id="registerConfirmPassword" class="form-control" placeholder="パスワードを確認">
        <span id="registerConfirmPasswordError" class="form-error" style="display: none;">パスワードが一致しません</span>
      </div>
      <button onclick="register()" class="btn btn-primary btn-block">登録</button>
      <button onclick="showLoginForm()" class="btn btn-secondary btn-block">ログインに戻る</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">プロフィール</h2>
      <div class="text-center mb-3">
        <img id="profileImg" class="profile-img" src="default_profile_img.jpg" alt="プロフィール画像">
      </div>
      <p class="text-center mb-2"><strong>氏名:</strong> <span id="profileName"></span></p>
      <p class="text-center mb-4"><strong>ユーザー名:</strong> <span id="profileUsername"></span></p>
      <div class="form-group">
        <input type="file" id="profileImageInput" accept="image/*" class="form-control-file">
      </div>
      <button onclick="uploadProfileImage()" class="btn btn-primary btn-block"><i class="fas fa-upload"></i> プロフィール画像をアップロード</button>
      <button onclick="logout()" class="btn btn-danger btn-block"><i class="fas fa-sign-out-alt"></i> ログアウト</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">投稿を作成</h2>
      <div class="form-group">
        <textarea id="postContent" class="form-control" rows="3" placeholder="今何を考えていますか?"></textarea>
      </div>
      <button onclick="createPost()" class="btn btn-primary btn-block">投稿</button>
    </div>
  </div>

  <!-- FEED登録フォーム -->
  <div id="feedForm" class="card w-75 mx-auto" style="display: none;">
    <div class="card-body">
      <h2 class="card-title text-center mb-4">FEEDを登録</h2>
      <div class="form-group">
        <input type="text" id="feedUrl" class="form-control" placeholder="RSSフィードのURL">
        <span id="feedUrlError" class="form-error" style="display: none;">RSSフィードのURLを入力してください</span>
      </div>
      <button onclick="registerFeed()" class="btn btn-primary btn-block">登録</button>
      <div class="form-group mt-4">
        <label for="feedInterval">自動投稿の間隔(秒):</label>
        <input type="number" id="feedInterval" class="form-control" placeholder="例:60" min="1">
      </div>
      <button onclick="setFeedInterval()" class="btn btn-secondary btn-block">間隔を設定</button>
    </div>
  </div>

  <!-- AIBOT投稿登録フォーム -->
  <div id="aiBotForm" class="card w-75 mx-auto" style="display: none;">
    <div class="card-body">
      <h2 class="card-title text-center mb-4">BOT投稿を登録</h2>
      <div class="form-group">
        <textarea id="aiBotContent" class="form-control" rows="3" placeholder="BOTに投稿させたい内容を入力してください"></textarea>
      </div>
      <button onclick="registerAiBotPost()" class="btn btn-primary btn-block">BOT投稿を登録</button>
    </div>
  </div>

  <!-- タイムライン -->
  <div id="timeline" class="w-75 mx-auto mt-4"></div>

  <!-- 通知エリア -->
  <div id="notificationArea" class="alert alert-info" style="display: none;"></div>

</div>

<!-- リプライモーダル -->
<div class="modal fade" id="replyModal" tabindex="-1" role="dialog" aria-labelledby="replyModalLabel" aria-hidden="true">
  <div class="modal-dialog" role="document">
    <div class="modal-content">
      <!-- モーダルの内容は後ほどJavaScriptで制御します -->
      <div class="modal-header">
        <h5 class="modal-title" id="replyModalLabel">投稿に返信</h5>
        <button type="button" class="close" data-dismiss="modal" aria-label="閉じる">
          <span aria-hidden="true">&times;</span>
        </button>
      </div>
      <div class="modal-body">
        <textarea id="replyContent" class="form-control" rows="3" placeholder="返信をここに書いてください..."></textarea>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-secondary" data-dismiss="modal">閉じる</button>
        <button type="button" onclick="postReply()" class="btn btn-primary">返信</button>
      </div>
    </div>
  </div>
</div>

<!-- スクリプトの読み込み -->
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<!-- BootstrapのJavaScript -->
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
<!-- RiTa.jsライブラリの読み込み(既にhead内で読み込み済み) -->
<script>
  // ユーザー、投稿、フィード、チャット関連のデータ
  let currentUser = null;
  let users = [];
  let posts = [];
  let feeds = [];
  let notifications = [];
  let aiBotPosts = [];
  let feedFetchInterval = 60000; // デフォルトは60,000ミリ秒(60秒)
  let feedFetchTimer;
  let markovBotInterval = 60000; // マルコフ連鎖BOTの投稿間隔

  // ローカルストレージの読み込み・保存関連の関数
  function saveUsersToLocalStorage() {
    localStorage.setItem('users', JSON.stringify(users));
  }

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

  function saveFeedsToLocalStorage() {
    localStorage.setItem('feeds', JSON.stringify(feeds));
  }

  function saveAiBotPostsToLocalStorage() {
    localStorage.setItem('aiBotPosts', JSON.stringify(aiBotPosts));
  }

  function saveCurrentUserToLocalStorage() {
    if (currentUser) {
      localStorage.setItem('currentUser', JSON.stringify(currentUser));
    } else {
      localStorage.removeItem('currentUser');
    }
  }

  function loadUsersFromLocalStorage() {
    const storedUsers = localStorage.getItem('users');
    if (storedUsers) {
      users = JSON.parse(storedUsers);
    }
  }

  function loadPostsFromLocalStorage() {
    const storedPosts = localStorage.getItem('posts');
    if (storedPosts) {
      posts = JSON.parse(storedPosts);
    }
  }

  function loadFeedsFromLocalStorage() {
    const storedFeeds = localStorage.getItem('feeds');
    if (storedFeeds) {
      feeds = JSON.parse(storedFeeds);
    }
  }

  function loadAiBotPostsFromLocalStorage() {
    const storedAiBotPosts = localStorage.getItem('aiBotPosts');
    if (storedAiBotPosts) {
      aiBotPosts = JSON.parse(storedAiBotPosts);
    }
  }

  function loadCurrentUserFromLocalStorage() {
    const storedCurrentUser = localStorage.getItem('currentUser');
    if (storedCurrentUser) {
      currentUser = JSON.parse(storedCurrentUser);
    }
  }

  // フォームの表示・非表示
  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';
    document.getElementById('feedForm').style.display = 'none';
    document.getElementById('aiBotForm').style.display = 'none';
  }

  function showLoginForm() {
    hideAll();
    document.getElementById('loginForm').style.display = 'block';
    document.getElementById('navProfile').style.display = 'none';
    document.getElementById('navLogout').style.display = 'none';
    document.getElementById('navFeed').style.display = 'none';
  }

  function showRegisterForm() {
    hideAll();
    document.getElementById('registerForm').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';
    document.getElementById('feedForm').style.display = 'block';
    document.getElementById('aiBotForm').style.display = 'block';
    loadProfileImage(); // プロフィール画像をロード
    displayTimeline(); // タイムラインを表示
    document.getElementById('navProfile').style.display = 'block';
    document.getElementById('navLogout').style.display = 'block';
    document.getElementById('navFeed').style.display = 'block'; // RSSナビゲーションを表示
  }

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

  // ユーザー認証関連の関数
  function validateLoginForm() {
    let valid = true;
    const username = document.getElementById('loginUsername').value;
    const password = document.getElementById('loginPassword').value;

    if (!username) {
      document.getElementById('loginUsernameError').style.display = 'block';
      valid = false;
    } else {
      document.getElementById('loginUsernameError').style.display = 'none';
    }

    if (!password) {
      document.getElementById('loginPasswordError').style.display = 'block';
      valid = false;
    } else {
      document.getElementById('loginPasswordError').style.display = 'none';
    }

    return valid;
  }

  function login() {
    if (!validateLoginForm()) {
      return;
    }

    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;
      saveCurrentUserToLocalStorage(); // currentUserを保存
      showProfile();
    } else {
      alert('ユーザー名またはパスワードが間違っています。');
    }
  }

  function logout() {
    currentUser = null;
    saveCurrentUserToLocalStorage(); // currentUserを削除
    hideAll();
    showLoginForm();
  }

  function validateRegisterForm() {
    let valid = true;
    const name = document.getElementById('registerName').value;
    const username = document.getElementById('registerUsername').value;
    const password = document.getElementById('registerPassword').value;
    const confirmPassword = document.getElementById('registerConfirmPassword').value;

    if (!name) {
      document.getElementById('registerNameError').style.display = 'block';
      valid = false;
    } else {
      document.getElementById('registerNameError').style.display = 'none';
    }

    if (!username) {
      document.getElementById('registerUsernameError').style.display = 'block';
      valid = false;
    } else {
      document.getElementById('registerUsernameError').style.display = 'none';
    }

    // ユーザー名の重複チェック
    if (users.find(u => u.username === username)) {
      document.getElementById('registerUsernameError').textContent = 'このユーザー名は既に使用されています';
      document.getElementById('registerUsernameError').style.display = 'block';
      valid = false;
    } else {
      document.getElementById('registerUsernameError').style.display = 'none';
    }

    if (!password) {
      document.getElementById('registerPasswordError').style.display = 'block';
      valid = false;
    } else {
      document.getElementById('registerPasswordError').style.display = 'none';
    }

    if (password !== confirmPassword) {
      document.getElementById('registerConfirmPasswordError').style.display = 'block';
      valid = false;
    } else {
      document.getElementById('registerConfirmPasswordError').style.display = 'none';
    }

    return valid;
  }

  function register() {
    if (!validateRegisterForm()) {
      return;
    }

    const name = document.getElementById('registerName').value;
    const username = document.getElementById('registerUsername').value;
    const password = document.getElementById('registerPassword').value;

    users.push({ name, username, password, profileImage: null });
    saveUsersToLocalStorage(); // ユーザー情報を保存
    alert('登録が完了しました!ログインしてください。');
    showLoginForm();
  }

  // プロフィール画像の処理
  function loadProfileImage() {
    const profileImg = document.getElementById('profileImg');
    // プロフィール画像が設定されている場合はその画像を表示
    if (currentUser.profileImage) {
      profileImg.src = currentUser.profileImage;
    } else {
      // デフォルトのプロフィール画像を表示
      profileImg.src = 'default_profile_img.jpg';
    }
  }

  function uploadProfileImage() {
    const input = document.getElementById('profileImageInput');
    const file = input.files[0];
    if (file) {
      // FileReaderを使用して画像を読み込む
      const reader = new FileReader();
      reader.onload = function(e) {
        // 読み込んだ画像をプロフィール画像として設定
        currentUser.profileImage = e.target.result;
        // プロフィール画像を更新して表示
        loadProfileImage();
        saveUsersToLocalStorage(); // プロフィール画像を保存
        saveCurrentUserToLocalStorage(); // currentUserを更新
      }
      reader.readAsDataURL(file);
    }
  }

  // AIBOT投稿を登録
  function registerAiBotPost() {
    const content = document.getElementById('aiBotContent').value;
    if (content.trim() !== '') {
      aiBotPosts.push(content);
      saveAiBotPostsToLocalStorage();
      alert("BOTの投稿が登録されました!");
      document.getElementById('aiBotContent').value = ''; // 入力欄を空にする
    }
  }

  // AIBOTが定期的に投稿
  function postAiBotContent() {
    if (aiBotPosts.length > 0) {
      const content = aiBotPosts[Math.floor(Math.random() * aiBotPosts.length)];
      const aiUser = {
        name: 'BOT',
        username: 'bot',
        profileImage: 'default_ai_profile_img.jpg'
      };
      const post = {
        id: Date.now(),
        content: content,
        author: aiUser.name,
        authorUsername: aiUser.username,
        authorProfileImage: aiUser.profileImage,
        likes: 0,
        comments: []
      };
      posts.unshift(post);
      savePostsToLocalStorage();
      displayTimeline();
    }
  }

  setInterval(postAiBotContent, 500000); // 50,000ミリ秒 = 50秒

  // マルコフ連鎖を使用した自動投稿BOT
  function generateMarkovChainText(order = 2) {
    const allTexts = posts.map(post => post.content).join(' ');
    if (allTexts.length < 100) return null; // テキストが短すぎる場合は生成しない

    const markov = new RiTa.Markov(order);
    markov.addText(allTexts);
    const sentences = markov.generate(1);
    return sentences[0];
  }

  function postMarkovBotContent() {
    const generatedText = generateMarkovChainText(3); // 階数を3に設定
    if (generatedText) {
      const botUser = {
        name: 'MarkovBot',
        username: 'markovbot',
        profileImage: 'markov_bot_profile_img.jpg'
      };
      const post = {
        id: Date.now(),
        content: generatedText,
        author: botUser.name,
        authorUsername: botUser.username,
        authorProfileImage: botUser.profileImage,
        likes: 0,
        comments: []
      };
      posts.unshift(post);
      savePostsToLocalStorage();
      displayTimeline();
    }
  }

  setInterval(postMarkovBotContent, markovBotInterval); // マルコフ連鎖BOTの投稿間隔

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

  function displayTimeline() {
    const timeline = document.getElementById('timeline');
    timeline.innerHTML = '';
    posts.forEach(post => {
      const postElement = document.createElement('div');
      postElement.classList.add('card', 'mb-3');
      postElement.innerHTML = `
        <div class="card-body">
          <div class="d-flex align-items-center mb-3">
            <img src="${post.authorProfileImage || 'default_profile_img.jpg'}" class="profile-img mr-2" alt="プロフィール画像">
            <div>
              <strong>${post.author}</strong> (@${post.authorUsername})
              ${post.shared ? '<span class="badge badge-info ml-2">シェア</span>' : ''}
            </div>
          </div>
          <p class="card-text">${post.content}</p>
          ${post.link ? `<a href="${post.link}" target="_blank" class="btn btn-link">続きを読む</a>` : ''}
          <div class="d-flex justify-content-between">
            <div>
              <button onclick="likePost(${post.id})" class="btn btn-primary btn-sm"><i class="fas fa-thumbs-up"></i> いいね (${post.likes})</button>
              <button onclick="toggleComments(${post.id})" class="btn btn-secondary btn-sm"><i class="fas fa-comments"></i> コメント (${post.comments.length})</button>
              <button onclick="replyToPost(${post.id})" class="btn btn-info btn-sm"><i class="fas fa-reply"></i> 返信</button>
              <button onclick="sharePost(${post.id})" class="btn btn-share btn-sm"><i class="fas fa-share"></i> シェア (${post.shares || 0})</button>
              <button onclick="shareToTwitter(${post.id})" class="btn btn-share-twitter btn-sm"><i class="fab fa-twitter"></i> Xでシェア</button>
            </div>
            <small class="text-muted">${new Date(post.id).toLocaleString()}</small>
          </div>
          <div id="comments-${post.id}" class="comment-section mt-3" style="display: none;">
            ${post.comments.map(comment => `
              <div class="card mb-2">
                <div class="card-body">
                  <p><strong>${comment.author}</strong>: ${comment.content}</p>
                </div>
              </div>
            `).join('')}
          </div>
        </div>
      `;
      timeline.appendChild(postElement);
    });
  }

  // いいね機能
  function likePost(postId) {
    const post = posts.find(p => p.id === postId);
    post.likes++;
    savePostsToLocalStorage();
    displayTimeline();
  }

  // コメント表示切替
  function toggleComments(postId) {
    const commentSection = document.getElementById(`comments-${postId}`);
    commentSection.style.display = commentSection.style.display === 'none' ? 'block' : 'none';
  }

  // リプライ機能
  function replyToPost(postId) {
    const modal = document.getElementById('replyModal');
    modal.dataset.postId = postId;
    $('#replyModal').modal('show');
  }

  function postReply() {
    const postId = parseInt(document.getElementById('replyModal').dataset.postId);
    const post = posts.find(p => p.id === postId);
    const replyContent = document.getElementById('replyContent').value;
    if (replyContent.trim() !== '') {
      post.comments.push({ author: currentUser.name, content: replyContent });
      $('#replyModal').modal('hide');
      savePostsToLocalStorage();
      displayTimeline();
    } else {
      alert('返信を入力してください。');
    }
  }

  // 投稿のシェア機能
  function sharePost(postId) {
    const post = posts.find(p => p.id === postId);
    if (post) {
      const sharedPost = {
        ...post,
        id: Date.now(),
        author: currentUser.name,
        authorUsername: currentUser.username,
        authorProfileImage: currentUser.profileImage,
        shared: true,
        likes: 0,
        comments: [],
        shares: post.shares ? post.shares + 1 : 1
      };
      posts.unshift(sharedPost);
      savePostsToLocalStorage();
      displayTimeline();
      alert('投稿をシェアしました。');
    }
  }

  // X(旧Twitter)にシェアする機能
  function shareToTwitter(postId) {
    const post = posts.find(p => p.id === postId);
    if (post) {
      let shareText = post.content;
      if (post.link) {
        shareText += ' ' + post.link;
      }
      const shareUrl = `https://twitter.com/intent/tweet?text=${encodeURIComponent(shareText)}&url=${encodeURIComponent(window.location.href)}`;
      window.open(shareUrl, '_blank');
    }
  }

  // FEEDの登録と自動投稿機能
  function registerFeed() {
    const feedUrl = document.getElementById('feedUrl').value;
    if (feedUrl.trim() !== '') {
      feeds.push(feedUrl);
      saveFeedsToLocalStorage(); // フィード情報を保存
      alert('RSSフィードが登録されました!');
      document.getElementById('feedUrl').value = ''; // 登録後、入力欄を空にする
    } else {
      document.getElementById('feedUrlError').style.display = 'block';
    }
  }

  function fetchFeeds() {
    feeds.forEach(feedUrl => {
      fetch(`https://api.rss2json.com/v1/api.json?rss_url=${encodeURIComponent(feedUrl)}`)
        .then(response => response.json())
        .then(data => {
          data.items.forEach(item => {
            // 同じ投稿が既に存在するかチェック
            if (!posts.some(p => p.link === item.link)) {
              const post = {
                id: Date.now() + Math.random(), // 重複しないIDを生成
                content: item.title,
                link: item.link, // リンクを追加
                author: 'FEEDBOT',
                authorUsername: 'feedbot',
                authorProfileImage: 'feedbot_profile_img.jpg', // FEEDBOTのプロフィール画像を設定
                likes: 0,
                comments: []
              };
              posts.unshift(post); // 最新の投稿を先頭に追加
              savePostsToLocalStorage(); // 投稿を保存
            }
          });
          displayTimeline(); // タイムラインを表示
        })
        .catch(error => console.error('RSSフィードの取得エラー:', error));
    });
  }

  function setFeedInterval() {
    const intervalInput = document.getElementById('feedInterval').value;
    if (intervalInput && parseInt(intervalInput) > 0) {
      feedFetchInterval = parseInt(intervalInput) * 1000; // ミリ秒に変換
      clearInterval(feedFetchTimer);
      feedFetchTimer = setInterval(fetchFeeds, feedFetchInterval);
      alert(`自動投稿の間隔が${intervalInput}秒に設定されました。`);
    } else {
      alert('有効な間隔を入力してください(1以上の数字)。');
    }
  }

  // ページロード時にフィードの自動取得を開始
  function startFeedFetch() {
    feedFetchTimer = setInterval(fetchFeeds, feedFetchInterval);
  }

  // 初期化
  function init() {
    loadUsersFromLocalStorage();
    loadPostsFromLocalStorage();
    loadFeedsFromLocalStorage();
    loadAiBotPostsFromLocalStorage();
    loadCurrentUserFromLocalStorage();

    if (currentUser) {
      showProfile();
    } else {
      hideAll();
      document.getElementById('loginForm').style.display = 'block';
    }

    displayTimeline();
    startFeedFetch(); // フィードの自動取得を開始
  }

  // ページロード時に初期化
  window.onload = init;

</script>

</body>
</html>

投稿者: chosuke

趣味はゲームやアニメや漫画などです

「ELDER.html」への30件のフィードバック

  1. If you asked me to point to a recent positive sign for the open web this site would be near the top, and a stop at shadowglowcorner reinforced that designation, the few sites that serve as evidence the web can still produce quality independent content are precious and this one has clearly become one for me.

  2. A piece that reads as if the writer trusted readers to fill in obvious gaps, and a look at silverpetalstore continued that respectful approach, content that does not over explain what the reader can infer is content that respects intelligence and this site has clearly chosen to write to capable readers rather than to the lowest common denominator.

  3. Found this useful, the points line up well with what I have been thinking about lately, and a stop at silverpetalcollective added some angles I had not considered yet, definitely walking away with more than I came for which is the best outcome from time spent reading online for any kind of topic.

  4. Took a chance on the headline and was rewarded, and a stop at velvetorchidmarket kept the rewards coming as I clicked through, the kind of place where every link leads somewhere worth the click is a small luxury on the modern web where so many sites are mostly empty calories disguised as content.

  5. Now saved this in a way that I will actually find again rather than the casual bookmark approach, and a stop at boldcartstation earned the same careful saving, organising my reading bookmarks so that high quality sources rise to the top is something I should do more of and this site triggered that organisation today.

  6. Reading this gave me material for a conversation I needed to have anyway, and a stop at frostgrovecrafts added even more talking points, content that connects to upcoming social or professional needs rather than just being interesting in the abstract is the kind that earns priority placement in my attention these days routinely.

  7. Now appreciating the way the post avoided the temptation to be longer than necessary, and a look at modernbuycorner continued that lean approach, content with the discipline to stop when finished rather than padding for length is content that respects both itself and its readers and this site has that disciplined editorial culture clearly throughout.

  8. Once I trust a site this much I tend to read everything they publish and that is the trajectory I am on with this one, and a stop at clevercartarena confirmed the trajectory, the rare progression from interested reader to comprehensive reader is something only certain sites earn and this one is earning that progression rapidly.

  9. Reading this brought back an idea I had set aside months ago, and a stop at elitebuyarena added more substance to that idea, content that revives dormant projects in my own thinking is content with serious creative value and this site is contributing to my own work in ways I had not expected when first clicking through.

  10. Now noticing how rare it is to find a site that does not feel rushed, and a look at elitecartstation extended that calm pace, content produced without time pressure has a different quality than content shipped to meet a deadline and this site reads as written without urgency which produces a different and better experience for readers.

  11. During a reading session that included several other sources this one stood out, and a look at royaltrendcorner continued the standout quality, the side by side comparison of sources during research is a useful exercise and this site has been winning those comparisons for me consistently across multiple research sessions during the last week.

  12. I learned more from this short post than from longer articles I read earlier today, and a stop at fastgoodsarena added even more useful detail without going off topic, this site clearly knows how to keep things focused without sacrificing depth which is a hard balance to strike for any writer.

  13. Took a chance on the headline and was rewarded, and a stop at luxuryfindshub kept the rewards coming as I clicked through, the kind of place where every link leads somewhere worth the click is a small luxury on the modern web where so many sites are mostly empty calories disguised as content.

  14. Picked up something useful for a side project, and a look at blossombaycollective added another piece I will incorporate, content that connects to specific projects I am working on is content with practical utility and the practical utility of this site is showing up across multiple posts I have read in the last hour or so.

  15. Glad I clicked through from where I did because this turned out to be worth the time spent, and after goldendunemarket I had a fuller picture, the kind of content that earns its visitors through delivering value rather than chasing them through aggressive advertising or constant pop ups appearing everywhere on the screen lately.

  16. I really like how the writer keeps the tone friendly without sounding fake or overly polished, and after a stop at vividcartzone the same calm pace was there, no rushing to make a point and no padding either, just clean honest writing that I can respect and come back to later again.

  17. Quality you can feel from the first paragraph, the writer clearly knows the topic and how to share it, and a quick look at urbancartzone confirmed the same depth runs throughout the rest of the site as well which is rare and worth pointing out when it happens online for any reader passing through.

  18. Now thinking I want more sites built on this kind of editorial foundation, and a stop at silversproutstore extended that wish into a broader hope, sites built on substance and care rather than on metrics and growth are the kind of sites I want to see more of and this one is a small example worth supporting.

  19. Now adding the homepage to my regular check rotation rather than waiting for individual links to find me, and a stop at copperwindessentials confirmed the rotation upgrade, the move from passive discovery to active checking is a vote of confidence in a sites ongoing quality and this site has earned that active engagement clearly.

  20. Walked away in a slightly better mood than when I started reading, that says something about the writing, and a stop at infinitybuyhub kept that going, content that leaves you feeling more capable rather than overwhelmed is the kind I keep coming back to again and again over the years and across many topics.

  21. Now thinking about this site as a small example of what good independent writing looks like, and a stop at neonpickmarket continued that exemplary status, the few sites that serve as good examples are sites worth holding up in conversations about quality and this one has earned that exemplary placement through patient consistent effort over time.

  22. Reading this in a moment of low energy still kept my attention, and a stop at perfectbuycorner continued that engagement under suboptimal conditions, content that survives the reader being tired is content with extra reserves of pull and this site has the kind of writing that holds up even when I am not at my reading best.

  23. Came away feeling slightly smarter than I was when I started, that is a real win, and a stop at crystalbloommarket added a bit more to that, the rare site that actually transfers some of its knowledge to the reader in a way that sticks rather than just creating an illusion of learning briefly.

  24. Now considering whether the post would translate well into a different form, and a look at goodslinkstore suggested similar versatility, content that could move into other media without losing its substance is content that has been built around ideas rather than around format and this site reads as idea first throughout posts.

  25. Really appreciate the confidence to make a clear point rather than hedging everything, and a quick visit to frostharbormarket maintained the same direct stance, writing that takes positions rather than equivocating is more useful even when the positions are debatable because at least the reader has something to react to clearly.

  26. Thanks for sharing this with the open internet rather than locking it behind a paywall like so many sites do now, and a stop at infinitydealcenter kept the same vibe going, generous helpful and clearly written by someone who actually wants people to learn from it rather than just charge them.

  27. Speaking as someone who used to recommend blogs frequently and got out of the habit this site is rekindling that impulse, and a look at nextcartstation extended the rekindling, the recovery of an old habit triggered by encountering work that justifies it is itself a small kind of pleasure and this site is providing that recovery experience.

  28. Closed the tab feeling I had spent the time well, and a stop at stylishgoodszone extended that feeling across more pages, the test of whether time on a site was well spent is one I apply silently after closing tabs and very few sites pass it but this one passed it cleanly today afternoon clearly.

  29. Started thinking about my own writing differently after reading, and a look at windspirecollective continued that reflective effect, content that influences how I work rather than just informing what I know is content with the highest kind of impact and this site has triggered some of that reflective influence today on me.

  30. Liked the natural conversational tone throughout, never stiff and never overly casual either, and a stop at globalgoodszone kept that comfortable middle ground going, finding a tone that respects the reader without becoming distant or overly familiar is harder than it sounds and this site nails that balance consistently across many different pieces.

コメントを残す

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です