// グローバルshowNotification関数(最初に定義) window.showNotification = function(message, type = 'info') { const notification = document.createElement('div'); const colors = { success: '#4CAF50', error: '#f44336', info: '#2196F3', warning: '#ff9800' }; notification.style.cssText = 'position: fixed;' + 'top: 20px;' + 'right: 20px;' + 'background: ' + colors[type] + ';' + 'color: white;' + 'padding: 16px 24px;' + 'border-radius: 8px;' + 'z-index: 10000;' + 'font-weight: 500;' + 'font-size: 14px;' + 'box-shadow: 0 4px 12px rgba(0,0,0,0.3);' + 'animation: slideInRight 0.5s ease;' + 'max-width: 300px;' + 'word-wrap: break-word;' + 'border: 2px solid rgba(255,255,255,0.3);'; notification.innerHTML = message; document.body.appendChild(notification); setTimeout(() => { notification.style.animation = 'slideOutRight 0.5s ease'; setTimeout(() => { if (notification.parentNode) { notification.parentNode.removeChild(notification); } }, 500); }, 4000); }; // リアクションシステム window.addReaction = async function(reaction, articleId) { const button = document.getElementById('reaction-' + reaction + '-' + (articleId || '')); const countSpan = button ? button.querySelector('.count') : null; if (button && countSpan) { // 既にリアクション済みかチェック(UI状態用にlocalStorageを使用) const hasReacted = localStorage.getItem('reaction-' + reaction + '-' + (articleId || window.location.pathname)); if (hasReacted) { window.showNotification('もう押してるで!'); return; } try { // リアクション追加のAPIコール const response = await fetch('/api/reactions', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ article_id: articleId, reaction_type: reaction }) }); if (response.ok) { const data = await response.json(); // UIを更新 countSpan.textContent = data.count; button.classList.add('active'); // 重複クリックを防ぐためlocalStorageに保存 localStorage.setItem('reaction-' + reaction + '-' + (articleId || window.location.pathname), 'true'); // 楽しいメッセージを表示 const messages = { 'いいね': 'いいね!ナイスや!', '草': '草生えるwwwww', 'やばい': 'やばいンゴねぇ...', 'ガチ': 'ガチでそれな!', 'ワロタ': 'ワロタwwwww', 'んご': 'んごんごんご〜' }; window.showNotification(messages[reaction] || 'リアクションありがとうやで!'); } else { const errorText = await response.text(); console.error('APIエラーレスポンス:', errorText); throw new Error('リアクション追加に失敗しました: ' + response.status); } } catch (error) { console.error('リアクション追加エラー:', error); window.showNotification('エラーが発生しました。もう一度試してください。'); } } }; // ソーシャル共有機能 window.showDeleteConfirm = function(articleId) { const password = prompt('管理者パスワードを入力してください:'); if (password === null) return; // キャンセル if (confirm('本当にこの記事を削除しますか?この操作は取り消せません。')) { deleteArticle(articleId, password); } }; // ユーティリティ関数 // ソーシャル共有機能をグローバルに定義 window.shareToTwitter = function() { const url = encodeURIComponent(window.location.href); const title = document.title; const nanjTexts = [ 'これはガチでやばいンゴwww', 'この記事草すぎて腹痛いンゴwww', 'なんJ民必見の神記事やんけ', 'ワロタwwwwwwみんな見ろや', 'これは盛り上がりまくりやな', 'ファーwwwwwwこれは草', 'やばすぎワロタンゴwww' ]; const randomText = nanjTexts[Math.floor(Math.random() * nanjTexts.length)]; const text = encodeURIComponent(randomText + ' ' + title + ' #なんJ #ニュース #話題沸騰'); window.open('https://twitter.com/intent/tweet?url=' + url + '&text=' + text, '_blank'); }; window.shareToLine = function() { const url = encodeURIComponent(window.location.href); const title = document.title; const lineText = '🔥' + title + '🔥' + String.fromCharCode(10) + String.fromCharCode(10) + 'これはガチで話題になってるンゴwww' + String.fromCharCode(10) + 'なんJ民も盛り上がってるで!'; const text = encodeURIComponent(lineText); window.open('https://social-plugins.line.me/lineit/share?url=' + url + '&text=' + text, '_blank'); }; window.shareToFacebook = function() { const url = encodeURIComponent(window.location.href); window.open('https://www.facebook.com/sharer/sharer.php?u=' + url + '"e=' + encodeURIComponent('なんJ民も驚愕の話題記事やで!みんなも見てや!'), '_blank'); }; window.copyLink = function() { const url = window.location.href; if (navigator.clipboard) { navigator.clipboard.writeText(url).then(() => { window.showNotification('リンクをコピーしました!', 'success'); }).catch(() => { fallbackCopy(url); }); } else { fallbackCopy(url); } }; function fallbackCopy(text) { const textArea = document.createElement('textarea'); textArea.value = text; document.body.appendChild(textArea); textArea.focus(); textArea.select(); try { document.execCommand('copy'); window.showNotification('リンクをコピーしました!', 'success'); } catch (err) { window.showNotification('コピーに失敗しました', 'error'); } document.body.removeChild(textArea); } // ページ読み込み時にリアクション数を取得 async function loadReactions() { // 現在のページから記事IDを取得 const articleId = getCurrentArticleId(); if (!articleId) { return; } try { // APIからリアクション数を取得 const response = await fetch('/api/reactions/' + articleId); if (response.ok) { const reactions = await response.json(); // 実際の数値でUIを更新 Object.keys(reactions).forEach(reactionType => { const button = document.getElementById('reaction-' + reactionType + '-' + articleId); if (button) { const countSpan = button.querySelector('.count'); if (countSpan) { countSpan.textContent = reactions[reactionType] || 0; } // ユーザーが既にリアクションしているかチェック(localStorageから) const hasReacted = localStorage.getItem('reaction-' + reactionType + '-' + window.location.pathname); if (hasReacted) { button.classList.add('active'); } } }); } else { const errorText = await response.text(); console.error('リアクションAPIエラー:', errorText); } } catch (error) { console.error('リアクション読み込みエラー:', error); } } // 現在の記事IDを取得するヘルパー関数 function getCurrentArticleId() { const path = window.location.pathname; const match = path.match(/\/article\/(.+)/); if (match) { // URLから記事IDを抽出 const parts = match[1].split('/'); const articleId = parts[parts.length - 1]; // 最後の部分を記事IDとして取得 return articleId; } return null; } // 記事削除機能 async function deleteArticle(articleId, password) { try { const response = await fetch('/api/admin/delete/' + articleId, { method: 'DELETE', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + password } }); const result = await response.json(); if (result.success) { alert('記事を削除しました'); window.location.href = '/'; } else { alert('削除に失敗しました: ' + (result.error || '不明なエラー')); } } catch (error) { alert('削除に失敗しました: ' + error.message); } } // コメントシステム機能 function initCommentSystem() { const commentForm = document.getElementById('commentForm'); const articleId = document.getElementById('articleId')?.value; if (!commentForm) { return; } if (!articleId) { console.error('記事IDが見つかりません'); return; } // コメント一覧を読み込み loadComments(articleId); // コメントフォーム送信イベント commentForm.addEventListener('submit', function(e) { e.preventDefault(); e.stopPropagation(); submitComment(articleId); return false; }); } async function loadComments(articleId) { try { const response = await fetch('/api/comments/' + articleId); if (!response.ok) { throw new Error('HTTP error! status: ' + response.status); } const data = await response.json(); const commentsList = document.getElementById('commentsList'); if (!commentsList) { console.error('コメントリスト要素が見つかりません'); return; } // エラーレスポンスの処理 if (data.error) { console.error('コメント取得エラー:', data.error); commentsList.innerHTML = '
コメントの読み込みに失敗しました
'; return; } // コメントが配列でない場合のハンドリング const comments = Array.isArray(data) ? data : []; if (comments.length === 0) { commentsList.innerHTML = '
まだコメントがありません。最初のコメントを投稿しませんか?
'; return; } // コメントをHTML形式で表示 const commentsHtml = comments.map((comment, index) => formatComment(comment, index + 1) ).join(''); commentsList.innerHTML = commentsHtml; } catch (error) { console.error('コメント読み込みエラー:', error); const commentsList = document.getElementById('commentsList'); if (commentsList) { commentsList.innerHTML = '
コメントの読み込みに失敗しました
'; } } } function formatComment(comment, number) { const commentDate = new Date(comment.created_at); const formattedDate = commentDate.toLocaleDateString('ja-JP', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); // なんJ風のコメントフォーマット const commentName = comment.name || '匿名'; const commentId = generateCommentId(comment.created_at, comment.name); return '
' + '
' + '' + number + '. ' + escapeHtml(commentName) + '' + 'ID:' + commentId + '' + '' + formattedDate + '' + '
' + '
' + formatCommentContent(comment.content) + '
' + '
'; } function generateCommentId(timestamp, name) { // タイムスタンプと名前からIDを生成(なんJ風) const hash = simpleHash(timestamp + (name || 'anonymous')); return hash.toString(16).substr(0, 8).toUpperCase(); } function simpleHash(str) { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // 32bit整数に変換 } return Math.abs(hash); } function formatCommentContent(content) { // HTMLエスケープ const escaped = escapeHtml(content); // なんJ風の文字装飾 let formatted = escaped // 草(w)の強調 .replace(/(w+)/gi, '$1') // やんけの強調 .replace(/(やんけ|ンゴ)/g, '$1') // 感嘆符の強調 .replace(/([!!??]{2,})/g, '$1') // 改行をBRに変換 .replace(/\n/g, '
'); return formatted; } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } async function submitComment(articleId) { const nameInput = document.getElementById('commentName'); const contentInput = document.getElementById('commentContent'); const submitBtn = document.querySelector('.submit-comment-btn'); if (!nameInput || !contentInput || !submitBtn) { console.error('必要なフォーム要素が見つかりません'); alert('フォームの要素が見つかりません'); return; } const name = nameInput.value.trim(); const content = contentInput.value.trim(); if (!content) { alert('コメント内容を入力してください'); return; } // バリデーション if (content.length > 1000) { alert('コメントは1000文字以内で入力してください'); return; } if (name.length > 50) { alert('名前は50文字以内で入力してください'); return; } // 送信ボタンを無効化 submitBtn.disabled = true; submitBtn.textContent = '送信中...'; try { const response = await fetch('/api/comments', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ article_id: articleId, name: name || '匿名', content: content }) }); if (!response.ok) { throw new Error('HTTP error! status: ' + response.status); } const result = await response.json(); if (result.success) { // フォームをクリア nameInput.value = ''; contentInput.value = ''; // コメント一覧を再読み込み await loadComments(articleId); // 成功メッセージ window.showNotification('コメントを投稿しました!', 'success'); } else { const errorMsg = result.error || '不明なエラー'; alert('コメントの投稿に失敗しました: ' + errorMsg); console.error('コメント送信失敗:', result); } } catch (error) { console.error('コメント投稿エラー:', error); alert('コメントの投稿に失敗しました: ' + error.message); } finally { // 送信ボタンを復活 if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = '書き込む'; } } } // ページ読み込み時に初期化 document.addEventListener('DOMContentLoaded', function() { // リアクション機能を初期化 loadReactions(); // コメントシステムも初期化 const commentForm = document.getElementById('commentForm'); if (commentForm) { initCommentSystem(); } else { } }); // 即座にシェア関数をテストするためのデバッグ関数 window.testShareFunctions = function() { console.log('Share functions test:'); console.log('shareToTwitter:', typeof window.shareToTwitter); console.log('shareToLine:', typeof window.shareToLine); console.log('shareToFacebook:', typeof window.shareToFacebook); console.log('copyLink:', typeof window.copyLink); console.log('showNotification:', typeof window.showNotification); }; // 安全なシェア関数の再定義(フォールバック) if (typeof window.shareToTwitter !== 'function') { console.warn('Redefining share functions due to loading issue'); window.shareToTwitter = function() { try { const url = encodeURIComponent(window.location.href); const title = document.title; const randomTexts = ['これはガチでやばいンゴwww', 'この記事草すぎて腹痛いンゴwww', 'なんJ民必見の神記事やんけ']; const randomText = randomTexts[Math.floor(Math.random() * randomTexts.length)]; const text = encodeURIComponent(randomText + ' ' + title + ' #なんJ #ニュース #話題沸騰'); window.open('https://twitter.com/intent/tweet?url=' + url + '&text=' + text, '_blank'); } catch (e) { console.error('Twitter share error:', e); } }; window.shareToLine = function() { try { const url = encodeURIComponent(window.location.href); const title = document.title; const lineText = '🔥' + title + '🔥' + '\n\n' + 'これはガチで話題になってるンゴwww' + '\n' + 'なんJ民も盛り上がってるで!'; const text = encodeURIComponent(lineText); window.open('https://social-plugins.line.me/lineit/share?url=' + url + '&text=' + text, '_blank'); } catch (e) { console.error('LINE share error:', e); } }; window.shareToFacebook = function() { try { const url = encodeURIComponent(window.location.href); window.open('https://www.facebook.com/sharer/sharer.php?u=' + url + '"e=' + encodeURIComponent('なんJ民も驚愕の話題記事やで!みんなも見てや!'), '_blank'); } catch (e) { console.error('Facebook share error:', e); } }; window.copyLink = function() { try { const url = window.location.href; if (navigator.clipboard) { navigator.clipboard.writeText(url).then(() => { alert('リンクをコピーしました!'); }).catch(() => { prompt('URLをコピーしてください:', url); }); } else { prompt('URLをコピーしてください:', url); } } catch (e) { console.error('Copy link error:', e); } }; }