<!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 => ({'&':'&','<':'<','>':'>','"':'"','\'':'''}[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>
タグ: Javascript
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!');
}
}
