<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>スフィア</title>
<style>
/* ======= 基本リセット ======= */
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: Arial, sans-serif; background: #f7f9fc; color: #333; line-height: 1.5; }
header { background: #4a90e2; color: #fff; padding: 15px; text-align: center; }
header h1 { font-size: 28px; }
main { width: 90%; max-width: 800px; margin: 20px auto; }
/* ======= ユーザーアイコン ======= */
.user-icon { width: 40px; height: 40px; border-radius: 50%; margin-right: 10px; vertical-align: middle; }
/* ======= 認証セクション ======= */
.auth-section { background: #fff; padding: 15px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.auth-section h2 { margin-bottom: 10px; }
.auth-section input { width: 100%; padding: 8px; margin-bottom: 10px; border: 1px solid #ccc; border-radius: 4px; }
.auth-section button { background: #4a90e2; color: #fff; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; }
.auth-section button:hover { background: #357ab8; }
.toggle-auth { margin-top: 10px; font-size: 14px; color: #4a90e2; cursor: pointer; text-decoration: underline; }
/* ======= 投稿フォーム ======= */
.post-form { background: #fff; padding: 15px; margin-bottom: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.post-form textarea { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 4px; resize: vertical; font-size: 14px; }
.post-form button { background: #4a90e2; color: #fff; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; margin-top: 10px; }
.post-form button:hover { background: #357ab8; }
/* ======= 検索セクション ======= */
.search-section { background: #fff; padding: 10px; margin-bottom: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.search-section input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 14px; }
/* ======= タイムライン ======= */
.timeline { display: flex; flex-direction: column; gap: 15px; }
.post-item { background: #fff; padding: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); position: relative; }
.post-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
.post-username { font-weight: bold; }
.post-meta { font-size: 12px; color: #777; }
.post-content { font-size: 16px; margin-bottom: 10px; }
.post-actions { margin-top: 10px; }
.post-actions button { background: transparent; border: none; color: #4a90e2; cursor: pointer; margin-right: 10px; font-size: 14px; }
.post-actions button:hover { text-decoration: underline; }
.replies { margin-top: 15px; border-top: 1px solid #eee; padding-top: 10px; }
.reply-item { background: #f2f2f2; padding: 10px; border-radius: 6px; margin-bottom: 10px; }
.reply-header { font-size: 13px; font-weight: bold; margin-bottom: 5px; }
.reply-meta { font-size: 11px; color: #555; text-align: right; }
.edit-area { margin-top: 10px; }
.edit-area textarea { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; }
.edit-area button { background: #4a90e2; color: #fff; border: none; padding: 6px 10px; border-radius: 4px; cursor: pointer; margin-top: 5px; }
.edit-area button:hover { background: #357ab8; }
/* ======= ユーティリティ(ログ/RSS)セクション ======= */
.utility-section { margin-top: 20px; text-align: center; }
.logs-section { background: #fff; padding: 15px; margin-top: 10px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-size: 12px; color: #555; max-height: 200px; overflow-y: auto; }
.log-entry { border-bottom: 1px solid #eee; padding: 5px 0; }
footer { text-align: center; padding: 15px; font-size: 14px; color: #777; margin-top: 20px; }
</style>
</head>
<body>
<header>
<h1>スフィア</h1>
</header>
<main>
<!-- 認証セクション(ログイン/登録 or ユーザー情報表示) -->
<section id="authSection" class="auth-section"></section>
<!-- 投稿フォーム(ログイン中のみ表示) -->
<section id="postFormSection" class="post-form" style="display: none;">
<textarea id="postContent" placeholder="いまどうしてる?"></textarea>
<button id="postButton">投稿する</button>
</section>
<!-- 検索セクション -->
<section class="search-section">
<input type="text" id="searchInput" placeholder="投稿を検索">
</section>
<!-- タイムライン -->
<section id="timeline" class="timeline"></section>
<!-- ユーティリティ:ログ表示/RSSダウンロード -->
<section class="utility-section">
<button id="toggleLogsButton">ログを表示</button>
<button id="downloadRSSButton">RSSフィードをダウンロード</button>
</section>
<!-- ログ表示セクション(初期は非表示) -->
<section id="logsSection" class="logs-section" style="display: none;"></section>
</main>
<footer>
<p>© 2023 スフィア</p>
</footer>
<script>
(function(){
'use strict';
// === 定数&キー定義 ===
const POSTS_KEY = 'posts';
const USERS_KEY = 'users';
const CURRENT_USER_KEY = 'currentUser';
const LOGS_KEY = 'logs';
// === データ管理変数 ===
let posts = [];
let users = [];
let currentUser = null;
let logs = [];
// === ストレージ関連関数 ===
const saveToStorage = (key, data) => localStorage.setItem(key, JSON.stringify(data));
const loadFromStorage = (key) => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : null;
};
const loadData = () => {
posts = loadFromStorage(POSTS_KEY) || [];
users = loadFromStorage(USERS_KEY) || [];
currentUser = loadFromStorage(CURRENT_USER_KEY) || null;
logs = loadFromStorage(LOGS_KEY) || [];
};
const saveData = () => {
saveToStorage(POSTS_KEY, posts);
saveToStorage(USERS_KEY, users);
saveToStorage(CURRENT_USER_KEY, currentUser);
saveToStorage(LOGS_KEY, logs);
};
// === ログ処理 ===
const addLog = (message) => {
const entry = { timestamp: new Date(), message };
logs.push(entry);
saveToStorage(LOGS_KEY, logs);
};
const renderLogs = () => {
const logsSection = document.getElementById('logsSection');
logsSection.innerHTML = '';
logs.forEach(log => {
const div = document.createElement('div');
div.className = 'log-entry';
div.textContent = `[${formatDate(log.timestamp)}] ${log.message}`;
logsSection.appendChild(div);
});
};
// === 日付フォーマット ===
const formatDate = (date) => {
const d = new Date(date);
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
const h = String(d.getHours()).padStart(2, '0');
const min = String(d.getMinutes()).padStart(2, '0');
return `${y}-${m}-${day} ${h}:${min}`;
};
// === RSS生成 ===
const generateRSS = () => {
const now = new Date();
let rss = `<?xml version="1.0" encoding="UTF-8"?>\n`;
rss += `<rss version="2.0">\n<channel>\n`;
rss += `<title>Advanced Mini SNS RSS Feed</title>\n`;
rss += `<link>${location.origin + location.pathname}</link>\n`;
rss += `<description>RSS Feed of posts from Advanced Mini SNS</description>\n`;
rss += `<lastBuildDate>${now.toUTCString()}</lastBuildDate>\n`;
rss += `<language>ja</language>\n`;
posts.forEach(post => {
const postUrl = `${location.origin + location.pathname}?post=${post.id}`;
const title = `Post by ${post.username}: ${post.content.substring(0,20)}...`;
rss += `<item>\n`;
rss += `<title>${title}</title>\n`;
rss += `<link>${postUrl}</link>\n`;
rss += `<description><![CDATA[${post.content}]]></description>\n`;
rss += `<pubDate>${new Date(post.timestamp).toUTCString()}</pubDate>\n`;
rss += `<guid>${post.id}</guid>\n`;
rss += `</item>\n`;
});
rss += `</channel>\n</rss>`;
return rss;
};
const downloadRSS = () => {
const rssContent = generateRSS();
const blob = new Blob([rssContent], { type: 'application/rss+xml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'feed.xml';
a.click();
URL.revokeObjectURL(url);
addLog('RSSフィードをダウンロード');
};
// === 認証処理 ===
const renderAuthSection = () => {
const authSection = document.getElementById('authSection');
authSection.innerHTML = '';
if (currentUser) {
// ログイン済み:アイコンとウェルカムメッセージ、ログアウトボタン
const container = document.createElement('div');
if (currentUser.icon) {
const img = document.createElement('img');
img.src = currentUser.icon;
img.className = 'user-icon';
container.appendChild(img);
}
const welcomeDiv = document.createElement('span');
welcomeDiv.textContent = `ようこそ、${currentUser.username} さん!`;
container.appendChild(welcomeDiv);
const logoutBtn = document.createElement('button');
logoutBtn.textContent = 'ログアウト';
logoutBtn.onclick = () => {
addLog(`ユーザー ${currentUser.username} ログアウト`);
currentUser = null;
saveData();
renderAuthSection();
togglePostForm();
renderTimeline();
};
authSection.appendChild(container);
authSection.appendChild(logoutBtn);
} else {
// 未ログイン:ログインフォームを表示
renderLoginForm(authSection);
}
};
const renderLoginForm = (container) => {
container.innerHTML = '<h2>ログイン</h2>';
const usernameInput = document.createElement('input');
usernameInput.type = 'text';
usernameInput.placeholder = 'ユーザー名';
const passwordInput = document.createElement('input');
passwordInput.type = 'password';
passwordInput.placeholder = 'パスワード';
const loginBtn = document.createElement('button');
loginBtn.textContent = 'ログイン';
loginBtn.onclick = () => {
const username = usernameInput.value.trim();
const password = passwordInput.value;
if (!username || !password) { alert('ユーザー名とパスワードを入力してください。'); return; }
const user = users.find(u => u.username === username && u.password === password);
if (user) {
currentUser = user;
saveData();
addLog(`ユーザー ${username} ログイン成功`);
renderAuthSection();
togglePostForm();
renderTimeline();
} else {
alert('ユーザー名またはパスワードが正しくありません。');
}
};
container.appendChild(usernameInput);
container.appendChild(passwordInput);
container.appendChild(loginBtn);
const toggleLink = document.createElement('div');
toggleLink.className = 'toggle-auth';
toggleLink.textContent = '新規登録はこちら';
toggleLink.onclick = () => renderRegisterForm(container);
container.appendChild(toggleLink);
};
const renderRegisterForm = (container) => {
container.innerHTML = '<h2>新規登録</h2>';
const usernameInput = document.createElement('input');
usernameInput.type = 'text';
usernameInput.placeholder = 'ユーザー名';
const passwordInput = document.createElement('input');
passwordInput.type = 'password';
passwordInput.placeholder = 'パスワード';
const confirmInput = document.createElement('input');
confirmInput.type = 'password';
confirmInput.placeholder = 'パスワード確認';
// アイコンアップロード用フィールド
const iconInput = document.createElement('input');
iconInput.type = 'file';
iconInput.accept = 'image/*';
iconInput.style.marginBottom = '10px';
const registerBtn = document.createElement('button');
registerBtn.textContent = '登録';
registerBtn.onclick = () => {
const username = usernameInput.value.trim();
const password = passwordInput.value;
const confirmPassword = confirmInput.value;
if (!username || !password || !confirmPassword) { alert('全ての項目を入力してください。'); return; }
if (password !== confirmPassword) { alert('パスワードが一致しません。'); return; }
if (users.some(u => u.username === username)) { alert('そのユーザー名は既に使用されています。'); return; }
// アイコンが選択されている場合、FileReader で Data URL に変換
if (iconInput.files && iconInput.files[0]) {
const reader = new FileReader();
reader.onload = function(e) {
const iconData = e.target.result;
const newUser = { username, password, icon: iconData };
users.push(newUser);
saveData();
addLog(`ユーザー ${username} 登録完了(アイコン設定済み)`);
alert('登録が完了しました。ログインしてください。');
renderLoginForm(container);
};
reader.readAsDataURL(iconInput.files[0]);
} else {
const newUser = { username, password, icon: '' };
users.push(newUser);
saveData();
addLog(`ユーザー ${username} 登録完了(アイコン未設定)`);
alert('登録が完了しました。ログインしてください。');
renderLoginForm(container);
}
};
container.appendChild(usernameInput);
container.appendChild(passwordInput);
container.appendChild(confirmInput);
container.appendChild(iconInput);
container.appendChild(registerBtn);
const toggleLink = document.createElement('div');
toggleLink.className = 'toggle-auth';
toggleLink.textContent = 'ログインはこちら';
toggleLink.onclick = () => renderLoginForm(container);
container.appendChild(toggleLink);
};
const togglePostForm = () => {
const postFormSection = document.getElementById('postFormSection');
postFormSection.style.display = currentUser ? 'block' : 'none';
};
// === タイムライン&投稿表示 ===
const renderTimeline = () => {
const timeline = document.getElementById('timeline');
timeline.innerHTML = '';
let filteredPosts = posts.slice();
const searchValue = document.getElementById('searchInput').value.trim().toLowerCase();
if (searchValue) {
filteredPosts = filteredPosts.filter(post =>
post.content.toLowerCase().includes(searchValue) ||
post.username.toLowerCase().includes(searchValue)
);
}
// 新しい投稿順にソート
filteredPosts.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
filteredPosts.forEach(post => {
timeline.appendChild(renderPost(post));
});
};
const renderPost = (post) => {
const postDiv = document.createElement('div');
postDiv.className = 'post-item';
postDiv.setAttribute('data-id', post.id);
// ヘッダー(投稿者名&日時)
const headerDiv = document.createElement('div');
headerDiv.className = 'post-header';
const usernameSpan = document.createElement('span');
usernameSpan.className = 'post-username';
usernameSpan.textContent = post.username;
const metaSpan = document.createElement('span');
metaSpan.className = 'post-meta';
metaSpan.textContent = formatDate(post.timestamp);
headerDiv.appendChild(usernameSpan);
headerDiv.appendChild(metaSpan);
postDiv.appendChild(headerDiv);
// 投稿内容(編集モードか通常表示か)
const contentDiv = document.createElement('div');
contentDiv.className = 'post-content';
if (post.editing) {
const editArea = document.createElement('div');
editArea.className = 'edit-area';
const editTextarea = document.createElement('textarea');
editTextarea.value = post.content;
editArea.appendChild(editTextarea);
const saveBtn = document.createElement('button');
saveBtn.textContent = '保存';
saveBtn.onclick = () => {
const newContent = editTextarea.value.trim();
if (!newContent) { alert('投稿内容が空です。'); return; }
post.content = newContent;
post.editing = false;
saveData();
addLog(`投稿 ${post.id} 編集完了`);
renderTimeline();
};
const cancelBtn = document.createElement('button');
cancelBtn.textContent = 'キャンセル';
cancelBtn.style.marginLeft = '5px';
cancelBtn.onclick = () => { post.editing = false; renderTimeline(); };
editArea.appendChild(saveBtn);
editArea.appendChild(cancelBtn);
contentDiv.appendChild(editArea);
} else {
contentDiv.textContent = post.content;
}
postDiv.appendChild(contentDiv);
// アクションボタン(いいね、編集、削除、返信)
const actionsDiv = document.createElement('div');
actionsDiv.className = 'post-actions';
const likeBtn = document.createElement('button');
likeBtn.textContent = `いいね (${post.likes || 0})`;
likeBtn.onclick = () => {
post.likes = (post.likes || 0) + 1;
saveData();
addLog(`投稿 ${post.id} にいいね`);
renderTimeline();
};
actionsDiv.appendChild(likeBtn);
if (currentUser && currentUser.username === post.username) {
const editBtn = document.createElement('button');
editBtn.textContent = '編集';
editBtn.onclick = () => { post.editing = true; renderTimeline(); };
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '削除';
deleteBtn.onclick = () => {
if (confirm('本当に削除しますか?')) {
posts = posts.filter(p => p.id !== post.id);
saveData();
addLog(`投稿 ${post.id} 削除`);
renderTimeline();
}
};
actionsDiv.appendChild(editBtn);
actionsDiv.appendChild(deleteBtn);
}
if (currentUser) {
const replyBtn = document.createElement('button');
replyBtn.textContent = '返信';
replyBtn.onclick = () => toggleReplyForm(post.id);
actionsDiv.appendChild(replyBtn);
}
postDiv.appendChild(actionsDiv);
// 返信一覧表示
const repliesDiv = document.createElement('div');
repliesDiv.className = 'replies';
if (post.replies && post.replies.length > 0) {
post.replies.forEach(reply => {
repliesDiv.appendChild(renderReply(reply));
});
}
postDiv.appendChild(repliesDiv);
// 返信フォーム(初期は非表示)
const replyFormDiv = document.createElement('div');
replyFormDiv.className = 'reply-form';
replyFormDiv.style.display = 'none';
const replyTextarea = document.createElement('textarea');
replyTextarea.placeholder = '返信内容を入力';
const submitReplyBtn = document.createElement('button');
submitReplyBtn.textContent = '返信する';
submitReplyBtn.onclick = () => {
const replyContent = replyTextarea.value.trim();
if (!replyContent) { alert('返信内容が空です。'); return; }
addReply(post.id, replyContent);
replyTextarea.value = '';
toggleReplyForm(post.id, true);
};
replyFormDiv.appendChild(replyTextarea);
replyFormDiv.appendChild(submitReplyBtn);
postDiv.appendChild(replyFormDiv);
return postDiv;
};
const renderReply = (reply) => {
const replyDiv = document.createElement('div');
replyDiv.className = 'reply-item';
const replyHeader = document.createElement('div');
replyHeader.className = 'reply-header';
replyHeader.textContent = reply.username;
const replyContent = document.createElement('div');
replyContent.textContent = reply.content;
const replyMeta = document.createElement('div');
replyMeta.className = 'reply-meta';
replyMeta.textContent = formatDate(reply.timestamp);
replyDiv.appendChild(replyHeader);
replyDiv.appendChild(replyContent);
replyDiv.appendChild(replyMeta);
return replyDiv;
};
const toggleReplyForm = (postId, forceHide = false) => {
const postDiv = document.querySelector(`.post-item[data-id="${postId}"]`);
if (!postDiv) return;
const replyForm = postDiv.querySelector('.reply-form');
replyForm.style.display = (forceHide || replyForm.style.display === 'block') ? 'none' : 'block';
};
const addReply = (postId, replyContent) => {
const post = posts.find(p => p.id === postId);
if (!post) return;
if (!post.replies) { post.replies = []; }
post.replies.push({
id: Date.now(),
username: currentUser.username,
content: replyContent,
timestamp: new Date()
});
saveData();
addLog(`投稿 ${post.id} に返信追加`);
renderTimeline();
};
// === 投稿作成処理 ===
const initPostButton = () => {
document.getElementById('postButton').addEventListener('click', () => {
const postContentElem = document.getElementById('postContent');
const content = postContentElem.value.trim();
if (!content) { alert('投稿内容が空です。'); return; }
const newPost = {
id: Date.now(),
username: currentUser.username,
content: content,
timestamp: new Date(),
likes: 0,
replies: []
};
posts.push(newPost);
saveData();
addLog(`投稿 ${newPost.id} 追加`);
renderTimeline();
postContentElem.value = '';
});
};
// === ユーティリティ処理:ログ/RSS ===
const initUtilityButtons = () => {
document.getElementById('toggleLogsButton').addEventListener('click', () => {
const logsSection = document.getElementById('logsSection');
if (logsSection.style.display === 'none' || logsSection.style.display === '') {
renderLogs();
logsSection.style.display = 'block';
document.getElementById('toggleLogsButton').textContent = 'ログを非表示';
} else {
logsSection.style.display = 'none';
document.getElementById('toggleLogsButton').textContent = 'ログを表示';
}
});
document.getElementById('downloadRSSButton').addEventListener('click', downloadRSS);
};
// === 初期化処理 ===
const init = () => {
loadData();
renderAuthSection();
togglePostForm();
renderTimeline();
initPostButton();
initUtilityButtons();
document.getElementById('searchInput').addEventListener('input', renderTimeline);
};
document.addEventListener('DOMContentLoaded', init);
})();
</script>
</body>
</html>